文章目录
一、容器技术发展史
1. Docker 崛起的原因
- Docker镜像机制解决了Paas打包问题;
在2013年,虚拟机和云计算已经是比较普遍的技术和服务了,那时主流用户的普遍用法,就是租一批 AWS 或者 OpenStack 的虚拟机,然后像以前管理物理服务器那样,用脚本或者手工的方式在这些机器上部署应用,这就是Paas提供的“应用托管”的能力。
当时以 Cloud Foundry 为代表的 PaaS 项目,最核心的组件就是一套应用的打包和分发机制。 而Docker 项目,实际上跟 Cloud Foundry 的容器并没有太大不同,并且在大部分功能和实现原理上都是一样的,可 Docker 镜像的功能却成为了它致胜的关键。Docker 镜像解决的其实就是打包问题。 所谓 Docker 镜像,其实就是一个压缩包。但是这个压缩包里的内容,比 PaaS 的应用可执行文件 + 启停脚本的组合就要丰富多了。实际上,大多数 Docker 镜像是直接由一个完整操作系统的所有文件和目录构成的,所以这个压缩包里的内容跟你本地开发和测试环境用的操作系统是完全一样的。
Docker 项目给 PaaS 世界带来的“降维打击”,其实是提供了一种非常便利的打包机制。这种机制直接打包了应用运行所需要的整个操作系统,从而保证了本地环境和云端环境的高度一致,避免了用户通过“试错”来匹配两种不同运行环境之间差异的痛苦过程。
- Docker 容器同开发者之间有着与生俱来的密切关系;
它将比如 Cgroups 和 Namespace 这种已经存在多年却很少被人们关心的特性,通过非常友好的设计和封装,交到了最广大的开发者群体手里。
- 当时 PaaS 概念已经深入人心
PaaS 的定义已经从 Cloud Foundry 定义的标准变成了一套以 Docker 容器为技术核心,以 Docker 镜像为打包标准的、全新的“容器化”思路。
2. Docker 公司的发展历程
Docker 公司在 2014 年发布的 Swarm 项目是以一个完整的整体来对外提供集群管理功能的产品。Swarm 的最大亮点,则是它完全使用 Docker 项目原本的容器管理 API 来完成集群管理。
同时,Docker收购了Fig 项目(后改名为Compose),成了 Docker 公司到目前为止第二大受欢迎的项目。
Fig 项目之所以受欢迎,在于它在开发者面前第一次提出了“容器编排”(Container Orchestration)的概念 。编排就是对 Docker 容器的一系列定义、配置和创建动作的管理。
Docker Compose、Swarm 和 Machine 共称为Docker“三件套”,使得 Docker 公司在重新定义 PaaS 的方向上走出了最关键的一步。
3. Docker 公司的尘埃落定
Google、RedHat 等开源基础设施领域玩家们,共同牵头发起了一个名为 CNCF(Cloud Native Computing Foundation)的基金会。这个基金会的目的是以 Kubernetes 项目为基础,建立一个由开源基础设施领域厂商主导的、按照独立基金会方式运营的平台级社区,来对抗以 Docker 公司为核心的容器商业生态。
CNCF 社区为了打造基于 k8s 的生态做出了两方面努力:
- 解决 Kubernetes 项目在编排领域的竞争力的问题
在容器编排领域,Kubernetes 项目需要面对来自 Docker 公司的 Swarm 项目和 Mesos 社区两个方向的压力。Swarm 擅长的是跟 Docker 生态的无缝集成,而 Mesos 擅长的则是大规模集群的调度与管理。
Kubernetes 选择的应对方式是:Borg。它将 Borg 和 Omega 系统的内部特性落地于 k8s 项目中,最终形成了 Pod、Sidecar 等功能和设计模式。
Kubernetes 项目让人耳目一新的设计理念和号召力,很快就构建出了一个与众不同的容器编排与管理的生态。
- CNCF 社区必须以 Kubernetes 项目为核心,覆盖足够多的场景
在已经囊括了容器监控事实标准的 Prometheus 项目之后,CNCF 社区迅速在成员项目中添加了 Fluentd、OpenTracing、CNI 等一系列容器生态的知名工具和项目。
同时开始在整个社区推进“民主化”架构,即:从 API 到容器运行时的每一层,Kubernetes 项目都为开发者暴露出了可以扩展的插件机制,鼓励用户通过代码的方式介入 Kubernetes 项目的每一个阶段。
催生出了大量的、基于 Kubernetes API 和扩展接口的二次创新工作,比如:目前热度极高的微服务治理项目 Istio;被广泛采用的有状态应用部署框架 Operator;还有像 Rook 这样的开源创业项目,它通过 Kubernetes 的可扩展接口,把 Ceph 这样的重量级产品封装成了简单易用的容器存储插件。
面对 Kubernetes 社区的崛起和壮大,Docker 公司只能选择逐步放弃开源社区而专注于自己的商业化转型。从 2017 年开始,Docker 公司先是将 Docker 项目的容器运行时部分 Containerd 捐赠给 CNCF 社区,标志着 Docker 项目已经全面升级成为一个 PaaS 平台;紧接着,Docker 公司宣布将 Docker 项目改名为 Moby,然后交给社区自行维护,而 Docker 公司的商业产品将占有 Docker 这个注册商标。
二、容器技术基础
容器本身没有价值,有价值的是“容器编排”。
容器技术的核心功能,就是通过约束和修改进程的动态表现,从而为其创造出一个“边界”。对于 Docker 等大多数 Linux 容器来说,Cgroups 技术是用来制造约束的主要手段,而 Namespace 技术则是用来修改进程视图的主要方法。
一个正在运行的 Docker 容器,其实就是一个启用了多个 Linux Namespace 的应用进程,而这个进程能够使用的资源量,则受 Cgroups 配置的限制。这也是容器技术中一个非常重要的概念,即:容器是一个“单进程”模型。
1. Namespace(看的视图的限制)
Linux系统中创建进程可以fork,也可以clone,clone系统调用可以指定新的命名空间。当我们用 clone() 系统调用创建一个新进程时,就可以在参数中指定 CLONE_NEWPID 参数,比如:
int pid = clone(main_function, stack_size, CLONE_NEWPID | SIGCHLD, NULL);
这时,新创建的这个进程将会“看到”一个全新的进程空间,在这个进程空间里,它的 PID 是 1。之所以说“看到”,是因为这只是一个“障眼法”,在宿主机真实的进程空间里,这个进程的 PID 还是真实的数值,比如 100。
除了刚刚用到的 PID Namespace,Linux 操作系统还提供了 Mount、UTS、IPC、Network 和 User 这些 Namespace,用来对各种不同的进程上下文进行“障眼法”操作。比如,Mount Namespace,用于让被隔离进程只看到当前 Namespace 里的挂载点信息;Network Namespace,用于让被隔离进程看到当前 Namespace 里的网络设备和配置。
在创建容器进程时,指定了这个进程所需要启用的一组 Namespace 参数。这样,容器就只能“看”到当前 Namespace 所限定的资源、文件、设备、状态,或者配置。而对于宿主机以及其他不相关的程序,它就完全看不到了。所以说,容器其实是一种特殊的进程而已。
Docker 为容器启用的6项 Namespace:
namespace | 系统调用参数 | 隔离的内容 |
---|---|---|
UTS | CLONE_NEWUTS | 主机名域名 |
IPC | CLONE_NEWIPC | 信号量、消息队列与共享内存 |
PID | CLONE_NEWPID | 进程编号 |
Network | CLONE_NEWNET | 网络设备、网络栈、端口等 |
MOUNT | CLONE_NEWNS | 挂载点 |
USER | CLONE_NEWUSER | 用户与组 |
Namespace 技术实际上修改了应用进程看待整个计算机“视图”,即它的“视线”被操作系统做了限制,只能“看到”某些指定的内容。但对于宿主机来说,这些被“隔离”了的进程跟其他进程并没有太大区别。
2. Cgroups(用的资源的限制)
2.1 容器与虚拟机的对比
优势:
- 敏捷
- 高性能
使用虚拟化技术作为应用沙盒,就必须要由 Hypervisor 来负责创建虚拟机,这个虚拟机是真实存在的,并且它里面必须运行一个完整的 Guest OS 才能执行用户的应用进程。这就不可避免地带来了额外的资源消耗和占用。
相比之下,容器化后的用户应用,却依然还是一个宿主机上的普通进程,这就意味着这些因为虚拟化而带来的性能损耗都是不存在的;而另一方面,使用 Namespace 作为隔离手段的容器并不需要单独的 Guest OS,这就使得容器额外的资源占用几乎可以忽略不计。
劣势:
- 隔离的不彻底
- 既然容器只是运行在宿主机上的一种特殊的进程,那么多个容器之间使用的就还是同一个宿主机的操作系统内核。
- 在 Linux 内核中,有很多资源和对象是不能被 Namespace 化的,最典型的例子就是:时间。
由于上述问题,尤其是共享宿主机内核的事实,容器给应用暴露出来的攻击面是相当大的,应用“越狱”的难度自然也比虚拟机低得多。
2.2 Linux Cgroups
Linux Cgroups 就是 Linux 内核中用来为进程设置资源限制的一个重要功能。它的全称是 Linux Control Group。它最主要的作用,就是限制一个进程组能够使用的资源上限,包括 CPU、内存、磁盘、网络带宽等等。
此外,Cgroups 还能够对进程进行优先级设置、审计,以及将进程挂起和恢复等操作。
在 Linux 中,Cgroups 给用户暴露出来的操作接口是文件系统,即它以文件和目录的方式组织在操作系统的 /sys/fs/cgroup 路径下。
cgroup通过sys文件系统提供接口。
- 创建一个目录,即是新建一个控制组;
- 目录下会根据对应的cgroup子系统(某一种资源)自动创建文件;
- 每个文件就是该资源的控制方法。
Linux Cgroups 的设计还是比较易用的,简单粗暴地理解呢,它就是一个子系统目录加上一组资源限制文件的组合。而对于 Docker 等 Linux 容器项目来说,它们只需要在每个子系统下面,为每个容器创建一个控制组(即创建一个新目录),然后在启动容器进程之后,把这个进程的 PID 填写到对应控制组的 tasks 文件中就可以了。
2.2.1 实验演示
(1)查看cgroup内容
$ mount -t cgroup
cpuset on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset)
cpu on /sys/fs/cgroup/cpu type cgroup (rw,nosuid,nodev,noexec,relatime,cpu)
cpuacct on /sys/fs/cgroup/cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpuacct)
blkio on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio)
memory on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
...
在 /sys/fs/cgroup 下面有很多诸如 cpuset、cpu、 memory 这样的子目录,也叫子系统。这些都是可以被 Cgroups 进行限制的资源种类。而在子系统对应的资源种类下,就可以看到该类资源具体可以被限制的方法。比如,对 CPU 子系统来说,我们就可以看到如下几个配置文件,这个指令是:
$ ls /sys/fs/cgroup/cpu
cgroup.clone_children cpu.cfs_period_us cpu.rt_period_us cpu.shares notify_on_release
cgroup.procs cpu.cfs_quota_us cpu.rt_runtime_us cpu.stat tasks
输出中的 cpu.cfs_period_us
和 cpu.cfs_quota_us
就是可以组合使用的配置,用来限制进程在长度为 cfs_period 的一段时间内,只能被分配到总量为 cfs_quota 的 CPU 时间。
除 CPU 子系统外,Cgroups 的每一个子系统都有其独有的资源限制能力,比如:
- blkio,为块设备设定I/O 限制,一般用于磁盘等设备;
- cpuset,为进程分配单独的 CPU 核和对应的内存节点;
- memory,为进程设定内存使用的限制。
(2)创建cgroup控制组
需要在对应的子系统下面创建一个目录,比如,我们现在进入 /sys/fs/cgroup/cpu 目录下:
root@ubuntu:/sys/fs/cgroup/cpu$ mkdir container
root@ubuntu:/sys/fs/cgroup/cpu$ ls container/
cgroup.clone_children cpu.cfs_period_us cpu.rt_period_us cpu.shares notify_on_release
cgroup.procs cpu.cfs_quota_us cpu.rt_runtime_us cpu.stat tasks
这个目录就称为一个“控制组”。操作系统会在你新创建目录下,自动生成该子系统对应的资源限制文件。
(3)使用cgroup
现在在后台执行这样一条脚本:
$ while : ; do : ; done &
[1] 226
它执行了一个死循环,可以把计算机的 CPU 吃到 100%,根据它的输出,我们可以看到这个脚本在后台运行的进程号(PID)是 226。
可以用 top 指令来确认一下 CPU 有没有被打满:
$ top
%Cpu0 :100.0 us, 0.0 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
可以看到 CPU 的使用率已经 100% 了(%Cpu0 :100.0 us)。
通过查看 container 目录下的文件,看到 container 控制组里的 CPU quota 还没有任何限制(即:-1),CPU period 则是默认的 100 ms(100000 us):
$ cat /sys/fs/cgroup/cpu/container/cpu.cfs_quota_us
-1
$ cat /sys/fs/cgroup/cpu/container/cpu.cfs_period_us
100000
可以通过修改这些文件的内容来设置限制。比如,向 container 组里的 cfs_quota 文件写入 20 ms(20000 us):
$ echo 20000 > /sys/fs/cgroup/cpu/container/cpu.cfs_quota_us
它意味着在每 100 ms 的时间里,被该控制组限制的进程只能使用 20 ms 的 CPU 时间,也就是说这个进程只能使用到 20% 的 CPU 带宽。
然后把被限制的进程的 PID 写入 container 组里的 tasks 文件,上面的设置就会对该进程生效了:
$ echo 226 > /sys/fs/cgroup/cpu/container/tasks
用 top 指令查看一下:
$ top
%Cpu0 : 20.3 us, 0.0 sy, 0.0 ni, 79.7 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
可以看到,计算机的 CPU 使用率立刻降到了 20%(%Cpu0 : 20.3 us)。
2.2.2 docker 容器的cgroup演示
执行 docker run 时指定 cpu 相关设置参数:
$ docker run -it --cpu-period=100000 --cpu-quota=20000 ubuntu /bin/bash
在启动这个容器后,可以通过查看 Cgroups 文件系统下,CPU 子系统中,“docker”这个控制组里的资源限制文件的内容来确认:
$ cat /sys/fs/cgroup/cpu/docker/5d5c9f67d/cpu.cfs_period_us
100000
$ cat /sys/fs/cgroup/cpu/docker/5d5c9f67d/cpu.cfs_quota_us
20000
这表明这个 Docker 容器,只能使用到 20% 的 CPU 带宽。
2.3 Cgroups 的缺点
Cgroups 对资源的限制能力也有很多不完善的地方,被提及最多的自然是 /proc 文件系统的问题。
Linux 下的 /proc 目录存储的是记录当前内核运行状态的一系列特殊文件,用户可以通过访问这些文件,查看系统以及当前正在运行的进程的信息,比如 CPU 使用情况、内存占用率等,这些文件也是 top 指令查看系统信息的主要数据来源。
如果在早期没有完整隔离的容器里执行 top 指令,就会发现,它显示的信息是宿主机的 CPU 和内存数据,而不是当前容器的数据。造成这个问题的原因就是,/proc 文件系统并不知道用户通过 Cgroups 给这个容器做了什么样的资源限制,即:/proc 文件系统是全局针对整个系统的,它不了解 Cgroups 限制的存在。
lxcfs 最初是为了增强 LXC(Linux 容器)和 LXD(容器的轻量级虚拟化管理器)的容器体验而设计的。尽管 Docker 最初是基于 LXC 构建的,但在 0.9 版本之后,Docker 引入了自己的容器运行时,称为 libcontainer(现在是容器的标准运行时 runc 的一部分),并逐渐摒弃了对 LXC 的依赖。使用 lxcfs 来提供容器内部的 /proc 和 /sys 文件系统的隔离视图,是一种补充方式,主要用于提升 LXC/LXD 容器的用户体验。Docker 的用户如果需要 lxcfs 类似的功能,通常需要手动进行配置和集成,因为 Docker 默认提供的隔离机制已经能够满足大多数使用场景的需求。
PID命名空间可以保证在容器内看到的 /proc 文件系统只包含该容器内的进程信息,而不是整个系统的信息。
2.3.1 lxcfs
lxcfs 是通过文件挂载的方式,把 cgroup 中关于系统的相关信息读取出来,通过 docker 的 volume 挂载给容器内部的 proc 系统。 然后让 docker 内的应用读取 proc 中信息的时候以为就是读取的宿主机的真实的 proc。
lxcfs 的工作原理架构图:
当我们把宿主机的 /var/lib/lxcfs/proc/memoinfo 文件挂载到 Docker 容器的 /proc/meminfo 位置后,容器中进程读取相应文件内容时,lxcfs 的 /dev/fuse 实现会从容器对应的 Cgroup 中读取正确的内存限制。从而使得应用获得正确的资源约束。 cpu 的限制原理也是一样的。