Docker 核心技術與實現原理




到虛擬化技術,我們首先想到的一定是 Docker,經過四年的快速發展 Docker 已經成為了很多公司的標配,也不再是一個只能在開發階段使用的玩具了。作為在生產環境中廣泛應用的產品,Docker 有著非常成熟的社區以及大量的使用者,代碼庫中的內容也變得非常龐大。

Docker 核心技術與實現原理

同樣,由於項目的發展、功能的拆分以及各種奇怪的改名 PR,讓我們再次理解 Docker 的的整體架構變得更加困難。

雖然 Docker 目前的組件較多,並且實現也非常複雜,但是本文不想過多的介紹 Docker 具體的實現細節,我們更想談一談 Docker 這種虛擬化技術的出現有哪些核心技術的支撐。

Docker 核心技術與實現原理

首先,Docker 的出現一定是因為目前的後端在開發和運維階段確實需要一種虛擬化技術解決開發環境和生產環境環境一致的問題,通過 Docker 我們可以將程序運行的環境也納入到版本控制中,排除因為環境造成不同運行結果的可能。但是上述需求雖然推動了虛擬化技術的產生,但是如果沒有合適的底層技術支撐,那麼我們仍然得不到一個完美的產品。本文剩下的內容會介紹幾種 Docker 使用的核心技術,如果我們瞭解它們的使用方法和原理,就能清楚 Docker 的實現原理。

Namespaces

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

Docker 核心技術與實現原理

在這種情況下,一旦服務器上的某一個服務被入侵,那麼入侵者就能夠訪問當前機器上的所有服務和文件,這也是我們不想看到的,而 Docker 其實就通過 Linux 的 Namespaces 對不同的容器實現了隔離。

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

進程

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

<code>$ ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 Apr08 ? 00:00:09 /sbin/init
root 2 0 0 Apr08 ? 00:00:00 [kthreadd]
root 3 2 0 Apr08 ? 00:00:05 [ksoftirqd/0]
root 5 2 0 Apr08 ? 00:00:00 [kworker/0:0H]
root 7 2 0 Apr08 ? 00:07:10 [rcu_sched]
root 39 2 0 Apr08 ? 00:00:00 [migration/0]
root 40 2 0 Apr08 ? 00:01:54 [watchdog/0]
.../<code>

當前機器上有很多的進程正在執行,在上述進程中有兩個非常特殊,一個是 pid 為 1 的 /sbin/init 進程,另一個是 pid 為 2 的 kthreadd 進程,這兩個進程都是被 Linux 中的上帝進程 idle 創建出來的,其中前者負責執行內核的一部分初始化工作和系統配置,也會創建一些類似 getty 的註冊進程,而後者負責管理和調度其他的內核進程。

Docker 核心技術與實現原理

如果我們在當前的 Linux 操作系統下運行一個新的 Docker 容器,並通過 exec 進入其內部的 bash 並打印其中的全部進程,我們會得到以下的結果:

<code>root@iZ255w13cy6Z:~# docker run -it -d ubuntu
b809a2eb3630e64c581561b08ac46154878ff1c61c6519848b4a29d412215e79
root@iZ255w13cy6Z:~# docker exec -it b809a2eb3630 /bin/bash
root@b809a2eb3630:/# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 15:42 pts/0 00:00:00 /bin/bash
root 9 0 0 15:42 pts/1 00:00:00 /bin/bash
root 17 9 0 15:43 pts/1 00:00:00 ps -ef/<code>

在新的容器內部執行 ps 命令打印出了非常乾淨的進程列表,只有包含當前 ps -ef 在內的三個進程,在宿主機器上的幾十個進程都已經消失不見了。

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

<code>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/<code>

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

Docker 核心技術與實現原理

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

<code>containerRouter.postContainersStart
└── daemon.ContainerStart
└── daemon.createSpec
└── setNamespaces
└── setNamespace/<code>

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

<code>func (daemon *Daemon) createSpec(c *container.Container) (*specs.Spec, error) {
\ts := oci.DefaultSpec()

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

\treturn &s, nil
}/<code>

在 setNamespaces 方法中不僅會設置進程相關的命名空間,還會設置與用戶、網絡、IPC 以及 UTS 相關的命名空間:

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

