走進docker(01):hello-world的背後發生了什麼?

在程序員的世界裏,hello world是個很特殊的存在,當咱們接觸一門新的語言、新的開發庫或者框架時,第一時間想了解的通常都是怎麼實現一個hello world,而後思考hello world的背後發生了什麼,在學習docker的時候,也是一樣的思路,本篇將會介紹hello world背後的故事html

運行hello world

docker的安裝不在本篇的介紹範圍內,本文假設你已經安裝好了17.03版本的docker。先來看看hello world運行的效果:linux

dev@dev:~$ docker run hello-world

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

    ......

hello-world這個容器運行的時候就是打印上面一段話到終端,而後退出。它在輸出結果中給咱們描述了它運行的大概步驟,下面咱們將對這個過程作進一步的分析。git

關係圖

這裏是運行hello-world過程當中,進程之間的關係程序員

+------------+
                              |            |
                              | Docker Hub |
                              |            |
                              +------------+
                                    ↑
                                    |
                                  2 | REST
                                    |
                                    ↓
                               +---------+
+--------+       REST          |         |    grpc      +-------------------+
| docker |<------------------->| dockerd |<------------>| docker-containerd |
+--------+         1           |         |      3       +-------------------+
                               +---------+                       ↑
                                                                 |
                                                                 | 4
                                                                 ↓
                                                      +------------------------+  5   +-------------+
                                                      | docker-containerd-shim |<---->| docker-runc |
                                                      +------------------------+      +-------------+
                                                                                             ↑
                                                                                             | 6
                                                                                             ↓
                                                                                         +-------+
                                                                                         | hello |
                                                                                         +-------+

步驟過程

1. docker <--> dockerd

docker進程是docker客戶端,dockerd進程是docker服務器端,它們的代碼都在moby項目裏面。github

當在shell裏面運行docker run hello-world後,docker程序被啓動,這個程序就是docker的客戶端,它的任務就是解析命令行參數,而後構造相應的啓動容器請求,經過rest的方式發給dockerd。docker

Engine API裏描述了dockerd支持的全部請求,docker v17.03.0對應的API版本爲v1.26,而docker v17.03.1對應的版本爲v1.27,API版本之間的差異能夠參考version-historyshell

2. dockerd <--> "docker hub"

當dockerd收到客戶端的運行容器請求後,發現本地沒有相應的鏡像(image),就會從docker hub取相應image。(實際過程要比這個步驟多,這裏爲了簡單直觀,省略掉了其它的步驟,後面有詳細的說明)json

docker hub是docker官方存放鏡像(image)的服務器,dockerd和它之間也是使用rest接口,協議爲Registry HTTP API V2api

取image的大概過程以下:bash

  • 首先獲取image的manifests,manifests裏面包含兩部份內容,一是image的配置文件的digest(sha256),另外一個是image包含的全部filesystem layer的digest(sha256)

  • 根據上一步獲得的image的配置文件的digest,在本地找是否已經存在對應的image,若是已經存在的話,就不用再往下走了,用現成的就能夠了,若是沒有,則繼續

  • 遍歷manifests裏面的全部layer,根據其digest在本地找,若是找到對應的layer,則跳過當前layer,不然從服務器取相應layer的壓縮包

  • 等上面的全部步驟完成後,就會拼出完整的image

這裏的每一個layer都是相對於上一個文件系統layer的變化狀況。
從上面的過程能夠看出,docker只會拉取本地沒有的layer。

3. dockerd <--> docker-containerd

dockerd拿到image後,就會在本地建立相應的容器,而後再作一些初始化工做後,最後經過grpc的方式通知docker-containerd進程啓動指定的容器

docker-containerd是和dockerd一塊兒啓動的後臺進程,他們之間使用unix socket通訊,協議是grpc.

4. docker-containerd <--> docker-containerd-shim

