08 | 白話容器基礎(chǔ)(四):重新認(rèn)識(shí)Docker容器

你好,我是張磊。今天我和你分享的主題是:白話容器基礎(chǔ)之重新認(rèn)識(shí) Docker 容器。

在前面的三次分享中,我分別從 Linux Namespace 的隔離能力、Linux Cgroups 的限制能力,以及基于 rootfs 的文件系統(tǒng)三個(gè)角度,為你剖析了一個(gè)Linux 容器的核心實(shí)現(xiàn)原理。

? ? ? ? ?備注:之所以要強(qiáng)調(diào) Linux 容器,是因?yàn)楸热?Docker on Mac,以及WindowsDocker(Hyper-V 實(shí)現(xiàn)),實(shí)際上是基于虛擬化技術(shù)實(shí)現(xiàn)的,跟我們這個(gè)專欄著重介紹的 Linux 容器完全不同。

而在今天的分享中,我會(huì)通過(guò)一個(gè)實(shí)際案例,對(duì)“白話容器基礎(chǔ)”系列的所有內(nèi)容做一次深入的總結(jié)和擴(kuò)展。希望通過(guò)這次的講解,能夠讓你更透徹地理解 Docker 容器的本質(zhì)。

在開(kāi)始實(shí)踐之前,你需要準(zhǔn)備一臺(tái) Linux 機(jī)器,并安裝Docker。這個(gè)流程我就不再贅述了。

這一次,我要用 Docker 部署一個(gè)用 Python 編寫(xiě)的 Web 應(yīng)用。這個(gè)應(yīng)用的代碼部分(app.py)非常簡(jiǎn)單:

1 from flask import Flask

2 import socket

3 import os

4

5 app = Flask(__name__)

6

7 @app.route('/')

8 def hello():

9? ? ? ? html = "<h3>Hello {name}!</h3>" \

10? ? ? ? ? ? ? ? ? "<b>Hostname:</b> {hostname}<br/>"

11? ? ? returnhtml.format(name=os.getenv("NAME", "world"),hostname=socket.gethostname())

12

13 if __name__ =="__main__":

14 app.run(host='0.0.0.0', port=80)

在這段代碼中,我使用 Flask 框架啟動(dòng)了一個(gè) Web 服務(wù)器,而它唯一的功能是:如果當(dāng)前環(huán)境中有“NAME”這個(gè)環(huán)境變量,就把它打印在“Hello”后,否則就打印“Hello world”,最后再打印出當(dāng)前環(huán)境的 hostname。

這個(gè)應(yīng)用的依賴,則被定義在了同目錄下的 requirements.txt 文件里,內(nèi)容如下所示:

1 $ cat requirements.txt

2 Flask

而將這樣一個(gè)應(yīng)用容器化的第一步,是制作容器鏡像。

不過(guò),相較于我之前介紹的制作 rootfs 的過(guò)程,Docker 為你提供了一種更便捷的方式,叫作Dockerfile,如下所示。

1 # 使用官方提供的 Python 開(kāi)發(fā)鏡像作為基礎(chǔ)鏡像

2 FROM python:2.7-slim

3

4 # 將工作目錄切換為/app

5 WORKDIR /app

6

7 # 將當(dāng)前目錄下的所有內(nèi)容復(fù)制到 /app 下

8 ADD . /app

9

10 # 使用 pip 命令安裝這個(gè)應(yīng)用所需要的依賴

11 RUN pip install --trusted-hostpypi.python.org -r requirements.txt

12

13 # 允許外界訪問(wèn)容器的 80 端口

14 EXPOSE 80

15

16 # 設(shè)置環(huán)境變量

17 ENV NAME World

18

19 # 設(shè)置容器進(jìn)程為:python app.py,即:這個(gè) Python 應(yīng)用的啟動(dòng)命令

20 CMD ["python","app.py"]

通過(guò)這個(gè)文件的內(nèi)容,你可以看到Dockerfile 的設(shè)計(jì)思想,是使用一些標(biāo)準(zhǔn)的原語(yǔ)(即大寫(xiě)高亮的詞語(yǔ)),描述我們所要構(gòu)建的 Docker 鏡像。并且這些原語(yǔ),都是按順序處理的。

比如 FROM 原語(yǔ),指定了“python:2.7-slim”這個(gè)官方維護(hù)的基礎(chǔ)鏡像,從而免去了安裝Python 等語(yǔ)言環(huán)境的操作。否則,這一段我們就得這么寫(xiě)了:

1 FROM ubuntu:latest

2 RUN apt-get update -yRUN apt-getinstall -y python-pip python-dev build-essential

3...

其中,RUN 原語(yǔ)就是在容器里執(zhí)行 shell 命令的意思。

而 WORKDIR,意思是在這一句之后,Dockerfile 后面的操作都以這一句指定的 /app 目錄作為當(dāng)前目錄。

所以,到了最后的 CMD,意思是 Dockerfile 指定 python app.py 為這個(gè)容器的進(jìn)程。這里,app.py 的實(shí)際路徑是 /app/app.py。所以,CMD [“python”, “app.py”] 等價(jià)于"docker run python app.py"。

另外,在使用 Dockerfile 時(shí),你可能還會(huì)看到一個(gè)叫作ENTRYPOINT 的原語(yǔ)。實(shí)際上,它和CMD 都是 Docker 容器進(jìn)程啟動(dòng)所必需的參數(shù),完整執(zhí)行格式是:“ENTRYPOINT CMD”。

但是,默認(rèn)情況下,Docker 會(huì)為你提供一個(gè)隱含的ENTRYPOINT,即:/bin/sh -c。所以,在不指定 ENTRYPOINT 時(shí),比如在我們這個(gè)例子里,實(shí)際上運(yùn)行在容器里的完整進(jìn)程是:/bin/sh -c “python app.py”,即 CMD 的內(nèi)容就是 ENTRYPOINT 的參數(shù)。

