640?wx_fmt=gif

640?wx_fmt=jpeg

作者 | 阿文

责编 | 郭芮

很多接触过类似KVM、Vmware 等虚拟化产品的开发者一定知道,传统的虚拟机其实是模拟真实计算机硬件,然后需要独立安装一个单独的操作系统。这个操作系统可以是 Linux 或者是 windows,而 Docker 容器则不需要你去安装动则几十个 G 的操作系统——它提供的镜像启动后能够做到很小,例如 busybox 镜像只有几 M 大小:

 

# docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
ubuntu              latest              3556258649b2        3 weeks ago         64.2MB
busybox             latest              db8ee88ad75f        4 weeks ago         1.22MB

 

而当我们执行:

 

docker exec -it docker 进程 /bin/bash

进入到 docker 容器中,得到一个和宿主机一样的 shell 终端,和我们连接虚拟机得到的终端几乎没什么不同,我们还可以在终端中执行例如:

 

apt-get update 

亦或是安装各种命令。

等等,这似乎这就是一台虚拟机嘛。可是事实上真的是这样吗?

作为一名云计算行业的从业人员,我见过很多小白用户把容器当初虚拟机来使用,因为在他们看来启动一个容器比启动一个虚拟机要快很多,而且不需要手动去安装操作系统,在容器的终端中执行各种命令也和虚拟机没什么区别?这些初学者,他们会使用 Dockerfile 构建 Docker 镜像,也能够熟练的使用 Docker 的各种命令来管理容器。可是当遇到一些问题时,他们便束手无策,因为他们把容器镜像当成一个小型的操作系统来使用,什么东西都往容器放,导致容器的体积不断变大,导致一个容器镜像就高达到了几个 G,而对此他们便束手无策了,归其原因,还是因为他们并不了解 Docker。

今天我就跟大家聊一聊 Docker 到底是个什么东西。

 

640?wx_fmt=png

容器其实是一种特殊进程

 

容器其实本质上就是一个进程,只不过容器的进程是比较特殊的。

容器技术的核心功能,就是通过约束和修改进程的动态表现,创造出一个“边界”,通过“障眼法”让人觉得它是一个独立的系统。大多数容器都是使用 Cgroups 技术来约束进程,通过 Namespace 技术来修改进程的视图。

Namespace

什么是 Namespace ?

我们通过一个案例来讲解,这里我们使用 Docker 运行一个 busybox 的容器:

 

# docker run -it busybox /bin/sh
/ # ps
PID   USER     TIME  COMMAND
    1 root      0:00 /bin/sh
    6 root      0:00 ps
/ #

 

我们可以看到在容器中,只有 2 个进程在云信,一个 PID 为 1 的进程,它就是我们的 sh 程序。

我们再重新打开一个窗口执行执行 ps aux | grep docker:

 

# ps aux | grep docker
root      3490  0.0  5.7 843208 58032 ?        Ssl  Aug14   0:22 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
root     14669  0.0  6.3 709900 63612 pts/0    Sl+  14:36   0:00 docker run -it busybox /bin/sh

可以看到进程 14669 正在执行`docker run -it busybox /bin/sh`,事实上这个 14669 才是这个 Docker 容器的真正 PID。

那它是如何做到在我们 exec 进入容器之后把进程 ID 改成 1 的呢?事实上进程 ID=1 正是操作系统的第一号进程,它是所有进程的父进程。我们可以通过ps aux | grep systemd 查看会发现 systemd 为 1 号进行:

 

ps aux | grep systemd
root         1  0.0  0.7 225308  7828 ?        Ss   Jun20   3:12 /lib/systemd/systemd --system --deserialize 39

 

现在 Docker 把这个/bin/sh 的程序运行在容器中,就需要给这个 ID=14669 的进程做一些手脚,把它自己变成 1 号进程来骗过其他进程。这种机制就是对隔离应用的进程空间做了手脚,使得这些进程只能看到重新计算过的进程 ID,可是实际上它还是宿主机的 14669 号进程。

这种技术被称为 Namespace。

Namespace 其实是在创建新进程时候加了一个可选参数,它利用 Linux 的系统调用 clone() 为新创建的进程指定一个 CLONE_NEWPID 的参数,那么新创建的进程就会看到一个全新的进程空间,在这个进程空间里面它的 PID 就是 1。

Namespace 除了可以模拟 PID 之外,还提供了 Mout、UTS、IPC、Network 和 User 等,在不同的进程上下文做隔离操作。

而这些就是 Linux 容器的基本实现原理。

Cgroups

