需要理解关于容器的一些事实:
- 容器技术的兴起源于 PaaS 技术的普及;
- Docker 公司发布的 Docker 项目具有里程碑式的意义;
- Docker 项目通过“容器镜像”,解决了应用打包这个根本性难题。
要明白一个道理:容器本身没有价值,有价值的是“容器编排”
容器技术的核心功能,是通过约束和修改进程的动态表现,为其创造一个“边界”。对于Docker和大多数Linux容器来说,Cgroups技术是用来制造约束的主要手段,Namespace技术则是用来修改进程视图的主要方法。
Namespace技术可以通过修改进程视图来对应用进行隔离,使得这些被修改的进程只能看到重新计算过的进程编号,比如让 PID=1。实际上,他们在宿主机的操作系统里,还是原来的进程,其进程编号未变。Namespace只是 Linux 创建新进程的一个可选参数;在 Linux 系统中创建线程的系统调用是 clone()。想要创建多个 PID Namespace只需要多次执行调用 clone(), 除了PID Namespace,Linux 操作系统还提供了 Mount、UTS、IPC、Network 和 User 这些 Namespace,用来对各种不同的进程上下文进行“障眼法”操作,这就是 Linux 容器最基本的实现原理了。所以说,容器,其实是一种特殊的进程而已。
在Linux系统中,PID 1 是一个特殊而重要的进程,它是系统启动后创建的第一个用户空间进程,通常被称为 init进程。
其核心特点如下:
- 负责启动其他所有进程和服务
- 挂载文件系统(如
/proc,/sys等) - 执行初始化脚本(如
/etc/rc.local) - 当一个进程的父进程先终止时,PID 1会成为其新的父进程
- 负责回收这些孤儿进程的资源,防止僵尸进程积累
- 默认情况下,PID 1会忽略大多数信号(包括SIGKILL)
- 这是为了防止系统被意外终止
Namespace 技术实际上修改了应用进程看待整个计算机“视图”,即它的“视线”被操作系统做了限制,只能“看到”某些指定的内容。但对于宿主机来说,这些被“隔离”了的进程跟其他进程并没有太大区别。
不过相比于虚拟机,基于 Linux Namespace 的隔离机制相比于虚拟化技术也有很多不足之处,其中最主要的问题就是:隔离得不彻底。
首先,既然容器只是运行在宿主机上的一种特殊的进程,那么多个容器之间使用的就还是同一个宿主机的操作系统内核。你可以在容器里通过 Mount Namespace 单独挂载其他不同版本的操作系统文件,但这并不能改变共享宿主机内核的事实。这意味着,如果你要在 Windows 宿主机上运行 Linux 容器,或者在低版本的 Linux 宿主机上运行高版本的 Linux 容器,都是行不通的。
Linux 内核中,有很多资源和对象是不能被 Namespace 化的,最典型的例子就是:时间。这意味着如果你通过容器中的程序进行系统调用修改了时间,整个宿主机的时间也会变更,这不符合预期。
对容器的资源也要限制,因为在宿主机看来容器的进程id是不变的,只是在容器的Namespace里面的pid是1,所以在宿主机看来容器与其他进程是平等的竞争关系。这意味着容器能够用到的计算机资源是可以随时被宿主机上的其他进程(容器)占用的。
Linux Cgroups 就是 Linux 内核中用来为进程设置资源限制的一个重要功能。Linux Cgroups 的全称是 Linux Control Group。它最主要的作用,就是限制一个进程组能够使用的资源上限,包括 CPU、内存、磁盘、网络带宽等等。
Cgroups 还能够对进程进行优先级设置、审计,以及将进程挂起和恢复等操作。在 Linux 中,Cgroups 给用户暴露出来的操作接口是文件系统,即它以文件和目录的方式组织在操作系统的 /sys/fs/cgroup 路径下。在 /sys/fs/cgroup 下面有很多诸如 cpuset、cpu、 memory 这样的子目录,也叫子系统。这些都是我这台机器当前可以被 Cgroups 进行限制的资源种类。而在子系统对应的资源种类下,你就可以看到该类资源具体可以被限制的方法。
Cgroups 的每一项子系统都有其独有的资源限制能力,比如:
- blkio,为块设备设定I/O 限制,一般用于磁盘等设备;
- cpuset,为进程分配单独的 CPU 核和对应的内存节点;
- memory,为进程设定内存使用的限制。
Linux Cgroups 的设计还是比较易用的,简单粗暴地理解呢,它就是一个子系统目录加上一组资源限制文件的组合。而对于 Docker 等 Linux 容器项目来说,它们只需要在每个子系统下面,为每个容器创建一个控制组(即创建一个新目录),然后在启动容器进程之后,把这个进程的 PID 填写到对应控制组的 tasks 文件中就可以了。
对 Docker 项目来说,它最核心的原理实际上就是为待创建的用户进程:
- 启用 Linux Namespace 配置;
- 设置指定的 Cgroups 参数;
- 切换进程的根目录(Change Root)。
在 Linux 操作系统里,chroot 的命令可以帮你“change root file system”,即改变进程的根目录到你指定的位置。
实际上,Mount Namespace 正是基于对 chroot 的不断改良才被发明出来的,它也是 Linux 操作系统里的第一个 Namespace。
为了能够让容器的这个根目录看起来更“真实”,我们一般会在这个容器的根目录下挂载一个完整操作系统的文件系统,而这个挂载在容器根目录上、用来为容器进程提供隔离后执行环境的文件系统,就是所谓的“容器镜像”。它还有一个更为专业的名字,叫作:rootfs(根文件系统)。
rootfs 只是一个操作系统所包含的文件、配置和目录,并不包括操作系统内核。在 Linux 操作系统中,这两部分是分开存放的,操作系统只有在开机启动时才会加载指定版本的内核镜像,不过,正是由于 rootfs 的存在,容器才有了一个被反复宣传至今的重要特性:一致性。
由于 rootfs 里打包的不只是应用,而是整个操作系统的文件和目录,也就意味着,应用以及它运行所需要的所有依赖,都被封装在了一起。
Docker 在镜像的设计中,引入了层(layer)的概念。也就是说,用户制作镜像的每一步操作,都会生成一个层,也就是一个增量 rootfs。这使得容器可以进行增量修改
一个正在运行的 Linux 容器,其实可以被“一分为二”地看待:
- 一组联合挂载在 /var/lib/docker/aufs/mnt 上的 rootfs,这一部分我们称为“容器镜像”(Container Image),是容器的静态视图;
- 一个由 Namespace+Cgroups 构成的隔离环境,这一部分我们称为“容器运行时”(Container Runtime),是容器的动态视图。