天天看點

容器底層原理探秘

作者:小蝦好望角

這篇分享用Namespace和Cgroup的底層原理帶着大家直覺感受一下容器的玩法,更多相關知識點請自行查閱官方文檔。

Namespace

Linux Namespace 有如下種類:

容器底層原理探秘

注意,後面的模拟代碼會用到相關的系統調用參數!

1.1 相關系統調用

與 Linux Namespace 相關的系統調用如下:

1、chroot(),即 change root directory(更改root目錄),在Linux系統中,系統預設的目錄結構都是以 “/”,即以根(root)開始的,而在使用chroot之後,系統的目錄結構将以指定的位置作為 “/” 位置

  • Linux Namespace 在此基礎上,提供了對UTS、IPC、mount、PID、network、User等的隔離機制
  • Docker優先使用 pivot_root 系統調用,如果系統不支援,才會使用chroot

2、clone(),實作線程的系統調用,用來建立一個新的程序,并可以通過設計上述參數達到隔離

3、unshare(),使某程序脫離某個namespace

4、setns(),把某程序加入到某個namespace

5、execv(),這個系統調用會把目前子程序的程序空間全部覆寫掉

  • 容器初始化程序 dockerinit 需要在容器内完成一系列初始化操作,比如完成根目錄的準備、挂載裝置和目錄、配置 hostname 等
  • 然後,通過 execv() 系統調用,讓應用程序(最後在Dockerfile裡執行的ENTRYPOINT)取代自己,成為容器裡的 PID=1 的程序

注意,後面的模拟代碼會用到相關的系統調用!

1.2 模拟Docker鏡像

一個最常見的 rootfs,或者說容器鏡像,會包括如下所示的目錄:

容器底層原理探秘

因為我們隻是模拟,是以隻建立幾個目錄:

容器底層原理探秘

拷貝指令到新建立的bin目錄:

容器底層原理探秘

注意,還需要把指令依賴的so檔案拷貝到lib目錄,查找so檔案需要用到 ldd 指令:

容器底層原理探秘

還有這些檔案我們不希望寫死在鏡像中,如下圖所示:

容器底層原理探秘

是以,我們在rootfs外面再建立一個conf目錄,把上面3個檔案放進去,然後把這個conf目錄mount進容器,具體操作看後面的代碼。

關于mount再多說一嘴,其主要作用就是,允許你将一個目錄或者檔案,而不是整個裝置,挂載到一個指定的目錄上。并且,這時你在該挂載點上進行的任何操作,隻是發生在被挂載的目錄或者檔案上,而原挂載點的内容則會被隐藏起來且不受影響。

簡單來說,綁定挂載實際上是一個inode(不清楚inode是啥的自行google吧)替換的過程,如下圖所示:

容器底層原理探秘

最後一步,執行chroot指令,告訴作業系統,我們将使用 $HOME/test 目錄作為 /bin/bash 程序的根目錄:

容器底層原理探秘

這個挂載在容器根目錄上,用來為容器程序提供隔離後執行環境的檔案系統,就是所謂的“容器鏡像”。至于很多同學都知道的 UnionFS,說白了,就是基于舊的rootfs以增量的方式來維護而已。

有興趣的同學可以用下面的代碼再來模拟一遍Docker:

容器底層原理探秘

1.3 模拟Docker網絡

容器有自己的 Network Namespace 和與之對應的網絡接口eth0,主控端上也有自己的eth0,而主控端上的eth0對應着真正的實體網卡,也就是說,主控端上的eth0才可以和外面通訊。那麼容器是怎麼跟外面通訊的呢?

容器底層原理探秘

如上圖所示,要解決容器與外面通訊需要解決兩個問題:

  • 讓資料包從容器的 Network Namespace 發送到主控端的 Network Namespace 上,一般有兩種方案,我們重點講解使用得最多的 veth 方案,至于 macvlan/ipvlan 會簡單提一下
  • 資料包發到主控端 Network Namespace 後,還要解決資料包怎麼從主控端上的eth0發送出去的問題

首先,啟動一個不帶網絡配置的容器:

容器底層原理探秘