有了 Namespace ,使得容器就像一个沙盒一样看起来在一个独立的系统内运行,与宿主机互不干扰,可是,事实上只是 Namespace 隔离起来并不彻底。因为容器只是运行在宿主机上的一种特殊进程,所有的容器还是要共享宿主机的操作系统内核。并且有一些资源和对象是不能被隔离的,比如时间,如果你修改了容器的时间,那么操作系统的实际也会被修改。这显然不是我们希望看到的。另外使用 Namespace 并不能限制一个容器使用资源的边界,例如我们要限制一个容器使用的 CPU 资源或内存等,这些是 Namespace 无法做到的,它需要 Cgroup 来实现。那么什么是 Cgroup?

Cgroups 是Linux内核提供的一种可以限制单个进程或者多个进程所使用资源的机制,全称是 control groups,可以对 CPU、内存等资源实现精细化的控制。典型的子系统介绍如下:

  • cpu 子系统,主要限制进程的 CPU 使用率。

  • cpuacct 子系统,可以统计 Cgroups 中的进程的 CPU 使用报告。

  • cpuset 子系统,可以为 Cgroups 中的进程分配单独的 CPU 节点或者内存节点。

  • memory 子系统,可以限制进程的 memory 使用量。

  • blkio 子系统,可以限制进程的块设备 IO。

  • devices 子系统,可以控制进程能够访问某些设备。

  • net_cls 子系统,可以标记 Cgroups 中进程的网络数据包,然后可以使用 tc 模块(traffic control)对数据包进行控制。

  • freezer 子系统,可以挂起或者恢复 Cgroups 中的进程。

  • ns 子系统,可以使不同 Cgroups 下面的进程使用不同的 Namespace。

 

 

 

 

 

 

 

 

 

在 Linux 系统中,我们可以使用 mount 查看:

 

# mount -t cgroup
cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,name=systemd)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids)
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpu,cpuacct)
cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,net_cls,net_prio)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,hugetlb)
cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices)
cgroup on /sys/fs/cgroup/rdma type cgroup (rw,nosuid,nodev,noexec,relatime,rdma)
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset)
cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,perf_event)
cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer)

 

会看到输出一系列的系统目录和文件。我们可以看到在 /sys/fs/cgroup 中有我们上述的这些资源的名称所对应的子目录,也被称为子系统。

例如下面就是 CPU 的相关配置文件:

 

 cd /sys/fs/cgroup/cpu
/sys/fs/cgroup/cpu# ls
cgroup.clone_children  cpuacct.stat          cpuacct.usage_percpu_sys   cpu.cfs_period_us  docker             tasks
cgroup.procs           cpuacct.usage         cpuacct.usage_percpu_user  cpu.cfs_quota_us   notify_on_release  user.slice
cgroup.sane_behavior   cpuacct.usage_all     cpuacct.usage_sys          cpu.shares         release_agent
container              cpuacct.usage_percpu  cpuacct.usage_user         cpu.stat           system.slice

例如 cpu.cfs_period_us cpu.cfs_quota_us 这样的文件,它们就是用来限制 CPU 在一定时间内只能分配总量是多少的 CPU 时间。

举个例子,我们执行:

 

while : ; do : ; done &
[1] 10108

 

 

这是一个死循环,它能够把系统的 CPU 资源消耗到 100%,它的进程 ID 我们记住是 10108。

我们可以使用 top 命令来查看 CPU 的使用率已经高达 98.7:

 

top - 11:28:29 up 57 days,  1:47,  2 users,  load average: 0.23, 0.23, 0.21
Tasks:  99 total,   3 running,  63 sleeping,   0 stopped,   0 zombie
%Cpu(s): 98.7 us,  1.3 sy,  0.0 ni,  0.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem :  1006788 total,   185284 free,   458532 used,   362972 buff/cache
KiB Swap:   969964 total,   928280 free,    41684 used.   355496 avail Mem

我们接下来在 CPU 的 Cgroup 中创建一个 demo 的文件:

 

/sys/fs/cgroup/cpu# mkdir demo
/sys/fs/cgroup/cpu# cd demo/
/sys/fs/cgroup/cpu/demo# ls
cgroup.clone_children  cpuacct.usage         cpuacct.usage_percpu_sys   cpuacct.usage_user  cpu.shares         tasks
cgroup.procs           cpuacct.usage_all     cpuacct.usage_percpu_user  cpu.cfs_period_us   cpu.stat
cpuacct.stat           cpuacct.usage_percpu  cpuacct.usage_sys          cpu.cfs_quota_us    notify_on_release

查看cfs_quota_us 和 cfs_period_us 的默认值,cfs_period_us 默认是 100ms(100000us):

 

# cat cpu.cfs_quota_us
-1
/sys/fs/cgroup/cpu/demo# cat cpu.cfs_period_us 
100000

 

我们修改 cfs_quota_us 的值为 10000,设置为 10ms:

 

# echo 10000 > cpu.cfs_quota_us
/sys/fs/cgroup/cpu/demo# cat cpu.cfs_quota_us
10000

然后我们将进程 ID 写入 task:

 

/sys/fs/cgroup/cpu/demo# echo 10108 > tasks

