容器其實是一種沙盒技術。顧名思義,沙盒就是能夠像一個集裝箱一樣,把你的應用“裝”起來的技術。這樣,應用與應用之間,因為有了邊界而不至于相互干擾;而被裝進集裝箱的應用,也可以被方便地搬來搬去,這也是 PaaS 最理想的狀態(tài)。
計算機的程序和進程初解
在將Docker的實現(xiàn)原理之前我們先來聊一聊計算機的程序。
計算機的程序歸根結底來說就是一組計算機能識別和執(zhí)行的指令,運行于電子計算機上,滿足人們某種需求的信息化工具。
由于計算機只認識 0 和 1,所以無論用哪種語言編寫這段代碼,最后都需要通過某種方式翻譯成二進制文件,才能在計算機操作系統(tǒng)中運行起來。而為了能夠讓這些代碼正常運行,我們往往還要給它提供數(shù)據(jù),比如一個加法程序需要提供一個輸入或輸入文件。這些數(shù)據(jù)加上代碼本身的二進制文件,放在磁盤上,就是我們平常所說的一個“程序”,也叫代碼的可執(zhí)行鏡像(executable image)。
執(zhí)行程序時,首先,操作系統(tǒng)從“程序”中發(fā)現(xiàn)輸入數(shù)據(jù)保存在一個文件中,所以這些數(shù)據(jù)就會被加載到內(nèi)存中待命。同時,操作系統(tǒng)又讀取到了計算加法的指令,這時,它就需要指示 CPU 完成加法操作。而 CPU 與內(nèi)存協(xié)作進行加法計算,又會使用寄存器存放數(shù)值、內(nèi)存堆棧保存執(zhí)行的命令和變量。同時,計算機里還有被打開的文件,以及各種各樣的 I/O 設備在不斷地調(diào)用中修改自己的狀態(tài)。
一旦“程序”被執(zhí)行起來,它就從磁盤上的二進制文件,變成了計算機內(nèi)存中的數(shù)據(jù)、寄存器里的值、堆棧中的指令、被打開的文件,以及各種設備的狀態(tài)信息的一個集合。這樣一個程序運行起來后的計算機執(zhí)行環(huán)境的總和,我們稱之為:進程
Docker的實現(xiàn)
回過頭來我們來看,虛擬機和容器,無論是虛擬機還是容器都可以理解是在做虛擬化(也就是一臺機器當做多臺來使用),虛擬化核心需要解決的問題:資源隔離與資源限制
虛擬機硬件虛擬化技術, 通過一個 hypervisor 層和獨立的Guest OS實現(xiàn)對資源的徹底隔離。
-
容器技術的核心功能,就是通過約束和修改進程的動態(tài)表現(xiàn),從而制造一個邊界。
- 邊界的實現(xiàn)方式是利用內(nèi)核的 Cgroup 和 Namespace 特性,此功能完全通過軟件實現(xiàn)。
這里雖然容器繼續(xù)也通過Namesapce和Cgroup實現(xiàn)了隔離,但它也有其弊端:隔離的不徹底
盡管可以在容器里通過 Mount Namespace 單獨掛載其他不同版本的操作系統(tǒng)文件,比如 CentOS 或者 Ubuntu,但這并不能改變共享宿主機內(nèi)核的事實。
隔離機制
首先,我這里使用centos7系統(tǒng)先嘗試啟動一個容器,去觀察它一下:
[root@cluster1 ~]$ docker run -it busybox /bin/sh
/ $ ps aux
PID USER TIME COMMAND
1 root 0:00 /bin/sh
7 root 0:00 ps aux
這里可以看到一個比較有趣的事情,我們?nèi)ゲ榭催M程時,我在容器中運行的/bin/sh進程ID變?yōu)榱恕?”,熟悉centos的人都知道進程為“1”的應該是systemd程序,而不是我運行的/bin/sh,這其實就是Docker對我們運行的進程使用了障眼法,對被隔離的應用的進程空間做了手腳,使得這個進程只能看到重新計算過的進程編號,比如 PID=1。這其實就是Linux當中的Namespace機制,這里的PID namespace被隔離后就會呈現(xiàn)這樣的效果。
除此以外,Linux還提供了Mount、UTS、IPC、Network 和 User 這些 Namespace,用來對各種不同的進程上下文進行“障眼法”操作。
這就是Docker對于隔離機制的實現(xiàn)。下面我來深入的了解一下
| namesapce 名稱 | 隔離的資源 |
|---|---|
| Mount | Linux 內(nèi)核實現(xiàn)的第一個 Namespace,Mount points(文件系統(tǒng)掛載點) |
| IPC | System V IPC(信號量、消息隊列、共享內(nèi)存) 和POSIX MESSAGE QUEUES |
| Network | Network devices、stacks、ports(網(wǎng)絡設備、網(wǎng)絡棧、端口等) |
| PID | Process IDs(進程編號) |
| User | User and Groups IDs(用戶和用戶組) |
| UTS | Hostname and NIS domain name(主機名與NIS域名) |
同樣的道理,我們?nèi)ビ^察一個運行的docker容器,然后手動的去模擬實現(xiàn)它的隔離,為了方便理解,先確認宿主機存在的內(nèi)容,并且制造一些可以用來區(qū)別的數(shù)據(jù)
[root@cluster1 ~]$ touch /root/host.txt
# 宿主機上制作一個掛載點
[root@cluster1 ~]$ mkdir /tmp/tmpfs
[root@cluster1 ~]$ mount -t tmpfs -o size=20m tmpfs /tmp/tmpfs
[root@cluster1 ~]$ df -h /tmp/tmpfs/
Filesystem Size Used Avail Use% Mounted on
tmpfs 20M 0 20M 0% /tmp/tmpfs
# 查看宿主機主機名
[root@cluster1 ~]$ hostname
cluster1
# 查看宿主機進程
[root@cluster1 ~]$ ps aux | head
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.2 191056 3960 ? Ss 21:30 0:01 /usr/lib/systemd/systemd --switched-root --system --deserialize 22
root 2 0.0 0.0 0 0 ? S 21:30 0:00 [kthreadd]
root 4 0.0 0.0 0 0 ? S< 21:30 0:00 [kworker/0:0H]
root 6 0.0 0.0 0 0 ? S 21:30 0:00 [ksoftirqd/0]
root 7 0.0 0.0 0 0 ? S 21:30 0:00 [migration/0]
....
# 為了驗證IPC namespace,需要創(chuàng)建一個系統(tǒng)間通信隊列
# ipcmk -Q 命令:用來創(chuàng)建系統(tǒng)間通信隊列。
# ipcs -q 命令:用來查看系統(tǒng)間通信隊列列表。
[root@cluster1 ~]$ ipcmk -Q
[root@cluster1 ~]$ ipcs -q
------ Message Queues --------
key msqid owner perms used-bytes messages
0x48d07ddf 0 root 644 0 0
# 查看宿主機用戶數(shù)量個數(shù)
[root@cluster1 ~]$ cat /etc/passwd | wc -l
20
# 查看宿主機網(wǎng)絡
[root@cluster1 ~]$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: ens33: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
link/ether 00:0c:29:88:77:6c brd ff:ff:ff:ff:ff:ff
inet 10.10.1.100/24 brd 10.10.1.255 scope global noprefixroute ens33
valid_lft forever preferred_lft forever
inet6 fe80::20c:29ff:fe88:776c/64 scope link
valid_lft forever preferred_lft forever
3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:b7:30:58:e4 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
valid_lft forever preferred_lft forever
inet6 fe80::42:b7ff:fe30:58e4/64 scope link
valid_lft forever preferred_lft forever
# 觀察宿主機內(nèi)核
[root@cluster1 ~]$ uname -a
Linux cluster1 3.10.0-1127.el7.x86_64 #1 SMP Tue Mar 31 23:36:51 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
下面開始觀察docker容器
# 啟動一個容器
[root@cluster1 ~]$ docker run -it busybox /bin/sh
# 完全獨立的目錄(非宿主機目錄)
/ $ ls /
bin dev etc home lib lib64 proc root sys tmp usr var
/ $ ls /root
/ $ df -h /tmp/tmpfs/
Filesystem Size Used Available Use% Mounted on
df: /tmp/tmpfs/: can't find mount point
# 獨立的主機名
/# hostname
d87b4acb91fc
# 獨立的進程
/$ ps aux
PID USER TIME COMMAND
1 root 0:00 /bin/sh
7 root 0:00 ps aux
# 獨立的系統(tǒng)通信隊列
/$ ipcs -q
------ Message Queues --------
key msqid owner perms used-bytes messages
# 獨立的用戶
/$ cat /etc/passwd | wc -l
9
# 獨立的網(wǎng)絡
/$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
8: eth0@if9: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue
link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
# 相同的宿主機內(nèi)核
/$ uname -a
Linux d87b4acb91fc 3.10.0-1127.el7.x86_64 #1 SMP Tue Mar 31 23:36:51 UTC 2020 x86_64 GNU/Linux
OK,現(xiàn)在驗證了我們上面的觀點,容器是被沙河隔離的,下面我們再來看看是如何實現(xiàn)的?
我們進行一下,用namespace模擬這個過程,看看我們能否達到docker的效果
首先我們先來了解一個命令—unshare
作用:一個用來取消與父進程共享指定的命名空間
[root@cluster1 ~]# unshare --help
Usage: unshare [options] <program> [<argument>...]
Run a program with some namespaces unshared from the parent.
Options: -m, --mount unshare mounts namespace -u, --uts unshare UTS namespace (hostname etc) -i, --ipc unshare System V IPC namespace -n, --net unshare network namespace -p, --pid unshare pid namespace -U, --user unshare user namespace -f, --fork fork before launching <program> --mount-proc[=<dir>] mount proc filesystem first (implies --mount) -r, --map-root-user map current user to root (implies --user) --propagation <slave|shared|private|unchanged> modify mount propagation in mount namespace
# 我們通過unshare模擬容器的創(chuàng)建
# CentOS7 默認允許創(chuàng)建的 User Namespace 為 0
# 所以需要先echo 65535 > /proc/sys/user/max_user_namespaces 打開限制
[root@cluster1 ~]$ echo 65535 > /proc/sys/user/max_user_namespaces
[root@cluster1 ~]$ unshare --mount --pid --mount-proc --uts --ipc --user -r --net --fork /bin/bash
# 沒有獨立的目錄(獨立思考一下,后面會講)
[root@cluster1 ~]$ ls /root/host.txt
/root/host.txt
# 沒有獨立的mount掛載(獨立思考一下,后面會講)
[root@cluster1 ~]$ df -h /tmp/tmpfs/
Filesystem Size Used Avail Use% Mounted on
tmpfs 20M 0 20M 0% /tmp/tmpfs
# 看起來沒有獨立的主機名
[root@cluster1 ~]$ hostname
cluster1
# 獨立的進程
[root@cluster1 ~]$ ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.1 116204 2912 pts/1 S 22:58 0:00 /bin/bash
root 25 0.0 0.0 155472 1852 pts/1 R+ 23:00 0:00 ps aux
# 獨立的系統(tǒng)通信隊列
[root@cluster1 ~]$ ipcs -q
------ Message Queues --------
key msqid owner perms used-bytes messages
# 獨立的網(wǎng)絡
[root@cluster1 ~]$ ip a
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
# 看起來沒有獨立的用戶
[root@cluster1 ~]$ cat /etc/passwd | wc -l
20
從上邊論證,我們還缺乏幾個沒有隔離的掛載點、主機名、目錄和用戶,我們接著分析
# 我們在執(zhí)行'unshare --mount --pid --mount-proc --uts --ipc --user -r --net --fork /bin/bash' 之后的終端繼續(xù)驗證
# 嘗試修改主機名
$ hostname -b yijiuweishu
$ hostname
yijiuweishu
# 新增掛載點
$ mkdir /tmp/containerfs
$ mount -t tmpfs -o size=20m tmpfs /tmp/containerfs
$ df -h /tmp/containerfs/
Filesystem Size Used Avail Use% Mounted on
tmpfs 20M 0 20M 0% /tmp/containerfs
在新開啟一個終端,去查看宿主機的主機名和掛載點
[root@cluster1 ~]$ hostname
cluster1
[root@cluster1 ~]$ df -h /tmp/containerfs/
Filesystem Size Used Avail Use% Mounted on
/dev/sda2 49G 2.4G 47G 5% /
可以看到實際上,主機名和掛載點也是獨立的,只不過剛取消共享那一刻,我們不做操作,主機名和掛載點是和宿主機一致的。
再去驗證user namespace
User Namespace 主要是用來隔離用戶和用戶組的。
一個比較典型的應用場景就是在主機上以非 root 用戶運行的進程可以在一個單獨的 User Namespace 中映射成 root 用戶。使用 User Namespace 可以實現(xiàn)進程在容器內(nèi)擁有 root 權限,而在主機上卻只是普通用戶。
而不是以root用戶映射成root用戶,這樣映射后還是有root的權限
# 我們體驗下用普通用戶去取消共享user namespace
[root@cluster1 ~]$ useradd test
[root@cluster1 ~]$ su - test
Last login: Fri Mar 17 23:08:56 CST 2023 on pts/2
[test@cluster1 ~]$ unshare --user -r --fork /bin/bash
# 可以看到,雖然我們變成了root用戶,但是并不能調(diào)用一些root能執(zhí)行的命令,由此可以推斷我們是進行了用戶隔離
[root@cluster1 ~]$ reboot
Failed to open /dev/initctl: Permission denied
Failed to talk to init daemon.
再來說一下我們看見的還是宿主機目錄,這里會引申出一個新的概念
根文件系統(tǒng)rootfs
根文件系統(tǒng)首先是內(nèi)核啟動時所mount(掛載)的第一個文件系統(tǒng),內(nèi)核代碼映像文件保存在根文件系統(tǒng)中,而系統(tǒng)引導啟動程序會在根文件系統(tǒng)掛載之后從中把一些基本的初始化腳本和服務等加載到內(nèi)存中去運行。
根文件系統(tǒng)一般叫做rootfs,這里所謂的文件系統(tǒng)并不是指FAT、FAT32、NTFS、XFS這類的文件系統(tǒng),熟悉Linux的人都知道,Linux一切皆文件,且目錄結構都在 /下開始,這里的根文件系統(tǒng)指的就是/。
namespace雖然解決了共享問題,但沒有改變?nèi)萜鞯母夸洠匀萜髦惺峭ㄟ^rootfs來實現(xiàn)的。
切換新的根目錄,我們需要一個根文件系統(tǒng)(有自己的/bin、/dev、/sys、/proc 等)
# 獲取一個根文件系統(tǒng)
# 方式一
$ wget https://github.com/ericchiang/containers-from-scratch/releases/download/v0.1.0/rootfs.tar.gz
# 方式二(docker每個容器也有自己的根文件系統(tǒng),我們拷貝一份)
$ docker run -d -i busybox /bin/sh
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
6bd3747bc339 busybox "/bin/sh" 3 minutes ago Up 3 minutes heuristic_napier
$ docker export 6bd3747bc339 -o busybox.tar
$ tar xf busybox.tar
$ ls
bin busybox.tar dev etc home lib lib64 proc root sys tmp usr var
chroot 隔離
chroot是在 Unix 和 Linux 系統(tǒng)的一個操作,針對正在運作的軟件行程和它的子進程,改變的根目錄。使它不能對改變后的目錄之外的訪問(讀寫、查看)
- chroot 是通過指定 新的根目錄 和運行的命令組成,所以這意味著新的根目錄中也要有可執(zhí)行的命令和層次結構
# chroot NEWROOT [COMMAND [ARG]...]
# 注意 此處的COMMAND需要是新的根目錄中存在的
[root@cluster1 ~]$ pwd
/root
[root@cluster1 ~]$ mkdir rootfs
[root@cluster1 ~]$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
6bd3747bc339 busybox "/bin/sh" 3 minutes ago Up 3 minutes heuristic_napier
[root@cluster1 ~]$ docker export 6bd3747bc339 -o busybox.tar
# 將busybox的根文件系統(tǒng) 放到/root/rootfs/下
[root@cluster1 ~]$ tar xf busybox.tar -C rootfs/
# 切換根目錄
[root@cluster1 ~]$ chroot rootfs/ /bin/bash
# 這表示busybox中,不存在bash終端
chroot: failed to run command ‘/bin/bash’: No such file or director
[root@cluster1 ~]$ chroot rootfs/ /bin/sh
/ # /bin/ls
bin dev etc home lib lib64 proc root sys tmp usr var
/ # /bin/ls /root
/ #
pivot_root隔離
pivot_root把當前進程的root文件系統(tǒng)放在put_old目錄,而使new_root成為新的root文件系統(tǒng)
new_root 與 put_old 必須是文件夾
new_root文件夾必須是一個掛載點 ,并且new_root文件夾里面有完整rootfs的各種文件
new_root 文件夾掛載應該是一個與主機不同的namespace
put_old文件夾必須在new_root文件夾內(nèi)
這從某方面也解釋了,為什么有時候docker run一個容器后,宿主機會多一個掛載點(/var/lib/docker/overlay2/xxx/merged)
- pviot_root主要是把整個系統(tǒng)切換到一個新的root目錄,然后去掉對之前root文件系統(tǒng)的依賴,以便于可以umount 之前的文件系統(tǒng)(pivot_root需要root權限)
- chroot是只改變即將運行的某進程的根目錄,而系統(tǒng)的其他部分依舊依賴于老的root文件系統(tǒng)
在Docker中,會優(yōu)先調(diào)用pivot_root,如果系統(tǒng)不支持才會使用 chroot
# 宿主機/root 目錄
[root@cluster1 ~]$ ls
anaconda-ks.cfg busybox.tar host.txt rootfs
[root@cluster1 ~]$ unshare --mount --fork /bin/bash #需要有獨立的命名空間
[root@cluster1 ~]$ mkdir /new_root
[root@cluster1 ~]$ mount -t tmpfs mytmpfs /new_root #new_root是一個獨立的掛載點
[root@cluster1 ~]$ mkdir /new_root/old_root # 可以是任意名字,
[root@cluster1 ~]$ tar xf busybox.tar -C /new_root
# busybox文件系統(tǒng)的/root
[root@cluster1 ~]$ ls /new_root/root
[root@cluster1 ~]$ touch /new_root/root/container.txt
[root@cluster1 ~]$ ls /new_root/root/
container.txt
# pivot_root 切換根文件系統(tǒng)
[root@cluster1 ~]$ pivot_root /new_root /new_root/old_root
[root@cluster1 ~]$ ls
bash: /usr/bin/ls: No such file or directory # 沒有環(huán)境變量了
[root@cluster1 ~]$ /bin/ls /root/
container.txt
從另一個角度驗證
Linux中每個進程都會記錄其所使用的namespace(一串id),下面我們來找一找
我們要知道一個環(huán)境變量, $$ 用于表示當前的終端所在的進程ID
# 首先查看當前系統(tǒng)所用的namespace
[root@cluster1 ~]$ echo $$
5210
[root@cluster1 ~]$ ls -l /proc/$$/ns
total 0
lrwxrwxrwx 1 root root 0 Mar 18 00:22 ipc -> ipc:[4026531839]
lrwxrwxrwx 1 root root 0 Mar 18 00:22 mnt -> mnt:[4026531840]
lrwxrwxrwx 1 root root 0 Mar 18 00:22 net -> net:[4026531956]
lrwxrwxrwx 1 root root 0 Mar 18 00:22 pid -> pid:[4026531836]
lrwxrwxrwx 1 root root 0 Mar 18 00:22 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0 Mar 18 00:22 uts -> uts:[4026531838]
# 查看一個容器所用的namesapce
[root@cluster1 ~]$ docker run -it busybox /bin/sh
/ # echo $$
1
/ # ls -l /proc/1/ns/
total 0
lrwxrwxrwx 1 root root 0 Mar 17 16:23 ipc -> ipc:[4026532700]
lrwxrwxrwx 1 root root 0 Mar 17 16:23 mnt -> mnt:[4026532698]
lrwxrwxrwx 1 root root 0 Mar 17 16:23 net -> net:[4026532703]
lrwxrwxrwx 1 root root 0 Mar 17 16:23 pid -> pid:[4026532701]
lrwxrwxrwx 1 root root 0 Mar 17 16:23 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0 Mar 17 16:23 uts -> uts:[4026532699]
# 再來試試我們手動實現(xiàn)的namesapce 隔離,是否生效
[root@cluster1 ~]$ unshare --mount --pid --mount-proc --uts --ipc --user -r --net --fork /bin/bash
[root@cluster1 ~]$ ls -l /proc/$$/ns
total 0
lrwxrwxrwx 1 root root 0 Mar 18 00:25 ipc -> ipc:[4026532699]
lrwxrwxrwx 1 root root 0 Mar 18 00:25 mnt -> mnt:[4026532697]
lrwxrwxrwx 1 root root 0 Mar 18 00:25 net -> net:[4026532702]
lrwxrwxrwx 1 root root 0 Mar 18 00:25 pid -> pid:[4026532700]
lrwxrwxrwx 1 root root 0 Mar 18 00:25 user -> user:[4026532633]
lrwxrwxrwx 1 root root 0 Mar 18 00:25 uts -> uts:[4026532698]
從這個角度看,我們手動
unshare的也都是獨立的namespace,也論證了我們的觀點。當然Docker可不僅僅只是創(chuàng)建了namesapce,實際上還做了很多細節(jié)上的處理
Cgroup資源限制
通過namespace可以保證容器之間的隔離,但是無法控制每個容器可以占用多少資源, 如果其中的某一個容器正在執(zhí)行 CPU 密集型的任務,那么就會影響其他容器中任務的性能與執(zhí)行效率,導致多個容器相互影響并且搶占資源。如何對多個容器的資源使用進行限制就成了解決進程虛擬資源隔離之后的主要問題。

