新聞中心
作者:vitovzhong,騰訊 TEG 應(yīng)用開發(fā)工程師

創(chuàng)新互聯(lián)建站主營青秀網(wǎng)站建設(shè)的網(wǎng)絡(luò)公司,主營網(wǎng)站建設(shè)方案,重慶APP軟件開發(fā),青秀h5小程序設(shè)計(jì)搭建,青秀網(wǎng)站營銷推廣歡迎青秀等地區(qū)企業(yè)咨詢
容器的實(shí)質(zhì)是進(jìn)程,與宿主機(jī)上的其他進(jìn)程是共用一個(gè)內(nèi)核,但與直接在宿主機(jī)執(zhí)行的進(jìn)程不同,容器進(jìn)程運(yùn)行在屬于自己的獨(dú)立的命名空間。命名空間隔離了進(jìn)程間的資源,使得 a,b 進(jìn)程可以看到 S 資源,而 c 進(jìn)程看不到。
1. 演進(jìn)
對于統(tǒng)一開發(fā)、測試、生產(chǎn)環(huán)境的渴望,要遠(yuǎn)遠(yuǎn)早于 docker 的出現(xiàn)。我們先來了解一下在 docker 之前出現(xiàn)過哪些解決方案。
1.1 vagrant
Vagarant 是筆者最早接觸到的一個(gè)解決環(huán)境配置不統(tǒng)一的技術(shù)方案。它使用 Ruby 語言編寫,由 HashCorp 公司在 2010 年 1 月發(fā)布。Vagrant 的底層是虛擬機(jī),最開始選用的是 virtualbox。一個(gè)個(gè)已經(jīng)配置好的虛擬機(jī)被稱作 box。用戶可自由在虛擬機(jī)內(nèi)部的安裝依賴庫和軟件服務(wù),并將 box 發(fā)布。通過簡單的命令,就能夠拉取 box,將環(huán)境搭建起來。
- // 拉取一個(gè)ubuntu12.04的box
- $ vagrant init hashicorp/precise32
- // 運(yùn)行該虛擬機(jī)
- $ vagrant up
- // 查看當(dāng)前本地都有哪些box
- $ vagrant box list
如果需要運(yùn)行多個(gè)服務(wù),也可以通過編寫 vagrantfile,將相互依賴的服務(wù)一起運(yùn)行,頗有如今 docker-compose 的味道。
- config.vm.define("web") do |web|web.vm.box = "apache"
- end
- config.vm.define("db") do |db|db.vm.box = "mysql”
- end
1.2 LXC (LinuX Container)
在 2008 年,Linux 2.6.24 將 cgroups 特性合入了主干。Linux Container 是 Canonical 公司基于 namespace 和 cgroups 等技術(shù),瞄準(zhǔn)容器世界而開發(fā)的一個(gè)項(xiàng)目,目標(biāo)就是要?jiǎng)?chuàng)造出運(yùn)行在 Linux 系統(tǒng)中,并且隔離性良好的容器環(huán)境。當(dāng)然它最早也就見于 Ubuntu 操作系統(tǒng)上。
2013 年,在 PyCon 大會(huì)上 Docker 正式面世。當(dāng)時(shí)的 Docker 是在 Ubuntu 12.04 上開發(fā)實(shí)現(xiàn)的,只是基于 LXC 之上的一個(gè)工具,屏蔽掉了 LXC 的使用細(xì)節(jié)(類似于 vagrant 屏蔽了底層虛擬機(jī)),讓用戶可以一句 docker run 命令行便創(chuàng)建出自己的容器環(huán)境。
2. 技術(shù)發(fā)展
容器技術(shù)是操作系統(tǒng)層面的虛擬化技術(shù),可以概括為使用 Linux 內(nèi)核的 cgroup,namespace 等技術(shù),對進(jìn)程進(jìn)行的封裝隔離。早在 Docker 之前,Linux 就已經(jīng)提供了今天的 Docker 所使用的那些基礎(chǔ)技術(shù)。Docker 一夜之間火爆全球,但技術(shù)上的積累并不是瞬間完成的。我們摘取其中幾個(gè)關(guān)鍵技術(shù)節(jié)點(diǎn)進(jìn)行介紹。
2.1 Chroot
軟件主要分為系統(tǒng)軟件和應(yīng)用軟件,而容器中運(yùn)行的程序并非系統(tǒng)軟件。容器中的進(jìn)程實(shí)質(zhì)上是運(yùn)行在宿主機(jī)上,與宿主機(jī)上的其他進(jìn)程共用一個(gè)內(nèi)核。而每個(gè)應(yīng)用軟件運(yùn)行都需要有必要的環(huán)境,包括一些 lib 庫依賴之類的。所以,為了避免不同應(yīng)用程序的 lib 庫依賴沖突,很自然地我們會(huì)想是否可以把他們進(jìn)行隔離,讓他們看到的庫是不一樣的?;谶@個(gè)樸素的想法,1979 年, chroot 系統(tǒng)調(diào)用首次問世。來舉個(gè)例子感受一下。在 devcloud 上申請的云主機(jī),現(xiàn)在我的 home 目錄下準(zhǔn)備好了一個(gè) alpine 系統(tǒng)的 rootfs,如下:
在該目錄下執(zhí)行:
- chroot rootfs/ /bin/bash
然后將/etc/os-release 打印出來,就看到是”Alpine Linux”,說明新運(yùn)行的 bash 跟 devcloud 主機(jī)上的 rootfs 隔離了。
2.1 Namespace
簡單來說 namespace 是由 Linux 內(nèi)核提供的,用于進(jìn)程間資源隔離的一種技術(shù),使得 a,b 進(jìn)程可以看到 S 資源;而 c 進(jìn)程看不到。它是在 2002 年 Linux 2.4.19 開始加入內(nèi)核的特性,到 2013 年 Linux 3.8 中 user namespace 的引入,對于我們現(xiàn)在所熟知的容器所需的全部 namespace 就都實(shí)現(xiàn)了。
Linux 提供了多種 namespace,用于對多種不同資源進(jìn)行隔離。容器的實(shí)質(zhì)是進(jìn)程,但與直接在宿主機(jī)執(zhí)行的進(jìn)程不同,容器進(jìn)程運(yùn)行在屬于自己的獨(dú)立的命名空間。因此容器可以擁有自己的 root 文件系統(tǒng)、自己的網(wǎng)絡(luò)配置、自己的進(jìn)程空間,甚至自己的用戶 ID 空間。
還是來看一個(gè)簡單的例子,讓我們有個(gè)感性認(rèn)識,namespace 到底是啥,在哪里能直觀的看到。在 devcloud 云主機(jī)上,執(zhí)行:ls-l /proc/self/ns 看到的就是當(dāng)前系統(tǒng)所支持的 namespace。
接著我們使用 unshare 命令,運(yùn)行一個(gè) bash,讓它不使用當(dāng)前的 pid namespace:
- unshare --pid --fork --mount-proc bash
然后運(yùn)行: ps -a 看看當(dāng)前 pid namespace 下的進(jìn)程都有哪些:
在新起的 bash 上執(zhí)行:ls -l /proc/self/ns, 發(fā)現(xiàn)當(dāng)前 bash 的 pid namespace 與之前是不相同的。
既然 docker 就是基于內(nèi)核的 namespace 特性來實(shí)現(xiàn)的,那么我們可以簡單來認(rèn)證一下,執(zhí)行指令:
- docker run –pid host --rm -it alpine sh
運(yùn)行一個(gè)簡單的 alpine 容器,讓它與主機(jī)共用同一個(gè) pid namespace。然后在容器內(nèi)部執(zhí)行指令 ps -a 會(huì)發(fā)現(xiàn)進(jìn)程數(shù)量與 devcloud 機(jī)器上的一樣;執(zhí)行指令 ls -l /proc/self/ns/ 同樣會(huì)看到容器內(nèi)部的 pid namespace 與 devcloud 機(jī)器上的也是一樣。
2.2 cgroups
cgroups 是 namespace 的一種,是為了實(shí)現(xiàn)虛擬化而采取的資源管理機(jī)制,決定哪些分配給容器的資源可被我們管理,分配容器使用資源的多少。容器內(nèi)的進(jìn)程是運(yùn)行在一個(gè)隔離的環(huán)境里,使用起來,就好像是在一個(gè)獨(dú)立于宿主的系統(tǒng)下操作一樣。這種特性使得容器封裝的應(yīng)用比直接在宿主運(yùn)行更加安全。例如可以設(shè)定一個(gè) memory 使用上限,一旦進(jìn)程組(容器)使用的內(nèi)存達(dá)到限額再申請內(nèi)存,就會(huì)出發(fā) OOM(out of memory),這樣就不會(huì)因?yàn)槟硞€(gè)進(jìn)程消耗的內(nèi)存過大而影響到其他進(jìn)程的運(yùn)行。
還是來看個(gè)例子感受一下。在 devcloud 機(jī)器上運(yùn)行一個(gè) apline 容器,限制只能使用前 2 個(gè) CPU 且只能使用 1.5 個(gè)核:
- docker run --rm -it --cpus "1.5" --cpuset-cpus 0,1 alpine
然后再開啟一個(gè)新的終端,先看看系統(tǒng)上有哪些資源是我們可以控制的:
- cat /proc/cgroups
最左邊一側(cè)就是可以設(shè)置的資源了。接著我們需要找到這些控制資源分配的信息都放在哪個(gè)目錄下:
- mount | grep cgroup
然后我們找到剛剛運(yùn)行的 alpine 鏡像的 cgroups 配置:
- cat /proc/`docker inspect --format='{{.State.Pid}}' $(docker ps -ql)`/cgroup
這樣,把二者拼接起來,就可以看到這個(gè)容器的資源配置了。我們先來驗(yàn)證 cpu 的用量是否是 1.5 個(gè)核:
- cat /sys/fs/cgroup/cpu,cpuacct/docker/c1f68e86241f9babb84a9556dfce84ec01e447bf1b8f918520de06656fa50ab4/cpu.cfs_period_us
輸出 100000,可以認(rèn)為是單位,然后再看配額:
- cat /sys/fs/cgroup/cpu,cpuacct/docker/c1f68e86241f9babb84a9556dfce84ec01e447bf1b8f918520de06656fa50ab4/cpu.cfs_quota_us
輸出 150000,與單位相除正好是設(shè)置的 1.5 個(gè)核,接著驗(yàn)證是否使用的是前兩個(gè)核心:
- cat /sys/fs/cgroup/cpuset/docker/c1f68e86241f9babb84a9556dfce84ec01e447bf1b8f918520de06656fa50ab4/cpuset.cpus
輸出 0-1。
目前來看,容器的資源配置都是按照我們設(shè)定的來分配的,但實(shí)際真能在 CPU0-CPU1 上限制使用 1.5 個(gè)核嗎?我們先看一下當(dāng)前 CPU 的用量:
- docker stats $(docker ps -ql)
因?yàn)闆]有在 alpine 中運(yùn)行程序,所以 CPU 用量為 0,我們現(xiàn)在回到最開始執(zhí)行 docker 指令的 alpine 終端,執(zhí)行一個(gè)死循環(huán):
- i=0; while true; do i=i+i; done
再來觀察當(dāng)前的 CPU 用量:
接近 1,但為啥不是 1.5?因?yàn)閯倓傔\(yùn)行的死循環(huán)只能跑在一個(gè)核上,所以我們再打開一個(gè)終端,進(jìn)入到 alpine 鏡像中,同樣執(zhí)行死循環(huán)的指令,看到 CPU 用量穩(wěn)定在了 1.5,說明資源的使用量確實(shí)是限制住了的。
現(xiàn)在我們對 docker 容器實(shí)現(xiàn)了進(jìn)程間資源隔離的黑科技有了一定認(rèn)識。如果單單就隔離性來說,vagrant 也已經(jīng)做到了。那么為什么是 docker 火爆全球?是因?yàn)樗试S用戶將容器環(huán)境打包成為一個(gè)鏡像進(jìn)行分發(fā),而且鏡像是分層增量構(gòu)建的,這可以大大降低用戶使用的門檻。
3. 存儲(chǔ)
Image 是 Docker 部署的基本單位,它包含了程序文件,以及這個(gè)程序依賴的資源的環(huán)境。Docker Image 是以一個(gè) mount 點(diǎn)掛載到容器內(nèi)部的。容器可以近似理解為鏡像的運(yùn)行時(shí)實(shí)例,默認(rèn)情況下也算是在鏡像層的基礎(chǔ)上增加了一個(gè)可寫層。所以,一般情況下如果你在容器內(nèi)做出的修改,均包含在這個(gè)可寫層中。
3.1 聯(lián)合文件系統(tǒng)(UFS)
Union File System 從字面意思上來理解就是“聯(lián)合文件系統(tǒng)”。它將多個(gè)物理位置不同的文件目錄聯(lián)合起來,掛載到某一個(gè)目錄下,形成一個(gè)抽象的文件系統(tǒng)。
如上圖,從右側(cè)以 UFS 的視角來看,lowerdir 和 upperdir 是兩個(gè)不同的目錄,UFS 將二者合并起來,得到 merged 層展示給調(diào)用方。從左側(cè)的 docker 角度來理解,lowerdir 就是鏡像,upperdir 就相當(dāng)于是容器默認(rèn)的可寫層。在運(yùn)行的容器中修改了文件,可以使用 docker commit 指令保存成為一個(gè)新鏡像。
3.2 Docker 鏡像的存儲(chǔ)管理
有了 UFS 的分層概念,我們就很好理解這樣的一個(gè)簡單 Dockerfile:
- FROM alpine
- COPY foo /foo
- COPY bar /bar
在構(gòu)建時(shí)的輸出所代表的含義了。
但是使用 docker pull 拉取的鏡像文件,在本地機(jī)器上存儲(chǔ)在哪,又是如何管理的呢?還是來實(shí)際操作認(rèn)證一下。在 devcloud 上確認(rèn)當(dāng)前 docker 所使用的存儲(chǔ)驅(qū)動(dòng)(默認(rèn)是 overlay2):
- docker info --format '{{.Driver}}'
以及鏡像下載后的存儲(chǔ)路徑(默認(rèn)存儲(chǔ)在/var/lib/docker):
- docker info --format '{{.DockerRootDir}}'
當(dāng)前我的 docker 修改了默認(rèn)存儲(chǔ)路徑,配置到/data/docker-data,我們就以它為例進(jìn)行展示。先查看一下該目錄下的結(jié)構(gòu):
- tree -L 1 /data/docker-data
關(guān)注一下其中的 image 和 overlay2 目錄。前者就是存放鏡像信息的地方,后者則是存放具體每一分層的文件內(nèi)容。我們先深入看一下 image 目錄結(jié)構(gòu):
- tree -L 2 /data/docker-data/image/
留心這個(gè) imagedb 目錄,接下來以我們以最新的 alpine 鏡像為例子,看看 docker 是如何管理鏡像的。執(zhí)行指令:
- docker pull alpine:latest
緊接著查看它的鏡像 ID:docker image ls alpine:latest
記住這個(gè) ID a24bb4013296,現(xiàn)在可以看一下 imagedb 目錄下的變化:
- tree -L 2 /data/docker-data/image/overlay2/imagedb/content/ | grep
- a24bb4013296
多了這么一個(gè)鏡像 ID 的文件,它是一個(gè) json 格式的文件,這里包含了該鏡像的參數(shù)信息:
- jq .
- /data/docker-data/image/overlay2/imagedb/content/sha256/a24bb4013296f61e89ba57005a7b3e52274d8edd3ae2077d04395f806b63d83e
接下來我們看看將一個(gè)鏡像運(yùn)行起來之后會(huì)有什么變化。運(yùn)行一個(gè) alpine 容器,讓它 sleep10 分鐘:
- docker run --rm -d alpine sleep 600
然后找到它的 overlay 掛載點(diǎn):
- docker inspect --format='{{.GraphDriver.Data}}' $(docker ps -ql) | grep MergedDir
結(jié)合上一節(jié)講到的 UFS 文件系統(tǒng),可以 ls 一下:
- ls /data/docker-data/overlay2/74e92699164736980c9e20475388568f482671625a177cb946c4b136e4d94a64/merged
看到它就是合并后所呈現(xiàn)在 alpine 容器的文件系統(tǒng)。先進(jìn)入到容器內(nèi):
- docker exec -it $(docker ps -ql) sh
緊接著新開一個(gè)終端查看容器運(yùn)行起來后跟鏡像相比,有哪些修改:
- docker diff $(docker ps -ql)
在/root 目錄下,增加了 sh 的歷史記錄文件。然后我們在容器中手動(dòng)增加一個(gè) hello.txt 文件:
- echo 'Hello Docker' > hello.txt
這時(shí)候來看看容器默認(rèn)在鏡像之上增加的可寫層 UpperDir 目錄的變化:
- ls /data/docker-data/overlay2/74e92699164736980c9e20475388568f482671625a177cb946c4b136e4d94a64/diff
這就認(rèn)證了 overlay2 驅(qū)動(dòng)是將鏡像和可寫層的內(nèi)容 merged 之后,供容器作為文件系統(tǒng)使用。多個(gè)運(yùn)行的容器共用一份基礎(chǔ)鏡像,而各自有獨(dú)立的可寫層,節(jié)省了存儲(chǔ)空間。
這個(gè)時(shí)候,我們也可以回答一下鏡像的實(shí)際內(nèi)容是存儲(chǔ)在哪里呢:
- cat /data/docker-data/overlay2/74e92699164736980c9e20475388568f482671625a177cb946c4b136e4d94a64/lower
查看這些分層:
- ls /data/docker-data/overlay2/l/ZIIZFSQUQ4CIKRNCMOXXY4VZHY/
就是 UFS 中低層的鏡像內(nèi)容。
總結(jié)
這一次跟大家分享了 Docker 所使用的底層技術(shù),包括 namespace,cgroups 和 overlay2 聯(lián)合文件系統(tǒng),著重介紹了隔離環(huán)境是如何在宿主機(jī)上演進(jìn)實(shí)現(xiàn)的。通過實(shí)際手動(dòng)操作,對這些概念有了真實(shí)的感受。希望下一次為大家再介紹 docker 的網(wǎng)絡(luò)實(shí)現(xiàn)機(jī)制。
【本文為專欄作者“騰訊技術(shù)工程”原創(chuàng)稿件,轉(zhuǎn)載請聯(lián)系原作者(微信號:Tencent_TEG)】
本文題目:Docker底層原理淺析
本文來源:http://m.fisionsoft.com.cn/article/cddhjcj.html


咨詢
建站咨詢