? ? ? ? 備注:基于以上原因,我們后面會(huì)統(tǒng)一稱 Docker 容器的啟動(dòng)進(jìn)程為ENTRYPOINT,而不是 CMD。

要注意的是,Dockerfile 里的原語(yǔ)并不都是指對(duì)容器內(nèi)部的操作。就比如 ADD,它指的是把當(dāng)前目錄(即 Dockerfile 所在的目錄)里的文件,復(fù)制到指定容器內(nèi)的目錄當(dāng)中。

讀懂這個(gè) Dockerfile 之后,我再把上述內(nèi)容,保存到當(dāng)前目錄里一個(gè)名叫“Dockerfile”的文件中:

1 $ ls

2 Dockerfile app.py requirements.txt

接下來(lái),我就可以讓 Docker 制作這個(gè)鏡像了,在當(dāng)前目錄執(zhí)行:

1$ docker build -t helloworld .

其中,-t 的作用是給這個(gè)鏡像加一個(gè) Tag,即:起一個(gè)好聽(tīng)的名字。docker build 會(huì)自動(dòng)加載當(dāng)前目錄下的 Dockerfile 文件,然后按照順序,執(zhí)行文件中的原語(yǔ)。而這個(gè)過(guò)程,實(shí)際上可以等同于 Docker 使用基礎(chǔ)鏡像啟動(dòng)了一個(gè)容器,然后在容器中依次執(zhí)行 Dockerfile 中的原語(yǔ)。

需要注意的是,Dockerfile 中的每個(gè)原語(yǔ)執(zhí)行后,都會(huì)生成一個(gè)對(duì)應(yīng)的鏡像層。即使原語(yǔ)本身并沒(méi)有明顯地修改文件的操作(比如,ENV 原語(yǔ)),它對(duì)應(yīng)的層也會(huì)存在。只不過(guò)在外界看來(lái),這個(gè)層是空的。

docker build 操作完成后,我可以通過(guò) docker images 命令查看結(jié)果:

1$ docker image ls

2????

3REPOSITORY? ? ??TAG? ? ? ? IMAGE ID

4helloworld? ? ? ? ? ? ?latest? ? ? ?653287cdf998

通過(guò)這個(gè)鏡像 ID,你就可以使用在《白話容器基礎(chǔ)(三):深入理解容器鏡像》中講過(guò)的方法,查看這些新增的層在 AuFS 路徑下對(duì)應(yīng)的文件和目錄了。

接下來(lái),我使用這個(gè)鏡像,通過(guò) docker run 命令啟動(dòng)容器:

1$ docker run -p 4000:80helloworld

在這一句命令中,鏡像名 helloworld 后面,我什么都不用寫(xiě),因?yàn)樵?Dockerfile 中已經(jīng)指定了 CMD。否則,我就得把進(jìn)程的啟動(dòng)命令加在后面:

1$ docker run -p 4000:80helloworld python app.py

容器啟動(dòng)之后,我可以使用 docker ps 命令看到:

1 $ docker ps

2 CONTAINER ID? ? ? ? ?IMAGE? ? ? ? ? ? ?COMMAND? ? ? ? ? ? ?CREATED

3 4ddf4638572d? ? ? ? ? ?helloworld? ? ? ??"python app.py"? ? ? ?10 seconds ago

同時(shí),我已經(jīng)通過(guò) -p 4000:80 告訴了 Docker,請(qǐng)把容器內(nèi)的 80 端口映射在宿主機(jī)的4000端口上。

這樣做的目的是,只要訪問(wèn)宿主機(jī)的 4000 端口,我就可以看到容器里應(yīng)用返回的結(jié)果:

1 $ curl http://localhost:400

2 <h3>Hello World!</h3><b>Hostname:</b> 4ddf4638572d<br/>

否則,我就得先用 docker inspect 命令查看容器的IP 地址,然后訪問(wèn)“http://< 容器 IP 地址>:80”才可以看到容器內(nèi)應(yīng)用的返回。

至此,我已經(jīng)使用容器完成了一個(gè)應(yīng)用的開(kāi)發(fā)與測(cè)試,如果現(xiàn)在想要把這個(gè)容器的鏡像上傳到DockerHub 上分享給更多的人,我要怎么做呢?

為了能夠上傳鏡像,我首先需要注冊(cè)一個(gè) Docker Hub 賬號(hào),然后使用docker login 命令登錄。

接下來(lái),我要用 docker tag 命令給容器鏡像起一個(gè)完整的名字:

1 $ docker tag helloworldgeektime/helloworld:v1

注意:你自己做實(shí)驗(yàn)時(shí),請(qǐng)將 "geektime" 替換成你自己的 Docker Hub 賬戶名稱,比如zhangsan/helloworld:v1

其中,geektime 是我在 Docker Hub 上的用戶名,它的“學(xué)名”叫鏡像倉(cāng)庫(kù)(Repository);“/”后面的 helloworld 是這個(gè)鏡像的名字,而“v1”則是我給這個(gè)鏡像分配的版本號(hào)。

然后,我執(zhí)行 docker push:

1$ docker pushgeektime/helloworld:v1

這樣,我就可以把這個(gè)鏡像上傳到 Docker Hub 上了。

此外,我還可以使用 docker commit 指令,把一個(gè)正在運(yùn)行的容器,直接提交為一個(gè)鏡像。一般來(lái)說(shuō),需要這么操作原因是:這個(gè)容器運(yùn)行起來(lái)后,我又在里面做了一些操作,并且要把操作結(jié)果保存到鏡像里,比如:

