一个“容器”,实际上是一个由 Linux Namespace、Linux Cgroups 和 rootfs 三种技术构建出来的进程的隔离环境。一个正在运行的 Linux 容器可以这样看待:
- 容器镜像(Container Image)静态视图:一组联合挂载在 /var/lib/docker/aufs/mnt 上的 rootfs
- 容器运行时(Container Runtime)动态视图:一个由 Namespace+Cgroups 构成的隔离环境
Linux Namespace
Linux提供的一种内核级别环境隔离的方法。chroot系统调用提供了一种简单的隔离模式:chroot内部的文件系统无法访问外部的内容。Linux Namespace在此基础上,提供了对UTS、IPC、mount、PID、network、User等的隔离机制。
Linux Namespace 有如下种类,官方文档《Namespace in Operation》
分类 | 系统调用参数 | 相关内核版本 |
---|---|---|
Mount namespaces | CLONE_NEWNS | Linux 2.4.19 |
UTS namespaces | CLONE_NEWUTS | Linux 2.6.19 |
IPC namespaces | CLONE_NEWIPC | Linux 2.6.19 |
PID namespaces | CLONE_NEWPID | Linux 2.6.24 |
Network namespaces | CLONE_NEWNET | 始于Linux 2.6.24 完成于 Linux 2.6.29 |
User namespaces | CLONE_NEWUSER | 始于 Linux 2.6.23 完成于 Linux 3.8) |
主要是三个系统调用
clone
() – 实现线程的系统调用,用来创建一个新的进程,并可以通过设计上述参数达到隔离unshare
() – 使某进程脱离某个namespacesetns
() – 把某进程加入到某个namespace
关于 Mount Namespace 的问题:容器里的应用进程,理应看到一份完全独立的文件系统。这样,它就可以在自己的容器目录(比如 /tmp)下进行操作,而完全不会受宿主机以及其他容器的影响。但实际情况是即使开启了 Mount Namespace,容器进程看到的文件系统也跟宿主机完全一样。Mount Namespace 修改的,是容器进程对文件系统“挂载点”的认知。意味着只有在“挂载”这个操作发生之后,进程的视图才会被改变。而在此之前,新创建的容器会直接继承宿主机的各个挂载点。
Mount Namespace 跟其他 Namespace 的使用略有不同的地方:它对容器进程视图的改变,一定是伴随着挂载操作(mount)才能生效
所谓的“容器镜像”就是挂载在容器根目录上、用来为容器进程提供隔离后执行环境的文件系统。它还有一个更为专业的名字,叫作:rootfs(根文件系统)
对 Docker 项目来说,它最核心的原理实际上就是为待创建的用户进程:
- 启用 Linux Namespace 配置
- 设置指定的 Cgroups 参数
- 切换进程的根目录(Change Root)
需要明确的是,rootfs 只是一个操作系统所包含的文件、配置和目录,并不包括操作系统内核。实际上,同一台机器上的所有容器,都共享宿主机操作系统的内核。
容器深入到操作系统级别的运行环境一致性,打通了应用在本地开发和远端执行环境之间难以逾越的鸿沟。
Linux Cgroups
Linux Control Group是 Linux 内核中用来为进程设置资源限制的一个重要功能。它最主要的作用,就是限制一个进程组能够使用的资源上限,包括 CPU、内存、磁盘、网络带宽等等,还能够对进程进行优先级设置、审计,以及将进程挂起和恢复等操作。在 Linux 中,Cgroups 给用户暴露出来的操作接口是文件系统,即它以文件和目录的方式组织在操作系统的 /sys/fs/cgroup。
cpuset、cpu、 memory 这样的子目录是机器上当前可以被 Cgroups 进行限制的资源种类,而在子系统对应的资源种类下,可以看到该类资源具体可以被限制的方法。
Cgroups就是一个子系统目录加上一组资源限制文件的组合。而对于 Docker 等 Linux 容器项目来说,只需要在每个子系统下面,为每个容器创建一个控制组(即创建一个新目录),然后在启动容器进程之后,把这个进程的 PID 填写到对应控制组的 tasks 文件中就可以了。
Namespace 的作用是“隔离”,它让应用进程只能看到该 Namespace 内的“世界”;而 Cgroups 的作用是“限制”,它给这个“世界”围上了一圈看不见的墙。Cgroups 对资源的限制能力也有很多不完善的地方,比如/proc 文件系统不了解 Cgroups 限制的存在。
Union File System
也叫 UnionFS,最主要的功能是将多个不同位置的目录联合挂载(union mount)到同一个目录下。AuFS 的全称是 Another UnionFS,后改名为 Alternative UnionFS,再后来改名为 Advance UnionFS
它是对 Linux 原生 UnionFS 的重写和改进;但Linus Torvalds(Linux 之父)一直不让 AuFS 进入 Linux 内核主干。
AuFS 最关键的目录结构在 /var/lib/docker 路径下的 diff 目录(镜像层),然后被联合挂载在 /var/lib/docker/aufs/mnt 里面
只读层(ro+wh,即 readonly+whiteout)
可读写层,read write。在没有写入文件之前,这个目录是空的。而一旦在容器里做了写操作,修改产生的内容就会以增量的方式出现在这个层中。删除文件AuFS 会在可读写层创建一个 whiteout 文件,把只读层里的文件“遮挡”起来,比如,要删除只读层里一个名叫 foo 的文件,实际上是在可读写层创建了一个名叫.wh.foo 的文件。这样,当这两个层被联合挂载之后,foo 文件就会被.wh.foo 文件“遮挡”起来,看上去“消失”了。这个功能,就是“ro+wh”的挂载方式,即只读 +whiteout 的含义。whiteout 可以理解为“遮罩”。
Init 层是 Docker 项目单独生成的一个内部层,专门用来存放 /etc/hosts、/etc/resolv.conf 等信息,这些文件本来属于只读的系统镜像的一部分,但是在启动容器时往往需要写入一些对当前容器有效的指定值,比如 hostname,所以就需要在可读写层对它们进行修改,但执行 docker commit 时,只提交可读写层,并不需要包含这些信息。所以Docker做法是,在修改了这些文件之后,以一个单独的层挂载出来。最终,这些层都被联合挂载到 /var/lib/docker/aufs/mnt 目录下。