找到這個容器程序的pid:

容器底層原理探秘

通過 /proc/$pid/ns/net 這個檔案得到容器 Network Namespace 的ID,然後在 /var/run/netns/ 目錄下建立一個符号連結,指向這個容器的 Network Namespace:

容器底層原理探秘

完成這步操作之後,在後面的 ip netns 操作裡,就可以用pid的值作為這個容器的 Network Namesapce 的辨別了。

接下來,用 ip link 指令來建立一對veth的虛拟裝置接口,分别是veth_container和veth_host:

容器底層原理探秘

把veth_container這個接口放入到容器的 Network Namespace 中:

容器底層原理探秘

把veth_container重新命名為eth0,因為這時候接口已經在容器的 Network Namesapce 裡了,是以不會和主控端上的eth0命名沖突:

容器底層原理探秘

接着對容器内的eth0做基本的網絡IP和預設路由配置:

容器底層原理探秘

完成這一步,就解決了讓資料包從容器的 Network Namespace 發送到主控端的 Network Namespace 上的問題,如下圖所示:

容器底層原理探秘

将veth_host這個裝置接入到docker0這個bridge上:

容器底層原理探秘

至此,容器和docker0組成了一個子網,docker0上的IP就是這個子網的網關IP,如下圖所示:

容器底層原理探秘

要讓子網可以通過主控端上eth0去通路外網,還需要添加下面的iptables規則:

容器底層原理探秘

最終的veth網絡配置如下圖所示:

容器底層原理探秘

最後測試一下是否能ping通外網:

容器底層原理探秘

如果發現ping不通,先檢查下是否打開IP轉發功能:

容器底層原理探秘

這裡簡單提一嘴 macvlan/ipvlan 方案:對于macvlan,每個虛拟網絡接口都有自己獨立的mac位址;而ipvlan的虛拟網絡接口是和實體網絡接口共享同一個mac位址。同時它們都有自己的 L2/L3 的配置方式。有興趣的同學可以按照如下步驟為容器手動配置上ipvlan的網絡接口:

docker run --init --name ipvlan-test --network none -d busybox sleep 36000
 
pid1=$(docker inspect ipvlan-test | grep -i Pid | head -n 1 | awk '{print $2}' | awk -F "," '{print $1}')
echo $pid1
ln -s /proc/$pid1/ns/net /var/run/netns/$pid1
 
ip link add link eth0 ipvt1 type ipvlan mode l2
ip link set dev ipvt1 netns $pid1
 
ip netns exec $pid1 ip link set ipvt1 name eth0
ip netns exec $pid1 ip addr add 172.17.3.2/16 dev eth0
ip netns exec $pid1 ip link set eth0 up           

完成上面的操作後,ipvlan網絡二層的連接配接如下圖所示:

容器底層原理探秘

有興趣的同學可以再深入思考下Kubernetes相關的網絡問題:

  • Pod内的網絡通信
  • 同節點Pod之間的網絡通信
  • 跨節點Pod通信
  • Pod與非Pod網絡的實體通信

Cgroup

使用Cgroup可以根據具體情況來控制系統資源的配置設定、優先順序、拒絕、管理和監控,提高總體效率。

2.1 Cgroup子系統

下面列舉的是 Cgroup V1 相關子系統:

  • blkio,這個子系統為塊裝置設定輸入/輸出限制,比如實體裝置(磁盤、固态硬碟、USB 等)
  • cpu,這個子系統使用排程程式提供對 CPU 的 cgroup 任務通路
  • cpuacct,這個子系統自動生成 cgroup 中任務所使用的 CPU 報告
  • cpuset,這個子系統為 cgroup 中的任務配置設定獨立 CPU(在多核系統)和記憶體節點
  • devices,這個子系統可允許或者拒絕 cgroup 中的任務通路裝置。
  • freezer,這個子系統挂起或者恢複 cgroup 中的任務
  • memory,這個子系統設定 cgroup 中任務使用的記憶體限制,并自動生成記憶體資源使用報告
  • net_cls,這個子系統使用等級識别符(classid)标記網絡資料包,可允許 Linux 流量控制程式(tc)識别從具體 cgroup 中生成的資料包
  • net_prio,這個子系統用來設計網絡流量的優先級
  • hugetlb,這個子系統主要針對于HugeTLB系統進行限制,這是一個大頁檔案系統

