Docker底層技術

Docker容器技術已經發展了好些年,在很多項目都有應用,線上運行也很穩定。整理了部分Docker的學習筆記以及新版本特性,對Docker感興趣的同學可以看看,之前整理過的Linux namespace可以見之前的博文。

1 容器 & Docker & 虛擬機

Container(容器)是一種輕量級的虛擬化技術,它不需要模擬硬件創建虛擬機。在Linux系統裡面,使用到Linux kernel的cgroups,namespace(ipc,network, user,pid,mount),capability等用於隔離運行環境和資源限制的技術,我們稱之為容器。容器技術早就出現。例如Solaris Zones 和 BSD jails 就是非 Linux 操作系統上的容器,而用於 Linux 的容器技術也有很多如 Linux-Vserver、OpenVZ 和 FreeVPS。雖然這些技術都已經成熟,但是這些解決方案還沒有將它們的容器支持集成到主流 Linux 內核。總的來說,容器不等同於Docker,容器更不是虛擬機

LXC項目由一個 Linux 內核補丁和一些 userspace 工具組成,它提供一套簡化的工具來維護容器,用於虛擬環境的環境隔離、資源限制以及權限控制。LXC有點類似chroot,但是它比chroot提供了更多的隔離性。

Docker最初目標是做一個特殊的LXC的開源系統,最後慢慢演變為它自己的一套容器運行時環境。Docker基於Linux kernel的CGroups,Namespace,UnionFileSystem等技術封裝成一種自定義的容器格式,用於提供一整套虛擬運行環境。毫無疑問,近些年來Docker已經成為了容器技術的代名詞,如其官網介紹的Docker is world's leading software containerization platform。本文會先簡單介紹Docker基礎概念,然後會分析下Docker背後用到的技術。

Docker底層技術

虛擬機和容器對比

2 Docker基礎

2.1 Docker Engine

Docker提供了一個打包和運行應用的隔離環境,稱之為容器,Docker的隔離和安全特性允許你在一個主機同時運行多個容器,而且它並不像虛擬機那樣重量級,容器都是基於宿主機的內核運行的,它是輕量的,不管你運行的是ubuntu, debian還是其他Linux系統,用的內核都是宿主機內核。Docker提供了工具和平臺來管理容器,而Docker Engine則是一個提供了大部分功能組件的CS架構的應用,如架構圖所示,Docker Engine負責管理鏡像,容器,網絡以及數據卷等。

docker engine

2.2 Docker架構

Docker更詳細的架構如圖所示,採用CS架構,client通過RESTFUL API發送docker命令到docker daemon進程,docker daemon進程執行鏡像編譯,容器啟停以及分發,數據卷管理等,一個client可以與多個docker daemon通信。

Docker底層技術

docker 架構

  • Docker Daemon:Docker後臺進程,用於管理鏡像,容器以及數據卷。
  • Docker Client:用於與Docker Daemon交互。
  • Docker Registry:用於存儲Docker鏡像,類似github,公共的Registry有Docker Hub和Docker Cloud。
  • Images:鏡像是用於創建容器的一種只讀模板。鏡像通常基於一個基礎鏡像,在此基礎上安裝額外的軟件。比如你的nginx鏡像可能基於debian然後安裝nginx並添加配置,你可以從Docker Hub上拉取已有的鏡像或者自己通過Dockerfile來編譯一個鏡像。
  • Containers:容器是鏡像的一個可運行示例,我們可通過Docker client或者API來創建,啟停或者刪除容器。默認情況下,容器與宿主機以及其他容器已經隔離,當然你可以控制隔離容器的網絡或者存儲的方式。
  • Services:服務是docker swarm引入的概念,可以用於在多宿主機之間伸縮容器數目,支持負載均衡已經服務路由功能。

2.3 Docker底層技術概覽

通過下面命令運行一個debian容器,attach到一個本機的命令行並運行/bin/bash。

docker run -i -t debian /bin/bash

