从无到有 - 用Go实现一个docker

前言

出于对各类平台开发经验的迫切需要, 和本着只有亲自创造的才是真正理解的费曼学习精神, 我准备写一个系列的从无到有的各类项目开发blogs, 一是记录编程过程, 遇到的问题, 个人领悟, 二是自我督促去做基础开发, 积累开发经验, 理解各个常用软件的实现原理, 三是水blogs, 三是为了最终研究高级安全技术夯实基本功

这里用golang完成一个简易docker, 直接从源码层面去理解容器的原理

开发

要真正理解软件世界中的容器是什么, 需要了解制作容器的过程. 在这个过程中, 将讨论容器vs容器化、linux容器(包括名称空间、cgroup和分层文件系统), 然后我们将遍历一些从头构建简单容器的代码, 最后讨论这一切的真正含义
要在低层次上讨论容器,我们必须讨论三个方面: 名称空间、cgroup和分层文件系统.

从无到有 - 用Go实现一个docker_第1张图片

名称空间 - 隔离

名称空间提供了在一台机器上运行多个容器所需的隔离, 同时为每个容器提供了类似于它自己的环境. demo中有六个名称空间, 每一个都可以被独立请求, 相当于给一个进程(及其子进程)一个机器资源子集的视图.
其中的6个名称空间是

  1. PID: PID名称空间为进程及其子进程提供系统中进程子集的视图. 可以把它想象成一个映射表. 当pid名称空间中的进程向内核请求进程列表时, 内核会查看映射表. 如果该表中存在进程, 则使用映射的ID而不是真实的ID. 如果它在映射表中不存在, 内核就会假装它根本不存在. pid命名空间使在其内部创建的第一个进程pid 1(通过将其主机ID映射到1), 从而在容器中呈现出一个独立的进程树.
  2. MNT: 挂载名称空间为其中包含的进程提供了自己的挂载表. 这意味着它们可以挂载和卸载目录, 而不影响其他名称空间(包括主机名称空间). 更重要的是, 结合pivot_root系统调用, 它允许进程拥有自己的文件系统. 通过交换容器看到的文件系统,可以让一个进程认为它正在ubuntu、busybox或alpine上运行. 这一点让程序运行在不同操作系统成为可能.
  3. NET: 网络名称空间为使用它的进程提供自己的网络堆栈. 一般来说, 只有主网络名称空间(即启动计算机时启动的进程)才会附加任何实际的物理网卡. 但是可以创建虚拟以太网对连接的以太网卡,其中一端可以放在一个网络名称空间中, 另一端放在另一个网络名称空间中, 从而在网络名称空间之间创建虚拟链路. 这有点像在一台主机上有多个ip堆栈相互通信. 通过一些路由技术, 允许每个容器与真实的网络交互同时隔离每个独立的网络栈.
  4. UTS: UTS名称空间为它的进程提供它们自己的系统主机名和域名视图. 输入UTS命名空间后, 设置主机名或域名不会影响其他进程.
  5. IPC: IPC命名空间隔离各种进程间通信机制, 如消息队列等.
  6. USER:用户名称空间是最近添加的, 并且从安全性的角度来看可能是最强大的. 用户命名空间将进程看到的uid映射到主机上的另一组uid(和gid), 使用用户命名空间, 可以将容器的根用户ID(即0)映射到主机上的任意(且无特权)uid. 这意味着我们可以让一个容器认为它拥有根访问权限, 甚至可以在特定于容器的资源上给它根权限, 而不实际在根名称空间中给它任何特权. 容器可以自由地以uid 0的形式运行进程(通常与具有根权限同义), 但是内核实际上是将这个uid映射到一个没有特权的真正uid. 大多数容器系统不将容器中的任何uid映射到调用命名空间中的uid 0.
cgroups - 资源共享

cgroups是一种提供容器间资源共享的机制.
cgroups收集一组进程或任务id, 并对它们进行限制. 在命名空间隔离进程的地方, cgroups在进程之间执行资源共享. 内核将cgroup公开为一个可以挂载的特殊文件系统. 只需将进程id添加到任务文件中, 就可以将进程或线程添加到cgroup中, 然后通过编辑该目录中的文件来读取和配置各种值.

