# Docker 的技术实现
Docker 的实现,主要归结于三大技术:命名空间 ( Namespaces )
、控制组 ( Control Groups )
和 联合文件系统 ( Union File System )
。
# Namespaces
Linux 内核的命名空间,就是能够将计算机资源进行切割划分,形成各自独立的空间。
就实现而言,Linux Namespaces
可以分为很多具体的子系统,如 User Namespace
、NetNamespace
、PID Namespace
、Mount Namespace
等等。
Namespace
的使用方式也非常有意思:它其实只是 Linux 创建新进程的一个可选参数。
这里我们以进程为例,在 Linux 系统中 创建线程
的系统调用是 clone()
,比如:
int pid = clone(main_function, stack_size, SIGCHLD, NULL);
这个系统调用就会为我们创建一个新的进程,并且返回它的进程号 pid。
而当我们用 clone() 系统调用创建一个新进程时,可以在参数中指定 CLONE_NEWPID
参数,比如:
int pid = clone(main_function, stack_size, CLONE_NEWPID | SIGCHLD, NULL);
这时,通过 PID Namespace
,新创建的这个进程将会“看到”一个全新的进程空间,在这个进程空间里,它的 PID 是 1。之所以说“看到”,是因为这只是一个“障眼法”,在宿主机真实的进程空间里,这个进程的 PID 还是真实的数值,比如 100。
Namespace 技术实际上修改了应用进程看待整个计算机“视图”,即它的“视线”被操作系统做了限制,只能“看到”某些指定的内容
。
所以,实际上是在创建容器进程时,指定了这个进程所需要启用的一组 Namespace 参数。这样,容器就只能“看”到当前 Namespace 所限定的资源、文件、设备、状态,或者配置。而对于宿主机以及其他不相关的程序,它就完全看不到了。
# 总结
所以说,容器,其实是一种特殊的进程而已
。
# Mount Namespace vs rootfs
一般情况下,即使开启了 Mount Namespace
,容器进程看到的文件系统也跟宿主机完全一样。
这是因为,Mount Namespace
修改的,是 容器进程对文件系统“挂载点”的认知
。但是,这也就意味着,只有在“挂载”这个操作发生之后
,进程的视图才会被改变。而在此之前,新创建的容器会 直接继承宿主机的各个挂载点
。
这就是 Mount Namespace 跟其他 Namespace 的使用略有不同的地方:它对容器进程视图的改变,一定是伴随着挂载操作(mount)才能生效
。
作为一个普通用户,我们希望的是一个更友好的情况:每当创建一个新容器时,我希望容器进程看到的文件系统就是一个独立的隔离环境,而不是继承自宿主机的文件系统。怎么才能做到这一点呢?
不难想到,我们可以 在容器进程启动之前重新挂载它的整个根目录“/”
。而由于 Mount Namespace
的存在,这个挂载对宿主机不可见
,所以容器进程就可以在里面随便折腾了。
在 Linux 操作系统里,有一个名为 chroot 的命令可以帮助你在 shell 中方便地完成这个工作。
实际上,Mount Namespace 正是基于对 chroot 的不断改良才被发明出来的,它也是 Linux 操作系统里的第一个 Namespace。
当然,为了能够让容器的这个根目录看起来更“真实”,我们一般会在这个容器的根目录下挂载一个完整操作系统的文件系统,比如 Ubuntu16.04 的 ISO。这样,在容器启动之后,我们在容器里通过执行 "ls /" 查看根目录下的内容,就是 Ubuntu 16.04 的所有目录和文件。
而这个挂载在容器根目录上、用来为容器进程提供隔离后执行环境的文件系统,就是所谓的“容器镜像”。它还有一个更为专业的名字,叫作:rootfs(根文件系统)
。
需要明确的是,rootfs 只是一个操作系统所包含的文件、配置和目录,并不包括操作系统内核。在 Linux 操作系统中,这两部分是分开存放的,操作系统只有在开机启动时才会加载指定版本的内核镜像。
通过结合使用 Mount Namespace
和 rootfs
,容器就能够为进程构建出一个完善的文件系统隔离环境。
# Control Groups
资源控制组 ( 常缩写为 CGroups ) 是 Linux 内核在 2.6 版本后逐渐引入的一项对 计算机资源控制
的模块。
我们知道容器只是运行在宿主机上的一种 特殊的进程
,那么多个容器之间使用的就还是 同一个宿主机的操作系统内核
。
虽然容器内的第 1 号进程在“障眼法”的干扰下只能看到容器里的情况,但是宿主机上,它作为第 100 号进程与其他所有进程之间依然是 平等的竞争关系
。这就意味着,虽然第 100 号进程表面上被隔离了起来,但是它所能够使用到的资源(比如 CPU、内存),却是可以随时被宿主机上的其他进程(或者其他容器)占用的。
当然,这个 100 号进程自己也可能把所有资源吃光。这些情况,显然都不是一个“沙盒”应该表现出来的合理行为。
而 Linux Cgroups
就是 Linux 内核中用来 为进程设置资源限制
的一个重要功能。
Linux Cgroups
的全称是 Linux Control Group
。它最主要的作用,就是 限制一个进程组能够使用的资源上限
,包括 CPU、内存、磁盘、网络带宽等等。
与以 隔离进程
、网络
、文件系统
等 虚拟资源
为目的 Namespace 不同,CGroups 主要做的是 硬件资源
的隔离。
需要再强调一次的是,CGroups
除了资源的隔离,还有 资源分配
这个关键性的作用。通过 CGroups
,我们可以指定任意一个隔离环境对任意资源的占用值或占用率,这对于很多分布式使用场景来说是非常有用的功能。
由于 CGroups
实现于操作系统,而操作系统垄断着系统资源的分配,所以其完全能够限制隔离环境下应用的资源占有量。
# 总结
一个正在运行的 Docker
容器,其实就是一个启用了多个 Linux Namespace
的应用进程,而这个进程能够使用的资源量,则受 Cgroups
配置的限制。
# Union File System
联合文件系统 ( Union File System ) 是一种能够同时挂载不同实际文件或文件夹到同一目录,形成一种联合文件结构的文件系统。联合文件系统本身与虚拟化并无太大的关系,但 Docker 却创新的将其引入到容器实现中,用它解决虚拟环境对文件系统占用过量,实现虚拟环境快速启停等问题。
在 Docker 中,提供了一种对 UnionFS 的改进实现,也就是 AUFS ( Advanced Union File System )。
AUFS 将文件的更新挂载到老的文件之上,而不去修改那些不更新的内容,这就意味着即使虚拟的文件系统被反复修改,也能保证对真实文件系统的空间占用保持一个较低水平。
同样的,通过 AUFS,Docker 大幅减少了虚拟文件系统对物理存储空间的占用。
# 总结
一个“容器”,实际上是一个由 Linux Namespace
、Linux Cgroups
和 rootfs
三种技术构建出来的 进程的隔离环境
。
一个正在运行的 Linux 容器,其实可以被“一分为二”地看待:
- 一组联合挂载在
/var/lib/docker/aufs/mnt
上的rootfs
,这一部分我们称为“容器镜像”(Container Image),是容器的静态视图
; - 一个由
Namespace+Cgroups
构成的隔离环境,这一部分我们称为“容器运行时”(Container Runtime),是容器的动态视图
。