這個命令背後都做了什麼?

  • 1.如果本機沒有debian鏡像,則會從你配置的Registry裡面拉取一個debian的latest版本的鏡像,跟你運行了docker pull debian效果一樣。
  • 2.創建容器。跟運行docker create一樣。
  • 3.給容器分配一個讀寫文件系統作為該容器的final layer,容器可以在它的文件系統創建和修改文件。
  • 4.Docker為容器創建了一套網絡接口,給容器分配一個ip。默認情況下,容器可以通過默認網絡連通到外部網絡。
  • 5.Docker啟動容器並執行 /bin/bash。因為啟動時指定了-i -t參數,容器是以交互模式運行且attach到本地終端,我們可以在終端上輸入命令並看到輸出。
  • 6.運行exit可以退出容器,但是此時容器並沒有被刪除,我們可以再次運行它或者刪除它。

可以發現,容器的內核版本是跟宿主機一樣的,不同的是容器的主機名是獨立的,它默認用容器ID做主機名。我們運行ps -ef可以發現容器進程是隔離的,容器裡面看不到宿主機的進程,而且它自己有PID為1的進程。此外,網絡也是隔離的,它有獨立於宿主機的IP。文件系統也是隔離的,容器有自己的系統和軟件目錄,修改容器內的文件並不影響宿主機對應目錄的文件。

root@stretch:/home/vagrant# uname -r4.9.0-6-amd64root@stretch:/home/vagrant# docker run -it --name demo alpine /bin/ash/ # uname -r ## 容器內4.9.0-6-amd64/ # ps -efPID USER TIME COMMAND 1 root 0:00 /bin/ash 7 root 0:00 ps -ef / # ip a1: lo:  mtu 65536 qdisc noqueue state UNKNOWN qlen 1 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever6: eth0@if7:  mtu 1500 qdisc noqueue state UP  link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0 valid_lft forever preferred_lft forever

這些隔離機制並不是Docker新開發的技術,而是依託Linux kernel以及一些已有的技術實現的,主要包括:

  • Linux Namespaces(Linux2.6.24後引入):命名空間用於進程(PID)、網絡(NET)、掛載點(MNT)、UTS、IPC等隔離。
  • Linux Control Groups(CGroups):用於限制容器使用的資源,包括內存,CPU等。
  • Union File Systems:UnionFS把多個目錄結合成一個目錄,對外使用,最上層目錄為讀寫層(通常只有1個),下面可以有一個或多個只讀層,見容器和鏡像分層圖。Docker支持OverlayFS,AUFS、DeviceMapper、btrfs等聯合文件系統。
  • Container Format: Docker Engine組合Namespaces,CGroups以及UnionFS包裝為一個容器格式,默認格式為libcontainer,後續可能會加入BSD Jails 或 Solaris Zones容器格式的支持。

3 Docker底層技術

3.1 Namespaces

Namespaces用於環境隔離,Linux kernel支持的Namespace包括UTS, IPC, PID, NET, NS, USER以及新加入的CGROUP等,UTS用於隔離主機名和域名,使用標識CLONE_NEWUTS,IPC用於隔離進程間通信資源如消息隊列等,使用標識CLONE_NEWIPC,PID隔離進程,NET用於隔離網絡,NS用於隔離掛載點,USER用於隔離用戶組。默認情況下,通過clone系統調用創建子進程的namespace與父進程是一致的,而你可以在clone系統調用中通過flag參數設置隔離的名字空間來隔離,當然也可以更加方便的直接用unshare命令來創建新的namespace。查看一個進程的各Namespace命令如下:

root@stretch:/home/vagrant# ls -ls /proc/self/ns/0 lrwxrwxrwx 1 root root 0 May 17 22:04 cgroup -> cgroup:[4026531835]0 lrwxrwxrwx 1 root root 0 May 17 22:04 ipc -> ipc:[4026531839]0 lrwxrwxrwx 1 root root 0 May 17 22:04 mnt -> mnt:[4026531840]0 lrwxrwxrwx 1 root root 0 May 17 22:04 net -> net:[4026531957]0 lrwxrwxrwx 1 root root 0 May 17 22:04 pid -> pid:[4026531836]0 lrwxrwxrwx 1 root root 0 May 17 22:04 user -> user:[4026531837]0 lrwxrwxrwx 1 root root 0 May 17 22:04 uts -> uts:[4026531838]

PID Namespace