Linux Cgroups 給使用者暴露出來的操作接口是檔案系統,即它以檔案和目錄的方式組織在作業系統的 /sys/fs/cgroup 路徑下,可以通過 mount 指令檢視:

容器底層原理探秘

如果沒有看到上述目錄,可以自行做mount,如下所示:

mkdir cgroup
mount -t tmpfs cgroup_root ./cgroup
mkdir cgroup/cpuset
mount -t cgroup -ocpuset cpuset ./cgroup/cpuset/
mkdir cgroup/cpu
mount -t cgroup -ocpu cpu ./cgroup/cpu/
mkdir cgroup/memory
mount -t cgroup -omemory memory ./cgroup/memory/           

一旦mount成功,那些mount的目錄下面就會有很多相關檔案,如下圖所示:

容器底層原理探秘

2.2 限制CPU的栗子

在使用 CPU Cgroup 前,先介紹三個重要的參數:

1、cpu.cfs_period_us,用來配置時間周期長度,一般它的值是 100000,機關是微秒(us)

2、cpu.cfs_quota_us,用來配置目前Cgroup在設定的周期長度内所能使用的CPU時間數,機關也是微秒(us),通常不會修改

3、cpu.shares,用來設定CPU的相對值,并且是針對所有的CPU(核心),預設值是1024。假設系統中有兩個Cgroup,分别是A和B,A的shares值是1024,B的shares值是512,那麼A将獲得1024/(1204+512)=66%的CPU資源,而B将獲得33%的CPU資源,注意相對值這個概念:

  • 如果A不忙,沒有使用到66%的CPU時間,那麼剩餘的CPU時間将會被系統配置設定給B,即B的CPU使用率可以超過33%
  • 如果又添加了一個新的Cgroup C,且它的shares值是1024,那麼,A的限額變成了1024/(1204+512+1024)=40%,B的變成了20%

了解了上面的參數,那麼應該能看懂下面這條docker指令:

容器底層原理探秘

接下來,我們使用原生的Cgroup來實作上面的指令。

首先,建立一個 CPU Cgroup:

容器底層原理探秘

設定 cpu.cfs_quota_us 為 20000:

容器底層原理探秘

檢視目前Shell程序的PID:

容器底層原理探秘

然後,在bash中啟動一個死循環來消耗cpu,正常情況下應該使用100%的CPU,即消耗一個核心:

容器底層原理探秘

将這個測試Shell程序的PID加入 CPU Cgroup:

容器底層原理探秘

加入之後可以看到CPU使用率降到了20%:

容器底層原理探秘

關于Kubernetes的CPU限制出門右轉看 [Assign CPU Resources to Containers and Pods](https://kubernetes.io/docs/tasks/configure-pod-container/assign-cpu-resource/)。

2.3 Cgroup V2

Cgroup有V1和V2兩個版本,V1版本中的 blkio Cgroup 隻能限制 Direct IO,不能限制 Buffered IO。

這是因為 Buffered IO 會把資料先寫入到記憶體 Page Cache 中,然後由核心線程把資料寫入磁盤,而Cgroup V1 的子系統都是獨立的,即 Cgroup V1 blkio 的子系統是獨立于 memory 子系,是以無法統計到由 Page Cache 刷入到磁盤的資料量。

這個 Buffered IO 無法被限速的問題,在 Cgroup V2 裡被解決了。Cgroup V2 從架構上允許一個控制組裡有多個子系統協同運作,這樣在一個控制組裡隻要同時有 io 和 memory 子系統,就可以對 Buffered IO 作磁盤讀寫的限速,如下圖所示:

容器底層原理探秘

雖然 Cgroup V2 能解決 Buffered IO 磁盤讀寫限速的問題,但是目前 runC、containerd以及Kubernetes都是剛剛開始支援 Cgroup V2,是以還需要等待一段時間。

繼續閱讀