# 第12章 Docker 網絡實現
在第11章里,我們了解了各種Docker原生支持的存儲引擎和它們是如何幫助Docker打包鏡像以及更高效地從已構建的鏡像中運行容器等內容。實際上,Docker真正的強大之處在于使用它來構建分布式應用。這些分布式應用通常由散布在整個計算機網絡內為了完成一些計算任務而交互的一定數量的服務程序組成。在本章中,我們將解答一個看上去很簡單的問題:如何才能使得運行在Docker容器內部的應用程序在網絡上能夠實現相互訪問?為了解答這一問題,我們首先需要了解一下Docker的網絡模型。
Docker的網絡實現包含3方面的內容。分別是IP的分配、(域)名的解析以及容器或服務的發現這3塊。上述所有的概念即是著名的[**零配置網絡**](http://zeroconf.org)(即zerocnf)的理論基礎。簡而言之,**零配置網絡**即是一組在沒有任何人工干預的情況下自動創建和配置一個TCP/IP網絡的技術。
在每臺宿主機上,Docker容器的分布密度快速波動(并且一般也是如此)。對于這樣的復雜情況,自動化網絡配置的概念就顯得尤為重要。管理和運維這樣的環境,以及針對其有時候橫跨多個云服務平臺的復雜情況建立起一套理想而安全的網絡基礎設施,想想都覺得會是一個非常艱巨的挑戰。在這些復雜場景下實現零配置網絡的“**愿景**”是我們在計算機網絡方面的終極目標。它可以幫助開發和運維人員從以前手工配置網絡的重擔中解放出來。接下來,將探討Docker為了盡可能的實現這一愿景所提出的眾多可選的網絡解決方案。
當外界的計算機網絡訪問這些在Docker容器內部運行的應用服務時,用戶們常常面臨如下問題:
- 該如何去訪問一個運行在Docker容器里的應用或者服務?
- 如果應用程序在Docker容器內部運行,那么該如何以一種相對安全的方式去連接或者發現所依賴的應用服務?
- 在網絡上,該如何加固這些運行在Docker容器內部的應用?
在這一章里,我們將試圖尋找上述問題的答案。我們將會從Docker內部的網絡實現原理講起,這應該可以讓你對當前的網絡模型有一個很好的大體上的了解。接下來,我們將研究一些具體的實戰案例,用戶可以在任意一臺Docker宿主機上執行里面的命令。最后,我們將探討在現有的網絡模型不滿足需求的情況下的一些可選的替代方案。話不多說,是時候實際行動了!
Docker的網絡模型非常簡單,但同時也相當強大。默認情況下(即Docker守護進程的默認配置),無需用戶任何人工的干預,所有新創建的Docker容器都會自動地連上Docker的內部網絡:你簡單地運行一下類似于`docker run <image> <cmd>`的命令,一旦你的容器啟動,它便會自動地出現在網絡上。這聽上去挺神奇的,不妨讓我們看一下它背后的實現原理吧。
當Docker守護進程以其默認的配置參數在一臺宿主機上啟動時,它會創建一個Linux[網橋設備](https://en.wikipedia.org/wiki/Bridging_%28networking%29)并將其命名為`docker0`。該網橋隨后會自動地分配一個滿足[RFC 1918](https://tools.ietf.org/html/rfc1918)定義的私有IP段的隨機IP地址和子網。該子網決定了所有新創建容器將被分配的容器IP地址所屬的網段。當前的網絡模型如圖12-1所示。

圖12-1
除了創建網橋設備,Docker守護進程還會在宿主機上修改一些`iptables`規則。它會創建一個叫做`DOCKER`的特殊過濾鏈并且把它**插入**到`FORWARD`鏈的最上面。它也修改了一些`iptables nat`表的規則使得容器可以建立對外的連接。設置了這些規則以后Docker容器內部之間的網絡連接在接收端便會顯示對方的內網IP地址,不過,容器對外的網絡連接仍由宿主機上的一個IP地址發出,而不是最開始發起連接的那個容器的IP地址。這一點有時候會讓初學者感到困惑。
如果不希望Docker修改宿主機上的`iptables`規則,那么**必須**以`--iptables`參數設置為`false`的方式啟動Docker服務。也可以通過簡單地設置環境變量`DOCKER_OPTS`,然后重啟服務進程達成這一點。默認情況下Docker配置里的這個參數是設置為`true`的。
**注意**:`DOCKER_OPTS`*必須在Docker守護進程啟動前設置好。如果你希望你所做的更改能夠持久化,甚至于在Docker宿主機重啟后仍然能生效,那么你必須修改一些init服務配置或腳本文件。這在各個Linux發行版之間存在著些許不同,在Ubuntu上你可以通過修改*`/etc/default/docker`*文件來完成這一需求。*
Docker通過創建一個單獨的`iptables`鏈來管理容器之間的訪問,這使得系統管理員們可以很方便地以修改`DOCKER`鏈的方式來管理容器的外部訪問,在此過程中他們無需接觸宿主機上任何其他的`iptables`規則,這也就避免了一些意外修改的情況。在`DOCKER`鏈里追加和修改規則本質上即是Docker如何管理容器之間的**鏈接**的具體實現,這些內容我們將稍后介紹。
Docker守護進程會為每個新建的容器創建一個新的網絡命名空間。然后它會生成一對veth設備。veth(Virtual Ethernet的簡寫)是一類特殊的Linux網絡設備,它通常是成對(或者說結對)出現的,而且大致上來說它扮演的角色類似于是一個“網絡管道”:從一端傳入的任何數據都會被傳到另外一端。事實證明,這很方便地實現了在Linux內核里不同網絡命名空間之間的相互通信。Docker會將veth對等接口中的其中一個連接到容器的網絡命名空間里然后在宿主機的網絡命名空間里持有另外一個,后者的名稱是一個帶有**veth**前綴的隨機生成的字串。veth對里的每一個對等接口只有在它當前所屬的命名空間下才能看到。
隨后,Docker會將宿主機上的veth對等接口綁定到`docker0`網橋上,然后為另外一個容器的veth對等接口分配一個之前Docker守護進程啟動時選定的私有IP網段里的IP地址。一旦宿主機上的veth對等接口橋接上了`docker0`設備,Docker會立馬在宿主機上的路由表上為之前選定的私有IP網段插入一條新的路由記錄,并且開啟宿主機上的IP轉發。這便使得用戶可以在宿主機上直接與容器通信。關于這一設定的詳細過程如圖12-2所示。

圖12-2
默認情況下,Docker容器只能從內網訪問,它們一般不對外提供路由。Docker并不提倡從外網訪問容器,因此外界的宿主機一般很難直接和它們通信。
**注意**:*如果以將*`--iptables`*參數設置為*`false`*的方式啟動Docker服務,它將不會再在宿主機上操作任何*`iptables`*的規則。它也不再會配置IP轉發,這就意味著你的容器將無法再訪問外界的應用甚至本地上其他的容器。如果這不是你預期的結果,可能就要考慮一下在系統上手動開啟IP轉發的配置。在Linux上,你可以通過設置*`/proc/sys/net/ipv4/ip_forward`*內核參數為**“**1**”**來實現這一點。*
在容器更換迭代頻繁的情況下通過手工的方式來管理IP地址畢竟不是一個長久之計,一旦容器的數量上了規模將沒辦法繼續這樣做。這正是Docker在IP地址分配方面需要解決的問題。正如之前提到的那樣,IP自動分配是**零配置網絡**實現的基石之一,并且Docker手上已然有了一個實現的方案。Docker能夠做到在沒有任何人工干預的情況下為新創建的容器自動地分配IP地址。Docker守護進程會維護一組已經分配給那些正在運行的Docker容器的IP地址以避免為新容器再分配相同的IP地址。當一個容器停止或者被刪除的時候,它的IP地址也會被回收到由Docker守護進程維護的IP地址池里,這樣,當新容器啟動時便可以直接復用這些IP資源。
如果在容器銷毀后,釋放的IP地址映射到容器對應[MAC地址](https://en.wikipedia.org/wiki/MAC_address)的[緩存ARP](https://en.wikipedia.org/wiki/Address_Resolution_Protocol)上的記錄沒有被立即清除,立刻復用該IP可能會導致宿主機本地網絡的ARP沖突。Docker采取的辦法是為每個已分配的IP地址生成一串隨機MAC地址來解決這個問題。該MAC地址生成器確保是強一致的:相同的IP地址生成的MAC地址將會是完全一致的。Docker也允許用戶在創建新容器時手動指定MAC地址,然而,由于上述所提到的ARP沖突的問題,我們并不建議這樣做,除非你想出一些其他的機制可以規避它。
太好了!IP(和MAC地址)的分配都是在沒有任何用戶手動干涉的情況下**“自動”**完成。一旦新容器被創建出來,它們便會以其自動分配的IP地址出現在Docker的私有網絡里。而這正是你想要借助**零配置網絡**實現的。Docker甚至還更進了一步:為了使得運行在容器里的服務之間能夠相互通信,Docker還必須支持**端口的分配**。
當一個容器啟動時,Docker**可以**為它自動分配任意的UDP或者TCP端口并且使之能夠在宿主機上被訪問。用戶可以在構建容器的鏡像時通過在`Dockerfile`里使用`EXPOSE`指令來指定對外**公開**的端口,也可以通過`--expose`命令在容器啟動的時候顯式聲明。該命令允許用戶定義某個范圍內的端口而不只是單個的端口映射。然而,要知道聲明大范圍的端口段的影響在于所有相關的信息都是可以通過Docker服務的遠程API獲取的,因此查詢一個占有巨大端口段的容器的話很容易就會暴露Docker守護進程。
Docker守護進程隨后會從Linux宿主機上的文件——`/proc/sys/net/ipv4/ip_local_port_range`定義的一個端口范圍里挑選出一個隨機的端口號。如果失敗,如當Docker守護進程運行在非Linux宿主機上時,Docker將會改為從這個端口范圍(49153~65535)里申請對應的端口。這并不是自動完成的:Docker守護進程只維護正在宿主機上運行的容器公開的端口號。用戶必須明確地通過被Docker稱之為**發布端口**的方式觸發一次宿主機的端口映射。發布端口使得用戶可以綁定任意**公開的端口**到宿主機上任何一個對外可路由的IP地址。如果用戶在之前構建鏡像的時候沒有設置任何公開的端口,那么發布端口將對運行在Docker容器里的服務的對外可用性沒有任何影響。
用戶可以通過執行如下命令來找出指定容器對外公開的具體端口信息:
`# docker inspect -f '{{.Config.ExposedPorts}}' <container_id>`**警告**:*用戶只能在啟動新容器的時候為其發布對應的公開端口。一旦容器已經開始運行,我們將沒有辦法再發布其他的公開端口。必須從頭開始重新創建容器!*
用戶可以選擇發布所有的公開端口或者只發布那些用戶挑選出來的愿意讓它對外可訪問的。Docker提供了非常便利的命令行參數來實現各種組合。用戶可以通過Docker幫助來了解所有可用的參數選項。
端口分配的背后主要在于`iptables`的妙用,Docker就是通過靈活運用之前提到的`DOCKER`鏈和`nat`表實現這一點的。為了幫助讀者更好地理解這項功能,我們將通過圖12-3所示的具體案例來講解相關內容。

圖12-3
假設,我們想在Docker容器里運行`nginx` Web服務并且需要通過宿主機上的可對外路由的IP地址`1.2.3.4`和對應的[TCP](https://en.wikipedia.org/wiki/Transmission_Control_Protocol)端口`80`使其能夠被外界訪問。我們需要用到`library/nginx`的Docker鏡像,實際上,當用戶運行`docker pull nginx`命令的時候,Docker從Docker Hub上拉取的默認鏡像便是我們需要用到的`library/nginx`鏡像。我們通過執行如下命令(記得帶上`-p`參數)來完成上述任務:
```
# sudo docker run -d -p 1.2.3.4:80:80 nginx
a10e2dc0fdfb2dc8259e9671dccd6853d77c831b3a19e3c5863b133976ca4691
#
```
可以通過執行以下命令來驗證由這個鏡像創建出的容器是否的確公開了TCP 80端口:
```
# sudo docker inspect -f '{{.Config.ExposedPorts}}'a10e2dc0fdfb
map[443/tcp:map[] 80/tcp:map[]]
```
可以看到,我們用來實例化容器的`nginx`鏡像本身還公開了443端口。現在,容器已經開始運行,用戶可以通過執行以下命令輕松地來檢索它公開的所有端口(也稱為主機端口綁定):
```
# sudo docker inspect -f '{{.HostConfig.PortBindings}}'
a10e2dc0fdfb
map[80/tcp:[map[HostIp:1.2.3.4 HostPort:80]]]
```
**提示**:*這里有一個簡便命令行來檢查一個特定Docker容器IP∶port綁定的內容:*`docker port container_id`*。*
太棒了!`nginx`如今在Docker容器里運行并且它能夠通過之前給定的IP地址和端口對外提供服務。我們現在可以通過執行簡單的`curl`命令在外面的宿主機(當然,我們假定你的防火墻沒有禁止外界對80端口的訪問)上訪問其默認的`nginx`站點:
```
# curl -I 1.2.3.4:80
HTTP/1.1 200 OK
Server: nginx/1.7.11
Date: Wed, 01 Apr 2015 12:48:47 GMT
Content-Type: text/html
Content-Length: 612
Last-Modified: Tue, 24 Mar 2015 16:49:43 GMT
Connection: keep-alive
ETag: "551195a7-264"
Accept-Ranges: bytes
```
當用戶在宿主機上對外公開一個端口時,Docker守護進程會在`DOCKER`鏈里追加一條新的`iptables`規則,它將會把宿主機上所有目標是`1.2.3.4:80`的流量重定向到一個特定容器的80端口上,并且會據此修改`nat`表的規則。用戶可以通過運行如下命令輕松地檢索這些信息:
```
# iptables -nL DOCKER
Chain DOCKER (1 references)
target prot opt source destination
**ACCEPT tcp -- 0.0.0.0/0 1.2.3.4
tcp dpt:80**
# iptables -nL -t nat
Chain PREROUTING (policy ACCEPT)
target prot opt source destination
DOCKER all -- 0.0.0.0/0 0.0.0.0/0
ADDRTYPE match dst-type LOCAL
Chain INPUT (policy ACCEPT)
target prot opt source destination
Chain OUTPUT (policy ACCEPT)
target prot opt source destination
DOCKER all -- 0.0.0.0/0 !127.0.0.0/8
ADDRTYPE match dst-type LOCAL
Chain POSTROUTING (policy ACCEPT)
target prot opt source destination
MASQUERADE all -- 172.17.0.0/16 0.0.0.0/0
**MASQUERADE tcp -- 172.17.0.5
172.17.0.5 tcp dpt:80**
Chain DOCKER (2 references)
target prot opt source destination
**DNAT tcp -- 0.0.0.0/0 1.2.3.4 tcp
dpt:80 to:172.17.0.5:80**
```
**注意**:*當關閉或刪除一個發布了一定數量端口的容器時,Docker會刪除它最初創建的所有必需的*`iptables`*規則,因此用戶不必再擔心這些。*
最后,讓我們來看一看如果我們使用`-P`選項來啟動一個新容器的話會發生什么,使用該參數意味著我們無需再為公開端口指定任何主機端口或者IP地址映射信息,Docker將自動幫我們完成端口的映射。我們將和之前的例子一樣使用相同的`nginx`鏡像:
```
# docker run -d -P nginx
995faf55ede505c001202b7ee197a552cb4f507bc40203a3d86705e9d08ee71d
```
同之前提到的那樣,由于我們沒有顯式地指定將容器端口綁定到宿主機的接口上,因此Docker將會把這些端口映射到宿主機上的一個隨機端口上。可以通過執行以下命令來發現該容器綁定的端口信息:
```
# docker port 995faf55ede5
443/tcp -> 0.0.0.0:49153
80/tcp -> 0.0.0.0:49154
```
當然,也可以通過執行以下命令直接檢索網絡端口的信息:
```
# docker inspect -f '{{ .NetworkSettings.Ports }}' 995faf55ede5
map[443/tcp:[map[HostIp:0.0.0.0 HostPort:49153]] 80/tcp:
[map[HostIp:0.0.0.0 HostPort:49154]]]
```
可以看到兩個公開的TCP端口分別被綁定到了**宿主機上所有網絡接口**的`49153`和`49154`端口。讀者可能已經猜到了,Docker正是通過它的遠程API公開的這些信息,它為需要相互通信的各個容器實現了一個非常簡單的服務發現。當用戶啟動一個新容器時可以通過一個環境變量來傳入對應的宿主機端口映射信息。關于這一點,我們還可以通過一個更好也更安全的方式來實現,這部分內容我們將在本章的稍后部分詳細介紹。
所有的這一切看起來棒極了。我們不用再去手動管理運行在宿主機上的Docker容器的IP地址:Docker包攬了一切!然而,當我們開始擴大Docker基礎設施的規模的時候,一些問題可能就會暴露出來。由于Docker守護進程接管了特定的Docker宿主機上所有IP分配的活,我們該怎樣確保它不會在不同的宿主機上針對不同的容器分配相同的IP地址呢?甚至于我們是否需要關注這個問題呢?針對這些問題的答案也是很常見的:**這得看情況**。我們將在12.5節進一步探討相關話題。現在讓我們先轉到零配置網絡實現的下一個重要組成部分:域名解析。
Docker提供了一些配置參數允許用戶在容器基礎設施里管理域名的解析。同以往一樣,為了利用好這些功能而又不會引發一些不可預料的結果,我們需要先了解一下Docker內部是如何處理域名解析的。
當Docker創建新容器時,默認情況下它會給該容器分配一個隨機生成的**主機名**以及一個唯一的**容器名**。這是兩個完全不同但是又容易搞混的概念,尤其對于新手來講,他們在使用時常常會混淆這兩個概念。
主機名可以理解為就是一個普通的Linux主機名:它允許運行在容器里的進程通過解析該容器的主機名來獲取它對應的IP地址。容器的主機名**并不是一個可以在容器外部環境解析的域名**,而且默認情況下它會被設置為**容器的ID**,即一串允許用戶從命令行或者通過遠程API在宿主機上定位任意容器的唯一字符串[\[1\]](part0018.xhtml#anchor121)。**容器名**,從另一方面來說,是一個Docker內部的概念,它與Linux無關。
可以通過使用Docker命令行工具執行如下命令來查詢容器名:
`docker inspect -f '{{.Name}}' container_id`容器名在Docker里主要有兩個用途:
- 與容器ID相比,它使得用戶可以通過遠程API使用友好、可讀的名稱來查找容器;
- 它有助于構建一個基本的基于Docker的容器發現。
用戶可以使用Docker客戶端提供的一些特定的命令行參數來重載Docker守護進程設定的默認值。
容器的主機名和容器名可以被設置成相同的值,但是除非你有一些自動管理它們的方法,否則我們建議不要這樣做,因為在一個高密度的容器部署的條件下,這可能會變成一個難以維護的情況并將成為運維人員的噩夢。
在容器鏡像構建時無論是主機名還是容器名都不會硬編碼到容器鏡像里面。Docker實際上會主動在Docker宿主機上生成`/etc/hostname`和`/etc/hosts`文件,然后在新創建的容器啟動時將兩者綁定掛載到里面。用戶可以通過檢查宿主機里下述文件的內容來確認這一點:
```
# cat /var/lib/docker/containers/container_id/hostname
# cat /var/lib/docker/containers/container_id/hosts
```
**提示**:*可以通過在命令行里運行*`docker inspect -f '{{printf "%s\n%s" .HostnamePath .HostsPath}}' container_id`*命令得到相應文件的具體路徑信息。*
我們將在本章的后面部分討論更多關于**容器名**概念的詳細內容;現在我們只需要記住,如果用戶想通過一個友好、可讀的名字而不是隨機生成的容器ID來查找Docker容器,就可以給它們分配一個自定義的名字。用戶**無法**修改一個已經創建好的容器的名稱——遇到這種情況只能從頭重新創建它。
**注意**:*從0.7版本起,Docker會用一些著名的科學家和黑客的名字來命名容器。如果你也想為Docker項目做一份貢獻,可以在[GitHub](https://github.com/docker/docker)上開一個[Pull Request](https://help.github.com/articles/using-pull-requests/)加入你認為值得推薦的名人。在Docker里,處理這個的[Go](https://golang.org)包叫做`namesgenerator,`在Docker的代碼庫中的`pkg`子目錄下找到它。*
現在你已經知道容器是如何將它的主機名解析為對應的IP地址了,那么是時候再去了解下它是如何解析外部DNS域名了。如果你猜到容器可能是像平常Linux主機那樣使用`/etc/resolv.conf`文件來實現,那么恭喜你,答對了!Docker會在宿主機上為每個新創建的容器生成`/etc/resolv.conf`文件,然后當啟動該容器時將這個文件掛載到容器里。用戶可以通過在Docker宿主機上檢索以下文件的內容來驗證這一點:
`# sudo cat /var/lib/docker/containers/container_id/resolv.conf`**提示**:*可以通過在命令行執行*`docker inspect -f '{{.ResolvConfPath}}' container_id`*來找出上述文件的具體路徑。*
默認情況下,Docker會將宿主機上的`/etc/resolv.conf`文件復用到新創建的容器里。在**1.5**版以后,當用戶在宿主機上通過修改這個文件更改了DNS配置時,如果希望這些變動也應用到現有已經在運行的容器(那些使用之前的配置創建的),則**必須重啟**這些容器方能使之生效。如果你還在用老版本,那就不是這樣了,你必須從頭**重新創建**這些容器。
如果不想Docker容器采用宿主機的DNS配置,可以通過修改環境變量`DOCKER_OPTS`來重載它們,可以在啟動守護進程時通過命令行參數或者通過修改宿主機上一個特定的配置文件將變動固化下來(在Ubuntu Linux發行版上這個文件是`/etc/default/docker`)。
假設用戶現在有一個專門的DNS服務器,并且希望自己的Docker容器都使用它來完成DNS的解析。再假定這個DNS服務器可以用`1.2.3.4` IP地址訪問并且它管理了[example.comexample.com](http://example.com)域。這樣的話,用戶可能需要按如下方式修改環境變量`DOCKER_OPTS`:
`DOCKER_OPTS="--dns 1.2.3.4 --dns-search example.com”`為了讓Docker守護進程應用新配置,需要重啟一次守護進程。用戶可以通過執行以下命令檢查`/etc/resolv.conf`的內容來驗證新創建的容器現在是否真的采用了新的DNS配置:
```
# sudo cat /var/lib/docker/containers/container_id/resolv.conf
nameserver 1.2.3.4
search example.com
```
所有在Docke守護進程的DNS配置修改之前已經啟動的容器將仍然保持原來的配置。如果希望這些容器采用新配置,就必須要從頭重新創建它們,簡單的重啟容器無法實現預期效果!
**注意**:*之前所提到的Docker守護進程的DNS選項參數將不會重載宿主機上的DNS配置。它們只會作為自Docker守護進程開始應用這一新配置起在宿主機上創建的所有容器的默認DNS配置。然而用戶也可以針對每個容器顯式地重載他們的配置。*
Docker甚至允許更細粒度地控制容器的DNS配置。用戶可以在啟動新容器時像這樣重載其全局的DNS配置:
```
# sudo docker run -d --dns 8.8.8.8 nginx
995faf55ede505c001202b7ee197a552cb4f507bc40203a3d86705e9d08ee71d
# sudo cat $(docker inspect -f '{{.ResolvConfPath}}'
995faf55ede5)
nameserver 8.8.8.8
search example.com
```
讀者可能已經注意到了這里面的一個小細節。正如在上述命令的輸出中所能看到的,我們只是為新容器設置了`--dns`的配置,但是`/etc/resolv.conf`里面的search指令已經被修改了。Docker將把命令行上指定的參數配置和Docker守護進程本身規定的配置兩者做一次**合并**。當Docker守護進程設置`--dns-search`為一些特定域時,如果用戶在啟動新容器的時候只重載了`--dns`參數,容器將會繼承Docker服務設定的搜索域配置,而不是宿主機上的配置。目前我們沒有辦法改變這一行為,因此必須留意這一行為!
此外,當Docker守護進程指定的DNS設置發生變動時,采用自定義DNS配置創建的容器將不會受到任何影響,即使在這之后重啟這些容器也同樣如此。如果想讓它們轉為采用Docker守護進程的配置,那么必須以不指定任何自定義參數的方式重新創建它們。
**警告**:*如果用戶在正在運行的容器里直接修改了*`/etc/resolv.conf、/etc/hostname`*或者*`/etc/hosts`*文件,要注意*`docker commit`*不會保存用戶所**做**的這些變更,并且在容器重啟之后,這些變更也不會被保留下來!*
正如在本章中所看到的,Docker在無需任何人工干預的情況下自動地完成了每個容器的DNS配置。這再一次完美的踐行了**零配置網絡**的準則。甚至于如果用戶把容器導出并遷移到其他的宿主機,Docker仍會采用之前的DNS配置,所以用戶無需擔心再為它們從頭配置一次DNS。
能夠在容器啟動后在其內部直接解析外部DNS域名固然是非常方便的,但是如果我們希望能從容器里訪問其他容器里的服務呢?正如之前所了解的,容器的主機名在他們外部是無法解析的,因此不能使用容器的主機名來完成容器之間的通信。而為了使一個容器能夠和另外一個容器通信,就必須知道其他容器的IP地址,可以通過查詢Docker遠程API得到這些信息。但是這一點在容器內部是沒有辦法做到的,除非用戶在容器啟動時綁定掛載Docker守護進程的套接字或者在`docker0`網橋接口上公開Docker API服務。此外,查詢API本身有一定的額外開銷,這會帶來不必要的復雜度,并且有點背離我們最初設定零配置網絡的初衷。要解決這個問題歸根結底還是得依靠零配置網絡的最后一個核心組成:服務發現。
Docker提供了一個開箱即用的、雖然基礎,但是功能卻非常強大的服務發現機制:**Docker鏈接**。正如我們之前所了解到的,在Docker的內網里,所有的容器都是可以訪問得到的,因此默認只要它們知道彼此的IP地址,相互之間便能夠直接通信。但是僅僅發現其他容器的IP地址還不夠,還得找出容器化的服務接受外界請求時連接所需的端口信息。
Docker鏈接使得用戶可以讓任意容器發現其他Docker容器的IP地址和公開的端口。Docker提供了一個非常方便的命令行參數來實現這一點,我們不必再大費周折,它會幫我們自動搞定這一切。該參數即`--link`。當創建一個新容器并將它*鏈接*到其他容器時,Docker會將所有連接方面的具體數據以多個環境變量的形式導出到源容器里。這可能比較難理解。讓我們通過以下的具體案例來講解得更清楚些。
我們將通過`nginx`容器來講解如何完成Docker鏈接,首先從檢索IP地址和分配端口開始。可以通過執行如下命令來找出容器的IP地址:
```
# docker ps -q
a10e2dc0fdfb
# docker inspect -f '{{.NetworkSettings.IPAddress}}'
a10e2dc0fdfb
172.17.0.2
```
`--link`參數遵循如下語法格式:`container_id:alias`。這里面的`container_id`是運行中的容器的`id`,而`alias`**(**別名**)**是一個隨便起的名字,關于這塊內容我們將在后面部分詳細解釋。我們將會試著在一個新的“一次性”容器(帶上`--rm`標志即意味著一旦容器退出便會將容器刪除)里ping `nginx`容器的IP地址。
```
# docker run --rm -it --link=a10e2dc0fdfb:test busybox ping -c
2 172.17.0.2
PING 172.17.0.2 (172.17.0.2): 56 data bytes
64 bytes from 172.17.0.2: seq=0 ttl=64 time=0.615 ms
64 bytes from 172.17.0.2: seq=1 ttl=64 time=0.248 ms
--- 172.17.0.2 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 0.248/0.431/0.615 ms
```
正如預期那樣,默認情況下,在內網里容器之間可以相互通信(除非容器之間的通信被禁用了,關于這一點,可以在12.5節中了解更多的內容)。
當容器被鏈接時,Docker會自動更新源容器上的`/etc/hosts`文件,將命令行里帶的鏈接別名和目標容器的IP地址關聯上,在我們這個例子里便是簡單的"test"。可以通過運行如下命令來驗證這一點(見輸出最底下那條記錄):
```
# docker run --rm -it --link=a10e2dc0fdfb:**test** busybox
cat /etc/hosts
172.17.0.13 a51e855bac00
127.0.0.1 localhost
::1 localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
**172.17.0.2 test**
```
這意味著相比于之前做的ping測試那樣直接使用目標容器的IP地址,還可以像下面這樣通過被鏈接的容器的別名來引用它:
```
# docker run --rm -it --link=edb055f7f592:test busybox ping -c
2 test
PING test (172.17.0.2): 56 data bytes
64 bytes from 172.17.0.2: seq=0 ttl=64 time=0.492 ms
64 bytes from 172.17.0.2: seq=1 ttl=64 time=0.230 ms
--- test ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 0.230/0.361/0.492 ms
```
我們前面還提到Docker在鏈接兩個容器時還會為之創建一些環境變量,它們可以幫助源容器輕松發現目標容器公開的服務端口。當源容器嘗試和目標容器建立連接時它無需知道對方的IP地址和公開的端口,只需讀取環境變量里的對應內容即可,它們是Docker為**每個公開的端口**自動創建的,并且會推送到源容器的執行環境里。這些環境變量的名字遵循以下格式:
```
ALIAS_NAME
ALIAS_PORT
ALIAS_PORT_<EXPOSEDPORT>_TCP
ALIAS_PORT_<EXPOSEDPORT>_TCP_PROTO
ALIAS_PORT_<EXPOSEDPORT>_TCP_PORT
ALIAS_PORT_<EXPOSEDPORT>_TCP_ADDR
...
...
```
可以通過運行以下命令和檢查對應的輸出結果來驗證這一點:
```
# docker run --rm -it --link=a10e2dc0fdfb:test busybox env
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/
bin
HOSTNAME=ff03fba501ea
TERM=xterm
TEST_PORT=tcp://172.17.0.2:80
TEST_PORT_80_TCP=tcp://172.17.0.2:80
TEST_PORT_80_TCP_ADDR=172.17.0.2
TEST_PORT_80_TCP_PORT=80
TEST_PORT_80_TCP_PROTO=tcp
TEST_PORT_443_TCP=tcp://172.17.0.2:443
TEST_PORT_443_TCP_ADDR=172.17.0.2
TEST_PORT_443_TCP_PORT=443
TEST_PORT_443_TCP_PROTO=tcp
TEST_NAME=/mad_euclid/test
TEST_ENV_NGINX_VERSION=1.7.11-1~wheezy
HOME=/root
```
從上述案例可以看出,我們用來測試鏈接的`nginx`容器,公開了兩個TCP端口:80和443。Docker在這里創建了兩個環境變量:針對每個公開的端口分別是`TEST_PORT_80_TCP`和`TEST_PORT_443_TCP`。通過鏈接的方式導出這些環境變量,Docker在實現了一個簡單可移植的服務發現功能的同時又保證了容器的安全可靠。然而天上永遠不會掉餡餅。鏈接雖然是一個很棒的概念,但是你會發現它又是相對靜態的。
當鏈接的目標容器消亡時,源容器便會丟失同鏈接容器所提供的服務的連接。在容器更替非常頻繁的動態環境下這可能會是一個問題。除非用戶在自己的應用里實現了一些基本的健康檢測機制,或者說至少會做一些故障轉移,否則還是應該時刻關注這方面的情況。
此外,當用戶恢復發生故障的目標容器時,源容器的`/etc/hosts`文件將會自動加上目標容器新申請的IP地址,但是**通過鏈接的方式注入源容器的環境變量將不會被更新**,因此如果用戶事先不知道服務的端口信息的話可能還是會不太理想。另外,最好不要依賴這些環境變量來發現目標容器的IP地址,我們更建議使用鏈接別名的方式,它會通過`/etc/hosts`文件自動解析更新后的IP地址。
解決鏈接本身數據更新不及時問題的一種辦法是使用一個名為[docker-grand-ambassador](https://github.com/cpuguy83/docker-grand-ambassador)的工具。它致力于解決在使用Docker鏈接時可能會遇到的一些問題。更高級并且廣泛適用的一個方案便是采用現有的一些不錯的DNS服務軟件來解決容器的服務發現,畢竟它不會引入額外的復雜度。目前,開源界已經有一些現成的DNS服務的實現方案,他們提供了對Docker容器開箱即用的服務發現的支持,并且不用耗費太大力氣便能將其集成到Docker基礎設施里。
加上這一節討論的Docker的服務發現,至此我們關于零配置網絡的幾個核心組件的介紹基本告一段落。可以看到,當用戶在同一臺宿主機上運行所有的容器時,Docker能夠高水準地滿足零配置網絡的所有需求。然而,遺憾的是,一旦用戶把自己的容器擴展到多臺機器或者甚至是多個云服務廠商時Docker便無法完美地交付。我們非常期待[Docker Swarm](https://github.com/docker/swarm/)即將發布的新特性以及新的[網絡模型](https://blog.docker.com/2015/04/docker-networking-takes-a-step-in-the-right-direction-2/),它們應該能解決這一節所遇到的問題[\[2\]](part0018.xhtml#anchor122)。
現在,讓我們轉到一些更高級的網絡主題,深入探討下Docker的核心網絡模型,并且我們會在這里討論下前面章節中遇到的一些問題的解決方案。
這一節我們將首先從網絡安全方面談起,然后我們會轉到探討關于多個Docker宿主機跨主機的容器間如何通信的話題,最后我們將討論一下網絡命名空間的共享。
#### 12.5.1 網絡安全
網絡安全實際上是一個相當復雜的話題,關于它的內容甚至可以出一本單獨的書來講解。在本節中我們將會只覆蓋到里面的幾個小部分,主要是在設計Docker基礎設施或者是使用外部供應商提供的Docker基礎服務時需要注意的一些地方。我們的重點會放在Docker原生提供的一些解決方案,當然我們會在本節的末尾部分介紹一些其他的替代方案。
默認情況下,Docker允許容器之間可以不受限制地隨意通信。很顯然,正如你所預見,這可能會存在一些潛在的安全風險。一旦Docker網絡上的某個容器出了問題并且對同一網絡內的其他容器發動了拒絕[服務攻擊](https://en.wikipedia.org/wiki/Denial-of-service_attack)的話該怎么辦?這些網絡攻擊,有些可能是惡意發起的,也有可能只是軟件bug所引發的。這類情況在多租戶的環境下尤其應該得到重視。
幸運的是,Docker允許完全禁用容器之間的通信,只要在Docker守護進程啟動時傳入一個特定的參數即可。該參數名為`--icc`[\[3\]](part0018.xhtml#anchor123),默認情況下該參數是設置為`true`的。如果想完全禁用Docker容器之間的通信,那么必須通過修改環境變量`DOCKER_OPTS`將該參數設置成`false`,隨后重啟Docker守護進程使之生效。此方法的實現原理是,Docker守護進程會在`FORWARD`鏈里插入一條新的`DROP`策略的`iptables`規則,它會丟棄所有目標是Docker容器的包。在Docker守護進程重啟完成后,可以通過執行如下命令來驗證這一點:
```
# sudo iptables -nL FORWARD
Chain FORWARD (policy ACCEPT)
target prot opt source destination
DOCKER all -- 0.0.0.0/0 0.0.0.0/0
**DROP all -- 0.0.0.0/0 0.0.0.0/0**
ACCEPT all -- 0.0.0.0/0 0.0.0.0/0
ctstate RELATED,ESTABLISHED
ACCEPT all -- 0.0.0.0/0 0.0.0.0/0
```
從這一刻起,容器之間就再也無法互相通信了。我們可以對之前已經在運行的`nginx`容器再作一次簡單的`ping`測試來驗證這一點。首先,我們得拿到`nginx`容器對應的IP地址:
```
# docker inspect -f '{{.NetworkSettings.IPAddress}}'
a10e2dc0fdfb
172.17.0.2
```
現在,讓我們從一個一次性容器里ping它:
```
# docker run --rm -it busybox ping -c 2 172.17.0.2
PING 172.17.0.2 (172.17.0.2): 56 data bytes
--- 172.17.0.2 ping statistics ---
2 packets transmitted, 0 packets received, 100% packet loss
```
正如所見,由于容器間的通信已經被禁用了,因此該ping測試的結果顯示的是**完全**失敗。然而,我們依舊可以在宿主機上連接`nginx`容器:
```
# ping -c 2 172.17.0.2
PING 172.17.0.2 (172.17.0.2) 56(84) bytes of data.
64 bytes from 172.17.0.2: icmp_seq=1 ttl=64 time=0.098 ms
64 bytes from 172.17.0.2: icmp_seq=2 ttl=64 time=0.066 ms
--- 172.17.0.2 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 999ms
rtt min/avg/max/mdev = 0.066/0.082/0.098/0.016 ms
```
此外,所有之前已經發布的端口保持不變,因此運行在`nginx`容器里的`nginx`服務仍然可以正常訪問:
```
# curl -I 1.2.3.4:80
HTTP/1.1 200 OK
Server: nginx/1.7.11
Date: Wed, 01 Apr 2015 12:48:47 GMT
Content-Type: text/html
Content-Length: 612
Last-Modified: Tue, 24 Mar 2015 16:49:43 GMT
Connection: keep-alive
ETag: "551195a7-264"
Accept-Ranges: bytes
```
那么我們現在該怎樣**只**啟用那些需要相互通信的容器之間的訪問呢?答案當然還是`iptables`。如果不想手工管理這些`iptables`規則,別擔心,Docker提供了一個很方便的命令行參數,這樣的話,當用戶創建新容器時,Docker便會幫助用戶自動地完成這些配置。事實上,這個參數我們在服務發現一節里便已經介紹過,沒錯,它就是:`--link`。Docker容器之間的鏈接不僅可以為我們提供一個簡單的服務發現機制,它也為鏈接的容器之間提供了一個安全的網絡通信:**只有**相互鏈接的容器之間才能相互通信,并且它們**只能**訪問那些公開的服務端口。Docker正是通過在`DOCKER`鏈里插入一個**“雙向通信”**的`iptables`規則來實現的這一點。
一些實際案例可能更有助于我們加強對這部分內容的理解。下面,我們仍然復用之前已經在運行的`nginx`容器,并且將它鏈接到一個一次性容器。讓我們首先驗證一下鏈接是否真的只允許特定的公開端口的通信。因為`nginx`容器仍然在運行,所以它的IP地址自然還是之前分配的那個:
```
# docker inspect -f '{{.NetworkSettings.IPAddress}}'
a10e2dc0fdfb
172.17.0.2
```
然后,我們再用一個一次性容器去ping它:
```
# docker run --rm -it -link=a10e2dc0fdfb:test busybox ping -c 2
172.17.0.2
PING 172.17.0.2 (172.17.0.2): 56 data bytes
--- 172.17.0.2 ping statistics ---
2 packets transmitted, 0 packets received, 100% packet loss
```
由于ping使用的是[ICMP協議](https://en.wikipedia.org/wiki/Internet_Control_Message_Protocol),它是一個網絡層協議并且沒有端口的概念,因此出現上述的ping**完全**失敗的結果也就不足為奇了。我們可以通過執行如下命令來驗證`nginx`容器里工作的默認站點是否仍然如預期那樣完美地提供服務:
```
# docker run --rm -link=a10e2dc0fdfb:test -ti busybox wget
172.17.0.2
--2015-04-01 14:11:58-- http://172.17.0.2/
Connecting to 172.17.0.2:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 612 [text/html]
Saving to: 'index.html'
100%
[===============================================================>]
612 --.-K/s in 0s
2015-04-01 14:11:58 (16.4 MB/s) - 'index.html' saved [612/612]
```
還可以通過執行如下命令來檢查`DOCKER`鏈的內容,從而查看Docker在容器被鏈接時創建的`iptables`規則:
```
# iptables -nL DOCKER
Chain DOCKER (1 references)
target prot opt source destination
ACCEPT tcp -- 0.0.0.0/0 172.17.0.2
tcp dpt:80
ACCEPT tcp -- 172.17.0.9 172.17.0.2
tcp dpt:443
ACCEPT tcp -- 172.17.0.2 172.17.0.9
tcp spt:443
ACCEPT tcp -- 172.17.0.9 172.17.0.2
tcp dpt:80
ACCEPT tcp -- 172.17.0.2 172.17.0.9
tcp spt:80
```
就像我們之前所講的那樣,加固容器間通信的另外一種做法便是直接修改`DOCKER`鏈里的`iptables`規則。當然,建議最好不要這么做,因為當宿主機上運行了大量高密度容器的時候,這樣做的話維護會是一個很大的問題,畢竟用戶必須跟蹤每個容器的生命周期,然后在容器交替迭代過程中不斷地增刪規則。
關于安全性另外一個很重要的點便是網絡分段的問題。目前Docker在容器網絡的分段方面沒有提供任何原生的支持。所有容器之間的網絡流量都是經過`docker0`網橋傳輸的。Linux里面的網橋是一種運行在**混雜**模式下的特殊的網絡設備。這意味著任意一個在宿主機擁有root權限的人都有能力查看所有容器之間的網絡交互的情況。這在多租戶的環境下可能尤其需要注意,因此用戶應該始終確保在自己的容器間兩端往返的任意網絡傳輸都是加密的。
正如之前所說,關于網絡安全有說不完的內容。在這里,我們只是討論了一些皮毛,并且重點放在一些基礎概念上。現在,我們將繼續下一個部分,探討一下我們該如何完成跨Docker宿主機的容器間網絡通信。
#### 12.5.2 多主機的容器間通信
與之前一樣,有幾種選擇可供我們實現這一目標。有了之前介紹的端口發布和容器鏈接方面的理論基礎,我們可以推廣到Docker社區里著名的**大使模式**(ambassador pattern),它巧妙地結合了這些概念。
大使模式的工作原理是在所有你想互聯的Docker宿主機上運行一個特殊的容器,然后使用它來完成不同Docker宿主機之間容器的相互通信。這個特殊容器會運行一個`socat`網絡代理,它負責Docker宿主機之間連接的代理轉發。圖12-4清晰地展現了這一具體過程。

圖12-4
讀者可通過閱讀Docker的[官方文檔](http://docs.docker.com/engine/articles/ambassador_pattern_linking/)來了解這一模式的更多內容。該模式的核心理念可以歸結為在宿主機上發布端口,然后通過鏈接到在其他宿主機上運行的大使容器,根據該宿主機上的環境變量找出發布并對我們公開的端口服務信息,以此來實現跨宿主機的容器間的網絡通信。該模式的弊端在于,用戶必須在每臺宿主機上運行一個額外的容器來處理代理轉發。而好處是,如果用戶是通過容器名稱而不是容器的ID引用目標容器,那么此模式有助于提高容器的可移植性。另外,當想導出一個容器并且把它遷移到其他的Docker宿主機時,如果那臺宿主機剛好運行了一個容器,并且它和用戶所導出的容器應該鏈接的那個容器名字相同的話,用戶只需要啟動那個遷移好的容器即可。
既然我們已經介紹了一些Docker原生提供的實現跨宿主機的容器間網絡通信的解決方案,那么是時候來看一些更復雜的概念了。具體來說,我們將介紹如何在用戶自己的專有內網里運行和集成容器服務。
Docker允許用戶為自己想要運行的容器明確指定一個IP網段。正如之前所了解到的,容器的IP地址是從Docker守護進程啟動時隨機選擇的一個內網IP網段里申請和分配的。如果用戶希望將容器運行在自己的內網環境里,可以在Docker守護進程啟動時傳入一個特定參數來指定自己的IP網段。這似乎是在用戶的私有內網里實現跨宿主機的容器間通信的首選方案。
必須首先為`docker0`網橋分配一個IP地址,然后再為Docker容器指定一個IP地址段。這個IP地址段**必須是**該網橋所在網段的**一個子網**。因此,如果我們想讓我們的容器在`192.168.20.0/24`網絡上,我們可以將`DOCKER_OPTS`環境變量設置成下面的值:
`DOCKER_OPTS="--bip=192.168.20.5/24--fixed-cidr=192.168.20.0/25"`這看起來似乎是一個非常簡便的實現Docker容器自定義內網配置的方法,實際上還需要為之在宿主機上添加一些特殊路由和`iptables`規則。這一方案的另外一個弊端或許你已經想到了,那便是IP地址的自動分配問題,這一點我們在之前討論過。由于Docker守護進程之間不會通信,因此IP地址的分配可能會導致地址沖突。這也正是Docker期望解決的問題,在之后的版本如果引入了[libnetwork](https://github.com/docker/libnetwork)的話,這個問題應該可以迎刃而解。在此期間,可以利用Docker的第三方工具的集成來解決這個問題。
針對Docker私有多主機網絡的實現,業界還有一種流行的解決方案便是使用開放[虛擬交換機](http://openvswitch.org)(OVS)和[GRE隧道](https://en.wikipedia.org/wiki/Generic_Routing_Encapsulation)。這背后的想法便是用OVS創建一個網橋來取代默認的`docker0`,然后在內網的宿主機之間創建一個安全的GRE隧道。這樣做的話,用戶至少需要對OVS如何運轉有一些基本的了解。而且,希望互聯的Docker宿主機越多,其復雜程度也越高(關于這一點,在Docker以后的版本里會通過引入[libnetwork](https://github.com/docker/libnetwork)加以解決,或者也可以使用[Swarm](https://github.com/docker/swarm)來規避這些問題)。默認情況下,使用OVS仍然無法解決我們早些時候提到的Docker跨多宿主機的IP地址分配的問題,因此用戶需要采取一些額外的措施。關于如何將OVS用于Docker的更多內容請閱讀這篇文章:https://goldmann.pl/blog/2014/01/21/ connecting-docker-containers-on-multiple-hosts/。
Docker還提供了另外一個更為簡單的方案來實現跨多主機的容器間網絡通信:**共享網絡命名空間**。
#### 12.5.3 共享網絡命名空間
共享網絡命名空間的概念自[Kubernetes](http://kubernetes.io)項目發起以來開始被廣泛大眾接受,而它還有一個眾所周知的名字,那便是[pod](https://github.com/GoogleCloudPlatform/kubernetes/blob/master/docs/pods.md)。下面,我們將介紹如何將網絡命名空間共享用于Docker宿主機的互聯和通信。
當啟動一個新容器時,可以為之指定一個特殊的**“網絡模式”**。網絡模式允許用戶指定Docker守護進程采用何種方式為新建的容器創建網絡命名空間。Docker客戶端也提供了`--net`命令行參數來實現這一點。默認情況下它設置為`bridge`,這一點本章的開頭部分便已經介紹過。
為了能夠共享任意數量的容器之間的網絡命名空間,我們傳入`--net`命令行參數時必須遵循下面的格式:`container:NAME_or_ID`。
為了使用這一參數,必須事先創建好一個“源”容器,該容器將創建一個其他容器都能加入的“基礎”網絡命名空間。同樣地,我們將通過一個簡單的案例來詳細說明這一點。
我們將會創建一個新容器,它將加入我們之前運行的`nginx`容器所創建的網絡命名空間里。但是在此之前,讓我們先列出`nginx`容器里所有的網絡接口的情況:
```
# docker exec -it a10e2dc0fdfb ip link list
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UN-
KNOWN mode DEFAULT
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
71: eth0: <BROADCAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state
UP mode DEFAULT
link/ether 02:42:ac:11:00:05 brd ff:ff:ff:ff:ff:ff
```
上述網絡接口信息應該沒什么特別的地方。現在,讓我們新建一個新的一次性容器,它將加入上述容器的網絡空間里并隨后列出它所有可用的網絡接口情況:
```
# docker run --rm -it --net=container:a10e2dc0fdfb busybox ip
link list
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UN-
KNOWN mode DEFAULT group default
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
71: eth0: <BROADCAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state
UP mode DEFAULT group default
link/ether 02:42:ac:11:00:05 brd ff:ff:ff:ff:ff:ff
```
與文檔里所描述的一樣,這也正是我們所預見的情況:新創建的容器可以看到并且綁定到`nginx`容器的網絡接口上。新容器沒有創建任何新的網絡命名空間——它所做的只是加入到一個現有的命名空間而已。
網絡空間共享的優勢在于它只占用Linux內核很小一部分的資源,并且它可以作為一些場景下簡單的網絡管理的解決方案:它不需要用戶關注任何`iptables`的規則,并且如果在內網上已經建立了網絡連接,那么新進來的網絡連接可以直接復用源自同一容器IP的連接。這一方案的另一個優勢在于用戶可以通過回環接口與運行在容器里的服務通信。但是,用戶無法再把不同的服務綁定到相同的IP地址和端口上。
此外,如果源容器停止運行,用戶必須重新創建它并讓所有其他的容器重新加入它建立的新的網絡命名空間里:網絡命名空間是不能循環利用的!當然,用戶可以通過創建一個“已命名”的網絡命名空間來解決這個問題。讀者可以在https://speakerdeck.com/gyre007/exploring-networking-in-linux-containers這篇演講中找到關于網絡命名空間的更多內容。目前,Docker并沒有一個很好的辦法來列舉出所有共享了網絡命名空間的容器。
現在,我們對于Docker容器之間的網絡命名空間共享已經有了一個很好的認識,接下來,讓我們一起來看看如何利用它來連接多個Docker宿主機以實現相互通信。竅門同樣在于`--net`命令行參數的靈活運用。
Docker允許用戶在創建容器的時候和它的宿主機共享網絡命名空間。該宿主機本身在PID 1進程的命名空間里運行它的網絡棧,這樣的話,對于容器來說它們可以很輕松地加入宿主機的網絡命名空間里。直白點兒講便是:容器的網絡棧和宿主機之間是共享的。
這就意味著加入宿主機的網絡命名空間的容器將具備宿主機網絡的**只讀**權限(除非帶上`—privileged`標志)。當用戶在與宿主機共享網絡命名空間的容器里啟動一個新服務時,該服務會綁定到宿主機上任何可用的IP地址上,因此用戶不必費多大勁便能實現它在宿主機上對外提供服務。
**警告**:*盡量避免在宿主機上共享網絡命名空間,因為這可能會導致用戶的宿主機存在一些潛在的安全風險。在Docker的安全問題完全解決之前用戶都必須時刻小心這一點!*
下面,我們將通過一個非常簡單的案例來加深對這一方案的實際理解。創建一個新容器,列出里面所有可用的網絡接口,然后和宿主機上的網絡接口做對比。首先,我們通過運行如下命令來列舉宿主機上所有的網絡接口:
```
# ip link list
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
link/ether 04:01:47:4f:c6:01 brd ff:ff:ff:ff:ff:ff
4: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default
link/ether 56:84:7a:fe:97:99 brd ff:ff:ff:ff:ff:ff
```
這里面沒什么特殊的地方:我們有一個回環設備、一個以太網設備和一個Docker網橋。現在,讓我們創建一個新的一次性容器,并且使得它和宿主機共享網絡命名空間,然后列舉該容器里所有可用的網絡接口信息:
```
# docker run --rm --net=host -it busybox ip link list
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
link/ether 04:01:47:4f:c6:01 brd ff:ff:ff:ff:ff:ff
4: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default
link/ether 56:84:7a:fe:97:99 brd ff:ff:ff:ff:ff:ff
```
如你所見,該容器沒有創建任何新的網絡接口,并且宿主機上的所有接口對它都是可見的。這樣一來便輕松實現了宿主機之間容器的相互連接和通信,它只是省去了Docker私有網絡這一部分而已。然而有利就有弊,這同樣也使得用戶的容器直接暴露在了網絡上,并且抹去了很多在私有網絡命名空間里運行容器時所具備的優勢。
此外,當我們在宿主機上共享網絡命名空間時必須留意一個看上去不是那么顯眼的事情。網絡命名空間里包含了Unix套接字,這意味著宿主機上所有各種操作系統服務所公開的Unix套接字同樣對容器可見。這有時候可能會導致一些意想不到的[事情](https://github.com/docker/docker/issues/6401)發生。當用戶在一個與宿主機共享網絡命名空間的容器里準備關閉服務的時候切記這一點,并且盡量避免將這些容器運行在授權模式下。
上述情況的一個簡單的可用場景便是當用戶沒有私有的SSH密鑰時,在**Docker構建時**(build time)用戶可以通過SSH客戶端進行網絡代理,如果將它打包到容器的鏡像里會造成一些安全隱患(除非用戶在每次構建后回收密鑰)。這個問題的解決辦法之一是運行一個`socat`代理容器,它與宿主機共享網絡命名空間,然后訪問宿主機上的`ssh-agent`。隨后該代理將會監聽一個TCP端口,在構建時用戶可以通過這個端口代理所有的內部傳輸。在這篇[gist](https://gist.github.com/milosgajdos83/0ca37bc08a28338d1475)中你可以看到和這相關的一個具體實例。
關于網絡命名空間共享更安全的一個版本是先照常創建一個網絡命名空間,然后使用`ip link set netns`命令把宿主機上的接口移到新的網絡命名空間中,隨后在容器里配置它。目前,Docker還沒有對此提供任何命令行方面的支持。
這種類型的配置在大多數的云環境下都無法正常運行,因為它們很少會提供多個物理網絡端口,并且也不接受在現有的端口里添加額外的MAC地址。然而,如果在帶有多個物理端口的物理硬件下運行,像SR-IOV端口、VLAN或者macvtap接口,這種辦法應該是可行的。
大多數的Docker部署都是使用標準的IPv4,但是許多大型數據中心的運營商,像Google和Facebook,在內部已經開始轉向使用新的IPv6協議。IPv6地址不會短缺,它的地址位長足足有128位,而且為單個主機分配的IP地址是一個/64,或者是18 446 744 073 709 551 616個地址!
這意味著完全可以為每個容器分配一個它自己的全局可路由的IPv6地址,這也代表著跨主機的容器之間的通信會變得簡單很多。
當前版本的Docker已經加入了對IPv6的支持,并且相應的[配置文檔](http://docs.docker.com/engine/userguide/networking/)也已經整理了出來。Docker需要為容器分配一個至少/80的地址段,這意味著它可以在一個/64位地址的宿主機上工作,或者甚至在更大規模的時候還可以把單個的/64段分到不同的多臺宿主機上,在這個配置下理論上最多可以有4096臺宿主機。不同宿主機之間的容器甚至可以直接路由。
同樣,在云環境下,至今仍然鮮有提供IPv6支持的廠商,但是這一點在未來有望得到改善。Digital Ocean每臺宿主機上只提供了最小僅16個的IPv6地址段,雖然它宣稱支持這一功能,但是實際實施起來還是有一定的難度,關于啟用IPv6地址支持的方法,在其官方文檔里的NDP代理部分有相應介紹。然而,也有像來自Bytemark的BigV和Vultr,他們提供的云服務支持一個全長的/64段IPv6的支持,有了它們,為Docker引入IPv6的支持也將不再是一件難事。
IPv4地址的短缺問題迫使更多的人把目光放到了IPv6的身上,它提供了一種沒有NAT回歸簡單的網絡拓撲結構,但是由于缺乏廣泛的支持,IPv6的實際應用仍然存在問題。
正如本章所介紹的,Docker引擎提供了大量可供選擇的方案來幫助連接基礎架構中的容器。遺憾的是,更高級的配置需要大量的人力投入,并且需要用戶對于像路由和`iptables`這樣的網絡內部原理有深刻的理解,而這對于只想運行應用而不關心底層網絡細節的用戶卻是一件很痛苦的事情。此外,Docker提供的原生參數是相對靜態的,并且在跨多宿主機方面的擴展性并不是很好。我們希望所有的問題最終都能通過將在新版Docker里發布的網絡API來解決,該API基于早前提過的`libnetwork`實現。當然,在實際發布前也不必太過擔心,快速迭代的Docker生態環境會幫你解決遇到的問題。
在標準的Docker網絡試圖將網絡配置抽象化的同時,使用`iptables`以及潛在的隧道協議是有性能開銷的。[New Relic](https://blog.newrelic.com)發現在某些情況下,對于需要高性能網絡的應用程序來說,使用標準的Docker網絡配置也許會比原生的本地網絡要慢上20多倍。
業界已經有大量可供使用的工具來解決上述討論的Docker網絡方面的問題。最簡單的,但也是非常強大的便是一個由Docker公司的[Je?ro?me Petazzoni](https://twitter.com/jpetazzo)獨立開發的名為[pipework](https://github.com/jpetazzo/pipework)的工具。pipework本質上是一個簡單的shell腳本,它支持像容器多IP地址配置等高級網絡配置,允許使用Mac VLAN設備甚至是DHCP來完成IP地址的分配。甚至有人制作了一個專門的[Docker鏡像](https://hub.docker.com/r/dreamcat4/pipework/),使得可以在容器中運行pipework。可以利用`docker compose`將其整合到應用程序中。
另一個非常有用的工具是由Weaveworks公司提供的,名為[weave](https://github.com/weaveworks/weave)。如果詳細介紹weave可能需要一個單獨的章節,但是在這里,我們只提供一些簡單的介紹,讀者可以通過本節提到的各種鏈接地址進一步了解與它相關的內容。weave可以跨多宿主機和云服務創建覆蓋網絡,如此一來用戶便可以輕松地連接到各個Docker容器。weave還提供了一些非常強大的功能,如安全性方面的[增強](http://www.weave.works/weave-net-cryptography-faq/)和早前提到過的`weave-dns`,它實現了一個簡易的基于DNS的服務發現。根據經驗來看,weave算是市場上目前最易上手的工具,因為它不需要用戶考慮太多底層網絡方面的細節。該企業最近發布了一些非常有意思的功能,如[weave scope](https://github.com/weaveworks/scope),這是一個為容器提供更好的可視化效果的工具,它在高密度容器的環境里可以說是無價之寶。讀者可以通過這篇\[文章\](http://thenewstack.io/how-to-detect-map-and-monitor-docker-containers-with-weave-scope- from-weaveworks/)對scope有一個大致的了解。weave版本1.0發布了一個重大特性,即[快速網絡路徑](http://www.weave.works/weave-fast-datapath/),它是基于我們之前討論過的OVS實現的。使用該快速路徑就意味著用戶的網絡不再是強加密的,但是這仍然是一個值得權衡的事情。最后,隨著Docker近期發布的[Docker插件系統](https://blog.docker.com/2015/06/extending-docker-with-plugins/)的支持,原生Docker的集成也變得更加容易,用戶只要把Weave當做[Docker的插件](http://www.weave.works/weave-as-a-docker-network-plugin/)使用就行了。
[CoreOS](https://coreos.com)也開發了一個名為[flannel](https://github.com/coreos/flannel)的覆蓋網絡解決方案。flannel最開始開發是為了解決Kubernetes原生依靠Google云平臺為中央網絡的網絡依賴問題,但是自第一個版本發布以來該項目發生了很大的轉變,如今它提供了一些如[VXLAN](https://en.wikipedia.org/wiki/Virtual_Extensible_LAN)這樣很有意思的特性。flannel將它的配置信息存儲在[etcd](https://github.com/coreos/etcd)中,這使得它可以和CoreOS操作系統輕松地整合在一起。
最后,在Docker網絡領域最新的一員是一個名為[calico](http://www.projectcalico.org)的項目,它和OpenStack一樣為Docker提供了一個3層的解決方案。目前該項目利用的是ClusterHQ的[powerstrip](https://github.com/ClusterHQ/powerstrip)工具,使用該工具可以在Docker引擎的原生插件實現前完成簡單的插件注冊功能。calico項目的最大優勢便是它提供了原生的IPv6支持,并且易于在多Docker宿主機之間擴展。calico本身是通過某種跨多宿主機的分布式BGP路由來實現的這一點。這是一個值得長期關注的項目。
介紹完這些第三方工具之后,我們也將結束本次的Docker網絡之旅。希望這里所描述的內容能夠真正地幫助你更好地了解該如何建立適合自己的Docker基礎設施的網絡模型。
現在,我們將繼續前行,在第13章中我們將討論如何完成跨多宿主機的Docker容器的調度。
- - - - - -
[\[1\]](part0018.xhtml#ac121) 從1.10版起,Docker會根據鏡像及鏡像層數據的安全散列生成一個內容可尋址的安全ID。——譯者注
[\[2\]](part0018.xhtml#ac122) 在本書紙版發行前,Docker v1.9已經發布,實現了跨主機的網絡互通,并且支持不同的插件方式。——譯者注
[\[3\]](part0018.xhtml#ac123) icc是Inter Container Communication的縮寫。——譯者注
- 版權信息
- 版權聲明
- 內容提要
- 對本書的贊譽
- 譯者介紹
- 前言
- 本書面向的讀者
- 誰真的在生產環境中使用Docker
- 為什么使用Docker
- 開發環境與生產環境
- 我們所說的“生產環境”
- 功能內置與組合工具
- 哪些東西不要Docker化
- 技術審稿人
- 第1章 入門
- 1.1 術語
- 1.1.1 鏡像與容器
- 1.1.2 容器與虛擬機
- 1.1.3 持續集成/持續交付
- 1.1.4 宿主機管理
- 1.1.5 編排
- 1.1.6 調度
- 1.1.7 發現
- 1.1.8 配置管理
- 1.2 從開發環境到生產環境
- 1.3 使用Docker的多種方式
- 1.4 可預期的情況
- 為什么Docker在生產環境如此困難
- 第2章 技術棧
- 2.1 構建系統
- 2.2 鏡像倉庫
- 2.3 宿主機管理
- 2.4 配置管理
- 2.5 部署
- 2.6 編排
- 第3章 示例:極簡環境
- 3.1 保持各部分的簡單
- 3.2 保持流程的簡單
- 3.3 系統細節
- 利用systemd
- 3.4 集群范圍的配置、通用配置及本地配置
- 3.5 部署服務
- 3.6 支撐服務
- 3.7 討論
- 3.8 未來
- 3.9 小結
- 第4章 示例:Web環境
- 4.1 編排
- 4.1.1 讓服務器上的Docker進入準備運行容器的狀態
- 4.1.2 讓容器運行
- 4.2 連網
- 4.3 數據存儲
- 4.4 日志
- 4.5 監控
- 4.6 無須擔心新依賴
- 4.7 零停機時間
- 4.8 服務回滾
- 4.9 小結
- 第5章 示例:Beanstalk環境
- 5.1 構建容器的過程
- 部署/更新容器的過程
- 5.2 日志
- 5.3 監控
- 5.4 安全
- 5.5 小結
- 第6章 安全
- 6.1 威脅模型
- 6.2 容器與安全性
- 6.3 內核更新
- 6.4 容器更新
- 6.5 suid及guid二進制文件
- 6.6 容器內的root
- 6.7 權能
- 6.8 seccomp
- 6.9 內核安全框架
- 6.10 資源限制及cgroup
- 6.11 ulimit
- 6.12 用戶命名空間
- 6.13 鏡像驗證
- 6.14 安全地運行Docker守護進程
- 6.15 監控
- 6.16 設備
- 6.17 掛載點
- 6.18 ssh
- 6.19 私鑰分發
- 6.20 位置
- 第7章 構建鏡像
- 7.1 此鏡像非彼鏡像
- 7.1.1 寫時復制與高效的鏡像存儲與分發
- 7.1.2 Docker對寫時復制的使用
- 7.2 鏡像構建基本原理
- 7.2.1 分層的文件系統和空間控管
- 7.2.2 保持鏡像小巧
- 7.2.3 讓鏡像可重用
- 7.2.4 在進程無法被配置時,通過環境變量讓鏡像可配置
- 7.2.5 讓鏡像在Docker變化時對自身進行重新配置
- 7.2.6 信任與鏡像
- 7.2.7 讓鏡像不可變
- 7.3 小結
- 第8章 存儲Docker鏡像
- 8.1 啟動并運行存儲的Docker鏡像
- 8.2 自動化構建
- 8.3 私有倉庫
- 8.4 私有registry的擴展
- 8.4.1 S3
- 8.4.2 本地存儲
- 8.4.3 對registry進行負載均衡
- 8.5 維護
- 8.6 對私有倉庫進行加固
- 8.6.1 SSL
- 8.6.2 認證
- 8.7 保存/載入
- 8.8 最大限度地減小鏡像體積
- 8.9 其他鏡像倉庫方案
- 第9章 CI/CD
- 9.1 讓所有人都進行鏡像構建與推送
- 9.2 在一個構建系統中構建所有鏡像
- 9.3 不要使用或禁止使用非標準做法
- 9.4 使用標準基礎鏡像
- 9.5 使用Docker進行集成測試
- 9.6 小結
- 第10章 配置管理
- 10.1 配置管理與容器
- 10.2 面向容器的配置管理
- 10.2.1 Chef
- 10.2.2 Ansible
- 10.2.3 Salt Stack
- 10.2.4 Puppet
- 10.3 小結
- 第11章 Docker存儲引擎
- 11.1 AUFS
- 11.2 DeviceMapper
- 11.3 BTRFS
- 11.4 OverlayFS
- 11.5 VFS
- 11.6 小結
- 第12章 Docker 網絡實現
- 12.1 網絡基礎知識
- 12.2 IP地址的分配
- 端口的分配
- 12.3 域名解析
- 12.4 服務發現
- 12.5 Docker高級網絡
- 12.6 IPv6
- 12.7 小結
- 第13章 調度
- 13.1 什么是調度
- 13.2 調度策略
- 13.3 Mesos
- 13.4 Kubernetes
- 13.5 OpenShift
- Red Hat公司首席工程師Clayton Coleman的想法
- 第14章 服務發現
- 14.1 DNS服務發現
- DNS服務器的重新發明
- 14.2 Zookeeper
- 14.3 基于Zookeeper的服務發現
- 14.4 etcd
- 基于etcd的服務發現
- 14.5 consul
- 14.5.1 基于consul的服務發現
- 14.5.2 registrator
- 14.6 Eureka
- 基于Eureka的服務發現
- 14.7 Smartstack
- 14.7.1 基于Smartstack的服務發現
- 14.7.2 Nerve
- 14.7.3 Synapse
- 14.8 nsqlookupd
- 14.9 小結
- 第15章 日志和監控
- 15.1 日志
- 15.1.1 Docker原生的日志支持
- 15.1.2 連接到Docker容器
- 15.1.3 將日志導出到宿主機
- 15.1.4 發送日志到集中式的日志平臺
- 15.1.5 在其他容器一側收集日志
- 15.2 監控
- 15.2.1 基于宿主機的監控
- 15.2.2 基于Docker守護進程的監控
- 15.2.3 基于容器的監控
- 15.3 小結
- DockOne社區簡介
- 看完了