在容器中,有自己的Pid namespace,因此我們看到的只有PID為1的初始進程以及它的子進程,而宿主機的其他進程容器內是看不到的。通常來說,Linux啟動後它會先啟動一個PID為1的進程,這是系統進程樹的根進程,根進程會接著創建子進程來初始化系統服務。PID namespace允許在新的namespace創建一棵新的進程樹,它可以有自己的PID為1的進程。在PID namespace的隔離下,子進程名字空間無法知道父進程名字空間的進程,如在Docker容器中無法看到宿主機的進程,而父進程名字空間可以看到子進程名字空間的所有進程。如圖所示:

Docker底層技術

pid namespace示意圖

Linux內核加入PID Namespace後,對pid結構進行了修改,新增的upid結構用於跟蹤namespace和pid。

## 加入PID Namespace之前的pid結構 struct pid { atomic_t count; /* reference counter */ int nr; /* the pid value */ struct hlist_node pid_chain; /* hash chain */ ...};## 加入PID Namespace之後的pid結構struct upid { int nr; /* moved from struct pid */ struct pid_namespace *ns;  struct hlist_node pid_chain; /* moved from struct pid */};struct pid { ... int level; /* the number of upids */ struct upid numbers[0];};

可以通過unshare測試下PID namespace,可以看到新的bash進程它的pid namespace與父進程的不同了,而且它的pid是1。

root@stretch:/home/vagrant# unshare --fork --pid bashroot@stretch:/home/vagrant# echo $$1root@stretch:/home/vagrant# ls -ls /proc/self/ns/0 lrwxrwxrwx 1 root root 0 May 19 15:24 cgroup -> cgroup:[4026531835]0 lrwxrwxrwx 1 root root 0 May 19 15:24 ipc -> ipc:[4026531839]0 lrwxrwxrwx 1 root root 0 May 19 15:24 mnt -> mnt:[4026531840]0 lrwxrwxrwx 1 root root 0 May 19 15:24 net -> net:[4026531957]0 lrwxrwxrwx 1 root root 0 May 19 15:24 pid -> pid:[4026532232]0 lrwxrwxrwx 1 root root 0 May 19 15:24 user -> user:[4026531837]0 lrwxrwxrwx 1 root root 0 May 19 15:24 uts -> uts:[4026531838]

NS Namespace

NS Namespace用於隔離掛載點,不同NS Namespace的掛載點互不影響。創建一個新的Mount Namespace效果有點類似chroot,不過它隔離的比chroot更加完全。這是歷史上的第一個Linux Namespace,由此得到了 NS 這個名字而不是用的 Mount。

在最初的NS Namespace版本中,掛載點是完全隔離的。初始狀態下,子進程看到的掛載點與父進程是一樣的。在新的Namespace中,子進程可以隨意mount/umount任何目錄,而不會影響到父Namespace。使用NS Namespace完全隔離掛載點初衷很好,但是也帶來了某些情況下不方便,比如我們新加了一塊磁盤,如果完全隔離則需要在所有的Namespace中都掛載一遍。為此,Linux在2.6.15版本中加入了一個shared subtree特性,通過指定Propagation來確定掛載事件如何傳播。比如通過指定MS_SHARED來允許在一個peer group(子namespace 和父namespace就屬於同一個組)共享掛載點,mount/umount事件會傳播到peer group成員中。使用MS_PRIVATE不共享掛載點和傳播掛載事件。其他還有MS_SLAVE和NS_UNBINDABLE等選項。可以通過查看cat /proc/self/mountinfo來看掛載點信息,若沒有傳播參數則為MS_PRIVATE的選項。

Docker底層技術

mount namespace掛載類型示意圖

例如你在初始namespace有兩個掛載點,通過mount --make-shared /dev/sda1 /mntS設置/mntS為shared類型,mount --make-private /dev/sda1 /mntP設置/mntP為private類型。當你使用unshare -m bash新建一個namespace並在它們下面掛載子目錄時,可以發現/mntS下面的子目錄mount/umount事件會傳播到父namespace,而/mntP則不會。

在前面例子Pid namespace隔離後,我們在新的名字空間執行 ps -ef可以看到宿主機進程,這是因為ps命令是從 /proc 文件系統讀取的數據,而文件系統我們還沒有隔離,為此,我們需要在新的 NS Namespace重新掛載 proc 文件系統來模擬類似Docker容器的功能。