1 $ docker exec -it 4ddf4638572d/bin/sh

2 # 在容器內(nèi)部新建了一個(gè)文件

3 root@4ddf4638572d:/app# touchtest.txt

4root@4ddf4638572d:/app# exit

5

6 # 將這個(gè)新建的文件提交到鏡像中保存

7 $ docker commit 4ddf4638572dgeektime/helloworld:v2

這里,我使用了 docker exec 命令進(jìn)入到了容器當(dāng)中。在了解了 Linux Namespace 的隔離機(jī)制后,你應(yīng)該會(huì)很自然地想到一個(gè)問(wèn)題:docker exec 是怎么做到進(jìn)入容器里的呢?

實(shí)際上,Linux Namespace 創(chuàng)建的隔離空間雖然看不見(jiàn)摸不著,但一個(gè)進(jìn)程的 Namespace 信息在宿主機(jī)上是確確實(shí)實(shí)存在的,并且是以一個(gè)文件的方式存在。

比如,通過(guò)如下指令,你可以看到當(dāng)前正在運(yùn)行的 Docker 容器的進(jìn)程號(hào)(PID)是 25686:

1 $ docker inspect --format '{{.State.Pid }}'? ? ?4ddf4638572d

2 25686

這時(shí),你可以通過(guò)查看宿主機(jī)的 proc 文件,看到這個(gè) 25686進(jìn)程的所有 Namespace 對(duì)應(yīng)的文件:

1 $ ls -l? /proc/25686/ns

2 total 0

3 lrwxrwxrwx 1 root root 0 Aug 1314:05 cgroup -> cgroup:[4026531835]

4 lrwxrwxrwx 1 root root 0 Aug 1314:05 ipc -> ipc:[4026532278]

5 lrwxrwxrwx 1 root root 0 Aug 1314:05 mnt -> mnt:[4026532276]

6 lrwxrwxrwx 1 root root 0 Aug 1314:05 net -> net:[4026532281]

7 lrwxrwxrwx 1 root root 0 Aug 1314:05 pid -> pid:[4026532279]

8 lrwxrwxrwx 1 root root 0 Aug 1314:05 pid_for_children -> pid:[4026532279]

9 lrwxrwxrwx 1 root root 0 Aug 1314:05 user -> user:[4026531837]

10 lrwxrwxrwx 1 root root 0 Aug 1314:05 uts -> uts:[4026532277]

可以看到,一個(gè)進(jìn)程的每種 Linux Namespace,都在它對(duì)應(yīng)的 /proc/[進(jìn)程號(hào)]/ns 下有一個(gè)對(duì)應(yīng)的虛擬文件,并且鏈接到一個(gè)真實(shí)的 Namespace 文件上。

有了這樣一個(gè)可以“hold 住”所有 Linux Namespace 的文件,我們就可以對(duì) Namespace 做一些很有意義事情了,比如:加入到一個(gè)已經(jīng)存在的 Namespace 當(dāng)中。

這也就意味著:一個(gè)進(jìn)程,可以選擇加入到某個(gè)進(jìn)程已有的 Namespace 當(dāng)中,從而達(dá)到“進(jìn)入”這個(gè)進(jìn)程所在容器的目的,這正是 docker exec 的實(shí)現(xiàn)原理。

而這個(gè)操作所依賴的,乃是一個(gè)名叫 setns() 的 Linux 系統(tǒng)調(diào)用。它的調(diào)用方法,我可以用如下一段小程序?yàn)槟阏f(shuō)明:

1#define _GNU_SOURCE

2 #include <fcntl.h>

3 #include <sched.h>

4 #include <unistd.h>

5 #include <stdlib.h>

6 #include <stdio.h>

7

8 #define errExit(msg) do { perror(msg); exit(EXIT_FAILURE);} while (0)

9

10 int main(int argc, char *argv[]) {

11? ? ? int d;

12

13? ? ? fd = open(argv[1], O_RDONLY);

14? ? ? if (setns(fd, 0) == -1) {

15? ? ? ? ? ? errExit("setns");

16? ? ? }

17? ? ? execvp(argv[2], &argv[2]);

18? ? ? errExit("execvp");

19 }

這段代碼功能非常簡(jiǎn)單:它一共接收兩個(gè)參數(shù),第一個(gè)參數(shù)是 argv[1],即當(dāng)前進(jìn)程要加入的Namespace 文件的路徑,比如 /proc/25686/ns/net;而第二個(gè)參數(shù),則是你要在這個(gè)Namespace 里運(yùn)行的進(jìn)程,比如 /bin/bash。

這段代碼的的核心操作,則是通過(guò) open() 系統(tǒng)調(diào)用打開(kāi)了指定的 Namespace 文件,并把這個(gè)文件的描述符 fd 交給 setns() 使用。在 setns() 執(zhí)行后,當(dāng)前進(jìn)程就加入了這個(gè)文件對(duì)應(yīng)的Linux Namespace 當(dāng)中了。

現(xiàn)在,你可以編譯執(zhí)行一下這個(gè)程序,加入到容器進(jìn)程(PID=25686)的 Network Namespace 中:

1 $ gcc -o set_ns set_ns.c

2 $ ./set_ns /proc/25686/ns/net /bin/bash

3 $ ifconfig

4 eth0? ? ? ? ? ? ? ? ? Link encap:Ethernet? HWaddr 02:42:ac:11:00:02

5? ? ? ? ? ? ? ? ? ? ? ? ? ? inet addr:172.17.0.2? Bcast:0.0.0.0? ? Mask:255.255.0.0

6? ? ? ? ? ? ? ? ? ? ? ? ? ? inet6 addr: fe80::42:acff:fe11:2/64 Scope:Link

