你也许对Docker命令很熟悉,但是你最好还是阅读下本文。
Docker 用的有多普及本文就不赘述了。今天只谈docker,不谈OCI,不谈CRI,不谈kubelet。由简入深。
1. 背景
自从 2013 年 docker 发布之后,docker 项目逐渐成为了一个庞然大物。为了能够降低项目维护的成本,内部代码能够回馈社区,他们于 2014 年开源了 libcontainer,docker 公司将 libcontainer
的实现移动到 runC 并捐赠给了 OCI。在 2016 年,docker 开源并将 containerd 捐赠给了 CNCF。
2. Docker 的内部逻辑
现代 docker 启动一个标准化容器需要经历这样的流程:
我们实际看下是不是这样。
-
首先我们先看下Docker版本:
[root@VM_0_12_centos lmxia]# docker -v Docker version 18.09.9, build 039a7df9ba
-
看下docker 的服务状态:
[root@VM_0_12_centos lmxia]# systemctl status docker ● docker.service - Docker Application Container Engine Loaded: loaded (/etc/systemd/system/docker.service; enabled; vendor preset: disabled) Active: active (running) since 五 2020-05-08 10:52:00 CST; 6h ago Docs: https://docs.docker.com Main PID: 16196 (dockerd) Tasks: 39 Memory: 893.9M CGroup: /system.slice/docker.service ├─16196 /usr/bin/dockerd ├─16203 containerd --config /var/run/docker/containerd/containerd.toml --log-level warn
可以看到,docker服务的后台驻留进程是PID为16196的Dockerd。我在主机上启动了一个docker 容器,python3。我们用这个容器做一个观察。
-
查看Dockerd的进程树(部分):
[root@VM_0_12_centos lmxia]# pstree -up 16196 dockerd(16196)─┬─containerd(16203)─┬─containerd-shim(22785)─┬─python3(22803) │ │ ├─{containerd-shim}(22786) │ │ ├─{containerd-shim}(22787) │ │ ├─{containerd-shim}(22788) │ │ ├─{containerd-shim}(22789) │ │ ├─{containerd-shim}(22790) │ │ ├─{containerd-shim}(22791) │ │ ├─{containerd-shim}(22792) │ │ ├─{containerd-shim}(22793) │ │ ├─{containerd-shim}(22885) │ │ └─{containerd-shim}(22891) │ ├─{containerd}(16204) │ ├─{containerd}(16205) │ ├─{containerd}(16206) │ ├─{containerd}(16207)
-
接下来我们再找找这些二进制,docker cli 和 runc:
[root@VM_0_12_centos lmxia]# which docker /usr/bin/docker [root@VM_0_12_centos lmxia]# which runc /usr/bin/runc
-
我们也看下,安装docker的步骤:
$ sudo yum install docker-ce docker-ce-cli containerd.io
其中,docker-ce 是dockerd, docker-ce-cli 就是docker 命令的二进制,contained.io,包含了剩下的组件。
至此我们全部找到了内容1中所提到的组件,其中根据进程树来看,调用关系的确和图中描述的一致:
containerd 囊括了单机运行一个容器运行时所需要的一切:执行,分发,监控,网络,构建,日志等。为了能够支持多种 OCI Runtime,containerd 内部使用
containerd-shim
,每启动一个容器都会创建一个新的containerd-shim
进程,指定容器 ID,Bundle 目录,运行时的二进制(比如 runc)。
3.Docker 的背景知识
-
根文件系统
根文件系统是内核启动时所mount的第一个文件系统,内核代码映像文件保存在根文件系统中,而系统引导启动程序会在根文件系统挂载之后从中把一些基本的初始化脚本和服务等加载到内存中去运行。
根文件系统首先是一种文件系统,它不仅仅可以用来存储文件,提供系统启动和运行时所必须的目录和关键性配置文件,还可以为其他文件系统提供挂载。“/” 就是根文件系统的挂载点。
我们简单回顾下Linux 系统的启动过程:
- BIOS自检以及加载MBR(512字节的最后两个字节0x55aa标示的可引导分区)
- GRUB 操作系统引导程序,grub.conf是它的配置文件,里面需要指明,操作系统所在的磁盘号、分区号,内核镜像的存放路径,比如:/boot/kernel-**.gz.
- 内核启动,加载根文件系统。
- 不同的runlevel,init N。
-
Cgroup技术
Cgroup是linux内核支持的一种资源控制机制,我用人话来解释。
容器说白了就是一个进程,进程里的跑的东西,谁也管不着。不做特殊操作的情况下,进程可以吃掉系统里的所有资源,比如CPU跑满全部的核,耗尽全部的内存。现在我们希望限制这个进程的资源使用,Cgroup就是干这个的。那么它是怎么实现的呢?我们先了解下Cgroup的基本概念。
-
任务(task)
就是进程
-
控制组(cgroup)
就是一组按照某种标准划分的进程,这个标准,指的是,你希望如何限制你的这组进程去使用资源。
-
层级(hierarchy)
就是控制组组成的树状结构,具有继承特性。
-
子系统(subsysystem)
一个子系统就是一类资源控制器,比如 cpu 子系统就是控制 cpu 时间分配的一个控制器。子系统必须附加(attach)到一个层级上才能起作用。
相互关系
-
每次在系统中创建新层级时,该系统中的所有任务都是那个层级的默认 cgroup(我们称之为 root cgroup,此 cgroup 在创建层级时自动创建,后面在该层级中创建的 cgroup 都是此 cgroup 的后代)的初始成员;
-
一个子系统最多只能附加到一个层级;
-
一个层级可以附加多个子系统;
-
一个任务可以是多个 cgroup 的成员,但是这些 cgroup 必须在不同的层级;
-
系统中的进程(任务)创建子进程(任务)时,该子任务自动成为其父进程所在 cgroup 的成员。然后可根据需要将该子任务移动到不同的 cgroup 中,但开始时它总是继承其父任务的 cgroup。
有些一头雾水对么?别急,找一个linux主机去操作下就全明白了。
-
补充一句话,cgroup的开发团队,用VFS(虚拟文件系统)的方式,巧妙的表达了层级,并给我一个灵活的方式去操作task 和 cgroup。我们去实操一下。
1. 先看下你当前挂载的cgroup 文件系统
[root@VM_0_12_centos rootfs]# mount | grep cgroup
tmpfs on /sys/fs/cgroup type tmpfs (ro,nosuid,nodev,noexec,mode=755)
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/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,net_prio,net_cls)
cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,perf_event)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,hugetlb)
cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer)
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,cpuacct,cpu)
cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio)
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
我们看到/sys/fs/cgroup/cpu,cpuacct
这个目录,就属于两个子系统(cpu 和 cpuacct)附加到一个层级的例子,每个子系统也必须最多只能挂载到一个层级。linux系统习惯把不同的子系统,单独存放。cpu 和cpuacct,我们可以看到,其实就是两个软连接,link 到真实的同一个目录下。
[root@VM_0_12_centos ~]# ls -al /sys/fs/cgroup/
总用量 0
drwxr-xr-x 13 root root 340 4月 29 11:35 .
drwxr-xr-x 6 root root 0 5月 11 16:04 ..
drwxr-xr-x 6 root root 0 4月 29 11:35 blkio
lrwxrwxrwx 1 root root 11 4月 29 11:35 cpu -> cpu,cpuacct
lrwxrwxrwx 1 root root 11 4月 29 11:35 cpuacct -> cpu,cpuacct
drwxr-xr-x 7 root root 0 4月 29 11:35 cpu,cpuacct
drwxr-xr-x 5 root root 0 5月 11 11:10 cpuset
drwxr-xr-x 6 root root 0 4月 29 11:35 devices
drwxr-xr-x 5 root root 0 5月 11 11:10 freezer
drwxr-xr-x 5 root root 0 5月 11 11:10 hugetlb
drwxr-xr-x 6 root root 0 4月 29 11:35 memory
lrwxrwxrwx 1 root root 16 4月 29 11:35 net_cls -> net_cls,net_prio
drwxr-xr-x 5 root root 0 5月 11 11:10 net_cls,net_prio
lrwxrwxrwx 1 root root 16 4月 29 11:35 net_prio -> net_cls,net_prio
drwxr-xr-x 5 root root 0 5月 11 11:10 perf_event
drwxr-xr-x 6 root root 0 4月 29 11:35 pids
drwxr-xr-x 6 root root 0 4月 29 11:35 systemd
接下来我们用一个进程来展示下Cgroup如何帮助我们限制资源。
[root@VM_0_12_centos test]# while true;do :;done &
[1] 14546
[root@VM_0_12_centos test]# top
top - 15:46:10 up 12 days, 4:11, 1 user, load average: 0.54, 0.20, 0.11
Tasks: 91 total, 2 running, 89 sleeping, 0 stopped, 0 zombie
%Cpu(s): 51.5 us, 0.8 sy, 0.0 ni, 47.7 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem : 3880228 total, 116576 free, 256652 used, 3507000 buff/cache
KiB Swap: 0 total, 0 free, 0 used. 3333744 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
14546 root 20 0 116844 1888 268 R 100.0 0.0 0:32.57 bash
我们写了一个死循环,进程号是14546。CPU使用率100%。我们这个进程不在任何控制组里,所以没有限制他的cpu 使用。
接下来,我们在cpu这个子系统所 attach到的层级 /sys/fs/cgroup/cpu 里创建一个cgroup出来,用来限制下这个进程的cpu使用。
[root@VM_0_12_centos lmxia]# cd /sys/fs/cgroup/cpu
[root@VM_0_12_centos cpu]# mkdir test
[root@VM_0_12_centos cpu]# ls test/
cgroup.clone_children cgroup.procs cpuacct.usage cpu.cfs_period_us cpu.rt_period_us cpu.shares notify_on_release
cgroup.event_control cpuacct.stat cpuacct.usage_percpu cpu.cfs_quota_us cpu.rt_runtime_us cpu.stat tasks
默认已经帮我们继承了关于父cgroup的内容,这里继承过来的cpu的时间片配额为-1,表示不限制的意思。
[root@VM_0_12_centos test]# cat cpu.cfs_quota_us
-1
[root@VM_0_12_centos cpu]# echo 50000 > /sys/fs/cgroup/cpu/test/cpu.cfs_quota_us
[root@VM_0_12_centos test]# cat cpu.cfs_quota_us
50000
[root@VM_0_12_centos test]# cat cpu.cfs_period_us
100000
[root@VM_0_12_centos test]# echo 14546 >> /sys/fs/cgroup/cpu/test/tasks
[root@VM_0_12_centos test]# top
top - 15:46:56 up 12 days, 4:11, 1 user, load average: 1.11, 0.40, 0.18
Tasks: 89 total, 2 running, 87 sleeping, 0 stopped, 0 zombie
%Cpu(s): 25.5 us, 0.3 sy, 0.0 ni, 74.2 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem : 3880228 total, 118784 free, 254284 used, 3507160 buff/cache
KiB Swap: 0 total, 0 free, 0 used. 3336112 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
14546 root 20 0 116844 1888 268 R 50.2 0.0 1:12.91 bash
我们向这个cgroup里修改下配置,并且把14546这个进程号写入tasks文件里,这个时候发现这个进程所能使用的cpu马上被限制到了50%(50000 / 100000)。
Docker 做资源隔离也是这个思路。我们创建的容器都在这个目录下,可以看到docker目录,和我们自己的test目录,其中我们docker run 起来的容器都在docker目录下。根据容器的ID分不同的目录,原理是一样的。
[root@VM_0_12_centos cpu,cpuacct]# ls -al
总用量 0
drwxr-xr-x 7 root root 0 4月 29 11:35 .
drwxr-xr-x 13 root root 340 4月 29 11:35 ..
-rw-r--r-- 1 root root 0 4月 29 11:35 cgroup.clone_children
--w--w--w- 1 root root 0 4月 29 11:35 cgroup.event_control
-rw-r--r-- 1 root root 0 4月 29 11:35 cgroup.procs
-r--r--r-- 1 root root 0 4月 29 11:35 cgroup.sane_behavior
-r--r--r-- 1 root root 0 4月 29 11:35 cpuacct.stat
-rw-r--r-- 1 root root 0 4月 29 11:35 cpuacct.usage
-r--r--r-- 1 root root 0 4月 29 11:35 cpuacct.usage_percpu
-rw-r--r-- 1 root root 0 4月 29 11:35 cpu.cfs_period_us
-rw-r--r-- 1 root root 0 4月 29 11:35 cpu.cfs_quota_us
-rw-r--r-- 1 root root 0 4月 29 11:35 cpu.rt_period_us
-rw-r--r-- 1 root root 0 4月 29 11:35 cpu.rt_runtime_us
-rw-r--r-- 1 root root 0 4月 29 11:35 cpu.shares
-r--r--r-- 1 root root 0 4月 29 11:35 cpu.stat
drwxr-xr-x 3 root root 0 5月 11 14:43 docker
drwxr-xr-x 4 root root 0 4月 29 11:46 kubepods
-rw-r--r-- 1 root root 0 4月 29 11:35 notify_on_release
-rw-r--r-- 1 root root 0 4月 29 11:35 release_agent
drwxr-xr-x 58 root root 0 5月 11 14:43 system.slice
-rw-r--r-- 1 root root 0 4月 29 11:35 tasks
drwxr-xr-x 2 root root 0 5月 11 15:42 test
-
通过Runc直接部署容器:
-
创建容器的根文件系统
mkdir -p ~/hello/rootfs && cd ~/hello docker export $(docker create busybox) | tar -C rootfs -xvf -
那么在rootfs目录下,可以看到一个很常见的“根文件系统”结构:
[root@VM_0_12_centos rootfs]# ls -al 总用量 56 drwxr-xr-x 12 root root 4096 5月 9 15:15 . drwxr-xr-x 3 root root 4096 5月 11 17:50 .. drwxr-xr-x 2 root root 12288 4月 14 09:10 bin drwxr-xr-x 4 root root 4096 5月 9 15:15 dev -rwxr-xr-x 1 root root 0 5月 9 15:15 .dockerenv drwxr-xr-x 3 root root 4096 5月 9 15:15 etc drwxr-xr-x 2 nobody 65534 4096 4月 14 09:10 home drwxr-xr-x 2 root root 4096 5月 9 15:15 proc drwx------ 2 root root 4096 4月 14 09:10 root drwxr-xr-x 2 root root 4096 5月 9 15:15 sys drwxrwxrwt 2 root root 4096 4月 14 09:10 tmp drwxr-xr-x 3 root root 4096 4月 14 09:10 usr drwxr-xr-x 4 root root 4096 4月 14 09:10 var
-
接着我们利用
runc spec
产生默认的config.json,这是一个容器的启动配置文件,并启动它。[root@VM_0_12_centos hello]# runc spec [root@VM_0_12_centos hello]# runc run xlm / # ls bin dev etc home proc root sys tmp usr var
-
查看容器列表,执行一个exec命令
[root@VM_0_12_centos ~]# runc list ID PID STATUS BUNDLE CREATED OWNER xlm 19251 running /root/mycontainer 2020-05-11T09:50:07.882651059Z root [root@VM_0_12_centos mycontainer]# runc exec xlm top Mem: 3742884K used, 137344K free, 652K shrd, 147732K buff, 3115792K cached CPU: 0.0% usr 0.0% sys 0.0% nic 100% idle 0.0% io 0.0% irq 0.0% sirq Load average: 0.02 0.07 0.07 2/183 10 PID PPID USER STAT VSZ %VSZ CPU %CPU COMMAND 1 0 root S 1300 0.0 1 0.0 sh 6 0 root R 1292 0.0 0 0.0 top
runc exec xlm top 命令,不是一个容器的init 进程,所以只要给这个进程切换到xlm这个容器id所在的进程namespace即可。具体的代码部分的详细解释,我在下一篇文章里讲解。
-