你好,我是張磊。我在今天這篇文章的最后颓芭,放置了一張 Kubernetes 的技能圖譜,希望對(duì)你有幫助柬赐。
在前兩次的分享中亡问,我講解了 Linux 容器最基礎(chǔ)的兩種技術(shù):Namespace和 Cgroups。希望此時(shí)肛宋,你已經(jīng)徹底理解了“容器的本質(zhì)是一種特殊的進(jìn)程”這個(gè)最重要的概念州藕。
而正如我前面所說(shuō)的,Namespace 的作用是“隔離”酝陈,它讓應(yīng)用進(jìn)程只能看到該Namespace內(nèi)的“世界”床玻;而 Cgroups 的作用是“限制”,它給這個(gè)“世界”圍上了一圈看不見的墻沉帮。這么一折騰锈死,進(jìn)程就真的被“裝”在了一個(gè)與世隔絕的房間里贫堰,而這些房間就是 PaaS 項(xiàng)目賴以生存的應(yīng)用“沙盒”。
可是馅精,還有一個(gè)問(wèn)題不知道你有沒有仔細(xì)思考過(guò):這個(gè)房間四周雖然有了墻严嗜,但是如果容器進(jìn)程低頭一看地面粱檀,又是怎樣一副景象呢洲敢?
換句話說(shuō),容器里的進(jìn)程看到的文件系統(tǒng)又是什么樣子的呢茄蚯?
可能你立刻就能想到压彭,這一定是一個(gè)關(guān)于 Mount Namespace 的問(wèn)題:容器里的應(yīng)用進(jìn)程,理應(yīng)看到一份完全獨(dú)立的文件系統(tǒng)渗常。這樣壮不,它就可以在自己的容器目錄(比如 /tmp)下進(jìn)行操作,而完全不會(huì)受宿主機(jī)以及其他容器的影響皱碘。
那么询一,真實(shí)情況是這樣嗎?
“左耳朵耗子”叔在多年前寫的一篇關(guān)于 Docker 基礎(chǔ)知識(shí)的博客里癌椿,曾經(jīng)介紹過(guò)一段小程序健蕊。
這段小程序的作用是,在創(chuàng)建子進(jìn)程時(shí)開啟指定的 Namespace踢俄。
下面缩功,我們不妨使用它來(lái)驗(yàn)證一下剛剛提到的問(wèn)題。
1 #define _GNU_SOURCE
2 #include <sys/mount.h>
3 #include <sys/types.h>
4 #include <sys/wait.h>
5 #include <stdio.h>
6 #include <sched.h>
7 #include <signal.h>
8 #include <unistd.h>
9 #define STACK_SIZE (1024 * 1024)
10 static char container_stack[STACK_SIZE];
11 char* const container_args[] = {
12? "/bin/bash",
13 NULL
14 };
15
16 int container_main(void* arg)
17 {
18? printf("Container - inside the container!\n");
19 execv(container_args[0], container_args);
20 printf("Something's wrong!\n");
21 return 1;
22 }
23
24 int main()
25 {
26 printf("Parent - start a container!\n");
27 int container_pid = clone(container_main, container_stack+STACK_SIZE, CLONE_NEWNS | SIGCHLD ,
28 waitpid(container_pid, NULL, 0);
29 printf("Parent - container stopped!\n");
30 return 0;
31 }
這段代碼的功能非常簡(jiǎn)單:在 main 函數(shù)里都办,我們通過(guò)clone() 系統(tǒng)調(diào)用創(chuàng)建了一個(gè)新的子進(jìn)程
container_main嫡锌,并且聲明要為它啟用 Mount Namespace(即:CLONE_NEWNS標(biāo)志)。
而這個(gè)子進(jìn)程執(zhí)行的琳钉,是一個(gè)“/bin/bash”程序势木,也就是一個(gè)shell。所以這個(gè) shell 就運(yùn)行在了 Mount Namespace 的隔離環(huán)境中歌懒。
我們來(lái)一起編譯一下這個(gè)程序:
1 $ gcc -o ns ns.c
2 $ ./ns
3 Parent - start a container!
4 Container - inside the container!
這樣啦桌,我們就進(jìn)入了這個(gè)“容器”當(dāng)中〖吲啵可是震蒋,如果在“容器”里執(zhí)行一下 ls 指令的話,我們就會(huì)發(fā)現(xiàn)一個(gè)有趣的現(xiàn)象: /tmp 目錄下的內(nèi)容跟宿主機(jī)的內(nèi)容是一樣的躲庄。
1 $ ls /tmp
2 # 你會(huì)看到好多宿主機(jī)的文件
也就是說(shuō):
? ? ? ?即使開啟了 Mount Namespace查剖,容器進(jìn)程看到的文件系統(tǒng)也跟宿主機(jī)完全一樣。
這是怎么回事呢噪窘?
仔細(xì)思考一下笋庄,你會(huì)發(fā)現(xiàn)這其實(shí)并不難理解:Mount Namespace 修改的,是容器進(jìn)程對(duì)文件系統(tǒng)“掛載點(diǎn)”的認(rèn)知。但是直砂,這也就意味著菌仁,只有在“掛載”這個(gè)操作發(fā)生之后,進(jìn)程的視圖才會(huì)被改變静暂。而在此之前济丘,新創(chuàng)建的容器會(huì)直接繼承宿主機(jī)的各個(gè)掛載點(diǎn)。
這時(shí)洽蛀,你可能已經(jīng)想到了一個(gè)解決辦法:創(chuàng)建新進(jìn)程時(shí)摹迷,除了聲明要啟用Mount Namespace之外,我們還可以告訴容器進(jìn)程郊供,有哪些目錄需要重新掛載峡碉,就比如這個(gè) /tmp 目錄。于是驮审,我們?cè)谌萜鬟M(jìn)程執(zhí)行前可以添加一步重新掛載 /tmp 目錄的操作:
int container_main(void* arg)
{
printf("Container - insidethe container!\n");
// 如果你的機(jī)器的根目錄的掛載類型是 shared鲫寄,那必須先重新掛載根目錄
// mount("","/", NULL, MS_PRIVATE, "");
mount("none","/tmp", "tmpfs", 0, "");
execv(container_args[0],container_args);
printf("Something'swrong!\n");
return 1;
}
可以看到,在修改后的代碼里疯淫,我在容器進(jìn)程啟動(dòng)之前地来,加上了一句 mount(“none”,“/tmp”, “tmpfs”, 0, “”) 語(yǔ)句。就這樣峡竣,我告訴了容器以 tmpfs(內(nèi)存盤)格式靠抑,重新掛載了 /tmp 目錄。
這段修改后的代碼适掰,編譯執(zhí)行后的結(jié)果又如何呢颂碧?我們可以試驗(yàn)一下:
1 $ gcc -o ns ns.c
2 $ ./ns
3 Parent - start a container!
4 Container - inside the container!
5 $ ls /tmp
可以看到,這次 /tmp 變成了一個(gè)空目錄类浪,這意味著重新掛載生效了载城。我們可以用 mount -l 檢查一下:
1 $ mount -l | grep tmpfs
2 none on /tmp type tmpfs(rw,relatime)
可以看到,容器里的 /tmp 目錄是以 tmpfs 方式單獨(dú)掛載的费就。
更重要的是诉瓦,因?yàn)槲覀儎?chuàng)建的新進(jìn)程啟用了 Mount Namespace,所以這次重新掛載的操作力细,只在容器進(jìn)程的 Mount Namespace 中有效睬澡。如果在宿主機(jī)上用 mount -l 來(lái)檢查一下這個(gè)掛載,你會(huì)發(fā)現(xiàn)它是不存在的:
1 # 在宿主機(jī)上
2 $ mount -l | grep tmpfs
這就是 Mount Namespace 跟其他 Namespace的使用略有不同的地方:它對(duì)容器進(jìn)程視圖的改變眠蚂,一定是伴隨著掛載操作(mount)才能生效煞聪。
可是,作為一個(gè)普通用戶逝慧,我們希望的是一個(gè)更友好的情況:每當(dāng)創(chuàng)建一個(gè)新容器時(shí)昔脯,我希望容器進(jìn)程看到的文件系統(tǒng)就是一個(gè)獨(dú)立的隔離環(huán)境啄糙,而不是繼承自宿主機(jī)的文件系統(tǒng)。怎么才能做到這一點(diǎn)呢云稚?
不難想到隧饼,我們可以在容器進(jìn)程啟動(dòng)之前重新掛載它的整個(gè)根目錄“/”。而由于Mount Namespace 的存在静陈,這個(gè)掛載對(duì)宿主機(jī)不可見燕雁,所以容器進(jìn)程就可以在里面隨便折騰了。
在 Linux 操作系統(tǒng)里窿给,有一個(gè)名為 chroot 的命令可以幫助你在 shell 中方便地完成這個(gè)工作贵白。顧名思義,它的作用就是幫你“change root file system”崩泡,即改變進(jìn)程的根目錄到你指定的位置。它的用法也非常簡(jiǎn)單猬膨。
假設(shè)角撞,我們現(xiàn)在有一個(gè) $HOME/test 目錄,想要把它作為一個(gè)/bin/bash 進(jìn)程的根目錄勃痴。
首先谒所,創(chuàng)建一個(gè) test 目錄和幾個(gè) lib 文件夾:
1 $ mkdir -p $HOME/test
2 $ mkdir -p$HOME/test/{bin,lib64,lib}
3 $ cd $T
然后,把 bash 命令拷貝到 test 目錄對(duì)應(yīng)的 bin 路徑下:
1 $ cp -v /bin/{bash,ls}$HOME/test/bin
接下來(lái)沛申,把 bash 命令需要的所有 so 文件劣领,也拷貝到 test 目錄對(duì)應(yīng)的 lib 路徑下。找到 so 文件可以用 ldd 命令:
1 $ T=$HOME/test
2 $list="$(ldd /bin/ls |egrep -o '/lib.*\.[0-9]')"
3 $ for i in $list; do cp -v"$i" "${T}${i}"; done
最后铁材,執(zhí)行 chroot 命令尖淘,告訴操作系統(tǒng),我們將使用$HOME/test 目錄作為 /bin/bash 進(jìn)程的根目錄:
1 $ chroot $HOME/test /bin/bash
這時(shí)著觉,你如果執(zhí)行 "ls /"村生,就會(huì)看到,它返回的都是 $HOME/test 目錄下面的內(nèi)容饼丘,而不是宿主機(jī)的內(nèi)容趁桃。
更重要的是,對(duì)于被 chroot 的進(jìn)程來(lái)說(shuō)肄鸽,它并不會(huì)感受到自己的根目錄已經(jīng)被“修改”成$HOME/test 了卫病。
這種視圖被修改的原理,是不是跟我之前介紹的 Linux Namespace 很類似呢典徘?
沒錯(cuò)蟀苛!
實(shí)際上,Mount Namespace 正是基于對(duì) chroot 的不斷改良才被發(fā)明出來(lái)的烂斋,它也是Linux操作系統(tǒng)里的第一個(gè) Namespace屹逛。
當(dāng)然础废,為了能夠讓容器的這個(gè)根目錄看起來(lái)更“真實(shí)”,我們一般會(huì)在這個(gè)容器的根目錄下掛載一個(gè)完整操作系統(tǒng)的文件系統(tǒng)罕模,比如 Ubuntu16.04 的ISO评腺。這樣,在容器啟動(dòng)之后淑掌,我們?cè)谌萜骼锿ㄟ^(guò)執(zhí)行 "ls /" 查看根目錄下的內(nèi)容蒿讥,就是 Ubuntu 16.04 的所有目錄和文件。
而這個(gè)掛載在容器根目錄上抛腕、用來(lái)為容器進(jìn)程提供隔離后執(zhí)行環(huán)境的文件系統(tǒng)芋绸,就是所謂的“容器鏡像”。它還有一個(gè)更為專業(yè)的名字担敌,叫作:rootfs(根文件系統(tǒng))摔敛。
所以,一個(gè)最常見的 rootfs全封,或者說(shuō)容器鏡像马昙,會(huì)包括如下所示的一些目錄和文件,比如/bin刹悴,/etc行楞,/proc 等等:
1 $ ls /
2 bin dev etc home lib lib64 mntopt proc root run sbin sys tmp usr var
而你進(jìn)入容器之后執(zhí)行的 /bin/bash,就是 /bin 目錄下的可執(zhí)行文件土匀,與宿主機(jī)的/bin/bash完全不同子房。
現(xiàn)在,你應(yīng)該可以理解就轧,對(duì) Docker 項(xiàng)目來(lái)說(shuō)证杭,它最核心的原理實(shí)際上就是為待創(chuàng)建的用戶進(jìn)程:
1. 啟用 Linux Namespace 配置;
2. 設(shè)置指定的 Cgroups 參數(shù)钓丰;
3. 切換進(jìn)程的根目錄(Change Root)躯砰。
這樣,一個(gè)完整的容器就誕生了携丁。不過(guò)琢歇,Docker 項(xiàng)目在最后一步的切換上會(huì)優(yōu)先使用pivot_root 系統(tǒng)調(diào)用,如果系統(tǒng)不支持梦鉴,才會(huì)使用 chroot李茫。這兩個(gè)系統(tǒng)調(diào)用雖然功能類似,但是也有細(xì)微的區(qū)別肥橙,這一部分小知識(shí)就交給你課后去探索了魄宏。
另外,需要明確的是存筏,rootfs 只是一個(gè)操作系統(tǒng)所包含的文件宠互、配置和目錄味榛,并不包括操作系統(tǒng)內(nèi)核。在 Linux 操作系統(tǒng)中予跌,這兩部分是分開存放的搏色,操作系統(tǒng)只有在開機(jī)啟動(dòng)時(shí)才會(huì)加載指定版本的內(nèi)核鏡像。
所以說(shuō)券册,rootfs 只包括了操作系統(tǒng)的“軀殼”频轿,并沒有包括操作系統(tǒng)的“靈魂”。
那么烁焙,對(duì)于容器來(lái)說(shuō)航邢,這個(gè)操作系統(tǒng)的“靈魂”又在哪里呢?
實(shí)際上骄蝇,同一臺(tái)機(jī)器上的所有容器膳殷,都共享宿主機(jī)操作系統(tǒng)的內(nèi)核。
這就意味著乞榨,如果你的應(yīng)用程序需要配置內(nèi)核參數(shù)秽之、加載額外的內(nèi)核模塊,以及跟內(nèi)核進(jìn)行直接的交互吃既,你就需要注意了:這些操作和依賴的對(duì)象,都是宿主機(jī)操作系統(tǒng)的內(nèi)核跨细,它對(duì)于該機(jī)器上的所有容器來(lái)說(shuō)是一個(gè)“全局變量”鹦倚,牽一發(fā)而動(dòng)全身。
這也是容器相比于虛擬機(jī)的主要缺陷之一:畢竟后者不僅有模擬出來(lái)的硬件機(jī)器充當(dāng)沙盒冀惭,而且每個(gè)沙盒里還運(yùn)行著一個(gè)完整的 Guest OS 給應(yīng)用隨便折騰震叙。
不過(guò),正是由于 rootfs 的存在散休,容器才有了一個(gè)被反復(fù)宣傳至今的重要特性:一致性媒楼。
什么是容器的“一致性”呢?
我在專欄的第一篇文章《小鯨魚大事記(一):初出茅廬》中曾經(jīng)提到過(guò):由于云端與本地服務(wù)器環(huán)境不同戚丸,應(yīng)用的打包過(guò)程划址,一直是使用 PaaS 時(shí)最“痛苦”的一個(gè)步驟。
但有了容器之后限府,更準(zhǔn)確地說(shuō)夺颤,有了容器鏡像(即 rootfs)之后,這個(gè)問(wèn)題被非常優(yōu)雅地解決了胁勺。
由于 rootfs 里打包的不只是應(yīng)用世澜,而是整個(gè)操作系統(tǒng)的文件和目錄,也就意味著署穗,應(yīng)用以及它運(yùn)行所需要的所有依賴寥裂,都被封裝在了一起嵌洼。
事實(shí)上,對(duì)于大多數(shù)開發(fā)者而言封恰,他們對(duì)應(yīng)用依賴的理解麻养,一直局限在編程語(yǔ)言層面。比如Golang 的 Godeps.json俭驮。但實(shí)際上回溺,一個(gè)一直以來(lái)很容易被忽視的事實(shí)是,對(duì)一個(gè)應(yīng)用來(lái)說(shuō)混萝,操作系統(tǒng)本身才是它運(yùn)行所需要的最完整的“依賴庫(kù)”遗遵。
有了容器鏡像“打包操作系統(tǒng)”的能力,這個(gè)最基礎(chǔ)的依賴環(huán)境也終于變成了應(yīng)用沙盒的一部分逸嘀。這就賦予了容器所謂的一致性:無(wú)論在本地车要、云端,還是在一臺(tái)任何地方的機(jī)器上崭倘,用戶只需要解壓打包好的容器鏡像翼岁,那么這個(gè)應(yīng)用運(yùn)行所需要的完整的執(zhí)行環(huán)境就被重現(xiàn)出來(lái)了。
這種深入到操作系統(tǒng)級(jí)別的運(yùn)行環(huán)境一致性司光,打通了應(yīng)用在本地開發(fā)和遠(yuǎn)端執(zhí)行環(huán)境之間難以逾越的鴻溝琅坡。
不過(guò),這時(shí)你可能已經(jīng)發(fā)現(xiàn)了另一個(gè)非常棘手的問(wèn)題:難道我每開發(fā)一個(gè)應(yīng)用残家,或者升級(jí)一下現(xiàn)有的應(yīng)用榆俺,都要重復(fù)制作一次 rootfs 嗎?
比如坞淮,我現(xiàn)在用 Ubuntu 操作系統(tǒng)的 ISO 做了一個(gè) rootfs茴晋,然后又在里面安裝了 Java 環(huán)境,用來(lái)部署我的 Java 應(yīng)用回窘。那么诺擅,我的另一個(gè)同事在發(fā)布他的Java 應(yīng)用時(shí),顯然希望能夠直接使用我安裝過(guò) Java 環(huán)境的 rootfs啡直,而不是重復(fù)這個(gè)流程烁涌。
一種比較直觀的解決辦法是,我在制作 rootfs 的時(shí)候付枫,每做一步“有意義”的操作烹玉,就保存一個(gè) rootfs 出來(lái),這樣其他同事就可以按需求去用他需要的rootfs 了阐滩。
但是二打,這個(gè)解決辦法并不具備推廣性。原因在于掂榔,一旦你的同事們修改了這個(gè) rootfs继效,新舊兩個(gè) rootfs 之間就沒有任何關(guān)系了症杏。這樣做的結(jié)果就是極度的碎片化。
那么瑞信,既然這些修改都基于一個(gè)舊的 rootfs厉颤,我們能不能以增量的方式去做這些修改呢?這樣做的好處是凡简,所有人都只需要維護(hù)相對(duì)于 base rootfs 修改的增量?jī)?nèi)容逼友,而不是每次修改都制造一個(gè)“fork”。
答案當(dāng)然是肯定的秤涩。
這也正是為何帜乞,Docker 公司在實(shí)現(xiàn) Docker 鏡像時(shí)并沒有沿用以前制作 rootfs 的標(biāo)準(zhǔn)流程,而是做了一個(gè)小小的創(chuàng)新:
Docker 在鏡像的設(shè)計(jì)中筐眷,引入了層(layer)的概念黎烈。也就是說(shuō),用戶制作鏡像的每一步操作匀谣,都會(huì)生成一個(gè)層照棋,也就是一個(gè)增量 rootfs。
當(dāng)然武翎,這個(gè)想法不是憑空臆造出來(lái)的烈炭,而是用到了一種叫作聯(lián)合文件系統(tǒng)(Union File System)的能力。
Union File System 也叫 UnionFS宝恶,最主要的功能是將多個(gè)不同位置的目錄聯(lián)合掛載(union mount)到同一個(gè)目錄下梳庆。比如,我現(xiàn)在有兩個(gè)目錄 A 和 B卑惜,它們分別有兩個(gè)文件:
1$ tree
2.
3 ├── A
4 │
5 │
? ├── a
? └── x
6└── B
7
8
?├── b
?└── x
然后,我使用聯(lián)合掛載的方式驻售,將這兩個(gè)目錄掛載到一個(gè)公共的目錄 C 上:
1 $ mkdir C
2 $ mount -t aufs -o dirs=./A:./Bnone ./C
這時(shí)露久,我再查看目錄 C 的內(nèi)容,就能看到目錄 A 和 B 下的文件被合并到了一起:
1 $ tree ./C
2 ./C
3 ├── a
4 ├── b
5 └── x
可以看到欺栗,在這個(gè)合并后的目錄 C 里毫痕,有 a、b迟几、x 三個(gè)文件消请,并且 x 文件只有一份。這类腮,就是“合并”的含義臊泰。此外,如果你在目錄 C 里對(duì) a蚜枢、b缸逃、x 文件做修改针饥,這些修改也會(huì)在對(duì)應(yīng)的目錄 A、B 中生效需频。
那么丁眼,在 Docker 項(xiàng)目中,又是如何使用這種 Union File System 的呢昭殉?
我的環(huán)境是 Ubuntu 16.04 和 Docker CE 18.05苞七,這對(duì)組合默認(rèn)使用的是 AuFS 這個(gè)聯(lián)合文件系統(tǒng)的實(shí)現(xiàn)。你可以通過(guò) docker info 命令挪丢,查看到這個(gè)信息蹂风。
AuFS 的全稱是 Another UnionFS,后改名為Alternative UnionFS吃靠,再后來(lái)干脆改名叫作Advance UnionFS硫眨,從這些名字中你應(yīng)該能看出這樣兩個(gè)事實(shí):
1. 它是對(duì) Linux 原生 UnionFS 的重寫和改進(jìn);
2. 它的作者怨氣好像很大巢块。我猜是 Linus Torvalds(Linux之父)一直不讓 AuFS 進(jìn)入Linux
內(nèi)核主干的緣故礁阁,所以我們只能在 Ubuntu 和 Debian 這些發(fā)行版上使用它。
對(duì)于 AuFS 來(lái)說(shuō)族奢,它最關(guān)鍵的目錄結(jié)構(gòu)在/var/lib/docker 路徑下的 diff 目錄:
1/var/lib/docker/aufs/diff/
而這個(gè)目錄的作用姥闭,我們不妨通過(guò)一個(gè)具體例子來(lái)看一下。
現(xiàn)在越走,我們啟動(dòng)一個(gè)容器棚品,比如:
1$ docker run -d ubuntu:latestsleep 3600
這時(shí)候,Docker 就會(huì)從 Docker Hub 上拉取一個(gè) Ubuntu 鏡像到本地廊敌。
這個(gè)所謂的“鏡像”铜跑,實(shí)際上就是一個(gè) Ubuntu 操作系統(tǒng)的rootfs,它的內(nèi)容是 Ubuntu 操作系統(tǒng)的所有文件和目錄骡澈。不過(guò)锅纺,與之前我們講述的 rootfs 稍微不同的是,Docker 鏡像使用的rootfs肋殴,往往由多個(gè)“層”組成:
$ docker image inspectubuntu:latest
"RootFS":{
"Type":"layers",
"Layers":[
"sha256:f49017d4d5ce9c0f544c...",
"sha256:8f2b771487e9d6354080...",
"sha256:ccd4d61916aaa2159429...",
"sha256:c01d74f99de40e097c73...",
"sha256:268a067217b5fe78e000..."
]
}
可以看到囤锉,這個(gè) Ubuntu 鏡像,實(shí)際上由五個(gè)層組成护锤。這五個(gè)層就是五個(gè)增量 rootfs官地,每一層都是 Ubuntu 操作系統(tǒng)文件與目錄的一部分;而在使用鏡像時(shí)烙懦,Docker會(huì)把這些增量聯(lián)合掛載在一個(gè)統(tǒng)一的掛載點(diǎn)上(等價(jià)于前面例子里的“/C”目錄)驱入。
這個(gè)掛載點(diǎn)就是 /var/lib/docker/aufs/mnt/,比如:
/var/lib/docker/aufs/mnt/6e3be5d2ecccae7cc0fcfa2a2f5c89dc21ee30e166be823ceaeba15dce645b3e
不出意外的,這個(gè)目錄里面正是一個(gè)完整的 Ubuntu 操作系統(tǒng):
1 $ ls/var/lib/docker/aufs/mnt/6e3be5d2ecccae7cc0fcfa2a2f5c89dc21ee30e166be823ceaeba15dce645b3e
2 bin boot dev etc home lib lib64media mnt opt proc root run sbin srv sys tmp usr var
那么沧侥,前面提到的五個(gè)鏡像層可霎,又是如何被聯(lián)合掛載成這樣一個(gè)完整的 Ubuntu 文件系統(tǒng)的呢?
這個(gè)信息記錄在 AuFS 的系統(tǒng)目錄 /sys/fs/aufs 下面宴杀。
首先癣朗,通過(guò)查看 AuFS 的掛載信息,我們可以找到這個(gè)目錄對(duì)應(yīng)的AuFS 的內(nèi)部 ID(也叫:si):
1 $ cat /proc/mounts| grep aufs
2 none/var/lib/docker/aufs/mnt/6e3be5d2ecccae7cc0fc... aufsrw,relatime,si=972c6d361e6b32ba,dio,d
即旺罢,si=972c6d361e6b32ba旷余。
然后使用這個(gè) ID,你就可以在 /sys/fs/aufs 下查看被聯(lián)合掛載在一起的各個(gè)層的信息:
1 $ cat/sys/fs/aufs/si_972c6d361e6b32ba/br[0-9]*
2 /var/lib/docker/aufs/diff/6e3be5d2ecccae7cc...=rw
3 /var/lib/docker/aufs/diff/6e3be5d2ecccae7cc...-init=ro+wh
4 /var/lib/docker/aufs/diff/32e8e20064858c0f2...=ro+wh
5 /var/lib/docker/aufs/diff/2b8858809bce62e62...=ro+wh
6 /var/lib/docker/aufs/diff/20707dce8efc0d267...=ro+wh
7 /var/lib/docker/aufs/diff/72b0744e06247c7d0...=ro+wh
8 /var/lib/docker/aufs/diff/a524a729adadedb90...=ro+wh
從這些信息里扁达,我們可以看到正卧,鏡像的層都放置在 /var/lib/docker/aufs/diff 目錄下,然后被聯(lián)合掛載在 /var/lib/docker/aufs/mnt 里面跪解。
而且炉旷,從這個(gè)結(jié)構(gòu)可以看出來(lái),這個(gè)容器的 rootfs 由如下圖所示的三部分組成:
第一部分叉讥,只讀層窘行。
它是這個(gè)容器的 rootfs 最下面的五層,對(duì)應(yīng)的正是ubuntu:latest 鏡像的五層图仓」蘅可以看到,它們的掛載方式都是只讀的(ro+wh救崔,即readonly+whiteout惶看,至于什么是 whiteout,我下面馬上會(huì)講到)六孵。
這時(shí)纬黎,我們可以分別查看一下這些層的內(nèi)容:
1$ ls/var/lib/docker/aufs/diff/72b0744e06247c7d0...
2etc sbin usr var
3$ ls/var/lib/docker/aufs/diff/32e8e20064858c0f2...
4run
5$ ls/var/lib/docker/aufs/diff/a524a729adadedb900...
6bin boot dev etc home lib lib64media mnt opt proc root run sbin srv sys tmp usr var
可以看到,這些層劫窒,都以增量的方式分別包含了 Ubuntu 操作系統(tǒng)的一部分莹桅。
第二部分,可讀寫層烛亦。
它是這個(gè)容器的 rootfs 最上面的一層(6e3be5d2ecccae7cc),它的掛載方式為:rw懂拾,即read write煤禽。在沒有寫入文件之前,這個(gè)目錄是空的岖赋。而一旦在容器里做了寫操作檬果,你修改產(chǎn)生的內(nèi)容就會(huì)以增量的方式出現(xiàn)在這個(gè)層中。
可是,你有沒有想到這樣一個(gè)問(wèn)題:如果我現(xiàn)在要做的选脊,是刪除只讀層里的一個(gè)文件呢杭抠?
為了實(shí)現(xiàn)這樣的刪除操作,AuFS 會(huì)在可讀寫層創(chuàng)建一個(gè)whiteout 文件恳啥,把只讀層里的文件“遮擋”起來(lái)偏灿。
比如,你要?jiǎng)h除只讀層里一個(gè)名叫 foo 的文件钝的,那么這個(gè)刪除操作實(shí)際上是在可讀寫層創(chuàng)建了一個(gè)名叫.wh.foo 的文件翁垂。這樣,當(dāng)這兩個(gè)層被聯(lián)合掛載之后硝桩,foo文件就會(huì)被.wh.foo 文件“遮擋”起來(lái)沿猜,“消失”了。這個(gè)功能碗脊,就是“ro+wh”的掛載方式啼肩,即只讀 +whiteout 的含義。我喜歡把 whiteout 形象地翻譯為:“白障”衙伶。
所以祈坠,最上面這個(gè)可讀寫層的作用,就是專門用來(lái)存放你修改 rootfs 后產(chǎn)生的增量痕支,無(wú)論是增颁虐、刪、改卧须,都發(fā)生在這里另绩。而當(dāng)我們使用完了這個(gè)被修改過(guò)的容器之后,還可以使用docker commit 和 push 指令花嘶,保存這個(gè)被修改過(guò)的可讀寫層笋籽,并上傳到Docker Hub 上,供其他人使用椭员;而與此同時(shí)车海,原先的只讀層里的內(nèi)容則不會(huì)有任何變化。這隘击,就是增量 rootfs 的好處侍芝。
第三部分裳擎,Init 層宪萄。
它是一個(gè)以“-init”結(jié)尾的層,夾在只讀層和讀寫層之間浩聋。Init層是 Docker 項(xiàng)目單獨(dú)生成的一個(gè)內(nèi)部層凶赁,專門用來(lái)存放 /etc/hosts咧栗、/etc/resolv.conf等信息逆甜。
需要這樣一層的原因是,這些文件本來(lái)屬于只讀的 Ubuntu 鏡像的一部分致板,但是用戶往往需要在啟動(dòng)容器時(shí)寫入一些指定的值比如 hostname交煞,所以就需要在可讀寫層對(duì)它們進(jìn)行修改。
可是斟或,這些修改往往只對(duì)當(dāng)前的容器有效素征,我們并不希望執(zhí)行 docker commit 時(shí),把這些信息連同可讀寫層一起提交掉缕粹。
所以稚茅,Docker 做法是,在修改了這些文件之后平斩,以一個(gè)單獨(dú)的層掛載了出來(lái)亚享。而用戶執(zhí)行docker commit 只會(huì)提交可讀寫層,所以是不包含這些內(nèi)容的绘面。
最終欺税,這 7 個(gè)層都被聯(lián)合掛載到/var/lib/docker/aufs/mnt 目錄下,表現(xiàn)為一個(gè)完整的Ubuntu 操作系統(tǒng)供容器使用揭璃。
總結(jié)
在今天的分享中晚凿,我著重介紹了 Linux 容器文件系統(tǒng)的實(shí)現(xiàn)方式。而這種機(jī)制瘦馍,正是我們經(jīng)常提到的容器鏡像歼秽,也叫作:rootfs。它只是一個(gè)操作系統(tǒng)的所有文件和目錄情组,并不包含內(nèi)核燥筷,最多也就幾百兆。而相比之下院崇,傳統(tǒng)虛擬機(jī)的鏡像大多是一個(gè)磁盤的“快照”肆氓,磁盤有多大,鏡像就至少有多大底瓣。
通過(guò)結(jié)合使用 Mount Namespace 和 rootfs谢揪,容器就能夠?yàn)檫M(jìn)程構(gòu)建出一個(gè)完善的文件系統(tǒng)隔離環(huán)境。當(dāng)然捐凭,這個(gè)功能的實(shí)現(xiàn)還必須感謝 chroot 和pivot_root 這兩個(gè)系統(tǒng)調(diào)用切換進(jìn)程根目錄的能力拨扶。
而在 rootfs 的基礎(chǔ)上,Docker 公司創(chuàng)新性地提出了使用多個(gè)增量 rootfs 聯(lián)合掛載一個(gè)完整rootfs 的方案茁肠,這就是容器鏡像中“層”的概念屈雄。
通過(guò)“分層鏡像”的設(shè)計(jì),以 Docker 鏡像為核心官套,來(lái)自不同公司、不同團(tuán)隊(duì)的技術(shù)人員被緊密地聯(lián)系在了一起。而且奶赔,由于容器鏡像的操作是增量式的惋嚎,這樣每次鏡像拉取、推送的內(nèi)容站刑,比原本多個(gè)完整的操作系統(tǒng)的大小要小得多另伍;而共享層的存在,可以使得所有這些容器鏡像需要的總空間绞旅,也比每個(gè)鏡像的總和要小摆尝。這樣就使得基于容器鏡像的團(tuán)隊(duì)協(xié)作,要比基于動(dòng)則幾個(gè)GB 的虛擬機(jī)磁盤鏡像的協(xié)作要敏捷得多因悲。
更重要的是堕汞,一旦這個(gè)鏡像被發(fā)布,那么你在全世界的任何一個(gè)地方下載這個(gè)鏡像晃琳,得到的內(nèi)容都完全一致讯检,可以完全復(fù)現(xiàn)這個(gè)鏡像制作者當(dāng)初的完整環(huán)境。這卫旱,就是容器技術(shù)“強(qiáng)一致性”的重要體現(xiàn)人灼。
而這種價(jià)值正是支撐 Docker 公司在 2014~2016 年間迅猛發(fā)展的核心動(dòng)力。容器鏡像的發(fā)明顾翼,不僅打通了“開發(fā) - 測(cè)試 - 部署”流程的每一個(gè)環(huán)節(jié)投放,更重要的是:
? ? ? ?容器鏡像將會(huì)成為未來(lái)軟件的主流發(fā)布方式。
思考題
1. 既然容器的 rootfs(比如适贸,Ubuntu 鏡像)灸芳,是以只讀方式掛載的,那么又如何在容器里修改 Ubuntu 鏡像的內(nèi)容呢取逾?(提示:Copy-on-Write)
2. 除了 AuFS耗绿,你知道 Docker 項(xiàng)目還支持哪些 UnionFS 實(shí)現(xiàn)嗎?你能說(shuō)出不同宿主機(jī)環(huán)境下推薦使用哪種實(shí)現(xiàn)嗎砾隅?
感謝你的收聽误阻,歡迎你給我留言,也歡迎分享給更多的朋友一起閱讀晴埂。
文章回復(fù):
Geek_6ef93d
有讀者反映究反,咱們重新掛載/tmp目錄的實(shí)驗(yàn)執(zhí)行完成后,在宿主機(jī)上居然可以看到這個(gè)掛載信息儒洛。精耐。這是怎么回事呢?實(shí)際上琅锻,大家自己裝的虛擬機(jī)卦停,或者云上的虛擬機(jī)的根目錄向胡,很多都是以share方式的掛載的。這時(shí)候惊完,你在容器里做mount也會(huì)繼承share方式僵芹。這樣就會(huì)把容器內(nèi)掛載傳播到宿主機(jī)上。解決這個(gè)問(wèn)題小槐,你可以在重新掛載/tmp之前拇派,在容器內(nèi)先執(zhí)行一句:mount(“”, “/“, NULL, MS_PRIVATE, “”) 這樣,容器內(nèi)的根目錄就是private掛載的了凿跳。
2018-09-08
Cloud*
1. 上面的讀寫層通常也稱為容器層件豌,下面的只讀層稱為鏡像層,所有的增刪查改操作都只會(huì)作用在容器層控嗜,相同的文件上層會(huì)覆蓋掉下層茧彤。知道這一點(diǎn),就不難理解鏡像文件的修改躬审,比如修改一個(gè)文件的時(shí)候棘街,首先會(huì)從上到下查找有沒有這個(gè)文件,找到承边,就復(fù)制到容器層中遭殉,修改,修改的結(jié)果就會(huì)作用到下層的文件博助,這種方式也被稱為copy-on-write险污。
2. 查了一下,包括但不限于以下這幾種:aufs, device mapper, btrfs, overlayfs,vfs, zfs富岳。aufs是ubuntu 常用的蛔糯,device mapper 是 centos,btrfs 是 SUSE窖式,overlayfs ubuntu 和centos 都會(huì)使用蚁飒,現(xiàn)在最新的 docker 版本中默認(rèn)兩個(gè)系統(tǒng)都是使用的overlayfs,vfs 和zfs常用在 solaris 系統(tǒng)萝喘。
歡迎補(bǔ)充和指正淮逻。
2018-09-07
作者回復(fù)
課代表又出現(xiàn)啦
2018-09-07
棲枝
whiteout,這就是我每次想把之前不需要的東西刪了,但是鏡像也沒變小的原因啊阁簸,手動(dòng)捂臉
2018-09-07
Jeff.W
繼Namespace構(gòu)建了四周的圍墻(進(jìn)程隔離)爬早,Cgroups構(gòu)建了受控的天空優(yōu)先使用陽(yáng)光雨露(資源限制),Mount namespace與rootfs構(gòu)建了腳下的大地启妹,這片土地是你熟悉和喜歡的筛严,不管你走到哪里,都可以帶著它饶米,就好像你從未離開過(guò)家鄉(xiāng)桨啃,沒有絲毫的陌生感(容器的一致性)~
2018-09-13
作者回復(fù)
于是车胡,你就做了一句詩(shī)
2018-09-13
A-
女朋友1 3 5追電視劇,我1 3 5追k8s照瘾。
2018-09-07
作者回復(fù)
女朋友應(yīng)該很開心??
2018-09-07
asdf100
目錄聯(lián)合掛載時(shí)吨拍,如果A和B目錄里的x文件內(nèi)容不一樣,這時(shí)如何處理网杆?
2018-09-07
作者回復(fù)
好問(wèn)題。aufs是一層一層往上蓋的伊滋,所以我給的例子里碳却,A里面的x會(huì)覆蓋B里面的x。
2018-09-07
oddrock
請(qǐng)問(wèn)老師笑旺,聽了您今天的課昼浦,認(rèn)識(shí)到容器使用的內(nèi)核是和宿主機(jī)內(nèi)核一致的,但如果容器需要不同的內(nèi)核怎么辦筒主?
2018-09-07
作者回復(fù)
沒关噪!辦!法乌妙!所以我去年一直在搞katacontainers使兔,這種基于虛擬化的容器是有獨(dú)立內(nèi)核的。
2018-09-07
snakorse
精彩到炸裂L僭稀E傲ぁ!
2018-09-07
湯尼房
老師你好泽艘,很好奇dockerhub上關(guān)于系統(tǒng)鏡像的制作過(guò)程欲险,比如對(duì)于centos7.4的鏡像,首先能想到的是應(yīng)該依據(jù)centos官方發(fā)布的centos7.4的系統(tǒng)鏡像匹涮,然后將此鏡像中的內(nèi)核給去掉天试,僅保留剩下的文件、配置和目錄或者是保留必要的文件然低、配置和目錄喜每;但通過(guò)查看centos7.4的Dockerfile之后發(fā)現(xiàn)其依賴的base image是scratch,然后在scratch的基礎(chǔ)上添加centos-7.4.1708-docker.tar.xz脚翘,后來(lái)查看ubuntu鏡像發(fā)現(xiàn)其base image也是scratch灼卢,老師能簡(jiǎn)單說(shuō)下scratch基礎(chǔ)鏡像嗎(鏡像文件的制作過(guò)程等),猜想scratch應(yīng)該是包含了多個(gè)系統(tǒng)之間共同的東西来农,否則為啥多個(gè)版本的系統(tǒng)鏡像以及不同版本的系統(tǒng)鏡像的基礎(chǔ)鏡像都用的是scratch鞋真,所以對(duì)scratch基礎(chǔ)鏡像很好奇,望老師指點(diǎn)一下
2018-09-10
作者回復(fù)
很簡(jiǎn)單啊沃于,因?yàn)閟cratch本身就是個(gè)空鏡像涩咖。你想想海诲,假如你是centos公司,你在發(fā)布centos鏡像的時(shí)候總得FROM吧檩互?所以docker公司就給你做了個(gè)scratch:萬(wàn)能的base鏡像特幔。
2018-09-11
武坤
在容器中修改文件時(shí),Docker會(huì)從上到下依次在各鏡像層中查找比文件闸昨。找到后蚯斯,會(huì)把此文件復(fù)制到容器層(可讀寫層),然后修改饵较。這就是Copy on Write.
2018-09-07
呂
請(qǐng)教一個(gè)低級(jí)的問(wèn)題拍嵌,我現(xiàn)在用docker部署,在多次部署以后循诉,會(huì)導(dǎo)致磁盤占用空間急劇的增大横辆,老是需要擴(kuò)容,雖然對(duì)不使用的鏡像定時(shí)做了刪除茄猫,但還是會(huì)出現(xiàn)這樣的問(wèn)題狈蚤,產(chǎn)生的文件也不敢在生產(chǎn)環(huán)境隨便進(jìn)行刪除,只能清理鏡像和容器划纽。
2018-09-07
作者回復(fù)
用kubernetes 脆侮,打開GC功能,定時(shí)清理
2018-09-07
silencedoctor
老師你好我想請(qǐng)問(wèn)一下 我的系統(tǒng)是Ubuntu16.04.4 docker是18.06.0 /var/lib/docker下并沒有aufs這個(gè)文件夾 執(zhí)行docker image他又確實(shí)分了層這是什么原因呢
2018-09-07
Hunsbiously
你好阿浓,這個(gè)視頻是我看到的一個(gè)很好的視頻他嚷,希望老師能夠找機(jī)會(huì),詳細(xì)分析下.https://m.youtube.com/watch?v=90kZRyPcRZw#fauxfullscreen
2018-09-09
作者回復(fù)
好
2018-09-10
long904
請(qǐng)教老師芭毙,以我理解容器鏡像依賴宿主機(jī)內(nèi)核筋蓖。那么如果鏡像是基于Linux的系統(tǒng)制作而成(線上運(yùn)行環(huán)境就是Linux),那么如果在Windows系統(tǒng)上它是不能運(yùn)行這個(gè)鏡像了退敦,對(duì)嗎粘咖?甚至目標(biāo)機(jī)器如果內(nèi)核跟鏡像制作不同(比如centos5和centos7)如果是,請(qǐng)問(wèn)該怎么理解容器跨平臺(tái)部署一說(shuō)侈百?謝謝瓮下。
2018-09-07
作者回復(fù)
如果你的應(yīng)用依賴內(nèi)核版本,那果斷跨不了平臺(tái)钝域,除非再創(chuàng)建對(duì)應(yīng)的虛擬機(jī)出來(lái)做宿主讽坏。說(shuō)跨平臺(tái)其實(shí)是因?yàn)榇蠖鄶?shù)應(yīng)用沒有內(nèi)核依賴。windows系統(tǒng)會(huì)給容器外面套一個(gè)vm例证,所以也能運(yùn)行l(wèi)inux容器路呜。
2018-09-07
shupian418
ns.c 啟動(dòng)失敗
Parent - start a container
Parent - container stopped
2018-09-10
作者回復(fù)
得看看報(bào)錯(cuò),八成是依賴之類的
2018-09-11
kyleqian
請(qǐng)問(wèn)所謂的“應(yīng)用依賴內(nèi)核”是指程序直接進(jìn)行內(nèi)核調(diào)用,而不是僅僅使用c運(yùn)行庫(kù)胀葱,可以這樣理解嗎漠秋?
2018-09-09
作者回復(fù)
是的〉钟欤或者庆锦,應(yīng)用依賴某個(gè)特定內(nèi)核版本才有的特性。
2018-09-10
Kaer
老師你好轧葛,如果依賴了內(nèi)核運(yùn)行的應(yīng)用搂抒,必須得重新在跟宿主機(jī)相同內(nèi)核下重新打包鏡像才行嗎?有沒有完美解決方案:比如在鏡像打包的時(shí)候尿扯,打一個(gè)兼容內(nèi)核的補(bǔ)丁燕耿。
2018-09-09
作者回復(fù)
重新打包也解決不了問(wèn)題,你的宿主機(jī)必須跟開發(fā)環(huán)境一致才行姜胖。linuxkit這個(gè)工具就是干這個(gè)的。
2018-09-10
賈鵬
問(wèn)題1淀散,我理解的是每次發(fā)生了修改右莱,都會(huì)在上層生成一個(gè)新的鏡像layer,而沒有發(fā)生修改的內(nèi)容是不會(huì)被記錄layer里的档插。就像dockerfile里面的run指令慢蜓,每一個(gè)run都會(huì)生成一個(gè)layer(docker history能看到順序)。并且如果是相關(guān)動(dòng)作郭膛,其實(shí)是以最后的指令為準(zhǔn)的(也就是覆蓋掉了前面的)晨抡。
問(wèn)題2,devicemapper的磁盤使用率低 overlay aufs容器啟動(dòng)快则剃,磁盤使用率高都是個(gè)人理解耘柱,也沒有查資料確認(rèn),有問(wèn)題歡迎指正
2018-09-07
假裝樂(lè)
可讀寫層增改好理解棍现,刪是怎么實(shí)現(xiàn)的呢
2018-09-07
作者回復(fù)
再仔細(xì)看看whiteout的解釋调煎?
2018-09-07
╯夢(mèng)深處゛
喜歡這種從出現(xiàn)到發(fā)展、由淺入深己肮、由一小段代碼講述某種實(shí)現(xiàn)的講述士袄,既容易理解,也深入了解了原理谎僻。娓娓道來(lái)娄柳,很有條理,就像身臨當(dāng)時(shí)docker的實(shí)現(xiàn)場(chǎng)景艘绍!
2018-09-27
作者回復(fù)
我一向反對(duì)源碼分析赤拒。那是偷懶的態(tài)度。