7? ? ? ? ? ? ? ? ? ? ? ? ? ? UP BROADCAST RUNNING MULTICAST? MTU:1500? Metric:1

8? ? ? ? ? ? ? ? ? ? ? ? ? ? RX packets:12 errors:0 dropped:0 overruns:0 frame:0

9? ? ? ? ? ? ? ? ? ? ? ? ? ? TX packets:10 errors:0 dropped:0 overruns:0 carrier:0

10? ? ? ? ? ? ? ? ? ? ? ? ? collisions:0 txqueuelen:0

11? ? ? ? ? ? ? ? ? ? ? ? ? RX bytes:976 (976.0 B)? ? TX bytes:796 (796.0 B)

12? ?

13 lo? ? ? ? ? ? ? ? ? ? ? Link encap:Local Loopback

14? ? ? ? ? ? ? ? ? ? ? ? ? inet addr:127.0.0.1? ? ? Mask:255.0.0.0

15? ? ? ? ? ? ? ? ? ? ? ? ? inet6 addr: ::1/128 Scope:Host

16? ? ? ? ? ? ? ? ? ? ? ? ? UP LOOPBACK RUNNING? ? ? MTU:65536? ? Metric:1

17? ? ? ? ? ? ? ? ? ? ? ? ? RX packets:0 errors:0 dropped:0 overruns:0 frame:0

18? ? ? ? ? ? ? ? ? ? ? ? ? TX packets:0 errors:0 dropped:0 overruns:0 carrier:0

19? ? ? ? ? ? ? ? ? ? ? ? ? ?collisions:0 txqueuelen:1000

20? ? ? ? ? ? ? ? ? ? ? ? ? ?RX bytes:0 (0.0 B)? TX bytes:0 (0.0 B)

正如上所示,當(dāng)我們執(zhí)行 ifconfig 命令查看網(wǎng)絡(luò)設(shè)備時(shí),我會(huì)發(fā)現(xiàn)能看到的網(wǎng)卡“變少”了:只有兩個(gè)。而我的宿主機(jī)則至少有四個(gè)網(wǎng)卡。這是怎么回事呢?

實(shí)際上,在 setns() 之后我看到的這兩個(gè)網(wǎng)卡,正是我在前面啟動(dòng)的 Docker 容器里的網(wǎng)卡。也就是說(shuō),我新創(chuàng)建的這個(gè) /bin/bash 進(jìn)程,由于加入了該容器進(jìn)程(PID=25686)的Network Namepace,它看到的網(wǎng)絡(luò)設(shè)備與這個(gè)容器里是一樣的,即:/bin/bash 進(jìn)程的網(wǎng)絡(luò)設(shè)備視圖,也被修改了。

而一旦一個(gè)進(jìn)程加入到了另一個(gè) Namespace 當(dāng)中,在宿主機(jī)的Namespace 文件上,也會(huì)有所體現(xiàn)。

在宿主機(jī)上,你可以用 ps 指令找到這個(gè) set_ns 程序執(zhí)行的 /bin/bash 進(jìn)程,其真實(shí)的 PID 是28499:

1 # 在宿主機(jī)上

2 ps aux | grep /bin/bash

3 root? ? ?28499? ??0.0? ??0.0 19944? ??3612 pts/0? ?S? ?14:15? ??0:00 /bin/bash

這時(shí),如果按照前面介紹過(guò)的方法,查看一下這個(gè) PID=28499 的進(jìn)程的 Namespace,你就會(huì)發(fā)現(xiàn)這樣一個(gè)事實(shí):

1 $ ls -l /proc/28499/ns/net

2 lrwxrwxrwx 1 root root 0 Aug 1314:18 /proc/28499/ns/net -> net:[4026532281]

3

4$ ls -l? ? ?/proc/25686/ns/net

5 lrwxrwxrwx 1 root root 0 Aug 1314:05 /proc/25686/ns/net -> net:[4026532281]

在 /proc/[PID]/ns/net 目錄下,這個(gè)PID=28499 進(jìn)程,與我們前面的 Docker 容器進(jìn)程(PID=25686)指向的 Network Namespace 文件完全一樣。這說(shuō)明這兩個(gè)進(jìn)程,共享了這個(gè)名叫 net:[4026532281] 的 Network Namespace。

此外,Docker 還專門(mén)提供了一個(gè)參數(shù),可以讓你啟動(dòng)一個(gè)容器并“加入”到另一個(gè)容器的Network Namespace 里,這個(gè)參數(shù)就是 -net,比如:

1 $ docker run -it --netcontainer:4ddf4638572d busybox ifconfig

這樣,我們新啟動(dòng)的這個(gè)容器,就會(huì)直接加入到 ID=4ddf4638572d 的容器,也就是我們前面的創(chuàng)建的 Python 應(yīng)用容器(PID=25686)的 Network Namespace 中。所以,這里ifconfig返回的網(wǎng)卡信息,跟我前面那個(gè)小程序返回的結(jié)果一模一樣,你也可以嘗試一下。

而如果我指定–net=host,就意味著這個(gè)容器不會(huì)為進(jìn)程啟用Network Namespace。這就意味著,這個(gè)容器拆除了 Network Namespace 的“隔離墻”,所以,它會(huì)和宿主機(jī)上的其他普通進(jìn)程一樣,直接共享宿主機(jī)的網(wǎng)絡(luò)棧。這就為容器直接操作和使用宿主機(jī)網(wǎng)絡(luò)提供了一個(gè)渠道。

轉(zhuǎn)了一個(gè)大圈子,我其實(shí)是為你詳細(xì)解讀了 docker exec 這個(gè)操作背后,Linux Namespace 更具體的工作原理。

這種通過(guò)操作系統(tǒng)進(jìn)程相關(guān)的知識(shí),逐步剖析 Docker 容器的方法,是理解容器的一個(gè)關(guān)鍵思路,希望你一定要掌握。

