1. 程式人生 > 其它 >Docker底層原理(圖解+秒懂+史上最全)

Docker底層原理(圖解+秒懂+史上最全)

Docker基礎

使用docker和 docker-compose 安裝 高可用mysql , 高可用 rocketmq ,等一系列的中介軟體。

作為大神或者準架構師/架構師,一定要了解一下docker的底層原理。

首先還是簡單說明一下docker的簡介。

Docker 簡介

Docker 是一個開源的應用容器引擎,基於 Go 語言 並遵從 Apache2.0 協議開源。

Docker 可以讓開發者打包他們的應用以及依賴包到一個輕量級、可移植的容器中,然後釋出到任何流行的 Linux 機器上,也可以實現虛擬化。

容器是完全使用沙箱機制,相互之間不會有任何介面(類似 iPhone 的 app),更重要的是容器效能開銷極低。

Docker 從 17.03 版本之後分為 CE(Community Edition: 社群版) 和 EE(Enterprise Edition: 企業版),我們用社群版就可以了

Docker的應用場景

  • Web 應用的自動化打包和釋出。
  • 自動化測試和持續整合、釋出。
  • 在服務型環境中部署和調整資料庫或其他的後臺應用。
  • 從頭編譯或者擴充套件現有的 OpenShift 或 Cloud Foundry 平臺來搭建自己的 PaaS 環境。

Docker 架構

Docker 包括三個基本概念:

  • 映象(Image):Docker 映象(Image),就相當於是一個 root 檔案系統。比如官方映象 ubuntu:16.04 就包含了完整的一套 Ubuntu16.04 最小系統的 root 檔案系統。
  • 容器(Container):映象(Image)和容器(Container)的關係,就像是面向物件程式設計中的類和例項一樣,映象是靜態的定義,容器是映象執行時的實體。容器可以被建立、啟動、停止、刪除、暫停等。
  • 倉庫(Repository):倉庫可看成一個程式碼控制中心,用來儲存映象。

Docker 使用客戶端-伺服器 (C/S) 架構模式,使用遠端API來管理和建立Docker容器。

Docker 容器通過 Docker 映象來建立。

