本文發表於一年多前。舊文章可能包含過時內容。請檢查頁面中的資訊自發布以來是否已變得不正確。

DockerCon 的 Quake 演示是如何工作的?

Docker 在 2013 年釋出後不久,便成為 Linux 上一種非常受歡迎的開源容器管理工具。Docker 擁有一套豐富的命令來控制容器的執行,例如 start、stop、restart、kill、pause 和 unpause。然而,仍然缺少透過 Docker 本身原生實現容器檢查點和恢復 (C/R) 的能力。

我們一直積極與上游和社群開發者合作,以在 Docker 中新增對原生 C/R 的支援,並希望在 Docker 1.8 中引入檢查點和恢復命令。撰寫本文時,由於此功能最近已合併到 libcontainer 中,因此可以透過外部方式對容器進行 C/R。

DockerCon 2015 上演示了外部容器 C/R

Screen Shot 2015-06-30 at 3.37.46 PM.png

容器 C/R 提供了許多好處,包括以下幾點:

  • 停止並重新啟動 Docker 守護程式(例如用於升級),而無需終止正在執行的容器並從頭開始重新啟動它們,從而避免丟失它們停止時完成的寶貴工作
  • 重新啟動系統而無需從頭開始重新啟動容器。與上述用例 1 具有相同的好處
  • 加快啟動緩慢應用程式的啟動時間
  • 透過檢查容器中執行的程序的檢查點映像(開啟的檔案、記憶體段等),對它們進行“取證除錯”
  • 透過在不同的機器上恢復容器來遷移它們

CRIU

從頭開始實現 C/R 功能是一項艱鉅而龐大的任務。幸運的是,有一個用 C 語言編寫的強大開源工具,已在生產環境中用於檢查點和恢復 Linux 中的整個程序樹。該工具名為 CRIU,意為使用者空間中的檢查點恢復 (http://criu.org)。CRIU 的工作原理是:

  • 凍結正在執行的應用程式。
  • 將整個程序樹的地址空間和狀態檢查點到一組“映像”檔案中。
  • 從檢查點映像檔案恢復程序樹。
  • 從應用程式凍結的點恢復應用程式。

2014 年 4 月,我們決定 выяснить CRIU 是否可以檢查點和恢復 Docker 容器以促進容器遷移。

第一階段 - 外部 C/R

這項工作的第一階段是直接呼叫 CRIU 來轉儲容器中執行的程序樹,並確定檢查點或恢復操作失敗的原因。導致 CRIU 失敗的問題相當多。以下三個問題是其中更具挑戰性的一些問題。

外部繫結掛載

Docker 將 /etc/{hostname,hosts,resolv.conf} 設定為目標,其原始檔位於容器掛載名稱空間之外。

CRIU 添加了 --ext-mount-map 命令列選項,用於指定外部繫結掛載的路徑。例如,假設是預設 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 處的外部繫結掛載進行對映。

ext_bind_mount.png

AUFS 路徑名

Docker 最初使用 AUFS 作為其首選檔案系統,該檔案系統目前仍被廣泛使用(現在首選的檔案系統是 OverlayFS)。由於一個 bug,/proc/<pid>/map_files 的 AUFS 符號連結路徑指向 AUFS 分支內部,而不是它們相對於容器根目錄的路徑名。這個問題已在 AUFS 原始碼中修復,但尚未推廣到所有發行版。CRIU 會在物理位置(在分支中)和邏輯位置(從掛載名稱空間的根目錄)看到相同的檔案而感到困惑。

以前只在恢復期間使用的 --root 命令列選項被泛化,以便在檢查點期間理解掛載名稱空間的根目錄,並自動“修復”暴露的 AUFS 路徑名。

Cgroups

檢查點後,Docker 守護程式會刪除容器的 cgroups 子目錄(因為容器已“退出”)。這會導致恢復失敗。

CRIU 添加了 --manage-cgroups 命令列選項,用於轉儲和恢復程序的 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  

在第一階段結束時,可以使用 CRIU v1.3 外部檢查點和恢復使用 VFS、AUFS 或 UnionFS 儲存驅動程式的 Docker 1.0 容器。

第二階段 - 原生 C/R

雖然外部 C/R 成功證明了容器 C/R 的概念,但它具有以下侷限性:

  1. 檢查點容器的狀態將顯示為“已退出”。
  2. 諸如 logs、kill 等 Docker 命令將無法在恢復的容器上工作。
  3. 恢復的程序樹將是 /etc/init 的子程序,而不是 Docker 守護程式的子程序。

因此,這項工作的第二階段集中於向 Docker 新增原生檢查點和恢復命令。

libcontainer, nsinit

Libcontainer 是 Docker 的原生執行驅動程式。它提供了一組 API 來建立和管理容器。新增原生支援的第一步是向 libcontainer 引入 checkpoint() 和 restore() 兩個方法,以及向 nsinit 引入相應的 checkpoint 和 restore 子命令。Nsinit 是一個用於測試和除錯 libcontainer 的簡單實用程式。

docker checkpoint, docker restore

在 libcontainer 中支援 C/R 後,下一步是向 Docker 本身新增檢查點和恢復子命令。這一步中的一個巨大挑戰是重建容器和守護程式之間的“管道”。當守護程式最初啟動容器時,它會在自身(父程序)與容器(子程序)的標準輸入、輸出和錯誤檔案描述符之間建立單獨的管道。這就是 docker logs 可以顯示容器輸出的方式。

當容器在檢查點後退出時,它與守護程式之間的管道將被刪除。在容器恢復期間,實際是 CRIU 作為父程序。因此,在子程序(容器)和不相關的程序(Docker 守護程式)之間建立管道並非易事。

為了解決這個問題,CRIU 添加了 --inherit-fd 命令列選項。使用此選項,Docker 守護程式告訴 CRIU 讓恢復的容器“繼承”從守護程式傳遞給 CRIU 的某些檔案描述符。

原生 C/R 的第一個版本在 2014 年 10 月的 Linux Plumbers Conference (LPC) 上進行了演示 (http://linuxplumbersconf.org/2014/ocw/proposals/1899)。

external_cr.png

LPC 演示是使用一個不需要網路連線的簡單容器完成的。網路連線的恢復支援於 2015 年初完成,並在這段 2 分鐘的影片剪輯中進行了演示。

容器 C/R 的當前狀態

2015 年 5 月,libcontainer 的 criu 分支合併到 master。使用新引入的輕量級 runC 容器執行時,容器遷移在 DockerCon15 上進行了演示。在這段demo(第 23:00 分鐘)影片中,一個執行 Quake 的容器被檢查點並恢復到不同的機器上,有效地實現了容器遷移。

撰寫本文時,GitHub 上有兩個支援 Docker 原生 C/R 的倉庫

C/R 功能正在合併到 Docker 中。您可以使用上述任何一個倉庫來試驗 Docker C/R。如果您使用的是 OverlayFS 或您的容器工作負載使用 AIO,請注意以下事項:

OverlayFS

當 OverlayFS 支援正式合併到 Linux 核心 3.18 版本後,它成為首選的儲存驅動程式(而不是 AUFS)。然而,3.18 中的 OverlayFS 存在以下問題:

  • /proc/<pid>/fdinfo/<fd> 包含 mnt_id,但其不在 /proc/<pid>/mountinfo 中
  • /proc/<pid>/fd/<fd> 不包含已開啟檔案的絕對路徑

這兩個問題都已在此補丁中修復,但該補丁尚未合併到上游。

AIO

如果您使用的核心版本早於 3.19 且您的容器使用 AIO,則需要 3.19 中的以下核心補丁: