首页 > 数据库 > 数据库开发技术 > 深入探索-Docker 的底层实现原理
Docker 是一种流行的容器化平台,它利用 Linux 内核中的 cgroups 和 namespaces 特性实现了轻量级的容器隔离。下面将详细介绍 Docker 的底层实现原理,并深入的看看探索其中使用到的三个系统调用与容器隔离的关系。
Docker 的底层实现原理主要涉及以下三个方面
(1)cgroups
cgroups(Control Groups)是 Linux 内核中的一种特性,它可以将进程分组并限制它们对系统资源(如 CPU、内存、磁盘和网络)的使用。Docker 使用 cgroups 来实现容器的资源隔离和限制,例如限制容器可以使用的 CPU 核心数量和内存大小。
在 Linux 系统中,cgroups 的配置存放在 /sys/fs/cgroup 目录下。其中,每个子目录都对应一个 cgroup,可以对其进行资源控制。以下是一些重要的配置文件:
cpu.cfs_quota_us:CPU 时间配额,以微秒为单位。如果值为 -1,则表示没有限制。cpu.cfs_period_us:CPU 时间周期,以微秒为单位。该值确定了 CPU 时间的单位。memory.limit_in_bytes:内存限制,以字节为单位。如果值为 -1,则表示没有限制。blkio.weight:块设备 IO 权重,范围为 10-1000。
(2)namespaces
namespaces 是 Linux 内核中的一种特性(进程隔离技术),它可以将全局系统资源抽象为一组本地资源。在 Docker 中,通过使用不同的 namespaces,可以实现容器的隔离。例如,PID 命名空间可以使容器只能看到自己内部的进程,网络命名空间可以使容器拥有自己的网络接口和 IP 地址,与主机网络隔离。
在 Linux 系统中,namespaces 的配置存放在 /proc/[pid]/ns 目录下,其中 [pid] 为进程 ID。以下是一些重要的配置文件:
ipc:进程间通信隔离,包括信号量、消息队列和共享内存等。uts:主机名和域名隔离。net:网络隔离,包括 IP 地址、网络接口、路由表等。pid:进程隔离,每个容器内的进程只能看到该容器内的进程。mnt:文件系统隔离,每个容器有自己的文件系统视图。
(3)联合文件系统
Docker 使用联合文件系统(UnionFS)实现镜像的分层存储。UnionFS 是一种文件系统堆叠技术,它允许多个文件系统层透明地合并为一个虚拟文件系统。Docker 的联合文件系统由多个镜像层组成,每个镜像层都可以看作一个只读的文件系统,只有最上层的可读写文件系统可以进行写操作。
联合文件系统的配置信息存放在容器的元数据中,包括镜像层的 ID、镜像层的挂载路径等。Docker 会在运行容器时,将不同镜像层的文件系统堆叠在一起,形成一个完整的文件系统。
容器的文件系统路径为 /var/lib/docker/overlay2/<container-id>/merged。
(1)clone()
clone() 系统调用可以创建一个新的进程,并可以通过设计参数来达到隔离的效果。在 Docker 中,每个容器都是一个独立的进程,可以拥有自己的文件系统、网络接口、进程空间和用户空间等。因此,在 Docker 中,clone() 系统调用被用于创建新的容器进程。
使用 clone() 系统调用时,可以通过指定不同的参数来实现不同程度的隔离,例如:
CLONE_NEWUTS:创建一个新的 UTS 命名空间,用于隔离主机的主机名和域名;CLONE_NEWIPC:创建一个新的 IPC 命名空间,用于隔离进程间通信(IPC)机制,例如信号量、共享内存和消息队列;CLONE_NEWPID:创建一个新的 PID 命名空间,用于隔离进程 ID;CLONE_NEWNET:创建一个新的网络命名空间,用于隔离容器的网络接口和 IP 地址;CLONE_NEWUSER:创建一个新的用户命名空间,用于隔离用户和用户组的 ID。
(2)unshare()
unshare() 系统调用可以将进程从主机命名空间中分离出来,并创建一个新的命名空间,使得容器拥有自己独立的命名空间。在 Docker 中,unshare() 系统调用被用于创建新的命名空间,并将容器进程与主机进程分离开来,以实现容器的隔离。这个命名空间将成为容器进程的根命名空间,容器进程只能访问该命名空间下的资源。在这个命名空间中,容器进程将看到自己的文件系统、进程空间和网络接口等,而不是主机上的资源。
(3)setns()
setns() 系统调用可以将进程加入到特定的命名空间中。在 Docker 中,当容器需要访问主机网络时,可以使用 setns() 系统调用将进程切换到主机网络命名空间中,并访问主机网络资源。因此,setns() 系统调用是 Docker 实现容器与主机网络之间通信的的关键系统调用。
在 Docker 中,容器的创建和运行可以分为以下四个步骤:
(1)镜像获取
Docker 的镜像是一个文件系统层的集合,包含了应用程序运行所需的所有组件。在创建容器之前,需要先获取容器的基础镜像,这可以通过从 Docker Hub 中下载镜像,或者使用本地构建的镜像。
(2)容器创建
使用 Docker 命令行工具(CLI)创建容器时,Docker 首先会调用 clone() 系统调用来创建一个新的进程。该进程将拥有自己的文件系统、进程空间、网络接口等,与主机和其他容器隔离。
示例代码
package mainimport ("fmt""os""syscall")func main() {cmd := "/bin/bash"args := []string{"bash"}cloneFlags := syscall.CLONE_NEWPID | syscall.CLONE_NEWUTS | syscall.CLONE_NEWNS | syscall.CLONE_NEWNET | syscall.CLONE_NEWIPC// 使用 clone() 系统调用创建子进程pid, err := syscall.Clone(func() {// 子进程中运行指定命令err := syscall.Exec(cmd, args, os.Environ())if err != nil {panic(err)}}, nil, cloneFlags)if err != nil {panic(err)}fmt.Printf("child process ID: %d\n", pid)// 等待子进程结束var status syscall.WaitStatus_, err = syscall.Wait4(pid, &status, 0, nil)if err != nil {panic(err)}fmt.Printf("child process exit status: %d\n", status.ExitStatus())}
(3)容器启动
在容器创建完成后,Docker 会使用 unshare() 系统调用创建新的命名空间,并将容器进程与主机进程隔离开来。然后,Docker 会将容器进程切换到容器的网络命名空间中,以获得独立的网络接口和 IP 地址。
示例代码
package mainimport ("fmt""syscall")func main() {// 创建新的 UTS 命名空间err := syscall.Unshare(syscall.CLONE_NEWUTS)if err != nil {panic(err)}// 获取当前主机的 hostnamehostname, err := syscall.Gethostname()if err != nil {panic(err)}fmt.Printf("hostname in current namespace: %s\n", hostname)// 修改当前命名空间中的 hostnameerr = syscall.Sethostname([]byte("newhostname"))if err != nil {panic(err)}// 再次获取当前主机的 hostnamehostname, err = syscall.Gethostname()if err != nil {panic(err)}fmt.Printf("hostname in new namespace: %s\n", hostname)}
(4)容器运行
在容器启动后,Docker 会使用 setns() 系统调用将容器进程切换到相应的命名空间中,并将容器进程加入到相应的 cgroups 中,以限制容器使用系统资源。此外,Docker 还会挂载容器的文件系统层,并启动容器中定义的应用程序。
示例代码
package mainimport ("fmt""os""syscall")func main() {pid := os.Getpid()// 打开指定进程的 PID namespacefd, err := syscall.Open(fmt.Sprintf("/proc/%d/ns/pid", pid), syscall.O_RDONLY, 0)if err != nil {panic(err)}defer syscall.Close(fd)// 加入指定进程的 PID namespaceerr = syscall.Setns(fd, syscall.CLONE_NEWPID)if err != nil {panic(err)}// 获取当前进程的 PIDpid = os.Getpid()fmt.Printf("PID in new namespace: %d\n", pid)}
Docker 利用 Linux 内核中的 cgroups 和 namespaces 特性实现了轻量级的容器隔离。在 Docker 中,clone() 系统调用被用于创建新的容器进程,unshare() 系统调用被用于创建新的命名空间,以实现容器的隔离,而 setns() 系统调用被用于将容器进程切换到相应的命名空间中。通过这些系统调用,Docker 可以在容器和主机之间实现高度的隔离和安全性,从而提供可移植和可重复的应用程序环境。
syscall 相关的知识可以到五分钟学GO的连载学习。
原文作者:五分钟学SRE;如有侵权,请联系管理员删除。