容器
-
基於鏡像
鏡像image就是附加一個JSON配置文件的tar包。鏡像常常是嵌套的,防止重複內容佔用空間 -
容器運行時會從某處下載鏡像,這個地方稱爲registry。Registry通常是一個通過HTTP協議暴露鏡像的元數據和文件以供下載的容器倉庫
-
運行時將層次化的鏡像解壓到支持Copy on Write(CoW)的文件系統裏。通常這通過覆蓋(overlay)文件系統來實現,所有的層次層層覆蓋來構成一個合併的文件系統。 這個步驟通常不能通過命令行直接訪問,而是當運行時創建容器時會自動在後臺發生
-
運行時來實際地執行容器。 它告訴內核給容器分配合適的資源限制,創建隔離的層(爲進程,網絡,文件系統等),使用各種機制的混合(包括cgroups,namespaces,capabilities, seccomp, AppArmor, SELinux,等等),底層實際調用的是runC命令
-
容器引擎常見的就是docker,也可以是其他容器引擎,創建容器就是把磁盤上的容器鏡像運行成宿主機的一個進程
Namespace
首先要理解docker本質上是一個運行在宿主機上的一個進程,用於幫助進程隔離出自己單獨的空間.
Linux Namespace可以隔離一系列的系統資源,如進程ID,網絡資源,用戶ID.
Linux現在實現了六種Namespace
-
UTS Namespace
主要用來進行hostname和domain的隔離 -
IPC Namespace
控制了進程兼通信的一些東西,比方說信號量。 -
PID Namespace
用來進行進程ID的隔離,保證了容器的 init 進程是以 1 號進程來啓動的。 -
Mount Namespace
用來隔離各個進程看到的掛載點視圖
是保證容器看到的文件系統的視圖,是容器鏡像提供的一個文件系統,也就是說它看不見宿主機上的其他文件
-
User Namespace
隔離用戶的用戶組ID,一個在宿主機上以非root用戶可以在一個User namespace內映射成root用戶 -
Network Namespace
網絡設備,IP地址,端口的隔離使得每個容器有自己獨立的虛擬網絡設備.即使不同容器的服務映射到相同的端口,都不會衝突.在宿主機搭建了網橋後,能很方便實現容器之間的通信.並且不同容器之間應用可以使用相同的端口.
Cgroups
Cgroup提供了對一組進程與將來子進程的資源限制,控制和統計的能力.
這些資源包括cpu,內存,存儲,網絡等.通過Cgroups可以方便限制某個進程的資源使用,並且可以實時進行對進程的監控與統計
兩種cgroup驅動
-
systemd
system daemon 的cgroup驅動 -
cgroupfs
要用 CPU share 爲多少,直接把 pid 寫入對應的一個 cgroup 文件,然後把對應需要限制的資源也寫入相應的 memory cgroup 文件和 CPU 的 cgroup 文件就可以了
三個組件
- cgroup
對進程分組管理的一種機制,負責把一組進程和一組subsystem的系統參數關聯起來 - subsystem
一組資源控制的模塊 - hierarchy
把cgroup串成一個樹狀的結構
容器常用的cgroup
- cpu
- memory
- device
上面三個好理解 - freezer
停止容器的時候,freezer會把當前進程全部寫入cgroup,然後所有進程都凍結掉,目的是防止在停止的時候,有進程會去做fork,這樣的話防止進程逃逸到宿主機上去。 - blkio
block io,主要限制磁盤IOPS和bps速率 - pid
限制容器裏可以用到的最大進程數量
操作cgroup
- 創建並且掛載一個hierarchy
mkdir cgroup-test
sudo mount -t cgroup -o none,name=cgroup-test cgroup-test ./cgroup-test
ls ./cgroup-test
- 在創建好的hierarchy上cgroup根節點擴展出兩個子cgroup,子cgroup會擴展父cgroupd的屬性
- 在cgroup中添加和移動進程
一個進程在一個cgroup的hirearchy進程中,只能在一個cgroup節點上存在,只要把該進程ID寫入該cgroup節點的tasks文件,就可以把進程移動到這個cgroup節點
sudo sh -c "echo $$ >> tasks"
- 通過subsystem限制cgroup中的進程資源
通過
mount | grep memory
查看哪個目錄掛在了memory subsystem的hierarchy上
sudo mkdir test-limit && cd test-limit
sudo sh -c "echo "100m" > memory.limit_in_bytes"
sudo sh -c "echo $$ > tasks"
stress --vm-bytes 200m --vm-keep -m 1
我們可以看到,雖然壓測聲明佔用內存爲200m,但實際上cgroup爲其限制了內存資源只能使用100m.
下面用go來模擬cgroup限制容器的資源
package main
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path"
"strconv"
"syscall"
)
//掛在了memory subsystem的hierarcy目錄
const cgroupMemoryHierarchyMount = "/sys/fs/cgroup/memory"
func main() {
if os.Args[0] == "/proc/self/exe" {
fmt.Printf("current pid %d", syscall.Getpid())
fmt.Println()
//啓動一個佔用內存200m的進程
cmd := exec.Command("sh", "-c" , `stress --vm-bytes 200m --vm-keep -m 1`)
cmd.SysProcAttr = &syscall.SysProcAttr{
}
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
cmd := exec.Command("/proc/self/exe")
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUTS|syscall.CLONE_NEWPID|syscall.CLONE_NEWNS,
}
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Start();err!=nil{
fmt.Println("ERROR", err)
os.Exit(1)
} else {
fmt.Printf("%v", cmd.Process.Pid)
//在掛載了memory subsystem的Hierarchy上創建cgroup文件
err := os.Mkdir(path.Join(cgroupMemoryHierarchyMount, "test"),0755)
if err != nil {
fmt.Println("mkdir failed ", err)
os.Exit(1)
}
writeErr := ioutil.WriteFile(path.Join(cgroupMemoryHierarchyMount, "test", "tasks"),
[]byte(strconv.Itoa(cmd.Process.Pid)),0644)
if writeErr != nil {
fmt.Println("write failed", writeErr)
os.Exit(1)
}
limitErr := ioutil.WriteFile(path.Join(cgroupMemoryHierarchyMount, "test", "memory.limit_in_bytes"),
[]byte("100m"),0644)
if limitErr != nil{
fmt.Println("write Limit file error", limitErr)
os.Exit(1)
}
}
_,waitErr := cmd.Process.Wait()
if waitErr != nil{
fmt.Println("wait error",waitErr)
}
}
這段程序實際上是模擬了用命令行創建cgroup並對進程的資源做限制(從200m到100m)
運行這段程序後,可以用top指令觀察stress進程佔用的內存會從200m變爲只有100m
Docker的存儲驅動
先介紹一下Docker是如何建立image的.然後又是如何在image的基礎上創建容器.
- docker鏡像由一層層layer組合起來
- 每一層layer對應的是Dockerfile中的一條指令
- 這些layer中,一層layer爲R/W layer即container layer
- 其他layer是read-only layer
- 通常最後一層爲CMD層或者ENTRYPOINT層,這一層是R/W的.
我們知道,多個容器可以依賴一份image啓動,那麼這些容器究竟是共用一個image,還是說各自把這個image複製了一份,然後各自獨立運行呢?
假設每個容器都複製了一份image,那麼其實對空間是一種巨大的浪費.
所以所有的容器其實都是共用一個image,但是如何保證一個容器的修改對其他容器是不可見的呢?
我們先不管這個問題,先討論一下鏡像的分層機制.
鏡像是隻讀的,每一層都是隻讀的.在內核的上面,最底層是一個基礎鏡像層(Debian),如果想要在ubuntu基礎鏡像上裝一個emacs編輯器,則只能在基礎鏡像之上,再去構建一個新的鏡像層.以此類推,如果想要在emacs鏡像層上添加一個apache,則只能再在其上面再構建一個新的鏡像層.每一層實際上都是掛載在宿主機相應的目錄下
容器讀寫層工作原理
其實在鏡像的最上層,還有一個讀寫層,這個讀寫層即在容器啓動的時候爲當前容器單獨掛載.所有對容器的操作都發生在讀寫層.一旦容器銷燬,這個讀寫層也會隨之銷燬.
所以 容器 = 鏡像 + 讀寫層
對這個讀寫層的操作主要有寫時複製和用時分配
- 寫時複製
只有在寫的時候纔去複製.所以所有容器而已共享鏡像的文件系統,所有的文件與數據都是從image讀取.當要對文件進行寫操作的時候,才從image把要寫的文件複製到容器內的文件系統進行修改.所以無論有多少個容器共享同一個image,所做的寫操作其實都是對從image中複製到自己的文件系統中的複本中進行,不會修改image的源文件.
所以不同容器對同一個文件的修改,實際上是把鏡像中的這個文件複製到容器的自己的文件系統內,然後對這個文件的複本進行修改,相互隔離,互不影響
- 用時配置
啓動一個新的容器的時候,並不會爲這個容器預分配一些磁盤空間,而是當有新文件寫入的時候,纔會按需分配新空間.
UnionFS
- 把其他文件系統聯合到一個聯合掛載點的文件系統服務
- 使用branch把不同文件系統的文件與目錄"透明地"覆蓋,形成一個單一一致的系統.
- 可以把其看作是一個虛擬的聯合文件系統.對這個虛擬的聯合文件系統進行寫操作的時候,系統是真正寫到了一個新文件中.
簡單來說就是支持將不同目錄掛載到同一個虛擬文件系統下的文件系統。這種文件系統可以一層一層地疊加修改文件。無論底下有多少層都是隻讀的,只有最上層的文件系統是可寫的。當需要修改一個文件時,AUFS創建該文件的一個副本,使用CoW將文件從只讀層複製到可寫層進行修改,結果也保存在可寫層。在Docker中,底下的只讀層就是image,可寫層就是Container。
OverlayFS
與AUFS多層不同的是,OverlayFS只有兩層.
upperdir可讀寫,對應爲讀寫層
lowerdir只讀,對應爲鏡像層
如果發生對文件的修改,把文件從lowerdir複製到upperdir,寫後結果也是保存在upperdir的.
我們自己來手動實現一個AUFS!
首先
mkdir aufs
然後創建一個container-layer,在裏面添加一個含有"I am container-layer"的文件
然後創建一個mnt文件夾.
然後分別創建4個image-layer{n}文件夾,每個文件夾都有一個含有"I am image-layer{n}"的文件.
這裏提供了一個腳本.
#!/bin/bash
for k in $(seq 1 4)
do
mkdir image-layer${k}
cd image-layer${k}
touch image-layer${k}.txt
echo "I am image layer${k}" >> image-layer${k}.txt
cd ..
done
然後我們就可以把container-layer和四個image-layer掛載到mnt文件夾.默認dirs=右邊第一個文件夾是讀寫的,其餘都是隻讀的.
sudo mount -t aufs -o dirs=./container-layer:./image-layer4:./image-layer3:./image-layer2:./image-layer1 none ./mnt
我們可以觀察一下現在mnt目錄結構
mnt
├── container-layer.txt
├── image-layer1.txt
├── image-layer2.txt
├── image-layer3.txt
└── image-layer4.txt
這時候查看一下文件的讀寫權限.可以看到只有container-layer是讀寫的,其他都是隻讀的.
mnt其實只是一個虛擬的掛載點.它裏面真實的文件是container-layer內的文件.
而且對mnt內的文件進行修改的時候,不會影響源文件image-layer4內的文件,實際上是從image-layer4中把源文件複製一個副本到讀寫層(container-layer)去修改並且保存的.
Docker 創建一個容器之後會發生什麼?
- 首先會發現PID=1的進程,即指定的前臺進程,容器創建的時候其實第一個執行的進程不是用戶的進程,而是init初始化的進程,即PID的進程.那和我們預想的不一樣,而且PID=1的進程是不能被kill掉的
- 因此需要藉助execve這個系統調用
int execve (const char& *filename, char *const argv[], char *const envp[])
可以覆蓋當前進程的鏡像,數據和堆棧信息包括PID,這些都會被要運行的進程覆蓋掉.
總結起來:
1.先創建容器,然後配置Namespace,創建父進程.
2,掛載文件系統然後替換init進程爲用戶進程
3.完成容器創建
容器引擎
上圖是containerd的架構圖
那麼什麼是containerd呢?
Containerd
Containerd是一個工業標準的容器運行時,重點是它簡潔,健壯,便攜,在Linux和window上可以作爲一個守護進程運行,它可以管理主機系統上容器的完整的生命週期:鏡像傳輸和存儲,容器的執行和監控,低級別的存儲和網絡。
容器運行時的定義是:能夠基於在線獲取的鏡像來創建和運行容器的程序.
Containerd把容器運行時以及管理功能從Docker Daemon剝離,其本身也是一個守護進程.容器的實際運行時由runC來控制.
主要職責是鏡像管理(鏡像、元信息等)、容器執行(調用最終運行時組件執行)
容器運行引擎runC
總結而言,containerd是對runC向上封裝了一層.runC可以理解爲是一個輕量級的容器運行引擎,包括所有Docker使用的和容器相關的系統調用的代碼.
containerd負責容器鏡像的拉取,容器的創建與停止,網絡以及存儲等,而具體運行容器由runC負責.
containerd和docker關係
containerd是從docker分離出來的容器執行引擎,本質上是一個Daemon進程.
Docker包括containerd,containerd專注於運行時的容器管理,而Docker除了容器管理還有鏡像構建的功能.
containerd,OCI和runC之間的關係
OCI:標準化的容器規範,包括運行時規範和鏡像規範
runC是基於這個規範的一個參考實現
containerd比runC層次更高,containerd可以使用runC來直接啓動容器.
containerd和容器編排系統的關係
K8s的Kubelet組件是負責容器的執行.
K8s之前可以直接使用Docker,也可以直接使用Containerd,而且在Containerd1.1 之後,kubelet可以直接通過CRI(容器運行時接口)
和containerd打交道
總結
namespace實現了訪問隔離,原理是針對一類資源做抽象,並將其封裝在一起給一個容器使用,所以每個容器都會對這類資源有不同的視角,而且他們彼此之間不可見.
cgroup實現了資源控制.原理是把一組進程放在一個控制組裏面,然後通過給這個控制組分配資源而實現給這一組進程分配資源的目的.
Docker的容器=鏡像(只讀層) + 讀寫層
分別對應AUFS/OverlayFS,在實現容器之間文件系統的隔離同時,也節省了大量的系統資源.
runC創建容器的進程:
ref
揭祕容器進行時
Docker存儲驅動
雲原生公開課
https://www.jianshu.com/p/d9bf66841a1e
https://my.oschina.net/u/2306127/blog/1600270