摘要:底層技術用于環境隔離,支持的包括以及新加入的等,用于隔離主機名和域名,使用標識,用于隔離進程間通信資源如消息隊列等,使用標識,隔離進程,用于隔離網絡,用于隔離掛載點,用于隔離用戶組。
本文已獲得原作者__七把刀__授權。
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 背后用到的技術。Debian 上安裝 Docker 方法參見docker-ce-installation-in-debian。
Docker 提供了一個打包和運行應用的隔離環境,稱之為容器,Docker 的隔離和安全特性允許你在一個主機同時運行多個容器,而且它并不像虛擬機那樣重量級,容器都是基于宿主機的內核運行的,它是輕量的,不管你運行的是ubuntu, debian 還是其他 Linux 系統,用的內核都是宿主機內核。Docker 提供了工具和平臺來管理容器,而 Docker Engine 則是一個提供了大部分功能組件的CS架構的應用,如架構圖所示,Docker Engine 負責管理鏡像,容器,網絡以及數據卷等。
Docker 更詳細的架構如圖所示,采用CS架構,client 通過 RESTFUL API 發送 docker 命令到 docker daemon 進程,docker daemon 進程執行鏡像編譯,容器啟停以及分發,數據卷管理等,一個 client 可以與多個 docker daemon 通信。
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 引入的概念,可以用于在多宿主機之間伸縮容器數目,支持負載均衡已經服務路由功能。
通過下面命令運行一個 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 -r 4.9.0-6-amd64 root@stretch:/home/vagrant# docker run -it --name demo alpine /bin/ash / # uname -r ## 容器內 4.9.0-6-amd64 / # ps -ef PID USER TIME COMMAND 1 root 0:00 /bin/ash 7 root 0:00 ps -ef / # ip a 1: 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 forever 6: 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,CGroup 以及 UnionFS 包裝為一個容器格式,默認格式為 libcontainer,后續可能會加入 BSD Jails 或 Solaris Zones 容器格式的支持。
3 Docker底層技術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 為1的初始進程以及它的子進程,而宿主機的其他進程容器內是看不到的。通常來說, Linux 啟動后它會先啟動一個 PID 為1的進程,這是系統進程樹的根進程,根進程會接著創建子進程來初始化系統服務。PID namespace 允許在新的namespace 創建一棵新的進程樹,它可以有自己的PID為1的進程。在 PID namespace 的隔離下,子進程名字空間無法知道父進程名字空間的進程,如在Docker容器中無法看到宿主機的進程,而父進程名字空間可以看到子進程名字空間的所有進程。如圖所示:
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 bash root@stretch:/home/vagrant# echo $$ 1 root@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 的掛載點互不影響。創建一個新的 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 的選項。
例如你在初始 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 則不會。關于 mount 各種模式詳解可以參考這篇文章。
在前面例子 Pid namespace 隔離后,我們在新的名字空間執行 ps -ef 可以看到宿主機進程,這是因為 ps 命令是從 /proc 文件系統讀取的數據,而文件系統我們還沒有隔離,為此,我們需要在新的 NS Namespace 重新掛載 proc 文件系統來模擬類似 Docker 容器的功能。
root@stretch:/home/vagrant# unshare --pid --fork --mount-proc bash root@stretch:/home/vagrant# ps -ef UID PID PPID C STIME TTY TIME CMD root 1 0 0 15:36 pts/1 00:00:00 bash root 2 1 0 15:36 pts/1 00:00:00 ps -ef
可以看到,隔離了 NS namespace 并重新掛載了 proc 后,ps 命令只能看到2個進程了,跟我們在 Docker 容器中看到的一致。
Docker 容器中另一個重要特性是網絡獨立(之所以不用隔離一詞是因為容器的網絡還是要借助宿主機的網絡來通信的),使用到 Linux 的 NET Namespace 以及 vet。veth 主要的目的是為了跨 NET namespace 之間提供一種類似于 Linux 進程間通信的技術,所以 veth 總是成對出現,如下面的 veth0 和 veth1。它們位于不同的 NET namespace 中,在 veth 設備任意一端接收到的數據,都會從另一端發送出去。veth 實現了不同namespace的網絡數據傳輸。
在 Docker 中,宿主機的 veth 端會橋接到網橋中,接收到容器中的 veth 端發過來的數據后會經由網橋 docker0 再轉發到宿主機網卡 eth0,最終通過 eth0 發送數據。當然在發送數據前,需要經過iptables MASQUERADE 規則將源地址改成宿主機 ip,這樣才能接收到響應數據包。而宿主機網卡接收到的數據會通過iptables DNAT 根據端口號修改目的地址和端口為容器的ip 和端口,然后根據路由規則發送到網橋 docker0 中,并最終由網橋 docker0 發送到對應的容器中。
Docker 里面網絡模式分為 bridge,host,overlay 等幾種模式,默認是采用 bridge 模式網絡如圖所示。如果使用 host 模式,則不隔離直接使用宿主機網絡。overlay 網絡則是更加高級的模式,可以實現跨主機的容器通信,后面會多帶帶總結下 Docker 網絡這個專題。
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 只能寫入一次,再次寫入會報錯。
docker 1.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/subuid vagrant:100000:65536 ssj:165536:65536 root@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=ssj root@stretch:/home/vagrant# docker run -it -v /bin:/host/bin --name demo alpine /bin/ash / # rm /host/bin/which rm: remove "/host/bin/which"? y rm: 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
UTS namespace 用于隔離主機名等。可以看到在新的 uts namespace 修改主機名并不影響原 namespace 的主機名。
root@stretch:/home/vagrant# unshare --uts --fork bash root@stretch:/home/vagrant# hostname stretch root@stretch:/home/vagrant# hostname modified root@stretch:/home/vagrant# hostname modified root@stretch:/home/vagrant# exit root@stretch:/home/vagrant# hostname stretch
IPC Namespace 用于隔離 IPC 消息隊列等。可以看到,新老 ipc namespace 的消息隊列互不影響。
root@stretch:/home/vagrant# ipcmk -Q Message queue id: 0 root@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 bash root@stretch:/home/vagrant# ipcs -q ------ Message Queues -------- key msqid owner perms used-bytes messages
CGROUP Namespace 是 Linux 4.6以后才支持的新 namespace。容器技術使用 namespace 和 cgroup 實現環境隔離和資源限制,但是對于 cgroup 本身并沒有隔離。沒有 cgroup namespace 前,容器中一旦掛載 cgroup 文件系統,便可以修改整全局的 cgroup 配置。有了 cgroup namespace 后,每個 namespace 中的進程都有自己的 cgroup 文件系統視圖,增強了安全性,同時也讓容器遷移更加方便。在我測試的 Docker 18.03.1-ce 版本中容器暫時沒有用到 cgroup namespace,這里就不再展開。
Linux CGroups 用于資源限制,包括限制 CPU、內存、blkio 以及網絡等。通過工具 cgroup-bin (sudo apt-get install cgroup-bin) 可以創建 CGroup 并進入該 CGroup 執行命令。
root@stretch:/home/vagrant# cgcreate -a vagrant -g cpu:cg1 root@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_release cgroup.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.py while 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_us root@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
我們在啟動容器時會時常看到這樣的參數 --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/ping 60 -rwsr-xr-x 1 root root 61240 Nov 10 2016 /bin/ping vagrant@stretch:~$ cp /bin/ping anotherping vagrant@stretch:~$ ls -ls anotherping 60 -rwxr-xr-x 1 vagrant vagrant 61240 May 19 10:18 anotherping vagrant@stretch:~$ ./anotherping -c1 yue.uu.163.com ping: socket: Operation not permitted vagrant@stretch:~$ sudo setcap cap_net_raw+ep ./anotherping vagrant@stretch:~$ ./anotherping -c1 yue.uu.163.com PING 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 0ms rtt min/avg/max/mdev = 53.919/53.919/53.919/0.000 ms
UnionFS (聯合文件系統)簡單來說就是支持將不同的目錄掛載到同一個目錄中的技術。Docker 支持的 UnionFS 包 括OverlayFS,AUFS,devicemapper,vfs 以及 btrfs 等,查看 UnionFS 版本可以用 docker info 查看對應輸出中的 Storage 項即可,早期的 Docker 版本用 AUFS 和 devicemapper 居多,新版本 Docker 在 Linux 3.18之后版本基本默認用 OverlayFS,這里以 OverlayFS 來分析。
OverlayFS 與早期用過的 AUFS 類似,不過它比 AUFS 更簡單,讀寫性能更好,在 docker-ce18.03 版本中默認用的存儲驅動是 overlay2,老版本 overlay 官方已經不推薦使用。它將兩個目錄 upperdir 和 lowdir 聯合掛載到一個 merged 目錄,提供統一視圖。其中 upperdir 是可讀寫層,對容器修改寫入在該目錄中,它也會隱藏 lowerdir 中相同的文件。而 lowdir 是只讀層, Docker 鏡像在這層。
在看 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 `-- work 5 directories, 3 files root@stretch:/home/vagrant/overlaytest# mount -t overlay overlay -olowerdir=./lowerdir,upperdir=./upperdir,workdir=./workdir ./merged root@stretch:/home/vagrant/overlaytest# tree . |-- lowerdir | |-- both.txt | `-- only.txt |-- merged | |-- both.txt | `-- only.txt |-- upperdir | `-- both.txt `-- workdir `-- work 5 directories, 5 files root@stretch:/home/vagrant/overlaytest# tree -a . |-- lowerdir | |-- both.txt | `-- only.txt |-- merged | |-- both.txt | `-- only.txt |-- upperdir | `-- both.txt `-- workdir `-- work 5 directories, 5 files root@stretch:/home/vagrant/overlaytest# echo "modified both" > merged/both.txt root@stretch:/home/vagrant/overlaytest# cat upperdir/both.txt modified both root@stretch:/home/vagrant/overlaytest# cat lowerdir/both.txt lower both.txt root@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 `-- work 5 directories, 6 files root@stretch:/home/vagrant/overlaytest# cat upperdir/only.txt modified only root@stretch:/home/vagrant/overlaytest# cat lowerdir/only.txt lower only.txt root@stretch:/home/vagrant/overlaytest# tree -a . |-- lowerdir | |-- both.txt | `-- only.txt |-- merged | |-- both.txt | `-- only.txt |-- upperdir | |-- both.txt | `-- only.txt `-- workdir `-- work 5 directories, 6 files root@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 `-- work root@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 nginx Using default tag: latest latest: Pulling from library/nginx f2aa67a397c4: Pull complete 3c091c23e29d: Pull complete 4a99993b8636: Pull complete Digest: sha256:0fb320e2a1b1620b4905facb3447e3d84ad36da0b2c8aa8fe3a5a81d1187b884 Status: Downloaded newer image for nginx:latest root@stretch:/home/vagrant# ls -ls /var/lib/docker/overlay2/ total 16 4 drwx------ 4 root root 4096 May 19 04:17 09495e5085bced25e8017f558147f82e61b012a8f632a0b6aac363462b1db8b0 4 drwx------ 3 root root 4096 May 19 04:17 8af95287a343b26e9c3dd679258773880e7bdbbe914198ba63a8ed1b4c5f5554 4 drwx------ 4 root root 4096 May 19 04:17 f311565fe9436eb8606f846e1f73f38287841773e8d041933a41259fe6f96afe 4 drwx------ 2 root root 4096 May 19 04:17 l root@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/4FHO2G5SWWRIX44IFDHU62Z7X2 root@stretch:/var/lib/docker/overlay2# cat 09495e5085bced25e8017f558147f82e61b012a8f632a0b6aac363462b1db8b0/lower l/4FHO2G5SWWRIX44IFDHU62Z7X2 root@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 01a873eeba41f00a5a3deb083adf5ed892c55b4680fbc2f1880e282195d3087b root@stretch:/var/lib/docker/overlay2# ls -ls 4 drwx------ 4 root root 4096 May 19 04:17 09495e5085bced25e8017f558147f82e61b012a8f632a0b6aac363462b1db8b0 4 drwx------ 5 root root 4096 May 19 09:11 11b7579a1f1775ad71fe0f0f45fcb74c241fce319f5125b1b92cb442385065b1 4 drwx------ 4 root root 4096 May 19 09:11 11b7579a1f1775ad71fe0f0f45fcb74c241fce319f5125b1b92cb442385065b1-init 4 drwx------ 3 root root 4096 May 19 04:17 8af95287a343b26e9c3dd679258773880e7bdbbe914198ba63a8ed1b4c5f5554 4 drwx------ 4 root root 4096 May 19 04:17 f311565fe9436eb8606f846e1f73f38287841773e8d041933a41259fe6f96afe 4 drwx------ 2 root root 4096 May 19 09:11 l root@stretch:/home/vagrant# ls -ls /var/lib/docker/overlay2/11b7579a1f1775ad71fe0f0f45fcb74c241fce319f5125b1b92cb442385065b1/ 4 drwxr-xr-x 4 root root 4096 May 19 09:11 diff 4 -rw-r--r-- 1 root root 26 May 19 09:11 link 4 -rw-r--r-- 1 root root 115 May 19 09:11 lower 4 drwxr-xr-x 1 root root 4096 May 19 09:11 merged 4 drwx------ 3 root root 4096 May 19 09:11 work root@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 var root@stretch:/var/lib/docker/overlay2# ls 11b7579a1f1775ad71fe0f0f45fcb74c241fce319f5125b1b92cb442385065b1/diff/ run var
如果我們在容器中修改文件,則會反映到容器層的 merged 目錄相關文件,容器層的 diff 目錄相當于 upperdir,其他層是 lowerdir。如果之前容器層 diff 目錄不存在該文件,則會拷貝該文件到 diff 目錄并修改。讀取文件時,如果 upperdir 目錄找不到,則會直接從下層的鏡像層中讀取。
4 總結隨著版本不斷更新,Docker 的一些技術細節也在變化,如鏡像層存儲目錄的變化,默認 UnionFileSystem 換成 OverlayFS,新的 Namespace 的支持等。這篇文章主要對以前的學習筆記和 Docker 的一些新的變化做了些許總結,如想了解更詳細內容,可以查看參考資料和 Docker 官方相關文檔。
作者:__七把刀__
鏈接:https://www.jianshu.com/p/7a1...
來源:簡書
更多相關閱讀:
Docker 容器操作
Docker 的那點事兒
Docker 基礎技術-Linux Namespace
docker-compose.yml 配置詳解
如果你還想了解更多,想和技術同僚分享切磋,可掃下方二維碼加好友,回復yw,加入掘金運維技術交流群
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/8042.html
摘要:底層技術用于環境隔離,支持的包括以及新加入的等,用于隔離主機名和域名,使用標識,用于隔離進程間通信資源如消息隊列等,使用標識,隔離進程,用于隔離網絡,用于隔離掛載點,用于隔離用戶組。 本文已獲得原作者__七把刀__授權。 Docker 容器技術已經發展了好些年,在很多項目都有應用,線上運行也很穩定。整理了部分 Docker 的學習筆記以及新版本特性,對Docker感興趣的同學可以看看,...
摘要:本文已獲得原作者授權。在構建鏡像的過程中會緩存一系列中間鏡像。鏡像時,會順序執行中的指令,并同時比較當前指令和其基礎鏡像的所有子鏡像,若發現有一個子鏡像也是由相同的指令生成,則命中緩存,同時可以直接使用該子鏡像而避免再去重新生成了。 本文已獲得原作者 CodeSheep 授權。 概述 Dockerfile 是專門用來進行自動化構建鏡像的編排文件(就像 Jenkins 2.0時代的 J...
摘要:第二具備輕量化特性容器的體積非常小巧。他們大多認為自己應該將應用程序部署至當前正在運行的容器當中。不要創建大型鏡像體積過大的鏡像會加大其發布難度。總體來講,在向生產環境中部署容器時,必須避免使用最新標簽。 當下最火爆的Docker,是一個開源的應用容器引擎。大家已經開始認同并接受容器技術,并意識到它能夠解決多種現實問題并具備一系列無可比擬的優勢。今天小數就和大家聊一聊容器技術的優勢和誤...
摘要:如果我們的容器使用,文件如下在這個例子中,我們可以重復創建和銷毀,同一個持久存儲會被提供給新的,無論容器位于哪個節點上。 前言 臨時性存儲是容器的一個很大的買點。根據一個鏡像啟動容器,隨意變更,然后停止變更重啟一個容器。你看,一個全新的文件系統又誕生了。 在docker的語境下: # docker run -it centos [root@d42876f95c6a /]# echo H...
摘要:如果我們的容器使用,文件如下在這個例子中,我們可以重復創建和銷毀,同一個持久存儲會被提供給新的,無論容器位于哪個節點上。 前言 臨時性存儲是容器的一個很大的買點。根據一個鏡像啟動容器,隨意變更,然后停止變更重啟一個容器。你看,一個全新的文件系統又誕生了。 在docker的語境下: # docker run -it centos [root@d42876f95c6a /]# echo H...
閱讀 1357·2021-11-22 15:25
閱讀 3350·2021-10-21 09:38
閱讀 1564·2021-10-19 13:21
閱讀 992·2021-09-06 15:00
閱讀 1674·2019-08-30 15:44
閱讀 2578·2019-08-29 15:40
閱讀 3432·2019-08-29 13:44
閱讀 2024·2019-08-26 16:56