\t// pid
\tif c.HostConfig.PidMode.IsContainer() {
\t\tns := specs.LinuxNamespace{Type: "pid"}
\t\tpc, err := daemon.getPidContainer(c)

\t\tif err != nil {
\t\t\treturn err
\t\t}
\t\tns.Path = fmt.Sprintf("/proc/%d/ns/pid", pc.State.GetPID())
\t\tsetNamespace(s, ns)
\t} else if c.HostConfig.PidMode.IsHost() {
\t\toci.RemoveNamespace(s, specs.LinuxNamespaceType("pid"))
\t} else {
\t\tns := specs.LinuxNamespace{Type: "pid"}
\t\tsetNamespace(s, ns)
\t}

\treturn nil
}/<code>

所有命名空間相關的設置 Spec 最後都會作為 Create 函數的入參在創建新的容器時進行設置:

<code>daemon.containerd.Create(context.Background(), container.ID, spec, createOptions)/<code>

所有與命名空間的相關的設置都是在上述的兩個函數中完成的,Docker 通過命名空間成功完成了與宿主機進程和網絡的隔離。

網絡

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

每一個使用 docker run 啟動的容器其實都具有單獨的網絡命名空間,Docker 為我們提供了四種不同的網絡模式,Host、Container、None 和 Bridge 模式。

Docker 核心技術與實現原理

在這一部分,我們將介紹 Docker 默認的網絡設置模式:網橋模式。在這種模式下,除了分配隔離的網絡命名空間之外,Docker 還會為所有的容器設置 IP 地址。當 Docker 服務器在主機上啟動之後會創建新的虛擬網橋 docker0,隨後在該主機上啟動的全部服務在默認情況下都與該網橋相連。

Docker 核心技術與實現原理

在默認情況下,每一個容器在創建時都會創建一對虛擬網卡,兩個虛擬網卡組成了數據的通道,其中一個會放在創建的容器中,會加入到名為 docker0 網橋中。我們可以使用如下的命令來查看當前網橋的接口:

<code>$ brctl show
bridge name\tbridge id\t\tSTP enabled\tinterfaces
docker0\t\t8000.0242a6654980\tno\t\tveth3e84d4f
\t\t\t\t\t\t\t veth9953b75/<code>

docker0 會為每一個容器分配一個新的 IP 地址並將 docker0 的 IP 地址設置為默認的網關。網橋 docker0 通過 iptables 中的配置與宿主機器上的網卡相連,所有符合條件的請求都會通過 iptables 轉發到 docker0 並由網橋分發給對應的機器。

<code>$ iptables -t nat -L
Chain PREROUTING (policy ACCEPT)
target prot opt source destination
DOCKER all -- anywhere anywhere ADDRTYPE match dst-type LOCAL

Chain DOCKER (2 references)
target prot opt source destination
RETURN all -- anywhere anywhere/<code>

我們在當前的機器上使用 docker run -d -p 6379:6379 redis 命令啟動了一個新的 Redis 容器,在這之後我們再查看當前 iptables 的 NAT 配置就會看到在 DOCKER 的鏈中出現了一條新的規則:

<code>DNAT       tcp  --  anywhere             anywhere             tcp dpt:6379 to:192.168.0.4:6379/<code>

上述規則會將從任意源發送到當前機器 6379 端口的 TCP 包轉發到 192.168.0.4:6379 所在的地址上。

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

<code>$ 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/<code>

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

Docker 核心技術與實現原理


當我們使用 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 容器暴露出的端口了。

<code>$ redis-cli -h 127.0.0.1 -p 6379 ping
PONG/<code>

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:

Docker 核心技術與實現原理

在容器網絡模型中,每一個容器內部都包含一個 Sandbox,其中存儲著當前容器的網絡棧配置,包括容器的接口、路由表和 DNS 設置,Linux 使用網絡命名空間實現這個 Sandbox,每一個 Sandbox 中都可能會有一個或多個 Endpoint,在 Linux 上就是一個虛擬的網卡 veth,Sandbox 通過 Endpoint 加入到對應的網絡中,這裡的網絡可能就是我們在上面提到的 Linux 網橋或者 VLAN。

想要獲得更多與 libnetwork 或者容器網絡模型相關的信息,可以閱讀 Design · libnetwork 瞭解更多信息,當然也可以閱讀源代碼瞭解不同 OS 對容器網絡模型的不同實現。

掛載點

雖然我們已經通過 Linux 的命名空間解決了進程和網絡隔離的問題,在 Docker 進程中我們已經沒有辦法訪問宿主機器上的其他進程並且限制了網絡的訪問,但是 Docker 容器中的進程仍然能夠訪問或者修改宿主機器上的其他目錄,這是我們不希望看到的。

