從 docker 到 runC

筆者在前文《RunC 簡介》和《Containerd 簡介》中分別介紹了 runC 和 containerd。本文咱們將結合 docker 中的其它組件探索 docker 是如何把這些組件組織起來協調工做的。html

Docker 的主要組件

安裝 docker ,實際上是安裝了 docker 客戶端、dockerd 等一系列的組件,其中比較重要的有下面幾個。docker

Docker CLI(docker)
docker 程序是一個客戶端工具,用來把用戶的請求發送給 docker daemon(dockerd)。該程序的安裝路徑爲:ubuntu

/usr/bin/docker

Dockerd
docker daemon(dockerd),通常也會被稱爲 docker engine。該程序的安裝路徑爲:segmentfault

/usr/bin/dockerd

Containerd
詳情請參考《Containerd 簡介》。該程序的安裝路徑爲:api

/usr/bin/docker-containerd

Containerd-shim
它是 containerd 的組件,是容器的運行時載體,咱們在 docker 宿主機上看到的 shim 也正是表明着一個個經過調用 containerd 啓動的 docker 容器。該程序的安裝路徑爲:bash

/usr/bin/docker-containerd-shim

RunC
詳情請參考《RunC 簡介》。該程序的安裝路徑爲:服務器

/usr/bin/docker-runc

從 hello world 開始

Docker 很貼心的爲咱們提供了 hello-world 鏡像來驗證安裝是否成功,可是透過這個鏡像咱們還能看到更多的信息:架構

$ docker run hello-world

上面的輸出信息指出,hello-world 容器的運行經歷了以下四步:curl

  1. Docker 客戶端向 docker daemon 發送請求
  2. Docker daemon 從 Docker Hub 上拉取鏡像
  3. Docker daemon 使用鏡像運行了一個容器併產生了輸出
  4. Docker daemon 把輸出的內容發送給了 docker 客戶端

這是一個很抽象也很容器理解的過程,可是咱們還想知道更多:docker daemon 是如何建立並運行容器的?
其實容器部分的操做和管理都被 dockerd 外包給 containerd 了,下圖描述了運行一個容器時各個組件之間的關係:tcp

Docker Engine API

從本質上說,docker 是一個客戶端/服務器架構的應用。Dockerd 以 Engine API (REST)的方式對外提供服務,Engine API 裏描述了 dockerd 支持的全部請求。Docker 客戶端與 dockerd 之間就是經過 REST 的方式通訊的。在 ubuntu 16.04 中,dockerd 默認是不監聽 tcp 端口的,爲了方便演示,咱們讓 dockerd 監聽 tcp 端口。這樣就可使用 curl 代替 docker 客戶端向 dockerd 發送請求了。具體的操做爲,先修改 /lib/systemd/system/docker.service 文件,註釋掉默認的 ExecStart 並添加新的 ExecStart 配置:

# ExecStart=/usr/bin/dockerd -H fd://
ExecStart=/usr/bin/dockerd -H tcp://0.0.0.0:2375 -H unix:///var/run/docker.sock

而後重啓 docker.service:

$ sudo systemctl daemon-reload
$ sudo systemctl restart docker.service

這樣 dockerd 就開始監聽 tcp 端口 2375 了:

Docker 與 Dockerd 的交互

Docker 客戶端與 dockerd 之間就是經過 REST 的方式通訊的。前面咱們已經讓 dockerd 監聽 tcp 端口了,因此咱們可使用 curl 來代替 docker 客戶端。這裏咱們簡單的演示如何請求 dockerd 從 docker hub 上下載 hello-world 鏡像:

$ curl '127.0.0.1:2375/v1.37/images/create?fromImage=hello-world&tag=latest' -X POST

若是去看看 Engine API,你會發現其它的請求也都是用相似方式發送的,是否是很簡單啊!

建立容器

容器鏡像的下載是由 dockerd 完成的,但容器的建立和運行就須要 containerd(docker-containerd) 來完成了。Dockerd 與 docker-containerd 之間是經過 grpc 協議通訊的。當 docker-containerd 收到 dockerd 啓動容器的請求以後,會作一些初始化工做,而後啓動 docker-containerd-shim 進程,並將相關配置做爲參數傳給它。docker-containerd 負責管理全部本機正在運行的容器,而一個 docker-containerd-shim 進程只負責管理一個運行的容器,它至關於 docker-runc 的一個封裝,充當 docker-containerd 和 docker-runc 之間的橋樑,docker-runc 能幹的就交給 docker-runc 來作,docker-runc 作不了的就放到這裏來作。下面咱們用 ubuntu 鏡像運行一個容器:

$ docker run -id ubuntu bash

上圖中黃線框起來的是幾個主要的進程,它們之間是有父子關係的(systemd 沒有出如今上圖):

systemd---dockerd---docker-containerd---docker-containerd-shim---bash

細心的朋友必定發現了,上圖中沒有出現 docker-runc 進程,這是爲何呢?
實際上,在容器啓動的過程當中,docker-runc 進程是做爲 docker-containerd-shim 的子進程存在的。docker-runc 進程根據配置找到容器的 rootfs 並建立子進程 bash 做爲容器中的第一個進程。當這一切都完成後 docker-runc 進程退出,而後容器進程 bash 由 docker-runc 的父進程 docker-containerd-shim 接管。

爲何須要 docker-containerd-shim?

也許你們會問,爲何在容器的啓動或運行過程當中須要一個 docker-containerd-shim 進程呢?把它移除掉整個架構會更簡潔也更優美一些!事實上 docker-containerd-shim 的存在是很是有必要的,其目的有以下幾點:

  • 它容許容器運行時(即 runC)在啓動容器以後退出,簡單說就是沒必要爲每一個容器一直運行一個容器運行時(runC)
  • 即便在 containerd 和 dockerd 都掛掉的狀況下,容器的標準 IO 和其它的文件描述符也都是可用的
  • 向 containerd 報告容器的退出狀態

前兩點尤爲重要,有了它們就能夠在不中斷容器運行的狀況下升級或重啓 dockerd(這對於生產環境來講意義重大)。 從這裏能夠看到對 containerd-shim 的一些解釋。

總結

筆者先在前文《RunC 簡介》和《Containerd 簡介》中分別介紹了 runC 和 containerd 等 docker 的核心組件。本文則經過 demo 演示了在建立、運行容器的過程當中這些組件如何配合 docker engine 完成相關的任務,以及相關進程之間的關係和做用。但願本文能夠幫助你們理解 docker 的總體架構及其組件間的協做方式。

參考:
How the docker container creation process works
走進docker:hello-world的背後發生了什麼?

相關文章
相關標籤/搜索