docker-containerd和docker-containerd-shim都屬於containerd項目,當docker-containerd收到dockerd的啓動容器請求以後,會作一些初始化工做,而後啓動docker-containerd-shim進程,並將相關配置所在的目錄做爲參數傳給它。

能夠簡單的理解成docker-containerd管理全部本機正在運行的容器,而docker-containerd-shim只負責管理一個運行的容器,至關因而對runc的一個包裝,充當containerd和runc之間的橋樑,runc能幹的就交給runc來作,runc作不了的就放到這裏來作。

5. docker-containerd-shim <--> docker-runc

docker-containerd-shim進程啓動後,就會按照runtime的標準準備好相關運行時環境,而後啓動docker-runc進程。

docker-runc就是runc程序的重命名,它們是相等的,若無特殊狀況,後面介紹中不區分docker-runc和runc

imageruntime標準都由Open Container Initiative(OCI)負責定義維護,而runc就是docker貢獻給OCI的一個標準runtime實現。

如何啓動runc是公開的標準,大概過程就是準備好rootfs和配置文件,而後使用合適的參數啓動runc進程就能夠了。

6. docker-runc <--> hello

runc進程打開容器的配置文件,找到rootfs的位置,並啓動配置文件中指定的相應進程,在hello-world的這個例子中,runc會啓動容器中的hello程序。

到此爲止,容器啓動成功。

進程間的關係

等runc將容器啓動起來後,runc進程就退出了,因而容器裏面的第一個進程(hello)的父進程就變成了docker-containerd-shim,在pstree的輸出裏面,進程樹的關係大概以下:

systemd───dockerd───docker-containerd───docker-containerd-shim───hello

實際操做過程當中可能看不到這樣的輸出,由於hello很快就運行退出了,接着docker-containerd-shim也退出了。

其中dockerd和docker-containerd是後臺常駐進程,而docker-containerd-shim則由docker-containerd按需啓動。

runc退出後其子進程hello不是應該由init進程接管嗎?怎麼就變成了docker-containerd-shim的子進程了呢?這是由於從Linux 3.4開始,prctl增長了對PR_SET_CHILD_SUBREAPER的支持,這樣就能夠控制孤兒進程能夠被誰接管,而不是像之前同樣只能由init進程接管。

輸出

hello進程啓動以後,往標準輸出打印一段話後就退出了,那這個標準輸出輸出到哪裏去了呢?docker客戶端是怎麼獲得這段話的呢?這就取決於docker將這個標準輸出重定向到哪裏去了,以及它是怎麼管理容器的這些輸出的,這涉及到docker的日誌管理方式,該部份內容會在後續作詳細介紹,這裏只須要知道容器的標準輸出的內容能被docker的這些進程一層一層的轉發給客戶端就好了。

詳細步驟

上面介紹的是一個精簡版的hello world運行步驟,實際過程要多幾個回合,下面用curl命令來模擬一下實際的操做流程:

#這裏假設已經配置了dockerd監聽本地tcp端口2375

#請求dockerd建立容器,但因爲dockerd在本地找不到相應的image,因而返回失敗
dev@debian:~$ curl 127.0.0.1:2375/v1.27/containers/create  -X POST -H "Content-Type: application/json" -d '{"Image": "hello-world"}'
{"message":"No such image: hello-world:latest"}

#請求dockerd去找registry服務器拿image
#這裏的http應答中的內容包含了不少跟進度條相關的內容
dev@debian:~$ curl '127.0.0.1:2375/v1.27/images/create?fromImage=hello-world&tag=latest' -X POST
{"status":"Pulling from library/hello-world","id":"latest"}
{"status":"Pulling fs layer","progressDetail":{},"id":"78445dd45222"}
{"status":"Downloading","progressDetail":{"current":971,"total":971},"progress":"[==================================================\u003e]    971 B/971 B","id":"78445dd45222"}
{"status":"Verifying Checksum","progressDetail":{},"id":"78445dd45222"}
{"status":"Download complete","progressDetail":{},"id":"78445dd45222"}
{"status":"Extracting","progressDetail":{"current":971,"total":971},"progress":"[==================================================\u003e]    971 B/971 B","id":"78445dd45222"}
{"status":"Extracting","progressDetail":{"current":971,"total":971},"progress":"[==================================================\u003e]    971 B/971 B","id":"78445dd45222"}
{"status":"Pull complete","progressDetail":{},"id":"78445dd45222"}
{"status":"Digest: sha256:c5515758d4c5e1e838e9cd307f6c6a0d620b5e07e6f927b07d05f6d12a1ac8d7"}
{"status":"Status: Downloaded newer image for hello-world:latest"}

