1. 程式人生 > >docker——核心實現技術

docker——核心實現技術

作為一種容器虛擬化技術,Docker深度應用了作業系統的多項底層支援技術。

早期版本的Docker是基於已經成熟的Linux Container(LXC)技術實現的。
自從0.9版本起,Docker逐漸從LXC轉移到新的libcontainer上,
並積極推動開放容器規則runc,試圖打造更通用的底層容器虛擬化庫。

從作業系統功能上看,目前Docker底層依賴的核心技術主要包括:
Linux作業系統的命令空間(Namesspace)控制組(Control Group)聯合檔案系統(Union File System)和Linux網路虛擬化支援

一、基本架構

Docker目前採用了標準的C/S架構。
客戶端和服務端既可以在同一臺機器上,也可以執行在不同機器上通過socket或RESTful API來進行通訊。

1.服務端

Docker Daemon一般在宿主主機後臺執行,作為服務端接收來自客戶的請求,並處理這些請求(建立、執行、分發容器)。
在設計上,Docker Daemon是一個模組化的架構,通過專門的Engine模組來分發管理各個來自客戶端的任務。
Docker服務端預設監聽本地的unix:///var/run/docker.sock套接字,只允許本地的root使用者或docker使用者組成員訪問。
可以通過-H選項來修改監聽的方式。例如,讓服務端監聽本地的TCP連線1234埠:docker daemon -H 0.0.0.0:1234
此外Docker還支援HTTPS認證方式來驗證訪問。

2.服務端

Docker客戶端為使用者提供了一系列可執行命令,使用者用這些命令與Docker Daemon互動。
使用者使用的Docker可執行命令即為客戶端程式。
與Docker Daemon不同的是,客戶端傳送命令後等待服務端返回,一旦受到返回後,客戶端立即執行結束並退出。
使用者執行新的命令,需要再次呼叫客戶端命令。

同樣,客戶端預設通過本地的unix:///var/run/docker/sock套接字向服務端傳送命令。
如果服務端沒有監聽預設地址,則需要客戶端在執行命令的時候顯式指定服務端地址。

使用docker -H tcp://127.0.0.1:1234指定接收地址。

我們一般使用的時候,docker的客戶端和服務端一般都在一臺機器上,即使要要遠端操控也是通過SSH連線宿主機。

 

3.新的架構設計

在使用Docker的時候,必須要保證Docker Daemon的正常執行,它既要管理容器的執行,又要負責對外部API進行響應。
而一旦Docker Daemon服務異常,則執行在Docker主機上的容器往往都無法使用。

在較新的版本中,開始將維護容器執行的任務放到一個單獨的元件containerd中來管理,並支援OCI的runc規範。
原先對客戶端的API支援仍然放在Docker Daemon,通過解耦,大大減少了對Docker Daemon的依賴

新的架構提高了容器的啟動速度,測試表明,可以達到每秒啟動100個容器。

 

 

二、名稱空間

名稱空間(namespace)是Linux核心的一個強大特徵,為容器虛擬化的實現帶來了極大的便利。
利用這一特徵,每個容器都可以擁有自己單獨的名稱空間,執行在其中的應用都像是在獨立的作業系統環境中一樣。
命令空間機制保證了容器彼此之間互不影響。

在作業系統中,包括核心、檔案系統、網路、PID、UID、IPC、記憶體、硬碟、CPU等資源,所有的資源都是應用程序共享的。
要想實現虛擬化,除了要實現對記憶體、CPU、網路IO、硬碟IO、儲存空間等的限制外,還要實現檔案系統、網路、PID、UID、IPC等的相互隔離。
前者相對容易實現一些,後者則需要宿主機系統的深入支援。

1.程序的名稱空間

Linux通過名稱空間管理程序號,對於同一個程序(即同一個task_struct),在不同的名稱空間中,
看到的程序號不相同,每個程序名稱空間有一套自己的程序號管理方法。
程序名稱空間是一個父子關係的結構,子空間中的程序對父空間是可見的。
新fork出的程序在父名稱空間和子名稱空間將分別有一個程序號來對應。

 

2.網路名稱空間