現(xiàn)在,我們?cè)僖黄鸹氐角懊嫣峤荤R像的操作 docker commit 上來(lái)吧。

docker commit,實(shí)際上就是在容器運(yùn)行起來(lái)后,把最上層的“可讀寫(xiě)層”,加上原先容器鏡像的只讀層,打包組成了一個(gè)新的鏡像。當(dāng)然,下面這些只讀層在宿主機(jī)上是共享的,不會(huì)占用額外的空間。

而由于使用了聯(lián)合文件系統(tǒng),你在容器里對(duì)鏡像 rootfs 所做的任何修改,都會(huì)被操作系統(tǒng)先復(fù)制到這個(gè)可讀寫(xiě)層,然后再修改。這就是所謂的:Copy-on-Write。

而正如前所說(shuō),Init 層的存在,就是為了避免你執(zhí)行 docker commit 時(shí),把 Docker 自己對(duì)/etc/hosts 等文件做的修改,也一起提交掉。

有了新的鏡像,我們就可以把它推送到 Docker Hub 上了:

1 $ docker pushgeektime/helloworld:v2

你可能還會(huì)有這樣的問(wèn)題:我在企業(yè)內(nèi)部,能不能也搭建一個(gè)跟 Docker Hub 類似的鏡像上傳系統(tǒng)呢?

當(dāng)然可以,這個(gè)統(tǒng)一存放鏡像的系統(tǒng),就叫作 Docker Registry。感興趣的話,你可以查看Docker 的官方文檔,以及VMware 的 Harbor 項(xiàng)目。

最后,我再來(lái)講解一下 Docker 項(xiàng)目另一個(gè)重要的內(nèi)容:Volume(數(shù)據(jù)卷)。

前面我已經(jīng)介紹過(guò),容器技術(shù)使用了 rootfs 機(jī)制和 Mount Namespace,構(gòu)建出了一個(gè)同宿主機(jī)完全隔離開(kāi)的文件系統(tǒng)環(huán)境。這時(shí)候,我們就需要考慮這樣兩個(gè)問(wèn)題:

1. 容器里進(jìn)程新建的文件,怎么才能讓宿主機(jī)獲取到?

2. 宿主機(jī)上的文件和目錄,怎么才能讓容器里的進(jìn)程訪問(wèn)到?

這正是 Docker Volume 要解決的問(wèn)題:Volume 機(jī)制,允許你將宿主機(jī)上指定的目錄或者文件,掛載到容器里面進(jìn)行讀取和修改操作。

在 Docker 項(xiàng)目里,它支持兩種 Volume 聲明方式,可以把宿主機(jī)目錄掛載進(jìn)容器的 /test 目錄當(dāng)中:

1 $ docker run -v /test ...

2 $ docker run -v /home:/test ...

而這兩種聲明方式的本質(zhì),實(shí)際上是相同的:都是把一個(gè)宿主機(jī)的目錄掛載進(jìn)了容器的 /test 目錄。

只不過(guò),在第一種情況下,由于你并沒(méi)有顯示聲明宿主機(jī)目錄,那么 Docker 就會(huì)默認(rèn)在宿主機(jī)上創(chuàng)建一個(gè)臨時(shí)目錄 /var/lib/docker/volumes/[VOLUME_ID]/_data,然后把它掛載到容器的/test 目錄上。而在第二種情況下,Docker 就直接把宿主機(jī)的/home 目錄掛載到容器的/test目錄上。

那么,Docker 又是如何做到把一個(gè)宿主機(jī)上的目錄或者文件,掛載到容器里面去呢?難道又是Mount Namespace 的黑科技嗎?

實(shí)際上,并不需要這么麻煩。

在《白話容器基礎(chǔ)(三):深入理解容器鏡像》的分享中,我已經(jīng)介紹過(guò),當(dāng)容器進(jìn)程被創(chuàng)建之后,盡管開(kāi)啟了 Mount Namespace,但是在它執(zhí)行chroot(或者 pivot_root)之前,容器進(jìn)程一直可以看到宿主機(jī)上的整個(gè)文件系統(tǒng)。

而宿主機(jī)上的文件系統(tǒng),也自然包括了我們要使用的容器鏡像。這個(gè)鏡像的各個(gè)層,保存在/var/lib/docker/aufs/diff 目錄下,在容器進(jìn)程啟動(dòng)后,它們會(huì)被聯(lián)合掛載在/var/lib/docker/aufs/mnt/ 目錄中,這樣容器所需的 rootfs 就準(zhǔn)備好了。

所以,我們只需要在 rootfs 準(zhǔn)備好之后,在執(zhí)行 chroot之前,把 Volume 指定的宿主機(jī)目錄(比如 /home 目錄),掛載到指定的容器目錄(比如 /test目錄)在宿主機(jī)上對(duì)應(yīng)的目錄(即/var/lib/docker/aufs/mnt/[可讀寫(xiě)層 ID]/test)上,這個(gè) Volume 的掛載工作就完成了。

更重要的是,由于執(zhí)行這個(gè)掛載操作時(shí),“容器進(jìn)程”已經(jīng)創(chuàng)建了,也就意味著此時(shí)Mount Namespace 已經(jīng)開(kāi)啟了。所以,這個(gè)掛載事件只在這個(gè)容器里可見(jiàn)。你在宿主機(jī)上,是看不見(jiàn)容器內(nèi)部的這個(gè)掛載點(diǎn)的。這就保證了容器的隔離性不會(huì)被 Volume 打破。