什么是Cgroup?
Control Groups(簡稱 CGroups)就是能夠隔離宿主機器上的物理資源,例如 CPU、內(nèi)存、磁盤 I/O 和網(wǎng)絡帶寬。每一個 CGroup 都是一組被相同的標準和參數(shù)限制的進程。而我們需要做的,其實就是把容器這個進程加入到指定的Cgroup中。

類似如上的圖片表示,創(chuàng)建了 2 個 cgroup(每個 cgroup 有 4 個進程),并且限制它們各自最多只能使用 2GB 的內(nèi)存。如果使用超過 2GB 的內(nèi)存,那么將會觸發(fā) OOM(Out Of Memory) 。
cgroup原理和用法并不復雜,但其內(nèi)核數(shù)據(jù)結構特別復雜,錯綜復雜的數(shù)據(jù)結構感覺才是cgroup真正的難點,了解即可。
感興趣可以看看:https://github.com/dongzhiyan-stack/kernel-code-comment/blob/master/linux-3.10.96/kernel/cgroup.c
Cgroup的特點
cgroups的API以一個偽文件系統(tǒng)的方式實現(xiàn),用戶態(tài)的程序可以通過文件操作實現(xiàn)cgroups 的組織
cgroups的組織管理操作單元可以細粒到線程級別,用戶可以創(chuàng)建銷毀cgroups,從而實現(xiàn)資源再分配管理
所有資源管理的功能都以子系統(tǒng)方式實現(xiàn),接口統(tǒng)一
子任務創(chuàng)建之初與其父進程處于同一個cgroups的控制組
Cgroup的作用
| 作用 | 說明 |
|---|---|
| 資源限制 | cgroups可以對任務使用的資源總額進行限制,如設定應用運行時使用的內(nèi)存上限,一但超過這個配額就發(fā)出OOM提示 |
| 優(yōu)先級分配 | 通過分配的CPU時間片數(shù)量及磁盤IO帶寬大小,實際就相當于控制了任務運行的優(yōu)先級 |
| 資源統(tǒng)計 | cgroups 可以統(tǒng)計系統(tǒng)的資源使用量,如CPU時長,內(nèi)存使用量,這個功能非常適用于計費 |
| 任務控制 | cgroups可以對任務執(zhí)行掛起 |
Cgroup的術語
| 術語 | 說明 |
|---|---|
| task(任務) | 在cgroups的術語中,任務表示系統(tǒng)的一個進程或者線程 |
| cgroups(控制組) | cgroups中的資源控制都以cgroups以單位實現(xiàn),cgroups表示按某種資源控制標準劃分而成的任務組,包含一個或多個子系統(tǒng),一個任務加入某個cgroups,也可以從某個cgroups簽到另一cgroups |
| subsystem(子系統(tǒng)) | cgroups中的子系統(tǒng)就是一個資源調(diào)度控制器。CPU子系統(tǒng)可以控制從CPU時間分配,內(nèi)存子系統(tǒng)可以設置cgroups的內(nèi)存使用量 |
| hierarchy(層級) | 層級由一系列cgroups以一個樹狀結構排列而成,每個層級通過綁定對應的子系統(tǒng)進行資源控制。層級中cgroups的節(jié)點可以包含零個節(jié)點或多個子節(jié)點,子節(jié)點繼承父節(jié)點掛載的子系統(tǒng)。整個操作系統(tǒng)可以有多個層級 |
Cgroup子系統(tǒng)
一個子系統(tǒng)代表一類資源調(diào)度控制器。例如內(nèi)存子系統(tǒng)可以限制內(nèi)存的使用量,CPU 子系統(tǒng)可以限制 CPU 的使用時間。子系統(tǒng)是真正實現(xiàn)某類資源的限制的基礎。

