在現實世界中,容器間通信的可靠性和安全性相當重要,即使容器分屬于不同網絡中的不同主機。這也是覆蓋網絡大展拳腳的地方,它允許創建扁平的、安全的二層網絡來連接多個主機,容器可以連接到覆蓋網絡并直接互相通信。
Docker 提供了原生覆蓋網絡的支持,易于配置且非常安全。其背后是基于 Libnetwork 以及相應的驅動來構建的。
Libnetwork 是 CNM 的典型實現,從而可以通過插拔驅動的方式來實現不同的網絡技術和拓撲結構。
Docker 提供了一些諸如 Overlay 的原生驅動,同時第三方也可以提供驅動。
在 2015 年 3 月,Docker 公司收購了一個叫作 Socket Plane 的網絡初創企業。收購的原因有二,首先是因為這會給 Docker 帶來真正意義的網絡架構,其次是讓容器間聯網變得非常簡單,以至于開發人員都可以配置它。
Docker 公司在這兩點上都取得了巨大的成功。但是,簡潔的網絡命令實際由大量的組件構成。這部分內容是在進行生產環境部署和問題定位前必須要了解的。
要完成下面的示例,需要兩臺 Docker 主機,并通過一個路由器上兩個獨立的二層網絡連接在一起。如下圖所示,注意節點位于不同網絡之上。
可以選擇 Linux 容器主機或者 Windows 容器主機。Linux 內核版本不能低于 4.4(高版本更好),Windows 需要 Windows Server 2016 版本,并且應安裝最新的補丁。
首先需要將兩臺主機配置為包含兩個節點的 Swarm 集群。接下來會在 node1 節點上運行 docker swarm init 命令使其成為管理節點,然后在 node2 節點上運行 docker swarm join 命令來使其成為工作節點。
在 node1 節點上運行下面的命令。
$ 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 上運行下面的命令。如果需要在 Windows 環境下生效,則需要修改 Windows 防火墻規則,打開 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.
現在就已經創建好了包含管理節點 node1 和工作節點 node2 兩個節點的 Swarm 集群了。
現在創建一個名為 uber-net 的覆蓋網絡。
在 node1(管理節點)節點上運行下面的命令。若要這些命令在 Windows 上也能運行,需要在 Windows Docker 節點上添加 4789/udp 規則。
$ docker network create -d overlay uber-net
c740ydi1lm89khn5kd52skrd9
剛剛創建了一個嶄新的覆蓋網絡,能連接 Swarm 集群內的所有主機,并且該網絡還包括一個 TLS 加密的控制層!如果還想對數據層加密的話,只需在命令中增加 -o encrypted 參數。
可以通過 docker network ls 命令列出每個節點上的全部網絡。
$ 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 主機上輸出內容如下。
NETWORK ID NAME DRIVER SCOPE
8iltzv6sbtgc ingress overlay swarm
6545b2a61b6f nat nat local
96d0d737c2ee none null local
nil5ouh44qco uber-net overlay swarm
列表的最下方就是剛剛創建的網絡 uber-net。其他的網絡是在安裝 Docker 以及初始化 Swarm 集群的時候創建的。
如果在 node2 節點上運行 docker network ls 命令,就會發現無法看到 uber-net 網絡。這是因為只有當運行中的容器連接到覆蓋網絡的時候,該網絡才變為可用狀態。這種延遲生效策略通過減少網絡梳理,提升了網絡的擴展性。
現在覆蓋網絡已經就緒,接下來新建一個 Docker 服務并連接到該網絡。Docker 服務會包含兩個副本(容器),一個運行 node1 節點上,一個運行在 node2 節點上。這樣會自動將 node2 節點接入 uber-net 網絡。
在 node1 節點上運行下面的命令。
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 中使用反引號來轉義換行字符。
該命令創建了名為 test 的新服務,連接到了 uber-net 這個覆蓋網絡,并且還基于指定的鏡像創建了兩個副本(容器)。在兩個示例中,均在容器中采用 sleep 命令來保持容器運行,并在休眠結束后退出該容器。
由于運行了兩個副本(容器),而 Swarm 包含兩個節點,因此每個節點上都會運行一個副本。
可以通過 docker service ps 命令來確認上面的操作。
$ 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
當 Swarm 在覆蓋網絡之上啟動容器時,會自動將容器運行所在節點加入到網絡當中。這意味著此時在 node2 節點上就可以看到 uber-net 網絡了。
目前已經成功在兩個由物理網絡連接的節點上創建了新的覆蓋網絡。同時,還將兩個容器連接到了該網絡當中。
現在使用 ping 命令來測試覆蓋網絡。
如下圖所示,在兩個獨立的網絡中分別有一臺 Docker 主機,并且兩者都接入了同一個覆蓋網絡。目前在每個節點上都有一個容器接入了覆蓋網絡。測試一下兩個容器之間是否可以 ping 通。
為了執行該測試,需要知道每個容器的 IP 地址(為了測試,暫時忽略相同覆蓋網絡上的容器可以通過名稱來互相 ping 通的事實)。
運行 docker network inspect 查看被分配給覆蓋網絡的 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 的子網是 10.0.0.0/24。注意,這與兩個節點的任意底層物理網絡 IP 均不相符(172.31.1.0/24 和 192.168.1.0/24)。
在 node1 和 node2 節點上運行下面兩條命令。這兩條命令可以獲取到容器 ID 和 IP 地址。在第二條命令中一定要使用讀者自己的環境中的容器 ID。
需要在兩臺節點上分別運行上述命令,獲取兩個容器的 ID 和 IP 地址。
下圖展示了配置現狀。在運行環境中,子網和 IP 地址信息可能不同。
由圖可知,一個二層覆蓋網絡橫跨兩臺主機,并且每個容器在覆蓋網絡中都有自己的 IP 地址。這意味著 node1 節點上的容器可以通過 node2 節點上容器的 IP 地址 10.0.0.4 來 ping 通,該 IP 地址屬于覆蓋網絡。盡管兩個節點分屬于不同的二層網絡,還是可以直接 ping 通。接下來驗證這一點。
登錄到 node1 的容器,并 ping 另一個的容器。
在 Linux Ubuntu 容器中執行該操作的話,需要安裝 ping 工具包。如果讀者使用 Windows PowerShell 示例,ping 工具已默認安裝。
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:\>
由運行結果可知 node1 上的容器可以通過覆蓋網絡 ping 通 node2 之上的容器了。
還可以在容器內部跟蹤 ping 命令的路由信息。路由信息只有一條,證明容器間通信確實通過覆蓋網絡直連。
如果希望 Linux 示例中的 traceroute 可執行,需要安裝 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.
到目前為止,已經通過單條命令創建了覆蓋網絡,并向該網絡中接入了容器。這些容器分布在兩個不同的主機上,兩臺主機分屬于不同的二層網絡。在找出兩臺容器的 IP 之后,驗證了容器可以通過覆蓋網絡完成直連。
現在已經知道如何創建并使用容器覆蓋網絡,接下來介紹一下這一切背后的技術原理。
首先必須知道,Docker 使用 VXLAN 隧道技術創建了虛擬二層覆蓋網絡。所以,在詳解之前,先快速了解一下 VXLAN。
在 VXLAN 的設計中,允許用戶基于已經存在的三層網絡結構創建虛擬的二層網絡。在前面的示例中創建了一個子網掩碼為 10.0.0.0/24 的二層網絡,該網絡是基于一個三層 IP 網絡實現的,三層 IP 網絡由 172.31.1.0/24 和 192.168.1.0/24 這兩個二層網絡構成。具體如下圖所示。
VXLAN 的美妙之處在于它是一種封裝技術,能使現存的路由器和網絡架構看起來就像普通的 IP/UDP 包一樣,并且處理起來毫無問題。
為了創建二層覆蓋網絡,VXLAN 基于現有的三層 IP 網絡創建了隧道。小伙伴可能聽過基礎網絡(Underlay Network)這個術語,它用于指代三層之下的基礎部分。
VXLAN 隧道兩端都是 VXLAN 隧道終端(VXLAN Tunnel Endpoint, VTEP)。VTEP 完成了封裝和解壓的步驟,以及一些功能實現所必需的操作,如下圖所示。
在前面的示例中,讀者通過 IP 網絡將兩臺主機連接起來。每個主機運行了一個容器,之后又為容器連接創建了一個 VXLAN 覆蓋網絡。
為了實現上述場景,在每臺主機上都新建了一個 Sandbox(網絡命名空間)。正如前文所講,Sandbox 就像一個容器,但其中運行的不是應用,而是當前主機上獨立的網絡棧。
在 Sandbox 內部創建了一個名為 Br0 的虛擬交換機(又稱做虛擬網橋)。同時 Sandbox 內部還創建了一個 VTEP,其中一端接入到名為 Br0 的虛擬交換機當中,另一端接入主機網絡棧(VTEP)。
在主機網絡棧中的終端從主機所連接的基礎網絡中獲取到 IP 地址,并以 UDP Socket 的方式綁定到 4789 端口。不同主機上的兩個 VTEP 通過 VXLAN 隧道創建了一個覆蓋網絡,如下圖所示。
這是 VXLAN 上層網絡創建和使用所必需的。
接下來每個容器都會有自己的虛擬以太網(veth)適配器,并接入本地 Br0 虛擬交換機。目前拓撲結構如下圖所示,雖然是在主機所屬網絡互相獨立的情況下,但這樣能更容易看出兩個分別位于不同主機上的容器之間是如何通過 VXLAN 上層網絡進行通信的。
在本例中,將 node1 上的容器稱為 C1,node2 上的容器稱為 C2,如下圖所示。假設 C1 希望 ping 通 C2,類似前面章節中的示例。
C1 發起 ping 請求,目標 IP 為 C2 的地址 10.0.0.4。該請求的流量通過連接到 Br0 虛擬交換機 veth 接口發出。虛擬交換機并不知道將包發送到哪里,因為在虛擬交換機的 MAC 地址映射表(ARP 映射表)中并沒有與當前目的 IP 對應的 MAC 地址。
所以虛擬交換機會將該包發送到其上的全部端口。連接到 Br0 的 VTEP 接口知道如何轉發這個數據幀,所以會將自己的 MAC 地址返回。這就是一個代理 ARP 響應,并且虛擬交換機 Br0 根據返回結果學會了如何轉發該包。接下來虛擬交換機會更新自己的 ARP 映射表,將 10.0.0.4 映射到本地 VTEP 的 MAC 地址上。
現在 Br0 交換機已經學會如何轉發目標為 C2 的流量,接下來所有發送到 C2 的包都會被直接轉發到 VTEP 接口。VTEP 接口知道 C2,是因為所有新啟動的容器都會將自己的網絡詳情采用網絡內置 Gossip 協議發送給相同 Swarm 集群內的其他節點。
交換機會將包轉發到 VTEP 接口,VTEP 完成數據幀的封裝,這樣就能在底層網絡傳輸。具體來說,封裝操作就是把 VXLAN Header 信息添加以太幀當中。
VXLAN Header 信息包含了 VXLAN 網絡 ID(VNID),其作用是記錄 VLAN 到 VXLAN 的映射關系。每個 VLAN 都對應一個 VNID,以便包可以在解析后被轉發到正確的 VLAN。
封裝的時候會將數據幀放到 UDP 包中,并設置 UDP 的目的 IP 字段為 node2 節點的 VTEP 的 IP 地址,同時設置 UDP Socket 端口為 4789。這種封裝方式保證了底層網絡即使不知道任何關于 VXLAN 的信息,也可以完成數據傳輸。
當包到達 node2 之后,內核發現目的端口為 UDP 端口 4789,同時還知道存在 VTEP 接口綁定到該 Socket。所以內核將包發給 VTEP,由 VTEP 讀取 VNID,解壓包信息,并根據 VNID 發送到本地名為 Br0 的連接到 VLAN 的交換機。在該交換機上,包被發送給容器 C2。
以上大體介紹了 Docker 覆蓋網絡是如何利用 VXLAN 技術的。
最后一件需要注意的是,Docker 支持使用同樣的覆蓋網絡實現三層路由。例如,讀者可以創建包含兩個子網的覆蓋網絡,Docker 會負責子網間的路由。創建的命令如 docker network create --subnet=10.1.1.0/24 --subnet=11.1.1.0/24 -d overlay prod-net。該命令會在 Sandbox 中創建兩個虛擬交換機,默認支持路由。