版權聲明:本文為部落客原創文章,未經部落客允許不得轉載。 https://blog.csdn.net/zhaobryant/article/details/79599432
Docker本質上是運作在主控端上的程序,它通過namespace實作了資源隔離,并通過cgroups實作了資源限制,同時通過寫時複制(copy-on-write)實作了高效的檔案操作。
一、通過namespace實作資源隔離
Linux核心中提供了6種namespace隔離的系統調用,分别完成對檔案系統、網絡、程序間通信、主機名、程序号以及使用者權限的隔離。
具體如下所示:
namespace | 系統調用參數 | 隔離内容 |
---|---|---|
UTS | CLONE_NEWUTS | 主機名與域名 |
IPC | CLONE_NEWIPC | 信号量/消息隊列/共享記憶體 |
PID | CLONE_NEWPID | 程序編号 |
Network | CLONE_NEWNET | 網絡裝置/網絡棧/端口等 |
Mount | CLONE_NEWNS | 挂載點(檔案系統) |
User | CLONE_NEWUSER | 使用者和使用者組 |
Linux核心實作namespace的主要目的之一就是實作輕量級虛拟化容器服務。
在同一個namespace下的程序可以感覺彼此的變化,而對外界的程序一無所知。這樣就可以讓容器中的程序産生錯覺,仿佛置身于一個獨立的系統環境中,進而達到獨立和隔離的目的。
1. 進行namespace API操作的4種方式
namespace的API包括clone()、setns()、unshare()以及/proc下的部分檔案。
(1)通過clone()在建立新程序的同時建立namespace
對于clone()系統調用,其調用方式如下:
int clone(int (*child_func)(void *), void *child_stack, int flags, void *args);
clone()實際上是Linux系統調用fork()的一種更通用的實作方式,它可以通過flags來控制使用多少功能。
clone()标志位參數中與namespace相關的四個參數分别是:
-
:用于傳入子程序運作的程式主函數;child_func
-
:用于傳入子程序使用的棧空間;child_stack
-
:表示使用哪些CLONE_*标志位,主要包括如上表所示的系統調用參數;flags
-
:表示可用于傳入的使用者參數。args
(2)通過setns()加入一個已經存在的namespace
對于setns()系統調用,其調用方式如下:
int setns(int fd, int nstype);
通過setns()系統調用,程序可以從原來的namespace加入某個已存在的namespace中。
setns()系統調用中的參數如下:
-
:表示要加入namespace的檔案描述符。namespace實際上也是一個檔案,有對應的檔案描述符。它是一個指向/proc/[pid]/ns目錄的檔案描述符。fd
-
:表示是否檢查fd指向的namespace類型是否符合實際要求。該參數為0表示不檢查。nstype
通常,為了不影響程序的調用者,也為了使新加入的pid namespace生效,會在setns()函數執行後使用clone()建立子程序繼續執行指令,讓原先的程序結束運作。
例如,新建立namespace,并在該namespace中調用/bin/bash并接受參數,以運作shell。用法如下所示:
fd = open(argv[1], O_RDONLY); // 擷取namespace檔案描述符
setns(fd, 0); // 加入新的namespace
execvp(argv[2], &argv[2]); // 執行程式
假設編譯後的程式名稱為setnsdemo,于是可以執行:
$ ./setnsdemo /proc/27514/ns/uts /bin/bash # uts是對應程序号為27514的程序對應uts的namespace号
至此,就可以在新加入的namespace中執行shell指令了。
(3)通過unshare()在原先程序上進行namespace隔離
對于unshare()系統調用,其調用方式:
int unshare(int flags);
與clone()不同的是,unshare()運作在原來的程序上,不需要啟動一個新程序。調用unshare()的主要作用是不啟動一個新程序就可以起到隔離的效果,相當于跳出原先的namespace進行操作。
(4)檢視/proc/[pid]/ns檔案
從3.8版本的核心開始,使用者就可以在
/proc/[pid]/ns
檔案夾下看到指向不同namespace号的檔案,如下圖所示:
注意:如果兩個程序指向的namespace編号相同,就說明它們在同一個namespace之下,否則便在不同namespace中。
那麼為什麼要在/proc/[pid]/ns中設定這些link連結呢?
- 其一:用于記錄[pid]所對應的程序的namespace資訊,友善查閱;
- 其二:一旦上述link檔案被打開,隻要打開的檔案描述符存在,就算該namespace下的所有程序都已經結束,這個namespace也會一直存在,後續程序也可以再加入進來。
在Docker中,通過檔案描述符定位和加入一個存在的namespace是最基本的方式。
2. UTS namespace
UTS(Unix Time-sharing System)namespace提供了主機名和域名的隔離,這樣,每個Docker容器就可以擁有獨立的主機名和域名了,在網絡上可以被當作一個獨立的節點,而非主控端上的一個程序,其标志位為
CLONE_NEWUTS
。
在Docker中,每個鏡像基本都以自身所提供的服務名稱來命名鏡像的hostname,且不會對主控端産生任何影響,其原理就是使用了UTS namespace。
使用對比:
-
沒有使用UTS的情況
運作結果如下:
- 使用UTS的情況
可見,當使用UTS隔離之後,整個子程序的主機名和域名發生了改變。
3. IPC namespace
程序間通信(Inter-Process Communication, IPC)涉及的IPC資源包括常見的信号量、消息隊列和共享記憶體,其标志位為
CLONE_NEWIPC
對于IPC資源申請,申請IPC資源就申請了一個全局唯一的32位ID,是以,IPC namespace中實際上包含了系統IPC辨別符以及實作POSIX消息隊列的檔案系統。
在同一個IPC namespace下的程序彼此可見,不同IPC namespace下的程序則互相不可見。
對于IPC namespace隔離的測試,可以參考:
http://crosbymichael.com/creating-containers-part-1.html4. PID namespace
PID namespace隔離對程序PID重新标号,即兩個不同namespace下的程序可以有相同的PID。每個PID namespace都有自己的計數程式,标志位為
CLONE_NEWPID
核心為所有的PID namespace維護了一個樹狀結構,最頂層的是系統初始時建立的,被稱為root namespace。它建立的新的PID namespace被稱為child namespace(樹的子節點),而原先的PID namespace就是建立的新的PID namespace的parent namespace(樹的父節點)。通過這種方式,不同的PID namespace就會形成一個層級體系。所屬的父節點可以看到子節點中的程序,并可以通過信号等方式對子節點中的程序産生影響,但是子節點卻不能看到父節點PID namespace中的任何内容。
關于PID namespace的相關結論:
- 每個PID namespace中的第一個程序“PID 1”,都如同init程序一樣具有特殊作用。
- namespace中的程序不能通過kill或ptrace來影響父節點或兄弟節點中的程序。
- 在root namespace中可以看到所有的程序,并且遞歸包含所有子節點中的程序。
一種在外部監控Docker中運作程式的方法:監控Docker daemon所在的PID namespace下的所有程序及其子程序,再進行篩選即可。
對于PID namespace中的init程序,其主要用于維護所有後續啟動程序的運作狀态。當系統中存在樹狀嵌套結構的PID namespace時,若某個子程序成為孤兒程序,收養該子程序的責任就交給了該子程序所屬的PID namespace中的init程序。通觀而言,PID namespace維護這樣一個樹狀結構有利于系統的資源監控與回收。是以,如果确實需要在一個Docker容器中運作多個程序,最先啟動的指令程序應該是具有資源監控與回收等管理能力的,如/bin/bash。另外,對于PID namespace中的init程序,其同時具有信号屏蔽的特權。也就是說,與init在同一個PID namespace下的程序(即使有超級權限)發送給它的所有信号都會被屏蔽,以防止init程序被誤殺。但是,當父節點PID namespace中的程序發送相同的信号給子節點PID namespace中的init程序時,如果該信号是SIGKILL(銷毀程序)或SIGSTOP(暫停程序),子節點的init程序會強制執行,其餘的信号則會被忽略。同時,一旦init程序被銷毀,同一PID namespace中的其他程序也随之接收到SIGKILL信号而被銷毀。
對于PID namespace的ps指令,如果隻想看到PID namespace本身應該看到的程序,需要重新挂載/proc,指令如下:
zjl@ubuntu:~$ mount -t proc proc /proc
zjl@ubuntu:~$ ps a
5. mount namespace
mount namespace通過隔離檔案系統挂載點對檔案系統的隔離提供支援,它是第一個Linux namespace,是以标志位比較特殊,為
CLONE_NEWNS
可以通過
/proc/[pid]/mount
檢視到所有挂載在目前namespace中的檔案系統,還可以通過
/proc/[pid]/mountstats
看到mount namespace中檔案裝置的統計資訊,包括挂載檔案的名字、檔案系統類型、挂在位置等。
具體如下圖所示:
$ vim /proc/2430/mounts
$ vim /proc/2430/mountstats
程序在建立mount namespace時,會把目前的檔案結構複制給新的mount namespace。新的mount namespace中的所有mount操作都隻影響自身的檔案系統,對外界不會産生任何影響。
mount namespace機制:挂載傳播(mount propagation)。
挂載傳播定義了挂載對象(mount object)之間的關系,這樣的關系包括共享關系和從屬關系,系統用這些關系決定任何挂載對象中的挂載事件如何傳播到其他挂載對象。
- 共享關系(share relationship):如果兩個挂載對象具有共享關系,那麼一個挂載對象中的挂載事件會傳播到另一個挂載對象,反之亦然。
- 從屬關系(slave relationship):如果兩個挂載對象形成從屬關系,那麼一個挂載對象中的挂載事件會傳播到另一個挂載對象,但是反之不行。
一個挂載狀态可能為以下一種:
- 共享挂載(shared):傳播事件的挂載對象。
- 從屬挂載(slave):接收傳播事件的挂載對象。
- 私有挂載(private):既不傳播也不接收傳播事件的挂載對象。
- 不可綁定挂載(unbindable):不允許執行綁定挂載。
示意圖如下:
如圖,可以得知:
- mount namespace下的/bin目錄與child namespace通過master/slave方式進行挂載傳播,當mount namespace中的/bin目錄發生變化時,其挂載事件會自動傳播到child namespace中;
- /lib目錄使用完全的共享挂載傳播,各namespace之間發生的變化都會互相影響;
- /proc目錄使用私有挂載傳播的方式,各個mount namespace之間互相隔離;
- /root目錄一般都是管理者所有,不能讓其他mount namespace挂載綁定。
在預設情況下,所有挂載狀态都是私有挂載。
設定為共享挂載的指令如下:
$ mount --make-shared <mount-object>
從共享挂載狀态的挂載對象克隆的挂載對象,其狀态也是共享的,它們互相傳播挂載事件。
設定為從屬挂載的指令如下:
$ mount --make-slave <shared-mount-object>
來源于從屬挂載對象克隆的挂載對象也是從屬挂載,它也從屬于原來的從屬挂載的主挂載對象。
将一個從屬挂載對象設定為共享/從屬挂載的指令如下:
$ mount --make-shared <slave-mount-object>
對于
CLONE_NEWNS
,當
CLONE_NEWNS
生效之後,子程序進行的挂載與解除安裝操作都将隻作用于該mount namespace。
6. network namespace
network namespace主要提供了關于網絡資源的隔離,包括網絡裝置、IPv4和IPv6協定棧、IP路由表、防火牆、
/proc/net
目錄、
/sys/class/net
目錄以及套接字(socket)等。
對于網絡裝置,其最多存在于一個network namespace中,可以通過建立veth pair在不同的network namespace間建立通道,以達到通信的目的。對于veth pair,其表示虛拟網絡映射對,它有兩端,類似管道,如果資料從一端傳入,另一端也能接收到。一般情況下,實體網絡裝置都配置設定在最初的root namespace中,但是,如果有多塊實體網卡,也可以把其中一塊或多塊配置設定給新建立的network namespace。
對于network namespace,我們可以認為其是将網絡獨立出來,模拟一個獨立網絡實體與外部使用者實體進行通信。對于該過程,容器的經典做法就是:建立一個veth pair,一端放置于新的network namespace中,通常命名為eth0,另一端放置在原來的network namespace中連接配接實體網絡裝置,然後,再通過把多個裝置接入網橋或進行路由轉發,以實作網絡通信的目的。
另外,在建立起veth pair之前,新的network namespace和舊的network namespace之間通過管道(pipe)來進行通信。具體示意圖如下圖所示:
與其他namespace類似,對network namespace的使用其實就是在建立的時候添加CLONE_NEWNET标志位。
7. user namespace
user namespace主要隔離了安全相關的辨別符(identifier)和屬性(attributes),包括使用者ID、使用者組ID、root目錄、密鑰以及特殊權限。也就是說,一個普通使用者的程序通過clone()建立的新程序在新user namespace中可以擁有不同的使用者和使用者組。這意味着一個程序在容器外屬于一個沒有特權的普通使用者,但是它建立的容器程序卻屬于擁有所有權限的超級使用者。相應的,其在clone()中的标志位為
CLONE_NEWUSER
8. 小結
本節從namespace使用的API開始,結合Docker逐漸對6個namespace進行了講解。
二、cgroups資源限制
對于cgroups,它可以用于限制被namespace隔離起來的資源,還可以為資源設定權重、計算使用量、操控任務啟動和停止等。
1. cgroups概念
cgroups是Linux核心提供的一種機制,這種機制可以根據需求把一系列系統任務及其子任務整合(或分隔)到按資源劃分等級的不同組内,進而為系統資源管理提供一個統一的架構。也就是說,cgroups可以限制、記錄任務組所使用的實體資源(包括CPU、memory、IO等),為容器實作虛拟化提供了基本保證,是建構Docker等一系列虛拟化管理工具的基石。
cgroups具有如下四個特點:
- cgroups的API以一個僞檔案系統的方式實作,使用者态的程式可以通過檔案操作實作cgroups的組織管理;
- cgroups的組織管理操作單元可以細粒度到線程級别,另外使用者可以建立和銷毀cgroup,進而實作資源再配置設定和管理;
- 所有資源管理的功能都以子系統的方式實作,接口統一;
- 子任務建立時與其父任務處于同一個cgroups的控制組。
從本質上,cgroups是核心附加在程式上的一系列鈎子(hook),通過程式運作時對資源的排程觸發相應的鈎子以達到資源追蹤和限制的目的。
2. cgroups作用
從單個任務的資源控制到作業系統層面的虛拟化,cgroups提供了如下四大功能:
- 資源限制:cgroups可以對任務使用的資源總額進行限制。
- 優先級配置設定:通過配置設定的CPU時間片數量以及磁盤IO帶寬大小,實際上就相當于控制了任務運作的優先級。
- 資源統計:cgroups可以統計系統的資源使用量,如CPU使用時長等。
- 任務控制:cgroups可以對任務執行挂起、恢複等操作。
3. cgroups結構
在cgroups中,主要有如下幾個術語:
- task(任務):任務表示系統的一個程序或線程;
- cgroup(控制組):cgroup是cgroups進行資源控制的機關,它表示按某種資源控制标準劃分而成的任務組,包含一個或多個子系統。一個任務可以加入某個group,也可以從某個cgroup遷移到另外一個cgroup中;
- subsystem(子系統):cgroups中的子系統就是一個資源排程控制器,如CPU子系統可以控制CPU時間配置設定,記憶體子系統可以限制cgroup記憶體使用量;
-
hierachy(層級):層級由一系列cgroup以一個樹狀結構排列而成,每個層級通過綁定對應的子系統進行資源控制。層級中的cgroup節點可以包含零或多個子節點,子節點繼承父節點挂載的子系統。
出于易于管理的目的,在Docker中,每個子系統獨自構成一個層級。
對于cgroups的組織結構,主要有以下幾個基本規則:
- 規則1:同一個層級可以附加一個或多個子系統。
- 規則2:一個子系統可以附加到多個層級,當且僅當目标層級隻有唯一一個子系統時。
- 規則3:系統每次建立一個層級時,該系統上的所有任務預設加入這個建立層級的初始化cgroup,這個cgroup又稱root cgroup。對于建立的每個層級,任務隻能存在于其中一個cgroup中,即一個任務不能存在于同一個層級的不同cgroup中,但一個任務可以存在于不同層級中的多個cgroup中。如果操作時把一個任務添加到同一個層級的另一個cgroup中,則會将它從第一個cgroup中移除。
- 規則4:任務在fork/clone自身時建立的子任務預設與原任務在同一個cgroup中,但是子任務允許被移動到不同的cgroup中。
4. cgroups子系統
在cgroups中,子系統實際上就是cgroups的資源控制系統,每種子系統獨立地控制一種資源,目前Docker使用如下9種子系統,具體如下:
- blkio:為塊裝置設定輸入/輸出限制,比如實體驅動裝置(包括磁盤、固态硬碟、USB等)。
- cpu:使用排程程式控制任務對CPU的使用。
- cpuacct:自動生成cgroup中任務對CPU資源使用情況的報告。
- cpuset:可以為cgroup中的任務配置設定獨立的CPU和記憶體。
- devices:可以開啟或關閉cgroup中任務對裝置的通路。
- freezer:可以挂起或恢複cgroup中的任務。
- memory:可以設定cgroup中任務對記憶體使用量的限定,并且自動生成這些任務對記憶體資源使用情況的報告。
- perfevent:使用後使得cgroup中的任務可以進行統一的性能測試。
- net_cls:Docker沒有直接使用,它通過使用等級識别符(classid)标記網絡資料包,進而允許Linux流量控制程式(TC:Traffic Controller)識别從具體cgroup中生成的資料包。
5. cgroups實作方式及工作原理
cgroups的實作本質上是給任務挂上鈎子,當任務運作的過程中涉及某種資源時,就會觸發鈎子上所附帶的子系統進行檢測,然後根據資源類别的不同使用對應的技術進行資源限制和優先級配置設定。
(1)cgroups如何判斷資源超限及超出限額之後的措施
對于不同的系統資源,cgroups提供了統一的接口對資源進行控制和統計,但限制的具體方式不盡相同。
(2)cgroup的子系統與任務之間的關聯關系
實作上,cgroup與任務之間是多對多的關系,是以它們并不直接關聯,而是通過一個中間結構把雙向的關聯資訊記錄起來。每個任務結構體
task_struct
都包含了一個指針,可以查詢到對應cgroup的情況,同時也可以查詢到各個子系統的狀态,這些子系統狀态中也包含了找到任務的指針,不同類型的子系統按需定義本身的控制資訊結構體,最終在自定義的結構體中把子系統狀态指針包含進去,然後核心通過
container_of
等宏定義來擷取對應的結構體,關聯到任務,以此達到資源限制的目的。
在實際的使用過程中,需要通過挂載cgroup檔案系統來建立一個層級結構,挂載時需要指定要綁定的子系統,預設情況下預設綁定系統所有子系統。在将cgroup檔案系統挂載以後,就可以像操作檔案一樣對cgroups的hierarchy層級進行浏覽和操作管理(包括權限管理、子檔案管理等等)。除了cgroup檔案系統以外,核心沒有為cgroups的通路和操作添加任何系統調用。當一個頂層的cgroup檔案系統被解除安裝時,如果其中建立後代cgroup目錄,那麼就算上層的cgroup被解除安裝了,層級也是激活狀态,其後代cgoup中的配置依舊有效。隻有遞歸式的解除安裝層級中的所有cgoup,那個層級才會被真正删除。層級激活後,
/proc
目錄下的每個task PID檔案夾下都會新添加一個名為cgroup的檔案,列出task所在的層級,對其進行控制的子系統及對應cgroup檔案系統的路徑。同時,一個cgroup建立完成,不管綁定了何種子系統,其目錄下都會生成以下幾個檔案,用來描述cgroup的相應資訊。同樣,把相應資訊寫入這些配置檔案就可以生效,内容如下:
-
:這個檔案中羅列了所有在該cgroup中task的PID。該檔案并不保證task的PID有序,把一個task的PID寫到這個檔案中就意味着把這個task加入這個cgroup中。tasks
-
:這個檔案羅列所有在該cgroup中的線程組ID。該檔案并不保證線程組ID有序和無重複。寫一個線程組ID到這個檔案就意味着把這個組中所有的線程加到這個cgroup中。cgroup.procs
-
:填0或1,表示是否在cgroup中最後一個task退出時通知運作release agent,預設情況下是0,表示不運作。notify_on_release
-
:指定release agent執行腳本的檔案路徑(該檔案在最頂層cgroup目錄中存在),在這個腳本通常用于自動化umount無用的cgroup。release_agent
可參考:
http://www.infoq.com/cn/articles/docker-kernel-knowledge-cgroups-resource-isolation6. 小結
本節淺入深出地講解了cgroups,從cgroups是什麼,到cgroups該怎麼用,最後對大量地cgroup子系統配置參數進行了梳理。