root@stretch:/home/vagrant# unshare --pid --fork --mount-proc bashroot@stretch:/home/vagrant# ps -efUID PID PPID C STIME TTY TIME CMDroot 1 0 0 15:36 pts/1 00:00:00 bashroot 2 1 0 15:36 pts/1 00:00:00 ps -ef

可以看到,隔離了NS namespace並重新掛載了proc後,ps命令只能看到2個進程了,跟我們在Docker容器中看到的一致。

NET Namespace

Docker容器中另一個重要特性是網絡獨立(之所以不用隔離一詞是因為容器的網絡還是要藉助宿主機的網絡來通信的),使用到Linux 的 NET Namespace以及vet。veth主要的目的是為了跨NET namespace之間提供一種類似於Linux進程間通信的技術,所以veth總是成對出現,如下面的veth0和veth1。它們位於不同的NET namespace中,在veth設備任意一端接收到的數據,都會從另一端發送出去。veth實現了不同namespace的網絡數據傳輸。

Docker底層技術

docker bridge網絡示意圖

在Docker中,宿主機的veth端會橋接到網橋中,接收到容器中的veth端發過來的數據後會經由網橋docker0再轉發到宿主機網卡eth0,最終通過eth0發送數據。當然在發送數據前,需要經過iptables MASQUERADE規則將源地址改成宿主機ip,這樣才能接收到響應數據包。而宿主機網卡接收到的數據會通過iptables DNAT根據端口號修改目的地址和端口為容器的ip和端口,然後根據路由規則發送到網橋docker0中,並最終由網橋docker0發送到對應的容器中。

Docker裡面網絡模式分為bridge,host,overlay等幾種模式,默認是採用bridge模式網絡如圖所示。如果使用host模式,則不隔離直接使用宿主機網絡。overlay網絡則是更加高級的模式,可以實現跨主機的容器通信,後面會單獨總結下Docker網絡這個專題。

USER Namespace

user namespace用於隔離用戶和組信息,在不同的namespace中用戶可以有相同的 UID 和 GID,它們之間互相不影響。父子namespace之間可以進行用戶映射,如父namespace(宿主機)的普通用戶映射到子namespace(容器)的root用戶,以減少子namespace的root用戶操作父namespace的風險。user namespace功能雖然在很早就出現了,但是直到Linux kernel 3.8之後這個功能才趨於完善。

創建新的user namespace之後第一步就是設置好user和group的映射關係。這個映射通過設置/proc/PID/uid_map(gid_map)實現,格式如下,ID-inside-ns是容器內的uid/gid,而ID-outside-ns則是容器外映射的真實uid/gid。比如0 1000 1表示將真實的uid=1000映射為容器內的uid=0,length為映射的範圍。

ID-inside-ns ID-outside-ns length

不是所有的進程都能隨便修改映射文件的,必須同時具備如下條件:

  • 修改映射文件的進程必須有PID進程所在user namespace的CAP_SETUID/CAP_SETGID權限。
  • 修改映射文件的進程必須是跟PID在同一個user namespace或者PID的父namespace。
  • 映射文件uid_map和gid_map只能寫入一次,再次寫入會報錯。

docker1.10之後的版本可以通過在docker daemon啟動時加上--userns-remap=[USERNAME]來實現USER Namespace的隔離。我們指定了username=ssj啟動dockerd,查看subuid文件可以發現ssj映射的uid範圍是165536到165536+65536= 231072,而且在docker目錄下面對應ssj有一個獨立的目錄165536.165536存在。

root@stretch:/home/vagrant# cat /etc/subuidvagrant:100000:65536ssj:165536:65536root@stretch:/home/vagrant# ls /var/lib/docker/165536.165536/builder/ containerd/ containers/ image/ network/ ...

運行docker images -a等命令可以發現在啟用user namespace之前的鏡像都看不到了。此時只能看到在新的user namespace裡面創建的docker鏡像和容器。而此時我們創建一個測試容器,可以在容器外看到容器進程的uid_map已經設置為ssj,這樣容器中的root用戶映射到宿主機就是ssj這個用戶了,此時如果要刪除我們掛載的/bin目錄中的文件,會提示沒有權限,增強了安全性。