如果有了PID名稱空間,那麼每個名稱空間中的程序就可以相互隔離,但是網路埠還是共享本地系統的埠。
通過網路名稱空間,可以實現網路隔離。網路名稱空間為程序提供了一個完全獨立的網路協議棧檢視,
包括網路裝置介面、IPv4和IPv6協議棧、IP路由表、防火牆規則、sockets等,這樣每個容器的網路就能隔離開來。
Docker採用虛擬網路裝置的方式,將不同名稱空間的網路裝置連線到一起。
預設情況下,容器中的虛擬網絡卡將同本地主機上的docker0網橋連線到一起。

 

3.IPC名稱空間

容器中程序互動還是採用了Linux常見的程序間互動方法(Interprocess Communication)IPC,包括訊號量、訊息佇列和共享記憶體等。
PID Namespace和IPC Namespace可以組合起來一起使用,同一個IPC名稱空間內的程序可以彼此可見,允許進行互動,不同空間的程序則無法互動。

 

4.掛載名稱空間

類似於chroot,將一個程序放到一個特定的目錄執行。
掛載名稱空間允許不同名稱空間的程序看到的檔案結構不同,這樣每個名稱空間中程序所看到的檔案目錄彼此被隔離。

 

5.UTS命令空間

UST(UNIX Time-sharing System)名稱空間允許獨立的主機名和域名,
從而可以虛擬出一個獨立主機名和網路空間環境,就跟網路上一臺獨立的主機一樣。

 

6.使用者名稱空間

每個容器可以有不同的使用者和組ID,也就是說可以在容器內使用特定的內部使用者執行程式,而非本地系統上存在的使用者。
每個容器內部都可以有root賬號,但跟宿主機不在一個名稱空間。
通過使用隔離的使用者名稱空間可以提高安全性,避免容器內程序獲取到額外的許可權。

 

 

三、控制組

控制組(CGroups)是Linux的一個特徵,主要用來對共享資源進行隔離、限制、審計等。
只有能控制分配到容器的資源,才能避免多個容器同時執行時對宿主機系統的資源競爭。

控制組技術最早是由Google的程式設計師2006年提出的,Linux核心自2.6.2開始原生支援。
控制組可以提供對容器的記憶體、CPU、磁碟IO等資源進行限制和計費模式
控制組的應用目標時為不同的應用情況提供統一的介面,從控制單一程序(比如nice工具)到系統級(OpenVZ、LXC)的虛擬化。
可以在建立或啟動容器的時候為每個容器指定資源限制。

具體來看,控制組提供:
  資源限制(Redource limiting):可以將組設定為不超過設定的記憶體限制。
  優先順序(Prioritization):通過優先順序讓一些組優先得到更多的CPU等資源。
  資源審計(Accounting):用來統計系統實際上把多少資源用到適合的目的上,可以使用cpuacct子系統記錄某個程序組使用的CPU時間。
  隔離(isolation):為組隔離名稱空間,這樣一個組不會看到另一個組的程序、網路連線和檔案系統。
  控制(Control):掛起、恢復和重啟等操作。

 

 

四、聯合檔案系統

聯合檔案系統(UnionFS)是一種輕量級的高效能分層檔案系統,它支援將檔案系統中的修改資訊作為一次提交,
並層層疊加,同時可以將不同目錄掛載到同一個虛擬檔案系統下,應用看到的是掛載的最終結果。

聯合檔案系統是實現Docker映象的技術基礎。Docker映象可以通過分層來進行繼承。
例如,使用者基於基礎映象來製作各種不同的應用映象。這些映象共享一個基礎映象層,提高了儲存效率。
此外,當用戶改變了一個Docker映象(比如升級程式到新的版本),則會建立一個新的層(layer)。
因此,使用者不用替換整個原映象或重新建立,只需要新增新層即可。
使用者分發映象的時候,也只需要分發被改動的新層內容(增量部分)。
這讓docker的映象管理變得十分輕量級和快速。

1.docker儲存

Docker目前通過外掛化方式支援多種檔案系統後端。
Debian/Ubuntu上成熟的AUFS就是一種聯合檔案系統實現。
AUFS支援為每一個成員目錄(類似git分支)設定只讀(readonly)、讀寫(readwrite)或寫出(whiteout-able)許可權,
同時AUFS裡有一個類似分層的概念,對只讀許可權的分支可以在邏輯上進行增量的修改(不影響只讀部分)。

Docker映象自身就是由多個檔案層組成,每一層有唯一的編號(層ID)。
可以通過docker history檢視一個映象有那些層組成。

對於docker來說,這些層的內容都是不可修改的、只讀的。
而當docker利用映象啟動一個容器時,將在映象檔案系統的最頂端再掛載一個新的可讀寫層給容器
容器中的內容更新將會發生在可讀寫層。當所操作物件位於較深的某層時,需要先複製到最上層的可讀寫層。
當資料物件較大時,往往意味IO效能較差。因此,一般推薦將容器修改的資料通過volume方式掛載,而不是修改映象內的資料。

此外,對於頻繁啟停docker容器的場景下,檔案系統的IO效能也將十分關鍵。

docker預設系統/var/lib/docker
docker預設配置檔案/etc/docker

2.多種檔案系統比較

Docker目前支援的聯合檔案系統種類包括:AUFS、OverlayFS、btrfs、vfs、zfs和Device Mapper等。
各種檔案系統情況如下:
  AUFS:最早支援的檔案系統,對Debian/Ubuntu支援好,雖然沒有合併到Linux核心中,但成熟度很高。
  OverlayFS:類似於AUFS,效能更好一些,已經合併到核心,未來會取代AUFS,但成熟度有待提高。
  Device Mapper:Redhat公司和Docker團隊一起開發用於支援RHEL的檔案系統,核心支援,效能略慢,成熟度高。
  btrfs:參考zfs等特性設計的檔案系統,由Linux社群開發,檢視未來取代Device Mapper,成熟度有待提高。
  vfs:基於普通檔案系統(ext、nfs)的中間層抽象,效能較差,比較佔用空間,成熟度也一般。
  zfs:最初設計為Solaris 10上寫的檔案系統,擁有不少好的特性,但對於Linux支援還不夠成熟。

AUFS和Device Mapper的應用最為廣泛,推薦使用。

 

 五、Linux網路虛擬化

Docker的本地網路實現其實就是利用了Linux上的網路命令空間虛擬網路裝置(特別是veth)。

1.基本原理

直觀上看,要實現網路通訊,機器需要至少一個網路介面,(物理介面或虛擬介面)與外界相同,並可以收發資料包;
此外,如果不同子網之間要進行通訊,需要額外的路由機制。

Docker的網路介面預設都是虛擬的介面。虛擬介面的優勢就是轉發效率極高。
這是因為Linux通過在核心中進行資料複製來實現虛擬介面之間的資料轉發,
即傳送介面傳送快取中的資料包將被直接複製到接收介面的接受快取中,
而無需通過外部網路裝置進行交換。對於本地系統和容器內系統來看,
虛擬介面跟一個正常的乙太網卡相比並無區別,只是它速度要快得多。

Docker容器網路就很好的利用了Linux虛擬網路技術,在本地主機和容器內分別建立一個虛擬介面
並讓他們彼此連通(這樣一對介面叫做veth pair)。

2.網路建立過程

一般情況下,docker建立一個容器的時候,會執行如下操作:
  1)建立一對虛擬介面,分別放到本地主機和新容器的名稱空間中;
  2)本地主機一端的虛擬介面連線到預設的docker()網橋並具有一個以veth開頭的唯一名字。
  3)容器一端的虛擬介面將放到新建立的容器中,並修改名字作為eth0.這個名字值在容器的名稱空間中可見。
  4)從網橋可用地址段中獲取一個空閒地址分配給容器eth0,並配置預設路由閘道器為docker()網絡卡的內部介面docker()的IP地址。
完成這些後,容器就可以使用他所能看到的eht0虛擬網絡卡來連線其他容器和訪問外部網路。

通過docker network命令來手動管理網路。
使用docker run或docker create命令啟動容器的時候,可以通過--net引數來指定容器的網路配置。
有5個可選值:bridge、none、container、host和使用者定義網路。
  --net=bridge:預設值,在Docker網橋docker()上為容器建立新的網路棧

  --net=none:讓Docker將新容器放到隔離的網路棧中,但是不進行網路配置。之後使用者可以自定義進行配置。

  --net=container:NAME_or_ID:讓Docker將新建立容器的程序放到一個已存在容器的網路棧中
    新容器程序有自己的檔案系統、程序列表和資源限制,但會已存在的容器共享IP地址和埠等資源,
    兩者程序可以直接通過lo環回介面通訊。

  --net=host:告訴Docker不要將容器網路放到隔離的名稱空間中,即不要容器化容器中的網路。
    此時容器使用本地主機的網路,它擁有完全的本地主機介面使用許可權。
    容器程序可以跟主機其它root程序一樣開啟低範圍的埠,可以訪問本地網路服務。
    比如D-bus,還可以讓容器做一些影響整個主機系統的事情,比如重啟主機。
    如果進一步使用--privileged=true引數,容器會被直接允許配置主機的網路棧。

  --net=user_defined_network:使用者自行用network相關命令建立一個網路,通過這種方式將容器連線到指定的已建立的網路上去。

 

 

六、docker、Containerd、runc

從Docker 1.11開始,Docker容器執行已經不是簡單的通過Docker Daemon來啟動,而是集成了containerd、runc等多個元件。
Docker服務啟動之後,我們可以看到系統啟動了dockerd、docker-containerd、docker-runc等程序。

1.Docker Daemon

作為Docker容器管理的守護程序,Docker Daemon從最初整合在docker命令中,
到後來的獨立成單獨的二進位制程式,其功能正在被逐漸拆分細化,被分配到各個單獨的模組中去。
從Docker服務的啟動指令碼,也能看到守護程序的逐漸剝離。

在Docker1.8之前,Docker守護程序啟動的命令為:docker -d,這個階段,守護程序看上去只是Docker client的一個選項。
Docker1.8開始,啟動命令就變成了:docker daemon,這個階段,守護程序看上去是docker命令的一個模組。
Docker1.11開始,守護程序啟動命令變成了:dockerd,此時已經和Docker client分離獨立成一個二進位制程式了。

當然,守護程序不停的在重構,其基本功能和定位沒有變化。
和一般的CS架構系統一樣,守護程序負責和Docker client互動,並管理Docker映象、容器。

下面會介紹獨立拆分出來的幾個模組。

 

2.Containerd

Containerd是容器技術標準化之後的產物,為了能夠相容OCI標準,將容器執行時及其管理功能從Docker Daemon剝離。
理論上是,即使不執行Dockerd也能夠直接通過Containerd來管理容器。
當然,Containerd本身只是一個守護程序,容器的實際執行時由後面介紹的RunC控制。

Containerd主要職責是映象管理(映象、元資訊等)、容器執行(呼叫元件執行)。

Containerd向上為Docker Daemon提供了RPC介面,使得Dokcer Daemon遮蔽下面的結構變化,確保原有介面向下相容。
向下通過containerd-shim結合runC,使得引擎可以獨立升級,避免之前Docker Daemon升級會導致所有容器不可用的問題。

Docker、containerd和containerd-shim之間的關係,可以通過啟動一個Docker容器,觀察程序之間的管理。

啟動一個Docker容器:

docker run -d busybox sleep 1000

通過pstree命令檢視程序之間的父子關係:

pstree -l -a -A 2950

我們可以看出,當Docker Daemon啟動之後,dockerd和docker-containerd程序一直存在。
當啟動容器之後,docker-containerd程序會建立docker-containerd-shim程序。

 

3.RunC

OCI定義了容器執行時標準,runC是Docker按照開放容器格式標準(OCF,Open containerd Format)制定的一種具體實現。

runC是從Docker的libcontainer中遷移而來的,實現了容器啟動停止、資源隔離等功能。
Docker預設提供了docker-runc實現,事實上,通過containerd的封裝,可以在Docker Daemon啟動的時候指定runC的實現。
docker daemon --add-runtime "custom=/usr/local/bin/my-runc-replacement"