在現(xiàn)實(shí)世界中,容器間通信的可靠性和安全性相當(dāng)重要,即使容器分屬于不同網(wǎng)絡(luò)中的不同主機(jī)。這也是覆蓋網(wǎng)絡(luò)大展拳腳的地方,它允許創(chuàng)建扁平的、安全的二層網(wǎng)絡(luò)來連接多個主機(jī),容器可以連接到覆蓋網(wǎng)絡(luò)并直接互相通信。
Docker 提供了原生覆蓋網(wǎng)絡(luò)的支持,易于配置且非常安全。其背后是基于 Libnetwork 以及相應(yīng)的驅(qū)動來構(gòu)建的。
Libnetwork 是 CNM 的典型實(shí)現(xiàn),從而可以通過插拔驅(qū)動的方式來實(shí)現(xiàn)不同的網(wǎng)絡(luò)技術(shù)和拓?fù)浣Y(jié)構(gòu)。
Docker 提供了一些諸如 Overlay 的原生驅(qū)動,同時第三方也可以提供驅(qū)動。
在 2015 年 3 月,Docker 公司收購了一個叫作 Socket Plane 的網(wǎng)絡(luò)初創(chuàng)企業(yè)。收購的原因有二,首先是因?yàn)檫@會給 Docker 帶來真正意義的網(wǎng)絡(luò)架構(gòu),其次是讓容器間聯(lián)網(wǎng)變得非常簡單,以至于開發(fā)人員都可以配置它。
Docker 公司在這兩點(diǎn)上都取得了巨大的成功。但是,簡潔的網(wǎng)絡(luò)命令實(shí)際由大量的組件構(gòu)成。這部分內(nèi)容是在進(jìn)行生產(chǎn)環(huán)境部署和問題定位前必須要了解的。
要完成下面的示例,需要兩臺 Docker 主機(jī),并通過一個路由器上兩個獨(dú)立的二層網(wǎng)絡(luò)連接在一起。如下圖所示,注意節(jié)點(diǎn)位于不同網(wǎng)絡(luò)之上。