### dockerd 啟動時加了 --userns-remap=ssjroot@stretch:/home/vagrant# docker run -it -v /bin:/host/bin --name demo alpine /bin/ash/ # rm /host/bin/which rm: remove '/host/bin/which'? yrm: can't remove '/host/bin/which': Permission denied### 宿主機查看容器進程uid_map文件root@stretch:/home/vagrant# CPID=`ps -ef|grep '\/bin\/ash'|awk '{printf $2}'`root@stretch:/home/vagrant# cat /proc/$CPID/uid_map 0 165536 65536 

其他Namespace

UTS namespace用於隔離主機名等。可以看到在新的uts namespace修改主機名並不影響原namespace的主機名。

root@stretch:/home/vagrant# unshare --uts --fork bashroot@stretch:/home/vagrant# hostnamestretchroot@stretch:/home/vagrant# hostname modifiedroot@stretch:/home/vagrant# hostnamemodifiedroot@stretch:/home/vagrant# exitroot@stretch:/home/vagrant# hostnamestretch

IPC Namespace用於隔離IPC消息隊列等。可以看到,新老ipc namespace的消息隊列互不影響。

root@stretch:/home/vagrant# ipcmk -QMessage queue id: 0root@stretch:/home/vagrant# ipcs -q------ Message Queues --------key msqid owner perms used-bytes messages 0x26c3371c 0 root 644 0 0 root@stretch:/home/vagrant# unshare --ipc --fork bashroot@stretch:/home/vagrant# ipcs -q------ Message Queues --------key msqid owner perms used-bytes messages 

CGROUP Namespace是Linux4.6以後才支持的新namespace。容器技術使用namespace和cgroup實現環境隔離和資源限制,但是對於cgroup本身並沒有隔離。沒有cgroup namespace前,容器中一旦掛載cgroup文件系統,便可以修改整全局的cgroup配置。有了cgroup namespace後,每個namespace中的進程都有自己的cgroup文件系統視圖,增強了安全性,同時也讓容器遷移更加方便。在我測試的Docker18.03.1-ce版本中容器暫時沒有用到cgroup namespace,這裡就不再展開。

3.2 CGroups

Linux CGroups用於資源限制,包括限制CPU、內存、blkio以及網絡等。通過工具cgroup-bin (sudo apt-get install cgroup-bin)可以創建CGroup並進入該CGroup執行命令。

root@stretch:/home/vagrant# cgcreate -a vagrant -g cpu:cg1root@stretch:/home/vagrant# ls /sys/fs/cgroup/cpu/cg1/cgroup.clone_children cpu.cfs_period_us cpu.shares cpuacct.stat cpuacct.usage_all cpuacct.usage_percpu_sys cpuacct.usage_sys notify_on_releasecgroup.procs cpu.cfs_quota_us cpu.stat cpuacct.usage cpuacct.usage_percpu cpuacct.usage_percpu_user cpuacct.usage_user tasks

cpu.cfs_period_us 和 cpu.cfs_quota_us,它們分別用來限制該組中的所有進程在單位時間裡可以使用的 cpu 時間,這裡的 cfs(Completely Fair Scheduler) 是完全公平調度器的意思。cpu.cfs_period_us是時間週期,默認為100000,即100毫秒。而cpu.cfs_quota_us是在時間週期內可以使用的時間,默認為-1即無限制。cpu.shares用於限制cpu使用的,它用於控制各個組之間的配額。比如組cg1的cpu.shares為1024,組cg2的cpu.shares也是1024,如果都有進程在運行則它們都可以使用最多50%的限額。如果cg2組內進程比較空閒,那麼cg1組可以將使用幾乎整個cpu,tasks存儲的是該組裡面的進程ID。(

注:debian8默認沒有cfs和memory cgroup支持,需要重新編譯內核及修改啟動參數,debian9默認已經支持)

我們先在默認的分組裡面運行一個死循環程序loop.py,因為默認分組/sys/fs/cgroup/cpu/cpu.cfs_period_us和cfs_quota_us是默認值,所以是沒有限制 cpu 使用的。可以發現1個cpu us立馬接近100%了。

# loop.pywhile True: pass

設置cg1組的cfs_quota_us位50000,即表示該組內進程最多使用50%的cpu時間,運行cgexec命令進入cg1的cpu組,然後運行loop.py,可以發現cpu us在50%以內了,此時也可以在tasks文件中看到我們剛剛cgexec創建的進程ID。