此时我们再次执行 top 就可以发现 CPU 利用率瞬间降到了 30%以下,其他限制进程的资源使用的方法也类似。

这就是 Cgroup 技术的魅力所在,但是 Cgroup 并非万能,例如 /proc 目录下存储记录当前内核运行状态的一些文件,例如 top 等命令的的信息就来源于这里,如果此时你在容器中执行 top,会发现它显示的是宿主机的 CPU 个数以及内存大小等信息,这又不符合我们的预期,因为 /proc 文件系统不了解 Cgroup 限制的存在。

此时我们可以利用 LXCFS 来实现,LXCFS,FUSE filesystem for LXC是一个常驻服务,它启动以后会在指定目录中自行维护与上面列出的 /proc 目录中的文件同名的文件,容器从 lxcfs 维护的 /proc 文件中读取数据时,得到的是容器的状态数据,而不是整个宿主机的状态。

 

640?wx_fmt=png

rootfs

 

现在我们知道了,容器最核心的原理实际上就是为用户创建进程,然后启动 Linux 的 Namespace 和配置 Cgroup 参数为用户创建了一个隔离环境。

但是仅仅这样还是不够的,我们进入容器其实会发现我们是在一个完全与宿主机不同的目录结构当作,这其实是容器通过 chroot 切换了进程的根目录来实现的,容器在启动时候重新挂载了它的整个根目录,并且依赖于 Namespace 的 mount,这个挂载点对于宿主机来说是不可见的,因此我们在容器中的任何操作宿主机都无感知,在 rootfs 中包含了操作系统所需要的文件、配置和目录,但是并不包含内核。

事实上,容器无法实现启动自己的独立内核,它只能使用宿主机的内核。同时由于 rootfs 里面打包的至少应用和相关依赖。保证了容器的非常重要的一个特性,即一致性。

 

640?wx_fmt=png

镜像

 

容器的镜像体积一般都很小,但是很多稍微使用过容器的肯定会发现往容器里面写入一些文件,有时候可能这些文件很小,但是它会导致容器的体积变得非常大,这是为什么呢?

上面我们介绍了 rootfs,我们模拟了一个独立的系统根目录环境,那我们每次要更新应用软件,难道都需要按照上面的操作重新做一遍系统镜像吗?这个时候我们希望在制作 rootfs 的时候,能够以增量的方法去实现,即每次修改我只修改我需要修改的地方,而其他部分则保持和原来一样,而不是每次都全部fork 一次之前的操作。

事实上,Docker 公司在实现 Docker 时候就做类似的技术创建,他们引入了层的概念,即用户在制作镜像时的每一步操作都会生成一个层,也就是增量的 rootfs。通过 Union file System 即 UnionFS 可以将多个不同位置的目录联合挂载到同一个目录中,Docker 镜像的层分为只读层、可读写层和 init 层三部分组成。

只读层

这一层包含了整个底层操作系统所必须的一些目录和依赖,例如:

 

 

它们的挂载方式都是只读的(ro+wh,即 readonly+whiteout 的方式挂载。

可读写层

在这一层,它的挂载方式是 rw,在没写入文件之前,这个目录是空的。而一旦有了写操作,修改的内容就会以增量的方式出现在这层中。

如果要删除只读层的一个文件,AUFS 会在可读写层创建一个 whiteout 文件,把只读层的文件隐藏起来。

init 层

这一层用来存放一些例如 /etc/hosts、/etc/resolv.conf等信息,因为这部分内容往往是需要用户在启动容器时候写入一些指定的值,比如 hostname。所以就需要在可读写层对他们进行修改,而这些修改一般只对当前容器有效。用户在 commit 时候只会提交可读写层,并不包含 init 层的内容。

最后这些层被合并为一个目录下,组成了一个完整的操作系统供容器使用。

好了,以上就是给大家总结的容器技术的几个核心知识点。

【END】

如何在短时间内成为Python工程师?

https://edu.csdn.net/topic/python115?utm_source=csdn_bw

640?wx_fmt=jpeg

 热 文 推 荐 

程序员破解推荐系统瓶颈,带来超百亿收入增量!

☞ 双手无法敲代码的程序员,该如何编程?

Java 8 之后,还有哪些进化的功能?

10 步教你接手同事的代码!

☞亚马逊首席科学家李沐国内首次亲授「深度学习实训营」

CSDN & 火星财经, 联手发起Libra超级节点竞选!

"学了阿里中台,却依然做不好系统?" 聊聊阿里的项目管理

如何写出让同事无法维护的代码?

640?wx_fmt=gif点击阅读原文,输入关键词,即可搜索您想要的 CSDN 文章。

 

 

 

 

 

 
Logo

20年前,《新程序员》创刊时,我们的心愿是全面关注程序员成长,中国将拥有新一代世界级的程序员。20年后的今天,我们有了新的使命:助力中国IT技术人成长,成就一亿技术人!

更多推荐