Subsystem(子系統(tǒng)) cgroups 中的子系統(tǒng)就是一個資源調(diào)度控制器(又叫 controllers)
最終在Linux表現(xiàn)其實是一個文件系統(tǒng)
下面我們在Centos7上查看一下Cgroup的子系統(tǒng)(Centos7 默認systemd也會使用cgroup)
# 控制cgroup可以使用命令工具行,lssubsys
[root@cluster1 ~]$ yum install -y libcgroup-tools
[root@cluster1 ~]$ lssubsys -m
cpuset /sys/fs/cgroup/cpuset
cpu,cpuacct /sys/fs/cgroup/cpu,cpuacct
memory /sys/fs/cgroup/memory
devices /sys/fs/cgroup/devices
freezer /sys/fs/cgroup/freezer
net_cls,net_prio /sys/fs/cgroup/net_cls,net_prio
blkio /sys/fs/cgroup/blkio
perf_event /sys/fs/cgroup/perf_event
hugetlb /sys/fs/cgroup/hugetlb
pids /sys/fs/cgroup/pids
# 通過查看文件也是一樣
[root@cluster1 ~]$ ll /sys/fs/cgroup/
total 0
drwxr-xr-x 2 root root 0 Mar 18 11:57 blkio
lrwxrwxrwx 1 root root 11 Mar 18 11:57 cpu -> cpu,cpuacct
lrwxrwxrwx 1 root root 11 Mar 18 11:57 cpuacct -> cpu,cpuacct
drwxr-xr-x 2 root root 0 Mar 18 11:57 cpu,cpuacct
drwxr-xr-x 2 root root 0 Mar 18 11:57 cpuset
drwxr-xr-x 3 root root 0 Mar 18 12:01 devices
drwxr-xr-x 2 root root 0 Mar 18 11:57 freezer
drwxr-xr-x 2 root root 0 Mar 18 11:57 hugetlb
drwxr-xr-x 2 root root 0 Mar 18 11:57 memory
lrwxrwxrwx 1 root root 16 Mar 18 11:57 net_cls -> net_cls,net_prio
drwxr-xr-x 2 root root 0 Mar 18 11:57 net_cls,net_prio
lrwxrwxrwx 1 root root 16 Mar 18 11:57 net_prio -> net_cls,net_prio
drwxr-xr-x 2 root root 0 Mar 18 11:57 perf_event
drwxr-xr-x 2 root root 0 Mar 18 11:57 pids
drwxr-xr-x 4 root root 0 Mar 18 11:57 systemd
# 通過掛載點查看關聯(lián)的cgroup子系統(tǒng)
[root@cluster1 ~]$ 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/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpuacct,cpu)
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/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,hugetlb)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
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/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices)
cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio)
cpu:使用調(diào)度程序控制任務對cpu的使用
cpuacct:自動生成cgroup中任務對cpu資源使用情況的報告
cpuset:主要用于設置CPU和內(nèi)存的親和性
blkio:塊設備 I/O 限制
devices:可以開啟或關閉cgroup中任務對設備的訪問
freezer: 可以掛起或恢復cgroup中的任務
pids:限制任務數(shù)量
memory:可以設定cgroup中任務對內(nèi)存使用量的限定,并且自動生成這些任務對內(nèi)存資源使用情況的報告
perf_event:增加了對每 group 的監(jiān)測跟蹤的能力,可以監(jiān)測屬于某個特定的 group 的所有線程以及運行在特定CPU上的線程
net_cls:docker沒有直接使用它,它通過使用等級識別符標記網(wǎng)絡數(shù)據(jù)包,從而允許linux流量控制程序識別從具體cgroup中生成的數(shù)據(jù)包
Cgroup控制組
控制組 說白了就是一組進程(進程組),cgroup 就是用來限制 控制組 的資源使用。為了能夠方便地向一個 控制組 添加或者移除進程(在命令行也能操作),內(nèi)核使用了 虛擬文件系統(tǒng) 來進行管理 控制組。
控制組可以類比成Linux的目錄樹結構,由于目錄有層級關系,所以 控制組 也有層級關系