root@stretch:/home/vagrant# echo 50000 > /sys/fs/cgroup/cpu/cg1/cpu.cfs_quota_usroot@stretch:/home/vagrant# cgexec -g cpu:cg1 /bin/bash

Docker裡面要限制內存和CPU使用,可以在啟動時指定相關參數即可。比如限制cpu使用率,加cpu-period和cpu-quota參數,限制執行的cpu核,加--cpuset-cpus參數。限制內存使用,加--memory參數。當然,我們可以看到在 /sys/fs/cgroup/cpu/docker/目錄下有個以containerid為名的分組,該分組下面的 cpu.cfs_period_us和cpu.cfs_quota_us的值就是我們在啟動容器時指定的值。

root@stretch:/home/vagrant# docker run -i -t --cpu-period=100000 --cpu-quota=50000 --memory=512000000 alpine /bin/ash

3.3 Capabilities

我們在啟動容器時會時常看到這樣的參數--cap-add=NET_ADMIN,這是用到了Linux的capability特性。capability是為了實現更精細化的權限控制而加入的。我們以前熟知通過設置文件的SUID位,這樣非root用戶的可執行文件運行後的euid會成為文件的擁有者ID,比如passwd命令運行起來後有root權限,有SUID權限的可執行文件如果存在漏洞會有安全風險。(查看文件的capability的命令為 filecap -a,而查看進程capability的命令為 pscap -a,pscap和filecap工具需要安裝 libcap-ng-utils這個包)。

對於capability,可以看一個簡單的例子便於理解。如Debian系統中自帶的ping工具,它是有設置SUID位的。這裡拷貝ping重命名為anotherping,anotherping的SUID位沒有設置,運行會提示權限錯誤。這裡,我們只要將其加上 cap_net_raw權限即可,不需要設置SUID位那麼大的權限。

vagrant@stretch:~$ ls -ls /bin/ping60 -rwsr-xr-x 1 root root 61240 Nov 10 2016 /bin/pingvagrant@stretch:~$ cp /bin/ping anotherpingvagrant@stretch:~$ ls -ls anotherping 60 -rwxr-xr-x 1 vagrant vagrant 61240 May 19 10:18 anotherpingvagrant@stretch:~$ ./anotherping -c1 yue.uu.163.comping: socket: Operation not permittedvagrant@stretch:~$ sudo setcap cap_net_raw+ep ./anotherping vagrant@stretch:~$ ./anotherping -c1 yue.uu.163.comPING yue.uu.163.com (59.111.137.252) 56(84) bytes of data.64 bytes from 59.111.137.252 (59.111.137.252): icmp_seq=1 ttl=63 time=53.9 ms--- yue.uu.163.com ping statistics ---1 packets transmitted, 1 received, 0% packet loss, time 0msrtt min/avg/max/mdev = 53.919/53.919/53.919/0.000 ms

3.4 Union File System

UnionFS(聯合文件系統)簡單來說就是支持將不同的目錄掛載到同一個目錄中的技術。Docker支持的UnionFS包括OverlayFS,AUFS,devicemapper,vfs以及btrfs等,查看UnionFS版本可以用docker info查看對應輸出中的Storage項即可,早期的Docker版本用AUFS和devicemapper居多,新版本Docker在Linux3.18之後版本基本默認用OverlayFS,這裡以OverlayFS來分析。

OverlayFS與早期用過的AUFS類似,不過它比AUFS更簡單,讀寫性能更好,在docker-ce18.03版本中默認用的存儲驅動是overlay2,老版本overlay官方已經不推薦使用。它將兩個目錄upperdir和lowdir聯合掛載到一個merged目錄,提供統一視圖。其中upperdir是可讀寫層,對容器修改寫入在該目錄中,它也會隱藏lowerdir中相同的文件。而lowdir是隻讀層,Docker鏡像在這層。

Docker底層技術

image

在看Docker鏡像和容器存儲結構前,可以先簡單操作下OverlayFS看下基本概念。創建了lowerdir和upperdir兩個目錄,然後用overlayfs掛載到merged目錄,這樣在merged目錄可以看到兩個目錄的所有文件 both.txt 和only.txt。其中upperdir是可讀寫的,而lowerdir只讀。通過merged目錄來操作文件可以發現:

  • 讀取文件時,如果upperdir不存在該文件,則會從lowerdir直接讀取。
  • 修改文件時並不影響lowerdir中的文件,因為它是隻讀的。
  • 如果修改的文件在upperdir不存在,則會從lowerdir拷貝到upperdir,然後在upperdir裡面修改該文件,並不影響lowerdir目錄的文件。
  • 刪除文件則是將upperdir中將對應文件設置成了c類型,即字符設備類型來隱藏已經刪除的文件(與AUFS創建一個whiteout文件略有不同)。
