VM安裝CentOS 7
1. Docker底層技術支撐
Linux 命令空間、控制組和UnionFS三大技術支撐了目前Docker的實現:
- namespace名稱空間:容器隔離的基礎,保證A容器看不到B容器
- cgroups控制組:容器資源統計和隔離
- UnionFS聯合檔案系統:分層映象實現的基礎
實際上Docker使用了很多Linux的隔離功能,讓容器看起來是一個輕量級的虛擬機器在獨立執行,容器的本質就是被限制了namespace和cgroup,具有邏輯上獨立的獨立檔案系統的一個程序
2. namespce
在Linux系統中,namespace是在核心級別以一種抽象的形式來封裝系統資源,通過將系統資源放在不同的namespace中,來實現資源隔離的目的
不同的namespace程式,都可以擁有一份獨立的系統資源
namespace是linux為我們提供的用於分離程序樹、網路介面、掛載點以及程序間通訊等資源的方法
Linux的namespace機制提供了以下七種不同的名稱空間,包括:
- CLONE_NEWCGROUP
- CLONE_NEWIPC:隔離程序間通訊
- CLONE_NEWNET:隔離網路資源
- CLONE_NEWNS:隔離檔案系統掛載點
- CLONE_NEWPID:隔離程序PID
- CLONE_NEWUSER
- CLONE_NEWUTS:隔離主機名和域名資訊
docker使用的是PID隔離
2.1 PID隔離
如果現在在宿主機上啟動兩個容器,在這兩個容器內各自都有一個PID=1的程序,但是眾所周知,PID在linux中是唯一的,那麼兩個容器是怎麼做到同時擁有PID=1的不同程序的?
本來,每當我們在宿主機上執行一個/bin/sh程式,作業系統就會分配給他一個PID,這個PID是程序的唯一標識,而PID=1的程序是屬於 /sbin/init 的
UID PID PPID C STIME TTY TIME CMD root 1 0 0 Mar21 ? 00:00:03 /sbin/init noibrs splash root 2 0 0 Mar21 ? 00:00:00 [kthreadd] root 4 2 0 Mar21 ? 00:00:00 [kworker/0:0H] root6 2 0 Mar21 ? 00:00:00 [mm_percpu_wq] root 7 2 0 Mar21 ? 00:00:11 [ksoftirqd/0]
什麼是/sbin/init?這個程序是被linux中的上帝程序 idle 創建出來的,主要負責執行核心的一部分初始化工作和系統配置,也會建立一些類似於 getty 的註冊程序
現在我們通過docker在容器執行 /bin/sh 就會發現PID=1的程序其實就是我們建立的這個程序,而不再是宿主機上那個 /sbin/init
UID PID PPID C STIME TTY TIME CMD mysql 1 0 0 Mar21 ? 00:10:24 mysqld root 86 0 0 09:14 pts/0 00:00:00 /bin/bash root 429 86 0 10:15 pts/0 00:00:00 ps -ef
這種技術就是linux的 PID namespace隔離
namespace的使用就是linux在建立程序的一個可選引數
我們知道,在linux中建立程序的系統呼叫是clone()方法:
int pid = clone(main_function, stack_size, SIGCHLD, NULL)
這個系統呼叫會為我們建立個新的程序,並返回它的PID
當我們使用clone()系統呼叫建立一個新程序時,就可以在引數中指定 CLONE_NEWPID 引數
int pid = clone(main_function, stack_size, CLONE_NEWPID | SIGCHLD, NULL)
此時,新建立的這個程序就是一個隔離的程序,它看不到宿主機上的任何程序
實際上,docker容器的pid隔離,就是在使用clone()建立新程序時傳入CLONE_NEWPID來實現的,也就是使用linux的名稱空間來實現程序的隔離,docker容器內部的任意程序都對宿主機的程序一無所知
每次當我們執行 docker run 時,都會在下面的方法中建立一個用於設定程序間隔離的spec:
func (daemon *Daemon) createSpec(c *container.Container) (*specs.Spec, error) { s := oci.DefaultSpec() // ... if err := setNamespaces(daemon, &s, c); err != nil { return nil, fmt.Errorf("linux spec namespaces: %v", err) } return &s, nil }
在setNamespaces方法中不僅會設定程序相關的名稱空間,還會設定與使用者、網路、IPC以及UTS相關的名稱空間:
func setNamespaces(daemon *Daemon, s *specs.Spec, c *container.Container) error { // user // network // ipc // uts // pid if c.HostConfig.PidMode.IsContainer() { ns := specs.LinuxNamespace{Type: "pid"} pc, err := daemon.getPidContainer(c) if err != nil { return err } ns.Path = fmt.Sprintf("/proc/%d/ns/pid", pc.State.GetPID()) setNamespace(s, ns) } else if c.HostConfig.PidMode.IsHost() { oci.RemoveNamespace(s, specs.LinuxNamespaceType("pid")) } else { ns := specs.LinuxNamespace{Type: "pid"} setNamespace(s, ns) } return nil }
所有名稱空間相關得設定Spec最後都會作為Create函式的入參在建立新容器時進行設定:
daemon.containerd.Create(context.Background(), container.ID, spec, createOptions)
PID namespace隔離非常實用,它對程序PID重新標號,即兩個不同namespace下的程序可以有同一個PID
每個PID namespace都有自己的計數程式。核心為所有的PID namespace維護了一個樹狀結構,最頂層的是系統初始時建立的,我們稱之為root namespace
他建立的新PID namespace就稱之為child namespace(樹的子節點),而原先的PID namespace就是新建立的PID namespace的parent namespace(樹的父節點)
通過這種方式,不同的PID namespace會形成一個等級體系,所屬的父節點可以看到子節點中的程序,並可以通過訊號燈等方式對子節點中的程序產生影響
但是子節點不能看到父節點PID namespace 中的任何內容
- 每個PID namespace 中的第一個程序 PID=1,就會像傳統linux程序中的init一樣,起特殊作用
- 一個namespace中的程序,不可能通過 kill 或者 ptrace影響父節點或者兄弟節點中的程序
- 如果在新的PID namespace中重新掛載/proc檔案系統,會發現其下只顯示同屬一個PID namespace中的其他程序
- 在root namespace中可以看到所有的程序,並且遞迴包含所有子節點中的程序
2.2 其它的作業系統基礎元件隔離
不僅僅是PID,當啟動容器之後,docker會為這個容器建立一系列其他namespaces
這些 namespaces 提供了不同層面的隔離,容器執行會受到各個層面 namesapce 的限制
Docker Engine 使用了以下 Linux 的隔離技術:
The pid namespace: 管理 PID 名稱空間 (PID: Process ID)
The net namespace: 管理網路名稱空間(NET: Networking)
The ipc namespace: 管理程序間通訊名稱空間(IPC: InterProcess Communication)
The mnt namespace: 管理檔案系統掛載點名稱空間 (MNT: Mount)
The uts namespace: Unix 時間系統隔離. (UTS: Unix Timesharing System)
通過這些技術,執行時的容器得以看到一個和宿主機上其他容器隔離的環境
3. cgroups
cgroups是linux核心中用來為進城設定資源閒置的一個重要功能
cgroups最主要的功能就是限制一個程序組能夠使用的資源上限,包括CPU、記憶體、磁碟、網路頻寬等
此外,cgroups還能對程序進行優先順序設定、審計,以及將程序掛起和恢復等操作
linux使用檔案系統來實現cgroups,我們可以直接使用命令來檢視當前的cgroup有哪些子系統:
root@root:~# mount -t cgroup cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,name=systemd) cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset) cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,net_cls,net_prio) cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,hugetlb) cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer) cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices) cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpu,cpuacct) cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids) cgroup on /sys/fs/cgroup/rdma type cgroup (rw,nosuid,nodev,noexec,relatime,rdma) cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio) cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,perf_event) cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
可以看到,在/sys/fs/cgroup下面有很多諸如cpuset、cpu、memory這樣的子目錄,這些就是可以被cgroups限制的資源種類
而在子目錄對應的資源種類下,可以看到這類資源具體可以被限制的方法,例如CPU:
root@root:~# ls /sys/fs/cgroup/cpu aegis cgroup.sane_behavior cpuacct.usage_percpu cpuacct.usage_user cpu.stat system.slice assist cpuacct.stat cpuacct.usage_percpu_sys cpu.cfs_period_us docker tasks cgroup.clone_children cpuacct.usage cpuacct.usage_percpu_user cpu.cfs_quota_us notify_on_release user.slice cgroup.procs cpuacct.usage_all cpuacct.usage_sys cpu.shares release_agent
我們可以看到其中有一個docker資料夾,cd到docker檔案下
其中四個帶有序號的資料夾其實就是我們docker中目前執行的四個容器,啟動這個容器時,docker會為這個容器建立一個與容器識別符號相同的cgroup,在當前主機上cgroup就會有以下層級關係:
每一個 CGroup 下面都有一個 tasks 檔案,其中儲存著屬於當前控制組的所有程序的 pid,作為負責 cpu 的子系統
cpu.cfs_quota_us 檔案中的內容能夠對 CPU 的使用作出限制,如果當前檔案的內容為 50000,那麼當前控制組中的全部程序的 CPU 佔用率不能超過 50%
如果系統管理員想要控制 Docker 某個容器的資源使用率就可以在 docker 這個父控制組下面找到對應的子控制組並且改變它們對應檔案的內容,當然我們也可以直接在程式執行時就使用引數,讓 Docker 程序去改變相應檔案中的內容
當我們使用 Docker 關閉掉正在執行的容器時,Docker 的子控制組對應的資料夾也會被 Docker 程序移除,Docker 在使用 CGroup 時其實也只是做了一些建立資料夾改變檔案內容的檔案操作,不過 CGroup 的使用也確實解決了我們限制子容器資源佔用的問題,系統管理員能夠為多個容器合理的分配資源並且不會出現多個容器互相搶佔資源的問題
除了CPU子系統外,cgroups的每一項子系統都有其獨有的資源限制能力:
- blkio:為塊裝置設定I/O限制,一般用於磁碟等裝置
- cpuset:為程序分配單獨的CPU核和對應的記憶體節點
- memory:為程序設定記憶體使用限制
linux cgroups的設計簡單而言,就是一個子系統目錄上加上一組資源限制檔案的組合。而對於docker等linux容器專案來說,它們只需要在每個子系統下面為每個容器建立一個控制組,然後在啟動容器程序之後,把這個程序的PID填寫到對應控制組的tasks檔案中即可。
至於在這些控制組下面的資原始檔裡填什麼值,就是使用者執行docker run 時指定的引數,例如這樣一條命令:
docker run -it --cpu-period=100000 --cpu-quota=20000 ubuntu /bin/bash
在啟動這個容器後,就可以通過檢視其資原始檔的內容來確認具體的資源限制,這意味著這個docker容器只能使用20%的cpu頻寬
4. UnionFS
UnionFS其實是一種為linux作業系統設計的用於把多個檔案系統聯合到同一個掛載點的檔案系統服務
首先,我們建立company和home兩個目錄,並且分別為他們建立兩個檔案:
# tree . . |-- company | |-- code | `-- meeting `-- home |-- eat `-- sleep
然後我們將通過mount命令把company和home兩個目錄聯合起來,建立一個AUFS的檔案系統,並掛載到當前目錄下的mnt目錄:
# mkdir mnt # ll total 20 drwxr-xr-x 5 root root 4096 Oct 25 16:10 ./ drwxr-xr-x 5 root root 4096 Oct 25 16:06 ../ drwxr-xr-x 4 root root 4096 Oct 25 16:06 company/ drwxr-xr-x 4 root root 4096 Oct 25 16:05 home/ drwxr-xr-x 2 root root 4096 Oct 25 16:10 mnt/ # mount -t aufs -o dirs=./home:./company none ./mnt # ll total 20 drwxr-xr-x 5 root root 4096 Oct 25 16:10 ./ drwxr-xr-x 5 root root 4096 Oct 25 16:06 ../ drwxr-xr-x 4 root root 4096 Oct 25 16:06 company/ drwxr-xr-x 6 root root 4096 Oct 25 16:10 home/ drwxr-xr-x 8 root root 4096 Oct 25 16:10 mnt/ root@rds-k8s-18-svr0:~/xuran/aufs# tree ./mnt/ ./mnt/ |-- code |-- eat |-- meeting `-- sleep 4 directories, 0 files
通過 ./mnt 目錄結構的輸出結果,可以看到原來兩個目錄下的內容被合併到了一個mnt目錄下
預設情況下,如果我們不對聯合的目錄指定許可權,核心將根據從左到右的順序將第一個目錄指定為可讀可寫,其餘的都為只讀
那麼,當我們向只讀的目錄做一些寫入操作的話,會發生什麼呢?
# echo apple > ./mnt/code # cat company/code # cat home/code apple
通過對上面程式碼短的觀察,可以看出當寫入操作發生在company/code 檔案時,對應的修改並沒有反映到原始的目錄中,而是在home目錄下又建立了一個名為code的檔案,並將apple寫了進去
這就是Union File System:
- Union File System聯合了多個不同的目錄,並且把他們掛載到一個統一的目錄上
- 在這些聯合的子目錄中,有一些是讀寫的,但有一部分是隻讀的
- 當對只讀的目錄內容做出修改時,其結果只會儲存在可寫的目錄下,不會影響只讀目錄
這就是docker映象分層技術的基礎
4.1 docker映象分層
docker image有一個層級結構,最底層的layer為 baseimage(一般為一個作業系統的ISO映象),然後順序執行每一條指令,生成的layer按照入棧的順序逐漸累加,形成一個image
每一層都是一個被聯合的目錄,大致如下圖所示:
4.2 Dockerfile
簡單來說,一個image是通過一個dockerfile來定義的,然後使用docker build命令構建它
dockerfile中的每一條指令的執行結果都會成為image中的一個layer
簡單看一個dockerfile的內容,觀察image分層機制:
# Use an official Python runtime as a parent image FROM python:2.7-slim # Set the working directory to /app WORKDIR /app # Copy the current directory contents into the container at /app COPY . /app # Install any needed packages specified in requirements.txt RUN pip install --trusted-host pypi.python.org -r requirements.txt # Make port 80 available to the world outside this container EXPOSE 80 # Define environment variable ENV NAME World # Run app.py when the container launches CMD ["python", "app.py"]
構建結果:
root@rds-k8s-18-svr0:~/xuran/exampleimage# docker build -t hello ./ Sending build context to Docker daemon 5.12 kB Step 1/7 : FROM python:2.7-slim ---> 804b0a01ea83 Step 2/7 : WORKDIR /app ---> Using cache ---> 6d93c5b91703 Step 3/7 : COPY . /app ---> Using cache ---> feddc82d321b Step 4/7 : RUN pip install --trusted-host pypi.python.org -r requirements.txt ---> Using cache ---> 94695df5e14d Step 5/7 : EXPOSE 81 ---> Using cache ---> 43c392d51dff Step 6/7 : ENV NAME World ---> Using cache ---> 78c9a60237c8 Step 7/7 : CMD python app.py ---> Using cache ---> a5ccd4e1b15d Successfully built a5ccd4e1b15d
通過構建可以看出,構建的過程就是執行Dockerfile檔案中我們寫入的命令
構建一共進行了7個步驟,每個步驟執行完都會生成一個隨機的ID來標識這一layer的內容,最後一行的 a5ccd4e1b15d 為映象的ID
通過了解了 Docker Image 的分層機制,可以看出Layer 和 Image 的關係與 AUFS 中的聯合目錄和掛載點的關係比較相似
參考:
Docker底層原理(圖解+秒懂+史上最全) - 瘋狂創客圈 - 部落格園 (cnblogs.com)
https://blog.csdn.net/wangqingjiewa/article/details/85000393
https://zhuanlan.zhihu.com/p/47683490
https://blog.csdn.net/weixin_37098404/article/details/102704159
《深入剖析Kubernetes》 張磊