每個控制組目錄中,都有一個名為 tasks 的文件,用于保存當前 控制組 包含的進程列表。如果我們想向某個 控制組 添加一個進程時,可以把進程的 PID 寫入到 tasks 文件中即可。
在 Linux 內(nèi)核中,可以存在多個 層級(控制組樹),每個層級可以關聯(lián)一個或多個 資源控制子系統(tǒng),但同一個 資源控制子系統(tǒng) 不能關聯(lián)到多個層級中。如下圖所示:
直白來說,即/cgrp4關聯(lián)在/cgrp1下,所以不能再直接關聯(lián)/sys/fs/cgroup/的某個子系統(tǒng)了

# 如果用戶想把資源控制子系統(tǒng)關聯(lián)到其他層級,那么可以使用 mount 命令來進行掛載
# 將內(nèi)存子系統(tǒng)重新關聯(lián)到 /sys/fs/cgroup/memory 這個層級
mount -t cgroup -o memory memory /sys/fs/cgroup/memory
動手操作
為了不被冗余的篇幅影響閱讀體驗,這里以cpuset設置獨占cpu舉例
其他的子系統(tǒng)請參考我的另一篇文章:
cpuset子系統(tǒng)
先來看下cpuset子系統(tǒng)下的文件作用
| 文件 | 說明 |
|---|---|
| cpuset.cpus | 允許cgroup中的進程使用的CPU列表。如0-2,16代表 0,1,2,16這4個CPU |
| cpuset.cpu_exclusive | cgroup是否獨占cpuset.cpus 中分配的cpu 。(默認值0,共享;1,獨占),如果設置為1,其他cgroup內(nèi)的cpuset.cpus值不能包含有該cpuset.cpus內(nèi)的值 |
| cpuset.mems | 允許cgroup中的進程使用的內(nèi)存節(jié)點列表。如0-2,16代表 0,1,2,16這4個可用節(jié)點 |
| cpuset.mem_exclusive | 是否獨占memory,(默認值0,共享;1,獨占) |
| cpuset.mem_hardwall | cgroup中任務的內(nèi)存是否隔離,(默認值0,不隔離;1,隔離,每個用戶的任務將擁有獨立的空間) |
| cpuset.memory_pressure | 衡量cpuset中內(nèi)存分頁壓力的大小 |
| cpuset.memory_spread_page | 是否在允許的節(jié)點上均勻分布頁面緩存 |
| cpuset.memory_spread_slab | 是否將 slab 緩存均勻分布在允許的節(jié)點上 |
| cpuset.sched_load_balance | 是否在該 cpuset 上的 CPU 內(nèi)進行負載平衡 |
| cpuset.sched_relax_domain_level | 遷移任務時的搜索范圍 |
| cpuset.memory_pressure_enabled | 僅在root cgroup中存在,表示是否計算內(nèi)存壓力 |
簡單了解一下stress命令
stress --cpu 2 --timeout 600
表示使用stress命令模擬將2個CPU使用到百分之百,一會我們只綁定一個CPU,用stress壓一下,看看會有幾個CPU被占滿
# 由于centos7已經(jīng)使用了cgroup,所以不需要我們額外掛載
# mount -t cgroup -ocpuset cpuset /sys/fs/cgroup/cpuset
cd /sys/fs/cgroup/cpuset
mkdir wzp
cd wzp
echo 1 > cpuset.cpus
echo 0 > cpuset.mems
/bin/echo $$ > tasks
bash
# 這里執(zhí)行之后,我們新執(zhí)行的bash終端是在wzp這個cgroup的限制中的,也就是說,他僅可以使用1號CPU
cat /proc/self/cpuset
# 執(zhí)行命令,模擬讓兩個CPU 100%
stress --cpu 2 --timeout 600
# 另開一個終端,top看看實際情況