在新的進程中創建隔離的掛載點命名空間需要在 clone 函數中傳入 CLONE_NEWNS,這樣子進程就能得到父進程掛載點的拷貝,如果不傳入這個參數子進程對文件系統的讀寫都會同步回父進程以及整個主機的文件系統

如果一個容器需要啟動,那麼它一定需要提供一個根文件系統(rootfs),容器需要使用這個文件系統來創建一個新的進程,所有二進制的執行都必須在這個根文件系統中。

Docker 核心技術與實現原理

想要正常啟動一個容器就需要在 rootfs 中掛載以上的幾個特定的目錄,除了上述的幾個目錄需要掛載之外我們還需要建立一些符號鏈接保證系統 IO 不會出現問題。

Docker 核心技術與實現原理

為了保證當前的容器進程沒有辦法訪問宿主機器上其他目錄,我們在這裡還需要通過 libcontainer 提供的 pivot_root 或者 chroot 函數改變進程能夠訪問個文件目錄的根節點。

<code>// pivor_root
put_old = mkdir(...);
pivot_root(rootfs, put_old);
chdir("/");
unmount(put_old, MS_DETACH);
rmdir(put_old);

// chroot
mount(rootfs, "/", NULL, MS_MOVE, NULL);
chroot(".");
chdir("/");/<code>

到這裡我們就將容器需要的目錄掛載到了容器中,同時也禁止當前的容器進程訪問宿主機器上的其他目錄,保證了不同文件系統的隔離。

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

chroot

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

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

CGroups

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

Docker 核心技術與實現原理

如果其中的某一個容器正在執行 CPU 密集型的任務,那麼就會影響其他容器中任務的性能與執行效率,導致多個容器相互影響並且搶佔資源。如何對多個容器的資源使用進行限制就成了解決進程虛擬資源隔離之後的主要問題,而 Control Groups(簡稱 CGroups)就是能夠隔離宿主機器上的物理資源,例如 CPU、內存、磁盤 I/O 和網絡帶寬。

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

Docker 核心技術與實現原理

Linux 的 CGroup 能夠為一組進程分配資源,也就是我們在上面提到的 CPU、內存、網絡帶寬等資源,通過對資源的分配,CGroup 能夠提供以下的幾種功能:

Docker 核心技術與實現原理

在 CGroup 中,所有的任務就是一個系統的一個進程,而 CGroup 就是一組按照某種標準劃分的進程,在 CGroup 這種機制中,所有的資源控制都是以 CGroup 作為單位實現的,每一個進程都可以隨時加入一個 CGroup 也可以隨時退出一個 CGroup。
– CGroup 介紹、應用實例及原理描述

Linux 使用文件系統來實現 CGroup,我們可以直接使用下面的命令查看當前的 CGroup 中有哪些子系統:

<code>$ 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/<code>

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

如果我們想要創建一個新的 cgroup 只需要在想要分配或者限制資源的子系統下面創建一個新的文件夾,然後這個文件夾下就會自動出現很多的內容,如果你在 Linux 上安裝了 Docker,你就會發現所有子系統的目錄下都有一個名為 docker 的文件夾:

<code>$ ls cpu
cgroup.clone_children
...
cpu.stat
docker
notify_on_release
release_agent
tasks

$ ls cpu/docker/
9c3057f1291b53fd54a3d12023d2644efe6a7db6ddf330436ae73ac92d401cf1
cgroup.clone_children
...

cpu.stat
notify_on_release
release_agent
tasks/<code>

9c3057xxx 其實就是我們運行的一個 Docker 容器,啟動這個容器時,Docker 會為這個容器創建一個與容器標識符相同的 CGroup,在當前的主機上 CGroup 就會有以下的層級關係:

Docker 核心技術與實現原理

每一個 CGroup 下面都有一個 tasks 文件,其中存儲著屬於當前控制組的所有進程的 pid,作為負責 cpu 的子系統,cpu.cfs_quota_us 文件中的內容能夠對 CPU 的使用作出限制,如果當前文件的內容為 50000,那麼當前控制組中的全部進程的 CPU 佔用率不能超過 50%。

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

<code>$ 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/<code>

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

UnionFS

Linux 的命名空間和控制組分別解決了不同資源隔離的問題,前者解決了進程、網絡以及文件系統的隔離,後者實現了 CPU、內存等資源的隔離,但是在 Docker 中還有另一個非常重要的問題需要解決 - 也就是鏡像。