概念 說明
Docker 映象(Images) Docker 映象是用於建立 Docker 容器的模板,比如 Ubuntu 系統。
Docker 容器(Container) 容器是獨立執行的一個或一組應用,是映象執行時的實體。
Docker 客戶端(Client) Docker 客戶端通過命令列或者其他工具使用 Docker SDK (https://docs.docker.com/develop/sdk/) 與 Docker 的守護程序通訊。
Docker 主機(Host) 一個物理或者虛擬的機器用於執行 Docker 守護程序和容器。
Docker Registry Docker 倉庫用來儲存映象,可以理解為程式碼控制中的程式碼倉庫。Docker Hub(https://hub.docker.com) 提供了龐大的映象集合供使用。一個 Docker Registry 中可以包含多個倉庫(Repository);每個倉庫可以包含多個標籤(Tag);每個標籤對應一個映象。通常,一個倉庫會包含同一個軟體不同版本的映象,而標籤就常用於對應該軟體的各個版本。我們可以通過 <倉庫名>:<標籤> 的格式來指定具體是這個軟體哪個版本的映象。如果不給出標籤,將以 latest 作為預設標籤。

最為常用的幾個命令

docker的守護程序檢視

systemctl status docker

docker 映象檢視

docker image ls

docker 容器檢視

docker ps

Docker Registry配置和檢視

cat /etc/docker/daemon.json

配置私有倉庫

cat>/etc/docker/daemon.json<<EOF

{

 "registry-mirrors":["http://10.24.2.30:5000","https://tnxkcso1.mirrors.aliyuncs.com"],

 "insecure-registries":["10.24.2.30:5000"]

}

EOF

Docker 的發展歷史

Docker 公司前身是 DotCloud,由 Solomon Hykes 在2010年成立,2013年更名 Docker。同年釋出了 Docker-compose 元件提供容器的編排工具。

2014年 Docker 釋出1.0版本,2015年Docker 提供 Docker-machine,支援 windows 平臺。

在此期間,Docker 專案在開源社群大受追捧,同時也被業界詬病的是 Docker 公司對於 Docker 發展具有絕對的話語權,比如 Docker 公司推行了 libcontainer 難以被社群接受。

為了防止 Docker 這項開源技術被Docker 公司控制,在幾個核心貢獻的廠商,於是在一同貢獻 Docker 程式碼的公司諸如 Redhat,谷歌的倡導下,成立了 OCI 開源社群。

OCI 開源社群旨在於將 Docker 的發展權利迴歸社群,當然反過來講,Docker 公司也希望更多的廠商安心貢獻程式碼到Docker 專案,促進 Docker 專案的發展。

於是通過OCI建立了 runc 專案,替代 libcontainer,這為開發者提供了除 Docker 之外的容器化實現的選擇。

OCI 社群提供了 runc 的維護,而 runc 是基於 OCI 規範的執行容器的工具。換句話說,你可以通過 runc,提供自己的容器實現,而不需要依賴 Docker。當然,Docker 的發行版底層也是用的 runc。在 Docker 宿主機上執行 runc,你會發現它的大多數命令和 Docker 命令類似,感興趣的讀者可以自己實踐如何用 runc 啟動容器。

至2017年,Docker 專案轉移到 Moby 專案,基於 Moby 專案,Docker 提供了兩種發行版,Docker CE 和 Docker EE, Docker CE 就是目前大家普遍使用的版本,Docker EE 成為付費版本,提供了容器的編排,Service 等概念。Docker 公司承諾 Docker 的發行版會基於 Moby 專案。這樣一來,通過 Moby 專案,你也可以自己打造一個定製化的容器引擎,而不會被 Docker 公司繫結。

Docker 與虛擬機器有何區別

Docker 的誤解:Docker 是輕量級的虛擬機器。

很多人將docker理解為, Docker 實現了類似於虛擬化的技術,能夠讓應用跑在一些輕量級的容器裡。這麼理解其實是錯誤的。

到底什麼是docker:

Docker是一個Client-Server結構的系統,Docker守護程序執行在主機上, 然後通過Socket連線從客戶端訪問Docker守護程序。

Docker守護程序從客戶端接受命令,並按照命令,管理執行在主機上的容器。

一個docker 容器,是一個執行時環境,可以簡單理解為程序執行的集裝箱。

如圖下所示

docker和kvm都是虛擬化技術,它們的主要差別:

1、Docker有著比虛擬機器更少的抽象層

2、docker利用的是宿主機的核心,VM需要的是Guest OS

二者的不同:

  • VM(VMware)在宿主機器、宿主機器作業系統的基礎上建立虛擬層、虛擬化的作業系統、虛擬化的倉庫,然後再安裝應用;
  • Container(Docker容器),在宿主機器、宿主機器作業系統上建立Docker引擎,在引擎的基礎上再安裝應用。

所以說,新建一個容器的時候,docker不需要像虛擬機器一樣重新載入一個作業系統,避免引導。docker是利用宿主機的作業系統,省略了這個複雜的過程,秒級!

虛擬機器是載入Guest OS ,這是分鐘級別的

與傳統VM特性對比:

作為一種輕量級的虛擬化方式,Docker在執行應用上跟傳統的虛擬機器方式相比具有顯著優勢:

  • Docker 容器很快,啟動和停止可以在秒級實現,這相比傳統的虛擬機器方式要快得多。
  • Docker 容器對系統資源需求很少,一臺主機上可以同時執行數千個Docker容器。
  • Docker 通過類似Git的操作來方便使用者獲取、分發和更新應用映象,指令簡明,學習成本較低。
  • Docker 通過Dockerfile配置檔案來支援靈活的自動化建立和部署機制,提高工作效率。
  • Docker 容器除了執行其中的應用之外,基本不消耗額外的系統資源,保證應用效能的同時,儘量減小系統開銷。
  • Docker 利用Linux系統上的多種防護機制實現了嚴格可靠的隔離。從1.3版本開始,Docker引入了安全選項和映象簽名機制,極大地提高了使用Docker的安全性。
特性 容器 虛擬機器
啟動速度 秒級 分鐘級
硬碟使用 一般為MB 一般為GB
效能 接近原生 弱於原生
系統支援量 單機支援上千個容器 一般幾十個

Docker 技術的基礎:

  • namespace,容器隔離的基礎,保證A容器看不到B容器. 6個名空間:User,Mnt,Network,UTS,IPC,Pid
  • cgroups,容器資源統計和隔離。主要用到的cgroups子系統:cpu,blkio,device,freezer,memory
  • unionfs,典型:aufs/overlayfs,分層映象實現的基礎

Linux 名稱空間、控制組和 UnionFS 三大技術支撐了目前 Docker 的實現,也是 Docker 能夠出現的最重要原因。

UnionFS

UnionFS 其實是一種為 Linux 作業系統設計的用於把多個檔案系統『聯合』到同一個掛載點的檔案系統服務。而 AUFS 即 Advanced UnionFS 其實就是 UnionFS 的升級版,它能夠提供更優秀的效能和效率。
AUFS 作為聯合檔案系統,它能夠將不同資料夾中的層聯合(Union)到了同一個資料夾中,這些資料夾在 AUFS 中稱作分支,整個『聯合』的過程被稱為聯合掛載(Union Mount):

每一個映象層或者容器層都是 /var/lib/docker/ 目錄下的一個子資料夾;在 Docker 中,所有映象層和容器層的內容都儲存在 /var/lib/docker/aufs/diff/ 目錄中:

$ ls /var/lib/docker/aufs/diff/00adcccc1a55a36a610a6ebb3e07cc35577f2f5a3b671be3dbc0e74db9ca691c       93604f232a831b22aeb372d5b11af8c8779feb96590a6dc36a80140e38e764d8
00adcccc1a55a36a610a6ebb3e07cc35577f2f5a3b671be3dbc0e74db9ca691c-init  93604f232a831b22aeb372d5b11af8c8779feb96590a6dc36a80140e38e764d8-init
019a8283e2ff6fca8d0a07884c78b41662979f848190f0658813bb6a9a464a90       93b06191602b7934fafc984fbacae02911b579769d0debd89cf2a032e7f35cfa
...

而 /var/lib/docker/aufs/layers/ 中儲存著映象層的元資料,每一個檔案都儲存著映象層的元資料,最後的 /var/lib/docker/aufs/mnt/ 包含映象或者容器層的掛載點,最終會被 Docker 通過聯合的方式進行組裝。

上面的這張圖片非常好的展示了組裝的過程,每一個映象層都是建立在另一個映象層之上的,同時所有的映象層都是隻讀的,只有每個容器最頂層的容器層才可以被使用者直接讀寫,所有的容器都建立在一些底層服務(Kernel)上,包括名稱空間、控制組、rootfs 等等,這種容器的組裝方式提供了非常大的靈活性,只讀的映象層通過共享也能夠減少磁碟的佔用。

什麼是映象

那麼問題來了,沒有作業系統,怎麼執行程式?

可以在Docker中建立一個ubuntu的映象檔案,這樣就能將ubuntu系統整合到Docker中,執行的應用就都是ubuntu的應用。

實際上 Docker 是使用了很多 Linux 的隔離功能,讓容器看起來像一個輕量級虛擬機器在獨立執行,容器的本質是被限制了的 Namespaces,cgroup,具有邏輯上獨立檔案系統,網路的一個程序。其底層運用瞭如下 Linux 的能力:

Namespaces

名稱空間(namespaces)是 Linux 為我們提供的用於分離程序樹、網路介面、掛載點以及程序間通訊等資源的方法。在日常使用 Linux 或者 macOS 時,我們並沒有執行多個完全分離的伺服器的需要,但是如果我們在伺服器上啟動了多個服務,這些服務其實會相互影響的,每一個服務都能看到其他服務的程序,也可以訪問宿主機器上的任意檔案,這是很多時候我們都不願意看到的,我們更希望執行在同一臺機器上的不同服務能做到完全隔離,就像執行在多臺不同的機器上一樣。

Linux 的名稱空間機制提供了以下七種不同的名稱空間,包括 CLONE_NEWCGROUP、CLONE_NEWIPC、CLONE_NEWNET、CLONE_NEWNS、CLONE_NEWPID、CLONE_NEWUSER 和 CLONE_NEWUTS,通過這七個選項我們能在建立新的程序時設定新程序應該在哪些資源上與宿主機器進行隔離。

Namespaces

這裡提出一個問題,在宿主機上啟動兩個容器,在這兩個容器內都各有一個 PID=1的程序,眾所周知,Linux 裡 PID 是唯一的,既然 Docker 不是跑在宿主機上的兩個虛擬機器,那麼它是如何實現在宿主機上執行兩個相同 PID 的程序呢?

這裡就用到了 Linux Namespaces,它其實是 Linux 建立新程序時的一個可選引數,在 Linux 系統中建立程序的系統呼叫是 clone()方法。

通過呼叫這個方法,這個程序會獲得一個獨立的程序空間,它的 pid 是1,並且看不到宿主機上的其他程序,這也就是在容器內執行 PS 命令的結果。

不僅僅是 PID,當你啟動啟動容器之後,Docker 會為這個容器建立一系列其他 namespaces。

這些 namespaces 提供了不同層面的隔離。容器的執行受到各個層面 namespace 的限制。

Docker Engine 使用了以下 Linux 的隔離技術:

The pid namespace: 管理 PID 名稱空間 (PID: Process ID).

The net namespace: 管理網路名稱空間(NET: Networking).

The ipc namespace: 管理程序間通訊名稱空間(IPC: InterProcess Communication).

The mnt namespace: 管理檔案系統掛載點名稱空間 (MNT: Mount).

The uts namespace: Unix 時間系統隔離. (UTS: Unix Timesharing System).

通過這些技術,執行時的容器得以看到一個和宿主機上其他容器隔離的環境。

程序

程序是 Linux 以及現在作業系統中非常重要的概念,它表示一個正在執行的程式,也是在現代分時系統中的一個任務單元。在每一個 *nix 的作業系統上,我們都能夠通過 ps 命令打印出當前作業系統中正在執行的程序,比如在 Ubuntu 上,使用該命令就能得到以下的結果:

當前的 Docker 容器成功將容器內的程序與宿主機器中的程序隔離,如果我們在宿主機器上列印當前的全部程序時,會得到下面三條與 Docker 相關的結果:

UID        PID  PPID  C STIME TTY          TIME CMD
root     29407     1  0 Nov16 ?        00:08:38 /usr/bin/dockerd --raw-logs
root      1554 29407  0 Nov19 ?        00:03:28 docker-containerd -l unix:///var/run/docker/libcontainerd/docker-containerd.sock --metrics-interval=0 --start-timeout 2m --state-dir /var/run/docker/libcontainerd/containerd --shim docker-containerd-shim --runtime docker-runc
root      5006  1554  0 08:38 ?        00:00:00 docker-containerd-shim b809a2eb3630e64c581561b08ac46154878ff1c61c6519848b4a29d412215e79 /var/run/docker/libcontainerd/b809a2eb3630e64c581561b08ac46154878ff1c61c6519848b4a29d412215e79 docker-runc

在當前的宿主機器上,可能就存在由上述的不同程序構成的程序樹:

這就是在使用 clone(2) 建立新程序時傳入 CLONE_NEWPID 實現的,也就是使用 Linux 的名稱空間實現程序的隔離,Docker 容器內部的任意程序都對宿主機器的程序一無所知。

containerRouter.postContainersStart
└── daemon.ContainerStart
└── daemon.createSpec
    └── setNamespaces
        └── setNamespace

Docker 的容器就是使用上述技術實現與宿主機器的程序隔離,當我們每次執行 docker run 或者 docker start 時,都會在下面的方法中建立一個用於設定程序間隔離的 Spec:

func (daemon *Daemon) createSpec(c *container.Container) (*specs.Spec, error) {
s := oci.DefaultSpec()

// ...
if err := setNamespaces(daemon, &s, c); err != nil {
    return nil, fmt.Errorf("linux spec namespaces: %v", err)
}

return &s, nil
} 

在 setNamespaces 方法中不僅會設定程序相關的名稱空間,還會設定與使用者、網路、IPC 以及 UTS 相關的名稱空間:

func setNamespaces(daemon *Daemon, s *specs.Spec, c *container.Container) error {
// user
// network
// ipc
// uts

// pid
if c.HostConfig.PidMode.IsContainer() {
    ns := specs.LinuxNamespace{Type: "pid"}
    pc, err := daemon.getPidContainer(c)
    if err != nil {
        return err
    }
    ns.Path = fmt.Sprintf("/proc/%d/ns/pid", pc.State.GetPID())
    setNamespace(s, ns)
} else if c.HostConfig.PidMode.IsHost() {
    oci.RemoveNamespace(s, specs.LinuxNamespaceType("pid"))
} else {
    ns := specs.LinuxNamespace{Type: "pid"}
    setNamespace(s, ns)
}

return nil
} 

所有名稱空間相關的設定 Spec 最後都會作為 Create 函式的入參在建立新的容器時進行設定:

daemon.containerd.Create(context.Background(), container.ID, spec, createOptions)

所有與名稱空間的相關的設定都是在上述的兩個函式中完成的,Docker 通過名稱空間成功完成了與宿主機程序和網路的隔離。

網路

如果 Docker 的容器通過 Linux 的名稱空間完成了與宿主機程序的網路隔離,但是卻有沒有辦法通過宿主機的網路與整個網際網路相連,就會產生很多限制,所以 Docker 雖然可以通過名稱空間建立一個隔離的網路環境,但是 Docker 中的服務仍然需要與外界相連才能發揮作用。

這個地址其實也是 Docker 為 Redis 服務分配的 IP 地址,如果我們在當前機器上直接 ping 這個 IP 地址就會發現它是可以訪問到的:

$ ping 192.168.0.4
PING 192.168.0.4 (192.168.0.4) 56(84) bytes of data.
64 bytes from 192.168.0.4: icmp_seq=1 ttl=64 time=0.069 ms
64 bytes from 192.168.0.4: icmp_seq=2 ttl=64 time=0.043 ms
^C
--- 192.168.0.4 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 999ms
rtt min/avg/max/mdev = 0.043/0.056/0.069/0.013 ms

從上述的一系列現象,我們就可以推測出 Docker 是如何將容器的內部的埠暴露出來並對資料包進行轉發的了;當有 Docker 的容器需要將服務暴露給宿主機器,就會為容器分配一個 IP 地址,同時向 iptables 中追加一條新的規則。

當我們使用 redis-cli 在宿主機器的命令列中訪問 127.0.0.1:6379 的地址時,經過 iptables 的 NAT PREROUTING 將 ip 地址定向到了 192.168.0.4,重定向過的資料包就可以通過 iptables 中的 FILTER 配置,最終在 NAT POSTROUTING 階段將 ip 地址偽裝成 127.0.0.1,到這裡雖然從外面看起來我們請求的是 127.0.0.1:6379,但是實際上請求的已經是 Docker 容器暴露出的埠了。

$ redis-cli -h 127.0.0.1 -p 6379 ping
PONG

Docker 通過 Linux 的名稱空間實現了網路的隔離,又通過 iptables 進行資料包轉發,讓 Docker 容器能夠優雅地為宿主機器或者其他容器提供服務。

Libnetwork

整個網路部分的功能都是通過 Docker 拆分出來的 libnetwork 實現的,它提供了一個連線不同容器的實現,同時也能夠為應用給出一個能夠提供一致的程式設計介面和網路層抽象的容器網路模型。

The goal of libnetwork is to deliver a robust Container Network Model that provides a consistent programming interface and the required network abstractions for applications.

libnetwork 中最重要的概念,容器網路模型由以下的幾個主要元件組成,分別是 Sandbox、Endpoint 和 Network:

想要獲得更多與 libnetwork 或者容器網路模型相關的資訊,可以閱讀 Design · libnetwork 瞭解更多資訊,當然也可以閱讀原始碼瞭解不同 OS 對容器網路模型的不同實現。

掛載點

雖然我們已經通過 Linux 的名稱空間解決了程序和網路隔離的問題,在 Docker 程序中我們已經沒有辦法訪問宿主機器上的其他程序並且限制了網路的訪問,但是 Docker 容器中的程序仍然能夠訪問或者修改宿主機器上的其他目錄,這是我們不希望看到的。
在新的程序中建立隔離的掛載點名稱空間需要在 clone 函式中傳入 CLONE_NEWNS,這樣子程序就能得到父程序掛載點的拷貝,如果不傳入這個引數子程序對檔案系統的讀寫都會同步回父程序以及整個主機的檔案系統。

這一部分的內容是作者在 libcontainer 中的 SPEC.md 檔案中找到的,其中包含了 Docker 使用的檔案系統的說明,對於 Docker 是否真的使用 chroot 來確保當前的程序無法訪問宿主機器的目錄,作者其實也沒有確切的答案,一是 Docker 專案的程式碼太多龐大,不知道該從何入手,作者嘗試通過 Google 查詢相關的結果,但是既找到了無人回答的問題,也得到了與 SPEC 中的描述有衝突的答案,如果各位讀者有明確的答案可以在部落格下面留言,非常感謝。

Chroot

在這裡不得不簡單介紹一下 chroot(change root),在 Linux 系統中,系統預設的目錄就都是以 / 也就是根目錄開頭的,chroot 的使用能夠改變當前的系統根目錄結構,通過改變當前系統的根目錄,我們能夠限制使用者的權利,在新的根目錄下並不能夠訪問舊系統根目錄的結構個檔案,也就建立了一個與原系統完全隔離的目錄結構。

與 chroot 的相關內容部分來自《理解 chroot》一文,各位讀者可以閱讀這篇文章獲得更詳細的資訊。

CGroups

Cgroups

在容器環境裡,如果不做限制,執行一個 Java 應用,當應用有 Bug 導致 JVM 進行 Full GC 的時候,會佔用大量的記憶體,也可能導致 CPU 飆升到100%,如何避免由單個容器的問題,導致整個叢集不可用?Docker 用到了 Cgroups。Cgroups 是 Control Group 的縮寫,由2007年穀歌工程師研發,2008年併入 Linux Kernel 2.6.24,由 C 語言編寫。

Docker 底層使用 groups 對程序進行 CPU,Mem,網路等資源的使用限制,從而實現在宿主機上的資源分配,不至於出現一個容器佔用所有宿主機的 CPU 或者記憶體。

具體的實現原理如上圖所示,通過使用 cgroups,為不同的程序設定不同的配額,上圖中 cgroup1 的程序只能使用60%的 CPU,cgroup2 使用20%的 CPU,同樣可以為程序設定容器。所以當你在 Kubernetes 裡為某個 pod 宣告 CPU 限額時,底層就是呼叫的 cgroup 的設定。具體實現如下:

進入宿主機 cgroup 目錄: cd /sys/fs/cgroup/cpu

為程序建立一個目錄,例如 my_container, 然後 cgroup 會自動在這個目錄下建立多個預設配置:

在cpu.cfs_quota_us裡輸入對應的數值,即可實現對 CPU 的配額設定:

echo 60000 > /sys/fs/cgroup/cpu/container/cpu.cfs_ q uota_us,這裡的單位是毫秒,意思是每一毫秒內,該程序能夠使用60%的 CPU 時間。如何將該配置應用到程序裡?

echo 22880 > /sys/fs/cgroup/cpu/container/task s,其中22880是宿主機上的 PID,這時候你通過呼叫 top 命令,能夠看到該程序的 CPU 使用率不會超過60%,這樣就能實現對程序的限制,避免之前的問題發生。

我們通過 Linux 的名稱空間為新建立的程序隔離了檔案系統、網路並與宿主機器之間的程序相互隔離,但是名稱空間並不能夠為我們提供物理資源上的隔離,比如 CPU 或者記憶體,如果在同一臺機器上運行了多個對彼此以及宿主機器一無所知的『容器』,這些容器卻共同佔用了宿主機器的物理資源。

每一個 CGroup 都是一組被相同的標準和引數限制的程序,不同的 CGroup 之間是有層級關係的,也就是說它們之間可以從父類繼承一些用於限制資源使用的標準和引數。

Linux 的 CGroup 能夠為一組程序分配資源,也就是我們在上面提到的 CPU、記憶體、網路頻寬等資源,通過對資源的分配,CGroup 能夠提供以下的幾種功能:

Linux 使用檔案系統來實現 CGroup,我們可以直接使用下面的命令檢視當前的 CGroup 中有哪些子系統:

$ lssubsys -m
cpuset /sys/fs/cgroup/cpuset
cpu /sys/fs/cgroup/cpu
cpuacct /sys/fs/cgroup/cpuacct
memory /sys/fs/cgroup/memory
devices /sys/fs/cgroup/devices
freezer /sys/fs/cgroup/freezer
blkio /sys/fs/cgroup/blkio
perf_event /sys/fs/cgroup/perf_event
hugetlb /sys/fs/cgroup/hugetlb

大多數 Linux 的發行版都有著非常相似的子系統,而之所以將上面的 cpuset、cpu 等東西稱作子系統,是因為它們能夠為對應的控制組分配資源並限制資源的使用。

如果系統管理員想要控制 Docker 某個容器的資源使用率就可以在 docker 這個父控制組下面找到對應的子控制組並且改變它們對應檔案的內容,當然我們也可以直接在程式執行時就使用引數,讓 Docker 程序去改變相應檔案中的內容。

$ docker run -it -d --cpu-quota=50000 busybox
53861305258ecdd7f5d2a3240af694aec9adb91cd4c7e210b757f71153cdd274
$ cd 53861305258ecdd7f5d2a3240af694aec9adb91cd4c7e210b757f71153cdd274/
$ ls
cgroup.clone_children  cgroup.event_control  cgroup.procs  cpu.cfs_period_us  cpu.cfs_quota_us  cpu.shares  cpu.stat  notify_on_release  tasks
$ cat cpu.cfs_quota_us
50000

當我們使用 Docker 關閉掉正在執行的容器時,Docker 的子控制組對應的資料夾也會被 Docker 程序移除,Docker 在使用 CGroup 時其實也只是做了一些建立資料夾改變檔案內容的檔案操作,不過 CGroup 的使用也確實解決了我們限制子容器資源佔用的問題,系統管理員能夠為多個容器合理的分配資源並且不會出現多個容器互相搶佔資源的問題。

UnionFS

Linux 的名稱空間和控制組分別解決了不同資源隔離的問題,前者解決了程序、網路以及檔案系統的隔離,後者實現了 CPU、記憶體等資源的隔離,但是在 Docker 中還有另一個非常重要的問題需要解決 - 也就是映象。
映象到底是什麼,它又是如何組成和組織的是作者使用 Docker 以來的一段時間內一直比較讓作者感到困惑的問題,我們可以使用 docker run 非常輕鬆地從遠端下載 Docker 的映象並在本地執行。

Docker 使用了一系列不同的儲存驅動管理映象內的檔案系統並執行容器,這些儲存驅動與 Docker 卷(volume)有些不同,儲存引擎管理著能夠在多個容器之間共享的儲存。

容器和映象的區別就在於,所有的映象都是隻讀的,而每一個容器其實等於映象加上一個可讀寫的層,也就是同一個映象可以對應多個容器。

dockers=LXC+AUFS

docker為LXC+AUFS組合:

  • LXC負責資源管理
  • AUFS負責映象管理;

而LXC包括cgroup,namespace,chroot等元件,並通過cgroup資源管理,那麼,從資源管理的角度來看,Docker,Lxc,Cgroup三者的關係是怎樣的呢?

cgroup是在底層落實資源管理,LXC在cgroup上面封裝了一層,隨後,docker有在LXC封裝了一層;

Cgroup其實就是linux提供的一種限制,記錄,隔離程序組所使用的物理資源管理機制;也就是說,Cgroup是LXC為實現虛擬化所使用資源管理手段,我們可以這樣說,底層沒有cgroup支援,也就沒有lxc,更別說docker的存在了,這是我們需要掌握和理解的,三者之間的關係概念

​ 我們在把重心轉移到LXC這個相當於中介軟體上,上述我們提到LXC是建立在cgroup基礎上的,我們可以粗略的認為LXC=Cgroup+namespace+Chroot+veth+使用者控制指令碼;LXC利用核心的新特性(cgroup)來提供使用者空間的物件,用來保證資源的隔離和對應用系統資源的限制;

​ Docker容器的檔案系統最早是建立在Aufs基礎上的,Aufs是一種Union FS,簡單來說就是支援將不同的目錄掛載到同一個虛擬檔案系統之下

並實現一種laver的概念,

由於Aufs未能加入到linux核心中,考慮到相容性的問題,便加入了Devicemapper的支援,Docker目前預設是建立在Devicemapper基礎上,

devicemapper使用者控制元件相關部分主要負責配置具體的策略和控制邏輯,比如邏輯裝置和哪些物理裝置建立對映,怎麼建立這些對映關係等,而具體過濾和重定向IO請求的工作有核心中相關程式碼完成,因此整個device mapper機制由兩部分組成--核心空間的device mapper驅動,使用者控制元件的device mapper庫以及它提供的dmsetup工具;

參考資料

https://docs.docker.com/storage/storagedriver/aufs-driver/#how-the-aufs-storage-driver-works

https://github.com/opencontainers/runc

http://www.sel.zju.edu.cn/?p=840

https://draveness.me/docker/

https://blog.csdn.net/wangqingjiewa/article/details/85000393