#再次建立容器成功,獲得容器ID
dev@debian:~$ curl 127.0.0.1:2375/v1.27/containers/create  -X POST -H "Content-Type: application/json" -d '{"Image": "hello-world"}'
{"Id":"2a4717ffb830bf4cff12ef6e6f1e93129970df273387797fd023e10292e3e928","Warnings":null}

#attach到容器的標準輸出,curl程序會暫停在這裏,等待容器的輸出
dev@debian:~$ curl '127.0.0.1:2375/v1.27/containers/2a4717ffb830bf4cff12ef6e6f1e93129970df273387797fd023e10292e3e928/attach?stderr=1&stdout=1&stream=1' -d '{"Connection": "Upgrade", "Upgrade":"tcp"}'
#等下一步容器啓動後,在這裏能夠看到容器的輸出

#另外打開一個shell窗口,啓動容器
dev@debian:~$ curl 127.0.0.1:2375/v1.27/containers/2a4717ffb830bf4cff12ef6e6f1e93129970df273387797fd023e10292e3e928/start -X POST

從上面的curl命令能夠看出,在hello-world這個場景中,docker客戶端主要發送了四個請求給dockerd,首先建立image,而後建立容器,接着attach標準輸出,最後啓動容器。(attach涉及到標準輸入輸出重定向,這裏不細說)

這裏是它們之間的交互流程:

+---------+
+--------+                     |         |
|        | 1.create container  |         |
|        |-------------------->|         |
|        |                     |         |
|        | 2.image not found   |         |
|        |<--------------------|         |
|        |                     |         |
|        | 3.create image      |         |
|        |-------------------->|         |
|        |                     |         |
|        | 4.image created     |         |
|        |<--------------------|         |
| docker |                     | dockerd |
|        | 5.create container  |         |
|        |-------------------->|         |
|        |                     |         |
|        | 6.container id      |         |
|        |<--------------------|         |
|        |                     |         |
|        | 7.start container   |         |
|        |-------------------->|         |
|        |                     |         |
|        | 8.container started |         |
|        |<--------------------|         |
+--------+                     |         |
                               +---------+

流程對應的文字描述以下:

  • 客戶端發送建立容器請求給dockerd,dockerd收到請求後,發現本地沒有相應的額image,因而返回失敗。

  • 客戶端收到失敗的響應後,當即發送建立image的請求過來,dockerd收到後,會去docker hub上拿相應的image,拿到後返回成功。

  • 客戶端再次發送建立容器請求給dockerd,dockerd收到會根據拿到的image建立一個新容器,並初始化容器運行時要用到的相關目錄和配置文件,裏面就包含了rootfs,容器建立完成後,dockerd返回容器的ID給客戶端。

  • 客戶端發啓動容器請求給dockerd,dockerd收到請求後,會通知docker-containerd啓動容器,啓動成功後返回成功給客戶端。

dockerd去docker hub上取image發生在這裏的3~4步之間,而docker-containerd───docker-containerd-shim───hello這些事發生在這裏的7~8步之間。

結束語

本文大概的介紹了一下hello-world是如何工做的,以及涉及了docker的哪些進程,裏面還有大量的細節沒有涉及,留給後續文章作進一步介紹。

參考

相關文章
相關標籤/搜索