分层文件系统 - 镜像移动

Layered Filesystems负责实现images的整体移动.
在基本层面上, 分层文件系统相当于优化调用, 为每个容器创建根文件系统的一个副本. 有很多方法可以做到这一点. Btrfs在文件系统层使用copy on write. Aufs使用“union mounts”. 这里只使用非常简单的方法: 真正地进行复制. (很慢但有效

设置框架

main.go 程序读入第一个参数. 如果是’ run ‘则运行parent()方法, 如果是child()则运行子方法. 父方法运行’ /proc/self/exe ', 这是一个包含当前可执行文件的内存映像的特殊文件.

package main

import (
	"fmt"
	"os"
	"os/exec"
)

func main() {
	switch os.Args[1] {
	case "run":
		parent()
	case "child":
		child()
	default:
		panic("invalid operation")
	}
}

func parent() {
	cmd := exec.Command("/proc/self/exe", append([]string{"child"}, os.Args[2:]...)...)
	cmd.Stdin = os.Stdin
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	if err := cmd.Run(); err != nil {
		fmt.Println("Error", err)
		os.Exit(1)
	}
}

func child() {
	cmd := exec.Command(os.Args[2], os.Args[3:]...)
	cmd.Stdin = os.Stdin
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	if err := cmd.Run(); err != nil {
		fmt.Println("Error", err)
		os.Exit(1)
	}
}

func must(err error) {
	if err != nil {
		panic(err)
	}
}

添加名称空间

在parent()函数添加名称空间, 告诉go在运行子进程时传递一些额外的标志.

cmd.SysProcAttr = &syscall.SysProcAttr{
	Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS,
}

根文件系统

至此进程处于一组独立的名称空间中, 不过文件系统看起来和主机一样. 这是因为进程处于挂载的名称空间中,而初始挂载是从创建的名称空间继承的. 为了改变挂载的文件系统, 在child()中添加

	must(syscall.Mount("rootfs", "rootfs", "", syscall.MS_BIND, ""))
	must(os.MKdirAll("rootfs/oldrootfs", 0700))
	must(syscall.PivotRoot("rootfs", "rootfs/oldrootfs"))
	must(os.Chdir("/"))

最后两行是最重要的, 将当前目录/移动到rootfs/oldrootfs,并将新的rootfs目录切换到/. 当pivoroot调用完成后, 容器中的/目录将指向rootfs

综合

一个省略了初始化操作, cgroups资源共享的简易docker就完成了

package main

import (
	"fmt"
	"os"
	"os/exec"
	"syscall"
)

func main() {
	switch os.Args[1] {
	case "run":
		parent()
	case "child":
		child()
	default:
		panic("invalid operation")
	}
}

func parent() {
	cmd := exec.Command("/proc/self/exe", append([]string{"child"}, os.Args[2:]...)...)
	cmd.Stdin = os.Stdin
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	cmd.SysProcAttr = &syscall.SysProcAttr{
		Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS,
	}

	if err := cmd.Run(); err != nil {
		fmt.Println("Error", err)
		os.Exit(1)
	}
}

func child() {
	must(syscall.Mount("rootfs", "rootfs", "", syscall.MS_BIND, ""))
	must(os.MKdirAll("rootfs/oldrootfs", 0700))
	must(syscall.PivotRoot("rootfs", "rootfs/oldrootfs"))
	must(os.Chdir("/"))

	cmd := exec.Command(os.Args[2], os.Args[3:]...)
	cmd.Stdin = os.Stdin
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	if err := cmd.Run(); err != nil {
		fmt.Println("Error", err)
		os.Exit(1)
	}
}

func must(err error) {
	if err != nil {
		panic(err)
	}
}

演示

后面会完善demo-docker的细节, 这里只是一个开始

总结

docker主要包括名称空间, cgroups, 分层文件系统, 分别实现隔离, 资源共享, 镜像功能

参考

https://www.infoq.com/articles/build-a-container-golang/

Liz Rize大佬的教学
https://www.youtube.com/watch?v=8fi7uSYlOdc

你可能感兴趣的:(Go,docker,docker,golang,容器)