注意:這里提到的 " 容器進(jìn)程 ",是 Docker 創(chuàng)建的一個(gè)容器初始化進(jìn)程(dockerinit),而不是應(yīng)用進(jìn)程 (ENTRYPOINT + CMD)。dockerinit會(huì)負(fù)責(zé)完成根目錄的準(zhǔn)備、掛載設(shè)備和目錄、配置 hostname 等一系列需要在容器內(nèi)進(jìn)行的初始化操作。最后,它通過(guò) execv() 系統(tǒng)調(diào)用,讓?xiě)?yīng)用進(jìn)程取代自己,成為容器里的 PID=1 的進(jìn)程。

而這里要使用到的掛載技術(shù),就是 Linux 的綁定掛載(bind mount)機(jī)制。它的主要作用就是,允許你將一個(gè)目錄或者文件,而不是整個(gè)設(shè)備,掛載到一個(gè)指定的目錄上。并且,這時(shí)你在該掛載點(diǎn)上進(jìn)行的任何操作,只是發(fā)生在被掛載的目錄或者文件上,而原掛載點(diǎn)的內(nèi)容則會(huì)被隱藏起來(lái)且不受影響。

其實(shí),如果你了解 Linux 內(nèi)核的話,就會(huì)明白,綁定掛載實(shí)際上是一個(gè) inode 替換的過(guò)程。在Linux 操作系統(tǒng)中,inode 可以理解為存放文件內(nèi)容的“對(duì)象”,而dentry,也叫目錄項(xiàng),就是訪問(wèn)這個(gè) inode 所使用的“指針”。

正如上圖所示,mount --bind /home /test,會(huì)將 /home 掛載到 /test 上。其實(shí)相當(dāng)于將/test 的 dentry,重定向到了 /home 的 inode。這樣當(dāng)我們修改 /test 目錄時(shí),實(shí)際修改的是/home 目錄的 inode。這也就是為何,一旦執(zhí)行 umount 命令,/test 目錄原先的內(nèi)容就會(huì)恢復(fù):因?yàn)樾薷恼嬲l(fā)生在的,是 /home 目錄里。

所以,在一個(gè)正確的時(shí)機(jī),進(jìn)行一次綁定掛載,Docker 就可以成功地將一個(gè)宿主機(jī)上的目錄或文件,不動(dòng)聲色地掛載到容器中。

這樣,進(jìn)程在容器里對(duì)這個(gè) /test 目錄進(jìn)行的所有操作,都實(shí)際發(fā)生在宿主機(jī)的對(duì)應(yīng)目錄(比如,/home,或者/var/lib/docker/volumes/[VOLUME_ID]/_data)里,而不會(huì)影響容器鏡像的內(nèi)容。

那么,這個(gè) /test 目錄里的內(nèi)容,既然掛載在容器 rootfs的可讀寫(xiě)層,它會(huì)不會(huì)被dockercommit 提交掉呢?

也不會(huì)。

這個(gè)原因其實(shí)我們前面已經(jīng)提到過(guò)。容器的鏡像操作,比如 docker commit,都是發(fā)生在宿主機(jī)空間的。而由于 Mount Namespace 的隔離作用,宿主機(jī)并不知道這個(gè)綁定掛載的存在。所以,在宿主機(jī)看來(lái),容器中可讀寫(xiě)層的 /test 目錄(/var/lib/docker/aufs/mnt/[可讀寫(xiě)層ID]/test),始終是空的。

不過(guò),由于 Docker 一開(kāi)始還是要?jiǎng)?chuàng)建 /test 這個(gè)目錄作為掛載點(diǎn),所以執(zhí)行了dockercommit 之后,你會(huì)發(fā)現(xiàn)新產(chǎn)生的鏡像里,會(huì)多出來(lái)一個(gè)空的 /test 目錄。畢竟,新建目錄操作,又不是掛載操作,Mount Namespace 對(duì)它可起不到“障眼法”的作用。

結(jié)合以上的講解,我們現(xiàn)在來(lái)親自驗(yàn)證一下:

首先,啟動(dòng)一個(gè) helloworld 容器,給它聲明一個(gè)Volume,掛載在容器里的 /test 目錄上:

1 $ docker run -d -v /testhelloworld

2 cf53b766fa6f

容器啟動(dòng)之后,我們來(lái)查看一下這個(gè) Volume 的 ID:

1 $ docker volume ls

2 DRIVER? ? ? ? ? ? ? ? ? ? ? ? ? ?VOLUME NAME

3 local? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?cb1c2f7221fa9b0971cc35f68aa1034824755ac44a034c0c0a1dd318838d3a6d

然后,使用這個(gè) ID,可以找到它在 Docker 工作目錄下的 volumes 路徑:

1 $ ls/var/lib/docker/volumes/cb1c2f7221fa/_data/

這個(gè) _data 文件夾,就是這個(gè)容器的 Volume 在宿主機(jī)上對(duì)應(yīng)的臨時(shí)目錄了。

接下來(lái),我們?cè)谌萜鞯?Volume 里,添加一個(gè)文件text.txt:

1 $ docker exec -it cf53b766fa6f/bin/sh

2 cd test/

3 touch text.txt

這時(shí),我們?cè)倩氐剿拗鳈C(jī),就會(huì)發(fā)現(xiàn) text.txt 已經(jīng)出現(xiàn)在了宿主機(jī)上對(duì)應(yīng)的臨時(shí)目錄里:

1 $ ls/var/lib/docker/volumes/cb1c2f7221fa/_data/?

2 text.txt

可是,如果你在宿主機(jī)上查看該容器的可讀寫(xiě)層,雖然可以看到這個(gè) /test 目錄,但其內(nèi)容是空的(關(guān)于如何找到這個(gè) AuFS 文件系統(tǒng)的路徑,請(qǐng)參考我上一次分享的內(nèi)容):

1 $ ls/var/lib/docker/aufs/mnt/6780d0778b8a/test