root@stretch:/home/vagrant/overlaytest# tree -a.|-- lowerdir| |-- both.txt| `-- only.txt|-- merged|-- upperdir| `-- both.txt`-- workdir `-- work5 directories, 3 filesroot@stretch:/home/vagrant/overlaytest# mount -t overlay overlay -olowerdir=./lowerdir,upperdir=./upperdir,workdir=./workdir ./mergedroot@stretch:/home/vagrant/overlaytest# tree.|-- lowerdir| |-- both.txt| `-- only.txt|-- merged| |-- both.txt| `-- only.txt|-- upperdir| `-- both.txt`-- workdir `-- work5 directories, 5 filesroot@stretch:/home/vagrant/overlaytest# tree -a.|-- lowerdir| |-- both.txt| `-- only.txt|-- merged| |-- both.txt| `-- only.txt|-- upperdir| `-- both.txt`-- workdir `-- work5 directories, 5 filesroot@stretch:/home/vagrant/overlaytest# echo "modified both" > merged/both.txt root@stretch:/home/vagrant/overlaytest# cat upperdir/both.txt modified bothroot@stretch:/home/vagrant/overlaytest# cat lowerdir/both.txt lower both.txtroot@stretch:/home/vagrant/overlaytest# echo "modified only" > merged/only.txt root@stretch:/home/vagrant/overlaytest# tree.|-- lowerdir| |-- both.txt| `-- only.txt|-- merged| |-- both.txt| `-- only.txt|-- upperdir| |-- both.txt| `-- only.txt`-- workdir `-- work5 directories, 6 filesroot@stretch:/home/vagrant/overlaytest# cat upperdir/only.txt modified onlyroot@stretch:/home/vagrant/overlaytest# cat lowerdir/only.txt lower only.txtroot@stretch:/home/vagrant/overlaytest# tree -a.|-- lowerdir| |-- both.txt| `-- only.txt|-- merged| |-- both.txt| `-- only.txt|-- upperdir| |-- both.txt| `-- only.txt`-- workdir `-- work5 directories, 6 filesroot@stretch:/home/vagrant/overlaytest# rm merged/both.txt root@stretch:/home/vagrant/overlaytest# tree -a.|-- lowerdir| |-- both.txt| `-- only.txt|-- merged| `-- only.txt|-- upperdir| |-- both.txt| `-- only.txt`-- workdir `-- workroot@stretch:/home/vagrant/overlaytest# ls -ls upperdir/both.txt 0 c--------- 1 root root 0, 0 May 19 02:31 upperdir/both.txt

回到Docker裡面,我們拉取一個nginx鏡像,有三層鏡像,可以看到在overlay2對應每一層都有個目錄(注意,這個目錄名跟鏡像層名從docker1.10版本後名字已經不對應了),另外的l目錄是指向鏡像層的軟鏈接。最底層存儲的是基礎鏡像debian/alpine,上一層是安裝了nginx增加的可執行文件和配置文件,而最上層是鏈接/dev/stdout到nginx日誌文件。而每個子目錄下面的diff目錄用於存儲鏡像內容,work目錄是OverlayFS內部使用的,而link文件存儲的是該鏡像層對應的短名稱,lower文件存儲的是下一層的短名稱。

root@stretch:/home/vagrant# docker pull nginxUsing default tag: latestlatest: Pulling from library/nginxf2aa67a397c4: Pull complete 3c091c23e29d: Pull complete 4a99993b8636: Pull complete Digest: sha256:0fb320e2a1b1620b4905facb3447e3d84ad36da0b2c8aa8fe3a5a81d1187b884Status: Downloaded newer image for nginx:latestroot@stretch:/home/vagrant# ls -ls /var/lib/docker/overlay2/total 164 drwx------ 4 root root 4096 May 19 04:17 09495e5085bced25e8017f558147f82e61b012a8f632a0b6aac363462b1db8b04 drwx------ 3 root root 4096 May 19 04:17 8af95287a343b26e9c3dd679258773880e7bdbbe914198ba63a8ed1b4c5f55544 drwx------ 4 root root 4096 May 19 04:17 f311565fe9436eb8606f846e1f73f38287841773e8d041933a41259fe6f96afe4 drwx------ 2 root root 4096 May 19 04:17 lroot@stretch:/var/lib/docker/overlay2# ls 09495e5085bced25e8017f558147f82e61b012a8f632a0b6aac363462b1db8b0/diff link lower work

