一个程序运行起来后的计算机执行环境的总和,就是我们今天的主角:进程。一个在运行的Docker镜像,其实也是一个进程,下面请听我细细道来。
我们都知道,容器是一个类似"沙盒"的存在,我们的应用可以放到里面,一个系统启动多个"沙盒",但是它怎么样实现"沙盒"这个功能的呢?
好了不卖关子
这个沙盒就是通过约束和修改进程的动态表现,从而为其创造出一个“边界”。
对于 Docker 等大多数 Linux 容器来说,Cgroups 技术是用来制造约束的主要手段,而 Namespace 技术则是用来修改进程视图的主要方法。
我们先来说说Namespace到底是个什么鬼!
我们先启动一个容器Ubuntu;
>$ docker run -it busybox /bin/sh
/ #
在里面执行ps,你会发现神奇的事情;
/ # ps
PID USER TIME COMMAND
1 root 0:00 /bin/sh
10 root 0:00 ps
我们会发/bin/sh 的进程号 PID = 1 在linux中,1号进程是系统刚启动时创建的进程,负责从创建其它子进程。
这就意味着,前面执行的 /bin/sh,以及我们刚刚执行的 ps,已经被 Docker 隔离在了一个跟宿主机完全不同的"世界"当中。
这究竟是怎么做到的呢?
其实你退出系统来看会发现我们所启动的这个docker镜像就是一个进程,比如它的PID=100。但是为什么在容器中就是1呢?
这时候我们的Namespace就起到作用了。
它其实只是 Linux 创建新进程的一个可选参数。我们知道,在 Linux 系统中创建线程的系统调用是 clone(),比如:
int pid = clone(main_function, stack_size, SIGCHLD, NULL);
而当我们用 clone() 系统调用创建一个新进程时,就可以在参数中指定 CLONE_NEWPID 参数,比如:
int pid = clone(main_function, stack_size, CLONE_NEWPID | SIGCHLD, NULL);
严格说,clone()是线程操作,但linux 的线程是用进程实现的
这个系统调用就会为我们创建一个新的进程,并且返回它的进程号 PID。
这时,新创建的这个进程将会“看到”一个全新的进程空间,在这个进程空间里,它的 PID 是 1。之所以说“看到”,是因为这只是一个“障眼法”,在宿主机真实的进程空间里,这个进程的 PID 还是真实的数值,比如 100。
当然,我们还可以多次执行上面的 clone() 调用,这样就会创建多个 PID Namespace,而每个 Namespace 里的应用进程,都会认为自己是当前容器里的第 1 号进程,它们既看不到宿主机里真正的进程空间,也看不到其他 PID Namespace 里的具体情况。
而除了我们刚刚用到的 PID Namespace,Linux 操作系统还提供了 Mount、UTS、IPC、Network 和 User 这些 Namespace,用来对各种不同的进程上下文进行“障眼法”操作。比如,Mount Namespace,用于让被隔离进程只看到当前 Namespace 里的挂载点信息;Network Namespace,用于让被隔离进程看到当前 Namespace 里的网络设备和配置。
这,就是 Linux 容器最基本的实现原理了。
所以,Docker 容器这个听起来玄而又玄的概念,实际上是在创建容器进程时,指定了这个进程所需要启用的一组 Namespace 参数。这样,容器就只能“看”到当前 Namespace 所限定的资源、文件、设备、状态,或者配置。而对于宿主机以及其他不相关的程序,它就完全看不到了。
所以说,容器,其实是一种特殊的进程而已。
Docker 项目默认会为容器启用的Namespace :PID、 UTS、 network、 user、 mount、 IPC、cgroup;
容器和虚拟机的区别
这是一张虚拟机和容器的对比图,通过这张图我来分析一下容器和虚拟机的区别。
其实你会发现,虚拟机和Docker很类似只是实现方式不同,容器只是一个普通的进程,也就是我们刚刚说的那些,真正对隔离环境负责的是宿主机操作系统本身。而创建启动一个虚拟机就要消耗很多资源,对虚拟机进行管理的是虚拟机程序。
由此也就有了一个好处"敏捷",这应该是容器技术比较受欢迎的地方吧。
但是容器也有缺点,基于 Linux Namespace 的隔离机制相比于虚拟化技术也有很多不足之处,其中最主要的问题就是:隔离得不彻底。在虚拟机软件上面,我们可以创建Windows系统也可以创建linux系统。但是容器显然是不可以的,因为容器只是一个特殊的进程它还是和宿主机共用一个操作系统内核。
而且有很多资源和对象是不能被 Namespace 化的,最典型的例子就是:时间。
在容器里面改了时间宿主机的时间也会改变,这显然是不能接受的。
不过有基于虚拟化或者独立内核技术的容器实现,则可以比较好地在隔离与性能之间做出平衡。
好了说完虚拟机和容器,我们继续说容器的“限制”问题吧
刚刚我们已经说过了Namespace的存在,我们已经可以做到简单的隔离了。但是容器的真实进程PID = 100 还存在于宿主机里面,如果这个容器把系统资源(CPU、内存等)给用光、占用,其他的容器或进程怎么办呢。
这就要说到**“限制”**的问题了,这就要说到Linux Cgroups(Linux Control Group),它是Linux 内核用来为进程设置资源限制的一个重要功能。它最主要的作用,就是限制一个进程组能够使用的资源上限,包括 CPU、内存、磁盘、网络带宽等等。
那么让我们去看看它到底怎么使用吧
限制条件是写在文本里面的,在linux下的/sys/fs/cgroup 路径下(我用的CentOS,Ubuntu也一样)。
我可以用 mount 指令把它们展示出来:
$ mount -t cgroup
cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,hugetlb)
cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,perf_event)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio)
cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,net_prio,net_cls)
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset)
cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpuacct,cpu)
cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices)
cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer)
它的输出结果,是一系列文件系统目录。如果你在自己的机器上没有看到这些目录,那你就需要自己去挂载 Cgroups,具体做法可以自行 Google。
可以看到,在 /sys/fs/cgroup 下面有很多诸如 cpuset、cpu、 memory 这样的子目录,也叫子系统。这些都是我这台机器当前可以被 Cgroups 进行限制的资源种类。而在子系统对应的资源种类下,你就可以看到该类资源具体可以被限制的方法。
$ 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资源,我们可以从上面显示出的目录看到cpu.cfs_quota_us、cpu.cfs_period_us这两个文件,可以用来限制进程在长度为 cfs_period 的一段时间内,只能被分配到总量为 cfs_quota 的 CPU 时间。下面看个例子可能会更好理解。
我们现在这个目录下mkdir一个新的目录
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
这个目录就称为一个“控制组”。你会发现,操作系统会在你新创建的 container 目录下,自动生成该子系统对应的资源限制文件。
现在,我们在后台执行这样一条脚本:
$ while : ; do : ; done &
[1] 3473
这执行了一个死循环,会把单核CPU吃到100%。然后记住输出的进程号。
我们使用TOP确定一下CPU的使用率
$ top | grep 3473
3473 root 20 0 115556 640 172 R 100.0 0.0 0:32.50 bash
3473 root 20 0 115556 640 172 R 99.7 0.0 0:35.51 bash
3473 root 20 0 115556 640 172 R 100.0 0.0 0:38.53 bash
3473 root 20 0 115556 640 172 R 100.0 0.0 0:41.54 bash
好了确定完之后 ,我要去对它做一些限制了
$ cat /sys/fs/cgroup/cpu/container/cpu.cfs_quota_us
-1
$ cat /sys/fs/cgroup/cpu/container/cpu.cfs_period_us
100000
这是这两个文件里面默认的值,第二个文件里面的数值是100 ms(100000 us)
然后我们改一下这个改为20ms
$ echo 20000 > /sys/fs/cgroup/cpu/container/cpu.cfs_quota_us
这就以为这只能使用 20 ms 的 CPU 时间,也就是说这个进程只能使用到 20% 的 CPU 带宽。因为原来的时间是100ms使用率是100我们没改使用率。
然后把进程号写入tasks文件就会对该进程生效了
echo 3473 > /sys/fs/cgroup/cpu/container/tasks
再来查一下
$ top | grep 3473
3473 root 20 0 115556 640 172 R 100.0 0.0 0:32.50 bash
3473 root 20 0 115556 640 172 R 99.7 0.0 0:35.51 bash
3473 root 20 0 115556 640 172 R 100.0 0.0 0:38.53 bash
3473 root 20 0 115556 640 172 R 100.0 0.0 0:41.54 bash
可以看到已经生效了,可以执行尝试。
除 CPU 子系统外,Cgroups 的每一个子系统都有其独有的资源限制能力,比如:
- blkio,为块设备设定I/O 限制,一般用于磁盘等设备;
- cpuset,为进程分配单独的 CPU 核和对应的内存节点;
- memory,为进程设定内存使用的限制。
Linux Cgroups 的设计还是比较易用的,简单粗暴地理解呢,它就是一个子系统目录加上一组资源限制文件的组合。
下面我们启动一个容器来试一下
$ docker run -it -d --cpu-period=100000 --cpu-quota=20000 ubuntu /bin/bash
7e6fcd478ba2
启动命令加了资源限制条件
$ pwd
/sys/fs/cgroup/cpu/docker/7e6fcd478ba257af191e5804ee3ca0467cfd0dab4d6c91d0483f6aeb24ff46db
$ cat cpu.cfs_period_us
100000
$ cat cpu.cfs_quota_us
20000
这就意味着这个 Docker 容器,只能使用到 20% 的 CPU 带宽。
好了Cgroups也说完了
当然Cgroups 跟 Namespace 的情况类似也有不完美的地方,提及最多的自然是 /proc 文件系统的问题。
Linux 下的 /proc 目录存储的是记录当前内核运行状态的一系列特殊文件,用户可以通过访问这些文件,查看系统以及当前正在运行的进程的信息,比如 CPU 使用情况、内存占用率等,这些文件也是 top 指令查看系统信息的主要数据来源。
但是,你如果在容器里执行 top 指令,就会发现,它显示的信息居然是宿主机的 CPU 和内存数据,而不是当前容器的数据。造成这个问题的原因就是,/proc 文件系统并不知道用户通过 Cgroups 给这个容器做了什么样的资源限制,即:/proc 文件系统不了解 Cgroups 限制的存在。
在生产环境中,这个问题必须进行修正,否则应用程序在容器里读取到的 CPU 核数、可用内存等信息都是宿主机上的数据,这会给应用的运行带来非常大的困惑和风险。这也是在企业中,容器化应用碰到的一个常见问题,也是容器相较于虚拟机另一个不尽如人意的地方。