可以確認(rèn),容器 Volume 里的信息,并不會(huì)被 docker commit 提交掉;但這個(gè)掛載點(diǎn)目錄/test 本身,則會(huì)出現(xiàn)在新的鏡像當(dāng)中。

以上內(nèi)容,就是 Docker Volume 的核心原理了。

總結(jié)

在今天的這次分享中,我用了一個(gè)非常經(jīng)典的 Python 應(yīng)用作為案例,講解了 Docke 容器使用的主要場(chǎng)景。熟悉了這些操作,你也就基本上摸清了 Docker 容器的核心功能。

更重要的是,我著重介紹了如何使用 Linux Namespace、Cgroups,以及 rootfs 的知識(shí),對(duì)容器進(jìn)行了一次庖丁解牛似的解讀。

借助這種思考問(wèn)題的方法,最后的 Docker 容器,我們實(shí)際上就可以用下面這個(gè)“全景圖”描述出來(lái):

這個(gè)容器進(jìn)程“python app.py”,運(yùn)行在由 Linux Namespace 和 Cgroups 構(gòu)成的隔離環(huán)境里;而它運(yùn)行所需要的各種文件,比如 python,app.py,以及整個(gè)操作系統(tǒng)文件,則由多個(gè)聯(lián)合掛載在一起的 rootfs 層提供。

這些 rootfs 層的最下層,是來(lái)自 Docker 鏡像的只讀層。

在只讀層之上,是 Docker 自己添加的 Init 層,用來(lái)存放被臨時(shí)修改過(guò)的 /etc/hosts 等文件。

而 rootfs 的最上層是一個(gè)可讀寫(xiě)層,它以Copy-on-Write 的方式存放任何對(duì)只讀層的修改,容器聲明的 Volume 的掛載點(diǎn),也出現(xiàn)在這一層。

通過(guò)這樣的剖析,對(duì)于曾經(jīng)“神秘莫測(cè)”的容器技術(shù),你是不是感覺(jué)清晰了很多呢?

思考題

1. 你在查看 Docker 容器的 Namespace 時(shí),是否注意到有一個(gè)叫 cgroup 的 Namespace?它是 Linux 4.6 之后新增加的一個(gè) Namespace,你知道它的作用嗎?

2. 如果你執(zhí)行 docker run -v /home:/test 的時(shí)候,容器鏡像里的 /test 目錄下本來(lái)就有內(nèi)容的話,你會(huì)發(fā)現(xiàn),在宿主機(jī)的 /home 目錄下,也會(huì)出現(xiàn)這些內(nèi)容。這是怎么回事?為什么它們沒(méi)有被綁定掛載隱藏起來(lái)呢?(提示:Docker 的“copyData”功能)

3. 請(qǐng)嘗試給這個(gè) Python 應(yīng)用加上 CPU 和 Memory 限制,然后啟動(dòng)它。根據(jù)我們前面介紹的 Cgroups 的知識(shí),請(qǐng)你查看一下這個(gè)容器的 Cgroups文件系統(tǒng)的設(shè)置,是不是跟我前面的講解一致。

感謝你的收聽(tīng),歡迎你給我留言,也歡迎分享給更多的朋友一起閱讀。


文章回復(fù):

一步

這樣把原理刨根究底的講解出來(lái),很好,理解的很透徹

2018-09-10

黃文剛

收貨很大,感謝張磊!請(qǐng)教一個(gè)問(wèn)題,請(qǐng)問(wèn)在容器內(nèi)部如何獲取宿主機(jī)的IP? 謝謝。

2018-09-10

作者回復(fù)

單靠容器,在隔離開(kāi)的情況下是拿不到的。但是有了kubernetes之后這些系統(tǒng)信息都可以從環(huán)境變量里拿到。這個(gè)功能叫downwardapi

2018-09-10

與路同飛

有預(yù)感專欄會(huì)破2萬(wàn)

2018-09-10

Liam

這節(jié)干貨滿滿啊

2018-09-10

多肉

一切從問(wèn)題出發(fā),根據(jù)問(wèn)題理解答案,總結(jié)問(wèn)題如下:

一、docker鏡像如何制作的兩種方式是什么?

二、容器既然是一個(gè)封閉的進(jìn)程,那么外接程序是如何進(jìn)入容器這個(gè)進(jìn)程的呢?

三、docker commit對(duì)掛載點(diǎn)volume內(nèi)容修改的影響是什么?

四、容器與宿主機(jī)如何進(jìn)行文件讀寫(xiě)?或volume是為了解決什么題?

五、Docker的copyData功能是什么?解決了什么問(wèn)題?

六、bind mount機(jī)制是什么?

七、cgroup Namespace的作用是什么?

2018-09-17

獸醫(yī)

講得非常不錯(cuò),曾經(jīng)翻遍了幾乎所有Docker官方文檔,都沒(méi)教程中來(lái)得深刻,謝謝。。

2018-09-13

假裝樂(lè)

聽(tīng)來(lái)清晰易懂,省去不少學(xué)習(xí)時(shí)間

蔡鵬飛

docker run 時(shí)指定-v掛載宿主機(jī)目錄到容器目錄,即使容器原有目錄內(nèi)有數(shù)據(jù),也會(huì)被我宿主機(jī)目錄數(shù)據(jù)替代的呀。難道是和我使用的storage-driver有關(guān)?我用的是overlay存儲(chǔ)。

2018-09-12

作者回復(fù)

這個(gè)行為其實(shí)是可配置的

2018-09-14

manatee

非常感謝老師的講解,咱們這個(gè)有群可以互相交流嗎?

2018-09-10

陶希陽(yáng)

想知道云服務(wù)器等技術(shù)是不是也是通過(guò)namespace + cgroup實(shí)習(xí)的?

2018-09-10

作者回復(fù)

當(dāng)然不是,那可是正兒八經(jīng)的虛擬化技術(shù)

2018-09-10

jason_liew

精彩精彩!有些地方自己折騰狠難理解到!多謝老師高維指點(diǎn)。

2018-09-12

jssfy

請(qǐng)問(wèn)docker掛載有何限制沒(méi),是否隨便一個(gè)目錄都可以掛載?在容器里應(yīng)該是root用戶,豈不是可以對(duì)目錄無(wú)節(jié)制地操作,哪怕原本主機(jī)目錄中有些文件并不允許當(dāng)前用戶訪問(wèn)?是否可以相應(yīng)限制

2018-09-11

作者回復(fù)

無(wú)限制。至于用戶權(quán)限,是有user namespace可以做一定的限制。

2018-09-11

Casper

對(duì)于linux大部分容器做不到在運(yùn)行容器中動(dòng)態(tài)添加宿主機(jī)目錄,那在什么特定場(chǎng)合下可以做到呢?給個(gè)大致思路即可,謝謝。

2018-09-10

作者回復(fù)

如果你用的是katacontainers這種基于虛擬化的容器,才可以實(shí)現(xiàn)。但原生其實(shí)也不提供這個(gè)功能。

2018-09-10

擇動(dòng)

在等更新,沙發(fā)!

2018-09-10

Leon廖

謝謝老師!前4節(jié)高屋建瓴地介紹了Docker和K8S的演進(jìn)歷史,接著4節(jié)Docker基礎(chǔ)深入淺出地介紹了重要的基礎(chǔ)知識(shí),受益匪淺,這4節(jié)Docker基礎(chǔ)再次讓我體會(huì)到了醍醐灌頂?shù)母杏X(jué)。

2018-09-26

多肉

課程干貨滿滿,一堂課下來(lái)不聽(tīng)上幾遍根本無(wú)法掌握課中的知識(shí)點(diǎn),一切從問(wèn)題出發(fā),能明白課中解決了什么問(wèn)題或提出了什么問(wèn)題,從這個(gè)角度出發(fā),更正理清課中講的知識(shí)點(diǎn),總結(jié)內(nèi)容如下:

一、docker鏡像如何制作的兩種方式是什么?

二、容器既然是一個(gè)封閉的進(jìn)程,那么外接程序是如何進(jìn)入容器這個(gè)進(jìn)程的呢?

三、docker commit對(duì)掛載點(diǎn)volume內(nèi)容修改的影響是什么?

四、容器與宿主機(jī)如何進(jìn)行文件讀寫(xiě)?或volume是為了解決什么題?

五、Docker的copyData功能是什么?解決了什么問(wèn)題?

六、bind mount機(jī)制是什么?

七、cgroup Namespace的作用是什么?

2018-09-17

long904

圖中不太明白為什么'CMD'屬于只讀層,那如果 dockfile 里面 yum install 并且 commit 的話,這些 CMD 執(zhí)行的 yum 命令修改的內(nèi)容還屬于只讀層?

2018-09-13

作者回復(fù)

任何鏡像里的內(nèi)容,都屬于只讀層。commit之后的東西當(dāng)然也屬于只讀層。

2018-09-13

一葉

你好,磊哥,謝謝你講得這么詳細(xì),我有一點(diǎn)不是很清楚:

容器中的主進(jìn)程在系統(tǒng)調(diào)用或調(diào)用一些lib時(shí),調(diào)用到的和容器只讀層提供的lib嗎?

2018-09-13

作者回復(fù)

對(duì)的

2018-09-13

jssfy

1. 如果使用user namespace的話,容器里的root是否還是對(duì)文件有權(quán)限?

2. centos 7貌似目前還不支持user namespace

3. 現(xiàn)在部署k8s會(huì)考慮用centos嗎?特別是對(duì)gpu有需求的場(chǎng)景

2018-09-12

作者回復(fù)

kubernetes 肯定不挑操作系統(tǒng)啊

2018-09-13

Geek_zz

你好,可讀寫(xiě)層的修改數(shù)據(jù)保存的話,會(huì)保存到哪里

2018-09-11

作者回復(fù)

所有的層都保存在diff目錄下

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 一、Docker 簡(jiǎn)介 Docker 兩個(gè)主要部件:Docker: 開(kāi)源的容器虛擬化平臺(tái)Docker Hub: 用...
    R_X閱讀 4,511評(píng)論 0 27
  • Docker容器技術(shù)已經(jīng)發(fā)展了好些年,在很多項(xiàng)目都有應(yīng)用,線上運(yùn)行也很穩(wěn)定。整理了部分Docker的學(xué)習(xí)筆記以及新...
    __七把刀__閱讀 11,622評(píng)論 0 58
  • 五、Docker 端口映射 無(wú)論如何,這些 ip 是基于本地系統(tǒng)的并且容器的端口非本地主機(jī)是訪問(wèn)不到的。此外,除了...
    R_X閱讀 1,961評(píng)論 0 7
  • 寫(xiě)這個(gè)系列文章主要是對(duì)之前做項(xiàng)目用到的docker相關(guān)技術(shù)做一些總結(jié),包括docker基礎(chǔ)技術(shù)Linux命名空間,...
    __七把刀__閱讀 5,924評(píng)論 0 16
  • 高效率的學(xué)生 看到學(xué)生這個(gè)詞又讓我想起了自己的學(xué)生時(shí)期,在那個(gè)時(shí)候,每天學(xué)習(xí)的目的僅僅是為了每學(xué)期期中或者期末時(shí)領(lǐng)...
    管飛機(jī)的舒克閱讀 314評(píng)論 1 1

友情鏈接更多精彩內(nèi)容