本文已超過一年。較舊的文章可能包含過時的內容。檢查頁面中的資訊自發布以來是否已變得不正確。
DockerCon 的 Quake 演示是如何運作的?
Docker 在 2013 年發布後不久,就成為 Linux 非常流行的開源容器管理工具。Docker 有豐富的命令集來控制容器的執行。例如 start、stop、restart、kill、pause 和 unpause 等命令。然而,仍然缺少的是透過 Docker 本身以原生方式檢查點和還原 (C/R) 容器的能力。
我們一直積極與上游和社群開發人員合作,在 Docker 中新增對原生 C/R 的支援,並希望檢查點和還原命令將在 Docker 1.8 中引入。截至撰寫本文時,可以從外部 C/R 容器,因為此功能最近已合併到 libcontainer 中。
外部容器 C/R 在 DockerCon 2015 上進行了演示
容器 C/R 提供了許多好處,包括以下內容
- 停止並重新啟動 Docker daemon(例如為了升級),而無需終止正在運行的容器並從頭開始重新啟動它們,從而避免丟失它們在停止時所做的寶貴工作
- 重新啟動系統,而無需從頭開始重新啟動容器。與上述用例 1 相同的優點
- 加速啟動緩慢啟動的應用程式
- 透過檢查檢查點映像(打開的文件、記憶體段等)對容器中運行的進程進行「鑑識偵錯」
- 透過在不同的機器上還原容器來遷移容器
CRIU
從頭開始實作 C/R 功能是一項重大且艱鉅的任務。幸運的是,有一個功能強大的開源工具以 C 語言編寫,已在生產環境中用於檢查點和還原 Linux 中的整個進程樹。該工具稱為 CRIU,代表用戶空間檢查點還原 (http://criu.org)。CRIU 的工作原理是
- 凍結正在運行的應用程式。
- 將整個進程樹的位址空間和狀態檢查點到一組「映像」文件中。
- 從檢查點映像文件還原進程樹。
- 從凍結點恢復應用程式。
在 2014 年 4 月,我們決定找出 CRIU 是否可以檢查點和還原 Docker 容器以促進容器遷移。
階段 1 - 外部 C/R
此項目的第一階段是直接調用 CRIU 以轉儲在容器內運行的進程樹,並確定檢查點或還原操作失敗的原因。導致 CRIU 失敗的問題相當多。以下三個問題是更具挑戰性的問題。
外部綁定掛載
Docker 將 /etc/{hostname,hosts,resolv.conf} 設定為目標,其源文件位於容器的掛載命名空間之外。
--ext-mount-map 命令列選項已新增到 CRIU,以指定外部綁定掛載的路徑。例如,假設預設 Docker 配置,容器掛載命名空間中的 /etc/hostname 是從 /var/lib/docker/containers/<container-id>/hostname 的來源綁定掛載而來的。在檢查點時,我們告訴 CRIU 將 /etc/hostname 的「映射」記錄為,例如,etc_hostname。在還原時,我們告訴 CRIU 先前記錄為 etc_hostname 的文件應從 /var/lib/docker/containers/<container-id>/hostname 的外部綁定掛載映射。
AUFS 路徑名
Docker 最初使用 AUFS 作為其首選文件系統,該文件系統仍被廣泛使用(首選文件系統現在是 OverlayFS)。。由於一個錯誤,/proc/<pid>/map_files 的 AUFS 符號連結路徑指向 AUFS 分支內部,而不是它們相對於容器根目錄的路徑名。此問題已在 AUFS 源代碼中修復,但尚未到達所有發行版。CRIU 會對在其物理位置(在分支中)及其邏輯位置(從掛載命名空間的根目錄)中看到相同文件感到困惑。
僅在還原期間使用的 --root 命令列選項已通用化,以了解檢查點期間掛載命名空間的根目錄,並自動「修復」公開的 AUFS 路徑名。
Cgroups
在檢查點之後,Docker daemon 會刪除容器的 cgroups 子目錄(因為容器已「退出」)。這會導致還原失敗。
--manage-cgroups 命令列選項已新增到 CRIU,以轉儲和還原進程的 cgroups 及其屬性。
簡單容器的 CRIU 命令列如下所示
$ docker run -d busybox:latest /bin/sh -c 'i=0; while true; do echo $i \>\> /foo; i=$(expr $i + 1); sleep 3; done'
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS
168aefb8881b busybox:latest "/bin/sh -c 'i=0; 6 seconds ago Up 4 seconds
$ sudo criu dump -o dump.log -v4 -t 17810 \
-D /tmp/img/\<container\_id\> \
--root /var/lib/docker/aufs/mnt/\<container\_id\> \
--ext-mount-map /etc/resolv.conf:/etc/resolv.conf \
--ext-mount-map /etc/hosts:/etc/hosts \
--ext-mount-map /etc/hostname:/etc/hostname \
--ext-mount-map /.dockerinit:/.dockerinit \
--manage-cgroups \
--evasive-devices
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS
168aefb8881b busybox:latest "/bin/sh -c 'i=0; 6 minutes ago Exited (-1) 4 minutes ago
$ sudo mount -t aufs -o br=\
/var/lib/docker/aufs/diff/\<container\_id\>:\
/var/lib/docker/aufs/diff/\<container\_id\>-init:\
/var/lib/docker/aufs/diff/a9eb172552348a9a49180694790b33a1097f546456d041b6e82e4d7716ddb721:\
/var/lib/docker/aufs/diff/120e218dd395ec314e7b6249f39d2853911b3d6def6ea164ae05722649f34b16:\
/var/lib/docker/aufs/diff/42eed7f1bf2ac3f1610c5e616d2ab1ee9c7290234240388d6297bc0f32c34229:\
/var/lib/docker/aufs/diff/511136ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158:\
none /var/lib/docker/aufs/mnt/\<container\_id\>
$ sudo criu restore -o restore.log -v4 -d
-D /tmp/img/\<container\_id\> \
--root /var/lib/docker/aufs/mnt/\<container\_id\> \
--ext-mount-map /etc/resolv.conf:/var/lib/docker/containers/\<container\_id\>/resolv.conf \
--ext-mount-map /etc/hosts:/var/lib/docker/containers/\<container\_id\>/hosts \
--ext-mount-map /etc/hostname:/var/lib/docker/containers/\<container\_id\>/hostname \
--ext-mount-map /.dockerinit:/var/lib/docker/init/dockerinit-1.0.0 \
--manage-cgroups \
--evasive-devices
$ ps -ef | grep /bin/sh
root 18580 1 0 12:38 ? 00:00:00 /bin/sh -c i=0; while true; do echo $i \>\> /foo; i=$(expr $i + 1); sleep 3; done
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS
168aefb8881b busybox:latest "/bin/sh -c 'i=0; 7 minutes ago Exited (-1) 5 minutes ago
docker\_cr.sh
由於 CRIU 的命令列參數很長,因此 CRIU 源代碼樹中提供了一個名為 docker_cr.sh 的輔助腳本來簡化該過程。因此,對於上述容器,只需按如下方式 C/R 容器即可(詳情請參閱 http://criu.org/Docker)
$ sudo docker\_cr.sh -c 4397
dump successful
$ sudo docker\_cr.sh -r 4397
restore successful
在階段 1 結束時,可以使用 CRIU v1.3 外部檢查點和還原 Docker 1.0 容器,並使用 VFS、AUFS 或 UnionFS 儲存驅動程式。
階段 2 - 原生 C/R
雖然外部 C/R 作為容器 C/R 的成功概念驗證,但它有以下限制
- 檢查點容器的狀態將顯示為「已退出」。
- Docker 命令(例如 logs、kill 等)在還原的容器上不起作用。
- 還原的進程樹將是 /etc/init 的子進程,而不是 Docker daemon。
因此,第二階段的工作重點是在 Docker 中新增原生檢查點和還原命令。
libcontainer、nsinit
Libcontainer 是 Docker 的原生執行驅動程式。它提供了一組 API 來創建和管理容器。新增原生支援的第一步是在 libcontainer 中引入了兩個方法 checkpoint() 和 restore(),以及 nsinit 中對應的 checkpoint 和 restore 子命令。Nsinit 是一個簡單的實用程式,用於測試和偵錯 libcontainer。
docker checkpoint、docker restore
有了 libcontainer 中的 C/R 支援,下一步就是在 Docker 本身中新增 checkpoint 和 restore 子命令。此步驟中的一個巨大挑戰是重建容器和 daemon 之間的「管道」。當 daemon 最初啟動容器時,它會在自身(父進程)和容器的標準輸入、輸出和錯誤文件描述符(子進程)之間建立單獨的管道。這就是 docker logs 可以顯示容器輸出的方式。
當容器在檢查點後退出時,它和 daemon 之間的管道將被刪除。在容器還原期間,實際上是 CRIU 作為父進程。因此,在子進程(容器)和不相關的進程(Docker daemon)之間建立管道並非易事。
為了解決這個問題,--inherit-fd 命令列選項已新增到 CRIU。使用此選項,Docker daemon 會告訴 CRIU 讓還原的容器「繼承」從 daemon 傳遞到 CRIU 的某些文件描述符。
原生 C/R 的第一個版本在 2014 年 10 月的 Linux Plumbers Conference (LPC) 上進行了演示 (http://linuxplumbersconf.org/2014/ocw/proposals/1899)。
LPC 演示是使用不需要網路連線的簡單容器完成的。對還原網路連線的支援在 2015 年初完成,並在此 2 分鐘的影片剪輯中進行了演示。
容器 C/R 的目前狀態
在 2015 年 5 月,libcontainer 的 criu 分支已合併到 master 中。在 DockerCon15 上,使用新引入的輕量級 runC 容器運行時演示了容器遷移。在這個 (第 23:00 分鐘),運行 Quake 的容器在不同的機器上進行了檢查點和還原,有效地實作了容器遷移。
在撰寫本文時,GitHub 上有兩個儲存庫在 Docker 中具有原生 C/R 支援
- Docker 1.5(舊版 libcontainer,相對穩定)
- Docker 1.7(較新,不太穩定)
將 C/R 功能合併到 Docker 的工作正在進行中。您可以使用上述任一儲存庫來試驗 Docker C/R。如果您使用 OverlayFS 或您的容器工作負載使用 AIO,請注意以下事項
OverlayFS
當 OverlayFS 支援正式合併到 Linux 核心版本 3.18 中時,它成為首選儲存驅動程式(取代 AUFS)。但是,3.18 中的 OverlayFS 存在以下問題
- /proc/<pid>/fdinfo/<fd> 包含 mnt_id,但 mnt_id 不在 /proc/<pid>/mountinfo 中
- /proc/<pid>/fd/<fd> 不包含已打開文件的絕對路徑
這兩個問題都在這個 修補程式中得到修復,但該修補程式尚未合併到上游。
AIO
如果您使用的核心版本低於 3.19 且您的容器使用 AIO,則您需要以下來自 3.19 的核心修補程式
- torvalds: bd9b51e7 作者:Al Viro
- torvalds: e4a0d3e72 作者:Pavel Emelyanov