Docker核心實現技術(命名空間&控制組&聯合文件系統&Linux網絡虛擬化支持)

做爲一種容器虛擬化技術,Docker深度應用了操做系統的多項底層支持技術。linux

早期版本的Docker是基於已經成熟的Linux Container(LXC)技術實現的。自Docker 0.9版本起,Docker逐漸從LXC轉移到新的libcontainer(https://github.com/docker/libcontainer)上,而且積極推進開放容器規範runc,試圖打造更通用的底層容器虛擬化庫。git

從操做系統功能上看,目前Docker底層依賴的核心技術主要包括Linux操做系統的命名空間(Namespace)、控制組(Control Group)、聯合文件系統(Union File System)和Linux網絡虛擬化支持github

基本架構

Docker目前採用了標準的C/S架構。客戶端和服務端既能夠運行在一個機器上,也可運行在不一樣機器上經過socket或者RESTful API來進行通訊。docker

1.服務端ubuntu

Docker Daemon通常在宿主主機後臺運行,做爲服務端接受來自客戶的請求,並處理這些請求(建立、運行、分發容器)。後端

在設計上,Docker Daemon是一個模塊化的架構,經過專門的Engine模塊來分發管理各個來自客戶端的任務。緩存

Docker服務端默認監聽本地的unix:///var/run/docker.sock套接字,只容許本地的root用戶或docker用戶組成員訪問。能夠經過-H選項來修改監聽的方式。安全

例如,讓服務端監聽本地的TCP鏈接1234端口,以下所示:bash

$ docker daemon -H 0.0.0.0:1234網絡

此外,Docker還支持經過HTTPS認證方式來驗證訪問。

Debian/Ubuntu 14.04等使用upstart管理啓動服務的系統中,Docker服務端的默認啓動配置文件在/etc/default/docker。對於使用systemd管理啓動服務的系統,配置文件在/etc/systemd/system/docker.service.d/docker.conf。

2.客戶端

Docker客戶端爲用戶提供一系列可執行命令,用戶用這些命令與Docker Daemon交互。

用戶使用的Docker可執行命令即爲客戶端程序。與Docker Daemon不一樣的是,客戶端發送命令後,等待服務端返回,一旦收到返回後,客戶端馬上執行結束並退出。用戶執行新的命令,須要再次調用客戶端命令。一樣,客戶端默認經過本地的unix:///var/run/docker.sock套接字向服務端發送命令。若是服務端沒有監聽在默認的地址,則須要客戶端在執行命令的時候顯式指定服務端地址。

例如,假定服務端監聽在本地的TCP鏈接1234端口tcp://127.0.0.1:1234,只有經過-H參數指定了正確的地址信息才能鏈接到服務端,以下所示:

$ docker version

Client:
Version:      1.12.0
API version:  1.24
Go version:   go1.6.3
Git commit:   8eab29e
Built:        Thu Sep 28 22:00:36 2016
OS/Arch:      linux/amd64
Cannot connect to the Docker daemon. Is the docker daemon running on this host?

$ docker -H tcp://127.0.0.1:1234 version

Client:
Version:      1.12.0
API version:  1.24
Go version:   go1.6.3
Git commit:   8eab29e
Built:        Thu Sep 28 22:00:36 2016
OS/Arch:      linux/amd64
Server:
Version:      1.12.0
API version:  1.24
Go version:   go1.6.3
Git commit:   8eab29e
Built:        Thu Sep 28 22:00:36 2016
OS/Arch:      linux/amd64

3.新的架構設計

C/S架構給Docker基本功能的實現帶來了許多便利,但同時也引入了一些限制。

使用Docker時,必需要啓動並保持Docker Daemon的正常運行,它既要管理容器的運行時,又要負責提供對外部API的響應。而一旦Docker Daemon服務不正常,則已經運行在Docker主機上的容器也每每沒法繼續使用。

Docker團隊已經意識到了這個問題,在較新的版本(1.11.0+)中,開始將維護容器運行的任務放到一個單獨的組件containerd中來管理,而且支持OCI的runc規範。原先的對客戶端API的支持則仍然放在Docker Daemon,經過解耦,大大減小了對Docker Daemon的依賴。同時,新的架構提升了啓動容器的速度,一項測試代表,能夠達到每秒啓動超過100個容器。

命名空間

命名空間(namespace)是Linux內核的一個強大特性,爲容器虛擬化的實現帶來極大便利。

利用這一特性,每一個容器均可以擁有本身單獨的命名空間,運行在其中的應用都像是在獨立的操做系統環境中同樣。命名空間機制保證了容器之間彼此互不影響。

在操做系統中,包括內核、文件系統、網絡、PID、UID、IPC、內存、硬盤、CPU等資源,全部的資源都是應用進程直接共享的。要想實現虛擬化,除了要實現對內存、CPU、網絡IO、硬盤IO、存儲空間等的限制外,還要實現文件系統、網絡、PID、UID、IPC等的相互隔離。前者相對容易實現一些,後者則須要宿主主機系統的深刻支持。

隨着Linux系統對於命名空間功能的逐步完善,如今已經能夠實現這些需求,讓進程在彼此隔離的命名空間中運行。雖然這些進程仍在共用同一個內核和某些運行時環境(runtime,例如一些系統命令和系統庫),可是彼此是不可見的,而且認爲本身是獨佔系統的。

1.進程命名空間

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

例如,查看Docker主進程的pid進程號是5989,以下所示:

$ ps -ef |grep docker

root      5989  5988  0 14:38 pts/6    00:00:00 docker -d

新建一個Ubuntu的「hello world」容器:

$  docker run -d ubuntu:14.04  /bin/sh -c "while true; do echo hello world;sleep 1; done"

ec559327572b5bf99d0f80b98ed3a3b62023844c7fdbea3f8caed4ffa5c62e86

查看新建容器進程的父進程,正是Docker主進程5989:

$ ps -ef |grep while

root      6126  5989  0 14:41 ?        00:00:00 /bin/sh -c while true; do echo hello world; sleep 1; done

2.網絡命名空間

若是有了pid命名空間,那麼每一個命名空間中的進程就能夠相互隔離,可是網絡端口仍是共享本地系統的端口。

經過網絡命名空間,能夠實現網絡隔離。網絡命名空間爲進程提供了一個徹底獨立的網絡協議棧的視圖,包括網絡設備接口、IPv4和IPv6協議棧、IP路由表、防火牆規則、sockets等,這樣每一個容器的網絡就能隔離開來。Docker採用虛擬網絡設備(Virtual Network Device)的方式,將不一樣命名空間的網絡設備鏈接到一塊兒。默認狀況下,容器中的虛擬網卡將同本地主機上的docker0網橋鏈接在一塊兒。

使用brctl工具能夠看到橋接到宿主主機docker0網橋上的虛擬網口:

$ brctl show

bridge name     bridge id                   STP enabled     interfaces

docker0            8000.56847afe9799       no              veth4148 vethd166 vethd533

3.IPC命名空間

容器中進程交互仍是採用了Linux常見的進程間交互方法(Interprocess Communication,IPC),包括信號量、消息隊列和共享內存等。PID Namespace和IPC Namespace能夠組合起來一塊兒使用,同一個IPC命名空間內的進程能夠彼此可見,容許進行交互;不一樣空間的進程則沒法交互。

4.掛載命名空間

相似於chroot,將一個進程放到一個特定的目錄執行。掛載命名空間容許不一樣命名空間的進程看到的文件結構不一樣,這樣每一個命名空間中的進程所看到的文件目錄彼此被隔離。

5.UTS命名空間

UTS(UNIX Time-sharing System)命名空間容許每一個容器擁有獨立的主機名和域名,從而能夠虛擬出一個有獨立主機名和網絡空間的環境,就跟網絡上一臺獨立的主機同樣。默認狀況下,Docker容器的主機名就是返回的容器ID

$ docker ps

CONTAINER ID    IMAGE            COMMAND               CREATED         STATUS           PORTS     NAMES

ec559327572b    ubuntu:14.04   /bin/sh -c 'while tr   18 minutes ago Up 18 minutes               furious_goodall

$ docker inspect  -f {{".Config.Hostname"}}  ec5

ec559327572b

6.用戶命名空間

每一個容器能夠有不一樣的用戶和組id,也就是說能夠在容器內使用特定的內部用戶執行程序,而非本地系統上存在的用戶。

每一個容器內部均可以有root賬號,但跟宿主主機不在一個命名空間。

經過使用隔離的用戶命名空間能夠提升安全性,避免容器內進程獲取到額外的權限

控制組

控制組(CGroups)是Linux內核的一個特性,主要用來對共享資源進行隔離、限制、審計等。只有能控制分配到容器的資源,才能避免多個容器同時運行時對宿主機系統的資源競爭。控制組能夠提供對容器的內存、CPU、磁盤IO等資源進行限制和計費管理。控制組的設計目標是爲不一樣的應用狀況提供統一的接口,從控制單一進程(好比nice工具)到系統級虛擬化(包括OpenVZ、Linux-VServer、LXC等)。

具體來看,控制組提供:

  1. 資源限制(Resource limiting):能夠將組設置爲不超過設定的內存限制。好比:內存子系統能夠爲進程組設定一個內存使用上限,一旦進程組使用的內存達到限額再申請內存,就會出發Out of Memory警告。
  2. 優先級(Prioritization):經過優先級讓一些組優先獲得更多的CPU等資源。
  3. 資源審計(Accounting):用來統計系統實際上把多少資源用到適合的目的上,可使用cpuacct子系統記錄某個進程組使用的CPU時間。
  4. 隔離(isolation):爲組隔離命名空間,這樣一個組不會看到另外一個組的進程、網絡鏈接和文件系統。
  5. 控制(Control):掛起、恢復和重啓動等操做。

安裝Docker後,用戶能夠在/sys/fs/cgroup/memory/docker/目錄下看到對Docker組應用的各類限制項,包括:

$ cd /sys/fs/cgroup/memory/docker 

$ ls   

用戶能夠經過修改這些文件值來控制組限制Docker應用資源。

例如,經過下面的命令可限制Docker組中全部進程使用的物理內存總量不超過100MB:

$ sudo echo 104857600 >/sys/fs/cgroup/memory/docker/memory.limit_in_bytes

進入對應的容器文件夾,能夠看到對應容器的一些狀態:

$ cd 42352bb6c1d1c5c411be8fa04e97842da87d14623495189c4d865dfc444d12ae/

$ ls

$ cat memory.stat

在開發容器工具時,每每須要一些容器運行狀態數據,這時就能夠從這裏獲得更多的信息。

能夠在建立或啓動容器時爲每一個容器指定資源的限制,例如使用-c|--cpu-shares[=0]參數來調整容器使用CPU的權重;使用-m|--memory[=MEMORY]參數來調整容器使用內存的大小。

聯合文件系統

聯合文件系統(UnionFS)是一種輕量級的高性能分層文件系統,它支持將文件系統中的修改信息做爲一次提交,並層層疊加,同時能夠將不一樣目錄掛載到同一個虛擬文件系統下,應用看到的是掛載的最終結果。

聯合文件系統是實現Docker鏡像的技術基礎。Docker鏡像能夠經過分層來進行繼承。例如,用戶基於基礎鏡像(用來生成其餘鏡像的基礎,每每沒有父鏡像)來製做各類不一樣的應用鏡像。這些鏡像共享同一個基礎鏡像層,提升了存儲效率。此外,當用戶改變了一個Docker鏡像(好比升級程序到新的版本),則會建立一個新的層(layer)。所以,用戶不用替換整個原鏡像或者從新創建,只須要添加新層便可。用戶分發鏡像的時候,也只須要分發被改動的新層內容(增量部分)。這讓Docker的鏡像管理變得十分輕量級和快速。

1.Docker存儲

Docker目前經過插件化方式支持多種文件系統後端。Debian/Ubuntu上成熟的AUFS(Another Union File System,或v2版本日後的Advanced Multilayered Unification File System),就是一種聯合文件系統實現。AUFS支持爲每個成員目錄(相似Git的分支)設定只讀(readonly)、讀寫(readwrite)或寫出(whiteout-able)權限,同時AUFS裏有一個相似分層的概念,對只讀權限的分支能夠在邏輯上進行增量地修改(不影響只讀部分的)。

Docker鏡像自身就是由多個文件層組成,每一層有惟一的編號(層ID)。

能夠經過docker history查看一個鏡像由哪些層組成。例如查看ubuntu:14.04鏡像由4層組成,每層執行了不一樣的命令:

$ docker history ubuntu:14.04

IMAGE         CREATED        CREATED BY     SIZE     COMMENT

2a274e3405ec  13 months ago  /bin/sh -c #(nop) CMD ["/bin/bash"]            0 B

df697c8b1bf4  13 months ago  /bin/sh -c sed -i 's/^#\s*\(deb.*universe\)$/  1.895 kB

371166fb96e0  13 months ago  /bin/sh -c echo '#!/bin/sh' > /usr/sbin/polic  194.5 kB

69191ca023af  13 months ago  /bin/sh -c #(nop) ADD file:c8f078961a543cdefa  188.1 MB

對於Docker鏡像來講,這些層的內容都是不可修改的、只讀的。而當Docker利用鏡像啓動一個容器時,將在鏡像文件系統的最頂端再掛載一個新的可讀寫的層給容器。容器中的內容更新將會發生在可讀寫層。當所操做對象位於較深的某層時,須要先複製到最上層的可讀寫層。當數據對象較大時,每每意味着IO性能較差。所以,通常推薦將容器修改的數據經過volume方式掛載,而不是直接修改鏡像內數據。此外,對於頻繁啓停Docker容器的場景下,文件系統的IO性能也將十分關鍵。具體看,Docker全部的存儲都在Docker目錄下,以Ubuntu系統爲例,默認路徑是/var/lib/docker。

在這個目錄下面,存儲由Docker鏡像和容器運行相關的文件和目錄,可能包括aufs、containers、graph、image、init、linkgraph.db、network、repositories-aufs、swarm、tmp、trust、volumes等。

最關鍵的就是aufs目錄,這是aufs文件系統所在,保存Docker鏡像相關數據和信息。該目錄包括layers、diff和mnt三個子目錄。1.9版本和以前的版本中,命名跟鏡像層的ID是匹配的,而自1.10開始,層數據相關的文件和目錄名與層ID再也不匹配。

layers子目錄包含層屬性文件,用來保存各個鏡像層的元數據:某鏡像的某層下面包括哪些層。

例如:某鏡像由5層組成,則文件內容應該以下:

# cat aufs/layers/78f4601eee00b1f770b1aecf5b6433635b99caa5c11b8858dd6c8cec03b4584f-init

d2a0ecffe6fa4ef3de9646a75cc629bbd9da7eead7f767cb810f9808d6b3ecb6

29460ac934423a55802fcad24856827050697b4a9f33550bd93c82762fb6db8f

b670fb0c7ecd3d2c401fbfd1fa4d7a872fbada0a4b8c2516d0be18911c6b25d6

83e4dde6b9cfddf46b75a07ec8d65ad87a748b98cf27de7d5b3298c1f3455ae4

diff子目錄包含層內容子目錄,用來保存全部鏡像層的內容數據。

例如:# ls aufs/diff/78f4601eee00b1f770b1aecf5b6433635b99caa5c11b8858dd6c8cec03b4584f-init/

dev  etc

mnt子目錄下面的子目錄是各個容器最終的掛載點,全部相關的AUFS層在這裏掛載到一塊兒,造成最終效果。一個運行中容器的根文件系統就掛載在這下面的子目錄上。一樣,1.10版本以前的Docker中,子目錄名和容器ID是一致的。其中,還包括容器的元數據、配置文件和運行日誌等。

2.多種文件系統比較

Docker目前支持的聯合文件系統種類包括AUFS、OverlayFS、btrfs、vfs、zfs和Device Mapper等。

各類文件系統目前的支持狀況以下:

  1. AUFS:最先支持的文件系統,對Debian/Ubuntu支持好,雖然沒有合併到Linux內核中,但成熟度很高;
  2. OverlayFS:相似於AUFS,性能更好一些,已經合併到內核,將來會取代AUFS,但成熟度有待提升;
  3. Device Mapper:Redhat公司和Docker團隊一塊兒開發用於支持RHEL的文件系統,內核支持,性能略慢,成熟度高;
  4. btrfs:參考zfs等特性設計的文件系統,由Linux社區開發,試圖將來取代Device Mapper,成熟度有待提升;
  5. vfs:基於普通文件系統(ext、nfs等)的中間層抽象,性能差,比較佔用空間,成熟度也通常。
  6. zfs:最初設計爲Solaris 10上的寫時文件系統,擁有很多好的特性,但對Linux支持還不夠成熟。

總結一下,AUFS和Device Mapper的應用最爲普遍,支持也相對成熟,推薦生產環境考慮。長期來看,OverlayFS將可能具備更好的特性。

Linux網絡虛擬化

Docker的本地網絡實現其實就是利用了Linux上的網絡命名空間和虛擬網絡設備(特別是veth pair)。

1.基本原理

直觀上看,要實現網絡通訊,機器須要至少一個網絡接口(物理接口或虛擬接口)與外界相通,並能夠收發數據包;此外,若是不一樣子網之間要進行通訊,須要額外的路由機制。

Docker中的網絡接口默認都是虛擬的接口。虛擬接口的最大優點就是轉發效率極高。這是由於Linux經過在內核中進行數據複製來實現虛擬接口之間的數據轉發,即發送接口的發送緩存中的數據包將被直接複製到接收接口的接收緩存中,而無需經過外部物理網絡設備進行交換。對於本地系統和容器內系統來看,虛擬接口跟一個正常的以太網卡相比並沒有區別,只是它速度要快得多。

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

 

通常狀況下,Docker建立一個容器的時候,會具體執行以下操做:

  1. 建立一對虛擬接口,分別放到本地主機和新容器的命名空間中;
  2. 本地主機一端的虛擬接口鏈接到默認的docker0網橋或指定網橋上,並具備一個以veth開頭的惟一名字,如veth1234;
  3. 容器一端的虛擬接口將放到新建立的容器中,並修更名字做爲eth0。這個接口只在容器的命名空間可見;
  4. 從網橋可用地址段中獲取一個空閒地址分配給容器的eth0(例如172.17.0.2/16),並配置默認路由網關爲docker0網卡的內部接口docker0的IP地址(例如172.17.42.1/16)。

完成這些以後,容器就可使用它所能看到的eth0虛擬網卡來鏈接其餘容器和訪問外部網絡。用戶也能夠經過docker network命令來手動管理網絡。

在使用docker run命令啓動容器的時候,能夠經過--net參數來指定容器的網絡配置。

有5個可選值bridge、none、container、host和用戶定義的網絡:

  1. --net=bridge:默認值,在Docker網橋docker0上爲容器建立新的網絡棧。
  2. --net=none:讓Docker將新容器放到隔離的網絡棧中,可是不進行網絡配置。以後,用戶能夠自行進行配置。
  3. --net=container:NAME_or_ID:讓Docker將新建容器的進程放到一個已存在容器的網絡棧中,新容器進程有本身的文件系統、進程列表和資源限制,但會和已存在的容器共享IP地址和端口等網絡資源,二者進程能夠直接經過lo環回接口通訊。
  4. --net=host:告訴Docker不要將容器網絡放到隔離的命名空間中,即不要容器化容器內的網絡。此時容器使用本地主機的網絡,它擁有徹底的本地主機接口訪問權限。容器進程能夠跟主機其餘root進程同樣打開低範圍的端口,能夠訪問本地網絡服務,好比D-bus,還可讓容器作一些影響整個主機系統的事情,好比重啓主機。所以使用這個選項的時候要很是當心。若是進一步的使用--privileged=true參數,容器甚至會被容許直接配置主機的網絡棧。
  5. --net=user_defined_network:用戶自行用network相關命令建立一個網絡,經過這種方式將容器鏈接到指定的已建立網絡上去。

3.手動配置網絡

用戶使用--net=none後,Docker將不對容器網絡進行配置。

下面,將手動完成配置網絡的整個過程。

首先,啓動一個/bin/bash容器,指定--net=none參數:

$ docker run -i -t --rm --net=none base /bin/bash

root@63f36fc01b5f:/#

在本地主機查找容器的進程id,併爲它建立網絡命名空間:

$ docker inspect -f '{{.State.Pid}}' 63f36fc01b5f

2778

$ pid=2778

$ sudo mkdir -p /var/run/netns

$ sudo ln -s /proc/$pid/ns/net /var/run/netns/$pid

檢查橋接網卡的IP和子網掩碼信息

$ ip addr show docker0

21: docker0: ...

inet 172.17.42.1/16 scope global docker0

...

建立一對「veth pair」接口A和B,綁定A接口到網橋docker0,並啓用它:

$ sudo ip link add A type veth peer name B

$ sudo brctl addif docker0 A

$ sudo ip link set A up

將B接口放到容器的網絡命名空間,命名爲eth0,啓動它並配置一個可用IP(橋接網段)和默認網關:

$ sudo ip link set B netns $pid

$ sudo ip netns exec $pid ip link set dev B name eth0

$ sudo ip netns exec $pid ip link set eth0 up

$ sudo ip netns exec $pid ip addr add 172.17.42.99/16 dev eth0

$ sudo ip netns exec $pid ip route add default via 172.17.42.1

以上,就是Docker配置網絡的具體過程。

當容器終止後,Docker會清空容器,容器內的網絡接口會隨網絡命名空間一塊兒被清除,A接口也會自動從docker0卸載並清除。

此外,在刪除/var/run/netns/下的內容以前,用戶可使用ip netns exec命令在指定網絡命名空間中進行配置,從而更新容器內的網絡配置。

相關文章
相關標籤/搜索