從我們示例可以看到,三層中 f311是最頂層,下面分別是0949和8af9這兩層。

root@stretch:/var/lib/docker/overlay2# cat f311565fe9436eb8606f846e1f73f38287841773e8d041933a41259fe6f96afe/lower l/7B2WM6DC226TCJU6QHJ4ABKRI6:l/4FHO2G5SWWRIX44IFDHU62Z7X2root@stretch:/var/lib/docker/overlay2# cat 09495e5085bced25e8017f558147f82e61b012a8f632a0b6aac363462b1db8b0/lower l/4FHO2G5SWWRIX44IFDHU62Z7X2root@stretch:/var/lib/docker/overlay2# cat 8af95287a343b26e9c3dd679258773880e7bdbbe914198ba63a8ed1b4c5f5554/link 4FHO2G5SWWRIX44IFDHU62Z7X2

此時我們啟動一個nginx容器,可以看到overlay2目錄多了兩個目錄,多出來的就是容器層的目錄和只讀的容器init層。容器目錄下面的merged就是我們前面提到的聯合掛載目錄了,而lowdir則是它下層目錄。而容器init層用來存儲與這個容器內環境相關的內容,如 /etc/hosts和/etc/resolv.conf文件,它居於其他鏡像層之上,容器層之下。

root@stretch:/var/lib/docker/overlay2# docker run -idt --name nginx nginx 01a873eeba41f00a5a3deb083adf5ed892c55b4680fbc2f1880e282195d3087broot@stretch:/var/lib/docker/overlay2# ls -ls4 drwx------ 4 root root 4096 May 19 04:17 09495e5085bced25e8017f558147f82e61b012a8f632a0b6aac363462b1db8b04 drwx------ 5 root root 4096 May 19 09:11 11b7579a1f1775ad71fe0f0f45fcb74c241fce319f5125b1b92cb442385065b14 drwx------ 4 root root 4096 May 19 09:11 11b7579a1f1775ad71fe0f0f45fcb74c241fce319f5125b1b92cb442385065b1-init4 drwx------ 3 root root 4096 May 19 04:17 8af95287a343b26e9c3dd679258773880e7bdbbe914198ba63a8ed1b4c5f55544 drwx------ 4 root root 4096 May 19 04:17 f311565fe9436eb8606f846e1f73f38287841773e8d041933a41259fe6f96afe4 drwx------ 2 root root 4096 May 19 09:11 lroot@stretch:/home/vagrant# ls -ls /var/lib/docker/overlay2/11b7579a1f1775ad71fe0f0f45fcb74c241fce319f5125b1b92cb442385065b1/4 drwxr-xr-x 4 root root 4096 May 19 09:11 diff4 -rw-r--r-- 1 root root 26 May 19 09:11 link4 -rw-r--r-- 1 root root 115 May 19 09:11 lower4 drwxr-xr-x 1 root root 4096 May 19 09:11 merged4 drwx------ 3 root root 4096 May 19 09:11 workroot@stretch:/var/lib/docker/overlay2# ls 11b7579a1f1775ad71fe0f0f45fcb74c241fce319f5125b1b92cb442385065b1/merged/bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr varroot@stretch:/var/lib/docker/overlay2# ls 11b7579a1f1775ad71fe0f0f45fcb74c241fce319f5125b1b92cb442385065b1/diff/run var

如果我們在容器中修改文件,則會反映到容器層的merged目錄相關文件,容器層的diff目錄相當於upperdir,其他層是lowerdir。如果之前容器層diff目錄不存在該文件,則會拷貝該文件到diff目錄並修改。讀取文件時,如果upperdir目錄找不到,則會直接從下層的鏡像層中讀取。

4 總結


分享到:


相關文章: