你也許對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即可。具體的代碼部分的詳細解釋,我在下一篇文章裏講解。
-