可以選擇 Linux 容器主機(jī)或者 Windows 容器主機(jī)。Linux 內(nèi)核版本不能低于 4.4(高版本更好),Windows 需要 Windows Server 2016 版本,并且應(yīng)安裝最新的補(bǔ)丁。
首先需要將兩臺主機(jī)配置為包含兩個節(jié)點(diǎn)的 Swarm 集群。接下來會在 node1 節(jié)點(diǎn)上運(yùn)行 docker swarm init 命令使其成為管理節(jié)點(diǎn),然后在 node2 節(jié)點(diǎn)上運(yùn)行 docker swarm join 命令來使其成為工作節(jié)點(diǎn)。
在 node1 節(jié)點(diǎn)上運(yùn)行下面的命令。
$ docker swarm init \
--advertise-addr=172.31.1.5 \
--listen-addr=172.31.1.5:2377
Swarm initialized: current node (1ex3...o3px) is now a manager.
在 node2 上運(yùn)行下面的命令。如果需要在 Windows 環(huán)境下生效,則需要修改 Windows 防火墻規(guī)則,打開 2377/tcp、7946/tcp 以及 7946/udp 等幾個端口。
$ docker swarm join \
--token SWMTKN-1-0hz2ec...2vye \
172.31.1.5:2377
This node joined a swarm as a worker.
現(xiàn)在就已經(jīng)創(chuàng)建好了包含管理節(jié)點(diǎn) node1 和工作節(jié)點(diǎn) node2 兩個節(jié)點(diǎn)的 Swarm 集群了。
現(xiàn)在創(chuàng)建一個名為 uber-net 的覆蓋網(wǎng)絡(luò)。
在 node1(管理節(jié)點(diǎn))節(jié)點(diǎn)上運(yùn)行下面的命令。若要這些命令在 Windows 上也能運(yùn)行,需要在 Windows Docker 節(jié)點(diǎn)上添加 4789/udp 規(guī)則。
$ docker network create -d overlay uber-net
c740ydi1lm89khn5kd52skrd9
剛剛創(chuàng)建了一個嶄新的覆蓋網(wǎng)絡(luò),能連接 Swarm 集群內(nèi)的所有主機(jī),并且該網(wǎng)絡(luò)還包括一個 TLS 加密的控制層!如果還想對數(shù)據(jù)層加密的話,只需在命令中增加 -o encrypted 參數(shù)。
可以通過 docker network ls 命令列出每個節(jié)點(diǎn)上的全部網(wǎng)絡(luò)。
$ docker network ls
NETWORK ID NAME DRIVER SCOPE
ddac4ff813b7 bridge bridge local
389a7e7e8607 docker_gwbridge bridge local
a09f7e6b2ac6 host host local
ehw16ycy980s ingress overlay swarm
2b26c11d3469 none null local
c740ydi1lm89 uber-net overlay swarm
在 Windows Docker 主機(jī)上輸出內(nèi)容如下。
NETWORK ID NAME DRIVER SCOPE
8iltzv6sbtgc ingress overlay swarm
6545b2a61b6f nat nat local
96d0d737c2ee none null local
nil5ouh44qco uber-net overlay swarm
列表的最下方就是剛剛創(chuàng)建的網(wǎng)絡(luò) uber-net。其他的網(wǎng)絡(luò)是在安裝 Docker 以及初始化 Swarm 集群的時候創(chuàng)建的。
如果在 node2 節(jié)點(diǎn)上運(yùn)行 docker network ls 命令,就會發(fā)現(xiàn)無法看到 uber-net 網(wǎng)絡(luò)。這是因?yàn)橹挥挟?dāng)運(yùn)行中的容器連接到覆蓋網(wǎng)絡(luò)的時候,該網(wǎng)絡(luò)才變?yōu)榭捎脿顟B(tài)。這種延遲生效策略通過減少網(wǎng)絡(luò)梳理,提升了網(wǎng)絡(luò)的擴(kuò)展性。
現(xiàn)在覆蓋網(wǎng)絡(luò)已經(jīng)就緒,接下來新建一個 Docker 服務(wù)并連接到該網(wǎng)絡(luò)。Docker 服務(wù)會包含兩個副本(容器),一個運(yùn)行 node1 節(jié)點(diǎn)上,一個運(yùn)行在 node2 節(jié)點(diǎn)上。這樣會自動將 node2 節(jié)點(diǎn)接入 uber-net 網(wǎng)絡(luò)。
在 node1 節(jié)點(diǎn)上運(yùn)行下面的命令。
Linux 示例如下。
$ docker service create --name test \
--network uber-net \
--replicas 2 \
ubuntu sleep infinity
Windows 示例如下。
> docker service create --name test `
--network uber-net `
--replicas 2 `
microsoft\powershell:nanoserver Start-Sleep 3600
Windows 示例使用反引號的方式將單條命令分為多行,以提高命令的可讀性。PowerShell 中使用反引號來轉(zhuǎn)義換行字符。
該命令創(chuàng)建了名為 test 的新服務(wù),連接到了 uber-net 這個覆蓋網(wǎng)絡(luò),并且還基于指定的鏡像創(chuàng)建了兩個副本(容器)。在兩個示例中,均在容器中采用 sleep 命令來保持容器運(yùn)行,并在休眠結(jié)束后退出該容器。
由于運(yùn)行了兩個副本(容器),而 Swarm 包含兩個節(jié)點(diǎn),因此每個節(jié)點(diǎn)上都會運(yùn)行一個副本。
可以通過 docker service ps 命令來確認(rèn)上面的操作。
$ docker service ps test
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE
77q...rkx test.1 ubuntu node1 Running Running
97v...pa5 test.2 ubuntu node2 Running Running
當(dāng) Swarm 在覆蓋網(wǎng)絡(luò)之上啟動容器時,會自動將容器運(yùn)行所在節(jié)點(diǎn)加入到網(wǎng)絡(luò)當(dāng)中。這意味著此時在 node2 節(jié)點(diǎn)上就可以看到 uber-net 網(wǎng)絡(luò)了。
目前已經(jīng)成功在兩個由物理網(wǎng)絡(luò)連接的節(jié)點(diǎn)上創(chuàng)建了新的覆蓋網(wǎng)絡(luò)。同時,還將兩個容器連接到了該網(wǎng)絡(luò)當(dāng)中。
現(xiàn)在使用 ping 命令來測試覆蓋網(wǎng)絡(luò)。
如下圖所示,在兩個獨(dú)立的網(wǎng)絡(luò)中分別有一臺 Docker 主機(jī),并且兩者都接入了同一個覆蓋網(wǎng)絡(luò)。目前在每個節(jié)點(diǎn)上都有一個容器接入了覆蓋網(wǎng)絡(luò)。測試一下兩個容器之間是否可以 ping 通。

為了執(zhí)行該測試,需要知道每個容器的 IP 地址(為了測試,暫時忽略相同覆蓋網(wǎng)絡(luò)上的容器可以通過名稱來互相 ping 通的事實(shí))。
運(yùn)行 docker network inspect 查看被分配給覆蓋網(wǎng)絡(luò)的 Subnet。
$ docker network inspect uber-net
[
{
"Name": "uber-net",
"Id": "c740ydi1lm89khn5kd52skrd9",
"Scope": "swarm",
"Driver": "overlay",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": null,
"Config": [
{
"Subnet": "10.0.0.0/24",
"Gateway": "10.0.0.1"
}
<Snip>
由以上輸出可見,uber-net 的子網(wǎng)是 10.0.0.0/24。注意,這與兩個節(jié)點(diǎn)的任意底層物理網(wǎng)絡(luò) IP 均不相符(172.31.1.0/24 和 192.168.1.0/24)。
在 node1 和 node2 節(jié)點(diǎn)上運(yùn)行下面兩條命令。這兩條命令可以獲取到容器 ID 和 IP 地址。在第二條命令中一定要使用讀者自己的環(huán)境中的容器 ID。
需要在兩臺節(jié)點(diǎn)上分別運(yùn)行上述命令,獲取兩個容器的 ID 和 IP 地址。
下圖展示了配置現(xiàn)狀。在運(yùn)行環(huán)境中,子網(wǎng)和 IP 地址信息可能不同。

由圖可知,一個二層覆蓋網(wǎng)絡(luò)橫跨兩臺主機(jī),并且每個容器在覆蓋網(wǎng)絡(luò)中都有自己的 IP 地址。這意味著 node1 節(jié)點(diǎn)上的容器可以通過 node2 節(jié)點(diǎn)上容器的 IP 地址 10.0.0.4 來 ping 通,該 IP 地址屬于覆蓋網(wǎng)絡(luò)。盡管兩個節(jié)點(diǎn)分屬于不同的二層網(wǎng)絡(luò),還是可以直接 ping 通。接下來驗(yàn)證這一點(diǎn)。
登錄到 node1 的容器,并 ping 另一個的容器。
在 Linux Ubuntu 容器中執(zhí)行該操作的話,需要安裝 ping 工具包。如果讀者使用 Windows PowerShell 示例,ping 工具已默認(rèn)安裝。
Linux 示例如下。
Windows 示例如下。
> docker container exec -it 1a4f29e5a4b6 pwsh.exe
Windows PowerShell
Copyright (C) 2016 Microsoft Corporation. All rights reserved.
PS C:\> ping 10.0.0.4
Pinging 10.0.0.4 with 32 bytes of data:
Reply from 10.0.0.4: bytes=32 time=1ms TTL=128
Reply from 10.0.0.4: bytes=32 time<1ms TTL=128
Reply from 10.0.0.4: bytes=32 time=2ms TTL=128
Reply from 10.0.0.4: bytes=32 time=2ms TTL=12
PS C:\>
由運(yùn)行結(jié)果可知 node1 上的容器可以通過覆蓋網(wǎng)絡(luò) ping 通 node2 之上的容器了。
還可以在容器內(nèi)部跟蹤 ping 命令的路由信息。路由信息只有一條,證明容器間通信確實(shí)通過覆蓋網(wǎng)絡(luò)直連。
如果希望 Linux 示例中的 traceroute 可執(zhí)行,需要安裝 traceroute 包。
Linux 示例如下。
$ root@396c8b142a85:/# traceroute 10.0.0.4
traceroute to 10.0.0.4 (10.0.0.4), 30 hops max, 60 byte packets
1 test-svc.2.97v...a5.uber-net (10.0.0.4) 1.110ms 1.034ms 1.073ms
Windows 示例如下。
PS C:\> tracert 10.0.0.3
Tracing route to test.2.ttcpiv3p...7o4.uber-net [10.0.0.4]
over a maximum of 30 hops:
1 <1 ms <1 ms <1 ms test.2.ttcpiv3p...7o4.uber-net [10.0.0.4]
Trace complete.
到目前為止,已經(jīng)通過單條命令創(chuàng)建了覆蓋網(wǎng)絡(luò),并向該網(wǎng)絡(luò)中接入了容器。這些容器分布在兩個不同的主機(jī)上,兩臺主機(jī)分屬于不同的二層網(wǎng)絡(luò)。在找出兩臺容器的 IP 之后,驗(yàn)證了容器可以通過覆蓋網(wǎng)絡(luò)完成直連。
現(xiàn)在已經(jīng)知道如何創(chuàng)建并使用容器覆蓋網(wǎng)絡(luò),接下來介紹一下這一切背后的技術(shù)原理。
首先必須知道,Docker 使用 VXLAN 隧道技術(shù)創(chuàng)建了虛擬二層覆蓋網(wǎng)絡(luò)。所以,在詳解之前,先快速了解一下 VXLAN。
在 VXLAN 的設(shè)計(jì)中,允許用戶基于已經(jīng)存在的三層網(wǎng)絡(luò)結(jié)構(gòu)創(chuàng)建虛擬的二層網(wǎng)絡(luò)。在前面的示例中創(chuàng)建了一個子網(wǎng)掩碼為 10.0.0.0/24 的二層網(wǎng)絡(luò),該網(wǎng)絡(luò)是基于一個三層 IP 網(wǎng)絡(luò)實(shí)現(xiàn)的,三層 IP 網(wǎng)絡(luò)由 172.31.1.0/24 和 192.168.1.0/24 這兩個二層網(wǎng)絡(luò)構(gòu)成。具體如下圖所示。

VXLAN 的美妙之處在于它是一種封裝技術(shù),能使現(xiàn)存的路由器和網(wǎng)絡(luò)架構(gòu)看起來就像普通的 IP/UDP 包一樣,并且處理起來毫無問題。
為了創(chuàng)建二層覆蓋網(wǎng)絡(luò),VXLAN 基于現(xiàn)有的三層 IP 網(wǎng)絡(luò)創(chuàng)建了隧道。小伙伴可能聽過基礎(chǔ)網(wǎng)絡(luò)(Underlay Network)這個術(shù)語,它用于指代三層之下的基礎(chǔ)部分。
VXLAN 隧道兩端都是 VXLAN 隧道終端(VXLAN Tunnel Endpoint, VTEP)。VTEP 完成了封裝和解壓的步驟,以及一些功能實(shí)現(xiàn)所必需的操作,如下圖所示。

在前面的示例中,讀者通過 IP 網(wǎng)絡(luò)將兩臺主機(jī)連接起來。每個主機(jī)運(yùn)行了一個容器,之后又為容器連接創(chuàng)建了一個 VXLAN 覆蓋網(wǎng)絡(luò)。
為了實(shí)現(xiàn)上述場景,在每臺主機(jī)上都新建了一個 Sandbox(網(wǎng)絡(luò)命名空間)。正如前文所講,Sandbox 就像一個容器,但其中運(yùn)行的不是應(yīng)用,而是當(dāng)前主機(jī)上獨(dú)立的網(wǎng)絡(luò)棧。
在 Sandbox 內(nèi)部創(chuàng)建了一個名為 Br0 的虛擬交換機(jī)(又稱做虛擬網(wǎng)橋)。同時 Sandbox 內(nèi)部還創(chuàng)建了一個 VTEP,其中一端接入到名為 Br0 的虛擬交換機(jī)當(dāng)中,另一端接入主機(jī)網(wǎng)絡(luò)棧(VTEP)。
在主機(jī)網(wǎng)絡(luò)棧中的終端從主機(jī)所連接的基礎(chǔ)網(wǎng)絡(luò)中獲取到 IP 地址,并以 UDP Socket 的方式綁定到 4789 端口。不同主機(jī)上的兩個 VTEP 通過 VXLAN 隧道創(chuàng)建了一個覆蓋網(wǎng)絡(luò),如下圖所示。

這是 VXLAN 上層網(wǎng)絡(luò)創(chuàng)建和使用所必需的。
接下來每個容器都會有自己的虛擬以太網(wǎng)(veth)適配器,并接入本地 Br0 虛擬交換機(jī)。目前拓?fù)浣Y(jié)構(gòu)如下圖所示,雖然是在主機(jī)所屬網(wǎng)絡(luò)互相獨(dú)立的情況下,但這樣能更容易看出兩個分別位于不同主機(jī)上的容器之間是如何通過 VXLAN 上層網(wǎng)絡(luò)進(jìn)行通信的。

在本例中,將 node1 上的容器稱為 C1,node2 上的容器稱為 C2,如下圖所示。假設(shè) C1 希望 ping 通 C2,類似前面章節(jié)中的示例。

C1 發(fā)起 ping 請求,目標(biāo) IP 為 C2 的地址 10.0.0.4。該請求的流量通過連接到 Br0 虛擬交換機(jī) veth 接口發(fā)出。虛擬交換機(jī)并不知道將包發(fā)送到哪里,因?yàn)樵谔摂M交換機(jī)的 MAC 地址映射表(ARP 映射表)中并沒有與當(dāng)前目的 IP 對應(yīng)的 MAC 地址。
所以虛擬交換機(jī)會將該包發(fā)送到其上的全部端口。連接到 Br0 的 VTEP 接口知道如何轉(zhuǎn)發(fā)這個數(shù)據(jù)幀,所以會將自己的 MAC 地址返回。這就是一個代理 ARP 響應(yīng),并且虛擬交換機(jī) Br0 根據(jù)返回結(jié)果學(xué)會了如何轉(zhuǎn)發(fā)該包。接下來虛擬交換機(jī)會更新自己的 ARP 映射表,將 10.0.0.4 映射到本地 VTEP 的 MAC 地址上。
現(xiàn)在 Br0 交換機(jī)已經(jīng)學(xué)會如何轉(zhuǎn)發(fā)目標(biāo)為 C2 的流量,接下來所有發(fā)送到 C2 的包都會被直接轉(zhuǎn)發(fā)到 VTEP 接口。VTEP 接口知道 C2,是因?yàn)樗行聠拥娜萜鞫紩⒆约旱木W(wǎng)絡(luò)詳情采用網(wǎng)絡(luò)內(nèi)置 Gossip 協(xié)議發(fā)送給相同 Swarm 集群內(nèi)的其他節(jié)點(diǎn)。
交換機(jī)會將包轉(zhuǎn)發(fā)到 VTEP 接口,VTEP 完成數(shù)據(jù)幀的封裝,這樣就能在底層網(wǎng)絡(luò)傳輸。具體來說,封裝操作就是把 VXLAN Header 信息添加以太幀當(dāng)中。
VXLAN Header 信息包含了 VXLAN 網(wǎng)絡(luò) ID(VNID),其作用是記錄 VLAN 到 VXLAN 的映射關(guān)系。每個 VLAN 都對應(yīng)一個 VNID,以便包可以在解析后被轉(zhuǎn)發(fā)到正確的 VLAN。
封裝的時候會將數(shù)據(jù)幀放到 UDP 包中,并設(shè)置 UDP 的目的 IP 字段為 node2 節(jié)點(diǎn)的 VTEP 的 IP 地址,同時設(shè)置 UDP Socket 端口為 4789。這種封裝方式保證了底層網(wǎng)絡(luò)即使不知道任何關(guān)于 VXLAN 的信息,也可以完成數(shù)據(jù)傳輸。
當(dāng)包到達(dá) node2 之后,內(nèi)核發(fā)現(xiàn)目的端口為 UDP 端口 4789,同時還知道存在 VTEP 接口綁定到該 Socket。所以內(nèi)核將包發(fā)給 VTEP,由 VTEP 讀取 VNID,解壓包信息,并根據(jù) VNID 發(fā)送到本地名為 Br0 的連接到 VLAN 的交換機(jī)。在該交換機(jī)上,包被發(fā)送給容器 C2。
以上大體介紹了 Docker 覆蓋網(wǎng)絡(luò)是如何利用 VXLAN 技術(shù)的。
最后一件需要注意的是,Docker 支持使用同樣的覆蓋網(wǎng)絡(luò)實(shí)現(xiàn)三層路由。例如,讀者可以創(chuàng)建包含兩個子網(wǎng)的覆蓋網(wǎng)絡(luò),Docker 會負(fù)責(zé)子網(wǎng)間的路由。創(chuàng)建的命令如 docker network create --subnet=10.1.1.0/24 --subnet=11.1.1.0/24 -d overlay prod-net。該命令會在 Sandbox 中創(chuàng)建兩個虛擬交換機(jī),默認(rèn)支持路由。