鏡像到底是什麼,它又是如何組成和組織的是作者使用 Docker 以來的一段時間內一直比較讓作者感到困惑的問題,我們可以使用 docker run 非常輕鬆地從遠程下載 Docker 的鏡像並在本地運行。

Docker 鏡像其實本質就是一個壓縮包,我們可以使用下面的命令將一個 Docker 鏡像中的文件導出:

<code>$ docker export $(docker create busybox) | tar -C rootfs -xvf -
$ ls
bin dev etc home proc root sys tmp usr var/<code>

你可以看到這個 busybox 鏡像中的目錄結構與 Linux 操作系統的根目錄中的內容並沒有太多的區別,可以說 Docker 鏡像就是一個文件

存儲驅動

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

想要理解 Docker 使用的存儲驅動,我們首先需要理解 Docker 是如何構建並且存儲鏡像的,也需要明白 Docker 的鏡像是如何被每一個容器所使用的;Docker 中的每一個鏡像都是由一系列只讀的層組成的,Dockerfile 中的每一個命令都會在已有的只讀層上創建一個新的層:

<code>FROM ubuntu:15.04
COPY . /app
RUN make /app
CMD python /app/app.py/<code>

容器中的每一層都只對當前容器進行了非常小的修改,上述的 Dockerfile 文件會構建一個擁有四層 layer 的鏡像:

Docker 核心技術與實現原理

當鏡像被 docker run 命令創建時就會在鏡像的最上層添加一個可寫的層,也就是容器層,所有對於運行時容器的修改其實都是對這個容器讀寫層的修改。

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

Docker 核心技術與實現原理

AUFS

UnionFS 其實是一種為 Linux 操作系統設計的用於把多個文件系統『聯合』到同一個掛載點的文件系統服務。而 AUFS 即 Advanced UnionFS 其實就是 UnionFS 的升級版,它能夠提供更優秀的性能和效率。

AUFS 作為聯合文件系統,它能夠將不同文件夾中的層聯合(Union)到了同一個文件夾中,這些文件夾在 AUFS 中稱作分支,整個『聯合』的過程被稱為聯合掛載(Union Mount):

Docker 核心技術與實現原理

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

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

而 /var/lib/docker/aufs/layers/ 中存儲著鏡像層的元數據,每一個文件都保存著鏡像層的元數據,最後的 /var/lib/docker/aufs/mnt/ 包含鏡像或者容器層的掛載點,最終會被 Docker 通過聯合的方式進行組裝。

Docker 核心技術與實現原理

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

其他存儲驅動

AUFS 只是 Docker 使用的存儲驅動的一種,除了 AUFS 之外,Docker 還支持了不同的存儲驅動,包括 aufs、devicemapper、overlay2、zfs 和 vfs 等等,在最新的 Docker 中,overlay2 取代了 aufs 成為了推薦的存儲驅動,但是在沒有 overlay2 驅動的機器上仍然會使用 aufs 作為 Docker 的默認驅動。

Docker 核心技術與實現原理

不同的存儲驅動在存儲鏡像和容器文件時也有著完全不同的實現,有興趣的讀者可以在 Docker 的官方文檔 Select a storage driver 中找到相應的內容。

想要查看當前系統的 Docker 上使用了哪種存儲驅動只需要使用以下的命令就能得到相對應的信息:

<code>$ docker info | grep Storage
Storage Driver: aufs/<code>

作者的這臺 Ubuntu 上由於沒有 overlay2 存儲驅動,所以使用 aufs 作為 Docker 的默認存儲驅動。

總結

Docker 目前已經成為了非常主流的技術,已經在很多成熟公司的生產環境中使用,但是 Docker 的核心技術其實已經有很多年的歷史了,Linux 命名空間、控制組和 UnionFS 三大技術支撐了目前 Docker 的實現,也是 Docker 能夠出現的最重要原因。

作者在學習 Docker 實現原理的過程中查閱了非常多的資料,從中也學習到了很多與 Linux 操作系統相關的知識,不過由於 Docker 目前的代碼庫實在是太過龐大,想要從源代碼的角度完全理解 Docker 實現的細節已經是非常困難的了,但是如果各位讀者真的對其實現細節感興趣,可以從 Docker CE 的源代碼開始瞭解 Docker 的原理。


分享到:


相關文章: