Docker 的主要組件
安裝 docker ,其實是安裝了 docker 客戶端、dockerd 等一系列的組件,其中比較重要的有下面幾個。
Docker CLI(docker)
docker 程序是一個客戶端工具,用來把用戶的請求發送給 docker daemon(dockerd)。該程序的安裝路徑為:
/usr/bin/docker
Dockerd
docker daemon(dockerd),一般也會被稱為 docker engine。該程序的安裝路徑為:
/usr/bin/dockerd
Containerd
詳情請參考《Containerd 簡介》。該程序的安裝路徑為:
/usr/bin/docker-containerd
Containerd-shim
它是 containerd 的組件,是容器的運行時載體,我們在 docker 宿主機上看到的 shim 也正是代表着一個個通過調用 containerd 啟動的 docker 容器。該程序的安裝路徑為:
/usr/bin/docker-containerd-shim
RunC
詳情請參考《RunC 簡介》。該程序的安裝路徑為:
/usr/bin/docker-runc
從 hello world 開始
Docker 很貼心的為我們提供了 hello-world 鏡像來驗證安裝是否成功,但是透過這個鏡像我們還能看到更多的信息:
$ docker run hello-world
上面的輸出信息指出,hello-world 容器的運行經歷了如下四步:
- Docker 客戶端向 docker daemon 發送請求
- Docker daemon 從 Docker Hub 上拉取鏡像
- Docker daemon 使用鏡像運行了一個容器並產生了輸出
- Docker daemon 把輸出的內容發送給了 docker 客戶端
這是一個很抽象也很容器理解的過程,但是我們還想知道更多:docker daemon 是如何創建並運行容器的?
其實容器部分的操作和管理都被 dockerd 外包給 containerd 了,下圖描述了運行一個容器時各個組件之間的關系:
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 的一些解釋。