PS:要刪除/sys/fs/cgroup/xxx/下 自己創(chuàng)建的目錄,不能用 rm -rf 只能用rmdir
觀察Docker容器
docker的cgroup存放在/sys/fs/cgroups/某子系統(tǒng)/docker/<container-ID>
下面我們啟動一個docker,并設置上cpuset、cpu和memory限制來驗證我們的觀點
# 設置nginx容器使用CPU0-1,共享CPU512,內(nèi)存大小500m
[root@cluster1 ~]$ docker run -d --cpuset-cpus="0-1" --cpu-shares=512 --memory=500m nginx:alpine
4431d5a9c9ff657dc826a947c8c556e96b2e66722bd0e65932f77ed8357769e6
# 查看cgroup配置
[root@cluster1 ~]$ cat /sys/fs/cgroup/cpuset/docker/4431d5a9c9ff657dc826a947c8c556e96b2e66722bd0e65932f77ed8357769e6/cpuset.cpus
0-1
[root@cluster1 ~]$ cat /sys/fs/cgroup/cpu/docker/4431d5a9c9ff657dc826a947c8c556e96b2e66722bd0e65932f77ed8357769e6/cpu.shares
512
[root@cluster1 ~]$ cat /sys/fs/cgroup/memory/docker/4431d5a9c9ff657dc826a947c8c556e96b2e66722bd0e65932f77ed8357769e6/memory.limit_in_bytes
524288000

下一篇,Docker的實現(xiàn)原理——網(wǎng)絡