1. 程式人生 > 實用技巧 >Docker容器網路-實現篇

Docker容器網路-實現篇

通常,Linux容器的網路是被隔離在它自己的Network Namespace中,其中就包括:網絡卡(Network Interface)、迴環裝置(Loopback Device)、路由表(Routing Table)和iptables規則。對於一個程序來說,這些要素,就構成了它發起和響應網路請求的基本環境。

前文說到容器網路對Linux虛擬化技術的依賴,這一篇章我們將一探究竟,看看Docker究竟是怎麼做的。

管中窺豹

我們在執行 docker run -d --name xxx   之後,進入容器內部:

## docker ps 可檢視所有docker
## 進入容器
docker exec -it 228ae947b20e /bin/bash

並執行 ifconfig :

$ ifconfig
eth0      Link encap:Ethernet  HWaddr 22:A4:C8:79:DD:1A
          inet addr:192.168.65.28  Bcast:0.0.0.0  Mask:255.255.255.255
          UP BROADCAST RUNNING MULTICAST  MTU:1440  Metric:1
          RX packets:2231528 errors:0 dropped:0 overruns:0 frame:0
          TX packets:3340914 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:249385222 (237.8 MiB)  TX bytes:590701793 (563.3 MiB)

lo        Link encap:Local Loopback
          inet addr:127.0.0.1  Mask:255.0.0.0
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

我們看到一張叫eth0的網絡卡,它正是一個Veth Pair裝置在容器的這一端。
我們再通過 route 檢視該容器的路由表:

$ route
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
default         169.254.1.1     0.0.0.0         UG    0      0        0 eth0
169.254.1.1     *               255.255.255.255 UH    0      0        0 eth0

我們可以看到這個eth0是這個容器的預設路由裝置。我們也可以通過第二條路由規則,看到所有對 169.254.1.1/16 網段的請求都會交由eth0來處理。
而Veth Pair 裝置的另一端,則在宿主機上,我們同樣也可以通過檢視宿主機的網路裝置來檢視它:

$ ifconfig
......
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 172.16.241.192  netmask 255.255.240.0  broadcast 172.16.255.255
        ether 00:16:3e:0a:f3:75  txqueuelen 1000  (Ethernet)
        RX packets 3168620550  bytes 727592674740 (677.6 GiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 2937180637  bytes 8661914052727 (7.8 TiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
......
docker0: flags=4099<UP,BROADCAST,MULTICAST>  mtu 1500
        inet 172.17.0.1  netmask 255.255.0.0  broadcast 172.17.255.255
        ether 02:42:16:58:92:43  txqueuelen 0  (Ethernet)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
......
vethd08be47: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        ether 16:37:8d:fe:36:eb  txqueuelen 0  (Ethernet)
        RX packets 193  bytes 22658 (22.1 KiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 134  bytes 23655 (23.1 KiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
......

在宿主機上,容器對應的Veth Pair裝置是一張虛擬網絡卡,我們再用 brctl show 命令檢視網橋:

$ brctl show
bridge name	bridge id		STP enabled	interfaces
docker0		8000.0242afb1a841	no		vethd08be47

可以清楚的看到Veth Pair的一端 vethd08be47 就插在 docker0 上。

我現在執行docker run 啟動兩個容器,就會發現docker0上插入兩個容器的 Veth Pair的一端。如果我們在一個容器內部互相ping另外一個容器的IP地址,是不是也能ping通?

$ brctl show
bridge name	bridge id		STP enabled	interfaces
docker0		8000.0242afb1a841	no		veth26cf2cc
																	veth8762ad2

容器1:

$ docker exec -it f8014a4d34d0 /bin/bash
$ ifconfig
eth0      Link encap:Ethernet  HWaddr 02:42:AC:11:00:03
          inet addr:172.17.0.3  Bcast:172.17.255.255  Mask:255.255.0.0
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:76 errors:0 dropped:0 overruns:0 frame:0
          TX packets:106 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:16481 (16.0 KiB)  TX bytes:14711 (14.3 KiB)

lo        Link encap:Local Loopback
          inet addr:127.0.0.1  Mask:255.0.0.0
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:48 errors:0 dropped:0 overruns:0 frame:0
          TX packets:48 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:2400 (2.3 KiB)  TX bytes:2400 (2.3 KiB)
$ route
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
default         172.17.0.1      0.0.0.0         UG    0      0        0 eth0
172.17.0.0      *               255.255.0.0     U     0      0        0 eth0

容器2:

$ docker exec -it 9a6f38076c04 /bin/bash
$ ifconfig
eth0      Link encap:Ethernet  HWaddr 02:42:AC:11:00:02
          inet addr:172.17.0.2  Bcast:172.17.255.255  Mask:255.255.0.0
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:133 errors:0 dropped:0 overruns:0 frame:0
          TX packets:193 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:23423 (22.8 KiB)  TX bytes:22624 (22.0 KiB)

lo        Link encap:Local Loopback
          inet addr:127.0.0.1  Mask:255.0.0.0
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:198 errors:0 dropped:0 overruns:0 frame:0
          TX packets:198 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:9900 (9.6 KiB)  TX bytes:9900 (9.6 KiB)
$ route
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
default         172.17.0.1      0.0.0.0         UG    0      0        0 eth0
172.17.0.0      *               255.255.0.0     U     0      0        0 eth0

從一個容器ping另外一個容器:

# -> 容器1內部 ping 容器2
$ ping 172.17.0.3
PING 172.17.0.3 (172.17.0.3): 56 data bytes
64 bytes from 172.17.0.3: seq=0 ttl=64 time=0.142 ms
64 bytes from 172.17.0.3: seq=1 ttl=64 time=0.096 ms
64 bytes from 172.17.0.3: seq=2 ttl=64 time=0.089 ms

我們看到,在一個容器內部ping另外一個容器的ip,是可以ping通的。也就意味著,這兩個容器是可以互相通訊的。

容器通訊

我們不妨結合前文時所說的,理解下為什麼一個容器能訪問另一個容器?先簡單看如一幅圖:

當在容器1裡訪問容器2的地址,這個時候目的IP地址會匹配到容器1的第二條路由規則,這條路由規則的Gateway是0.0.0.0,意味著這是一條直連規則,也就是說凡是匹配到這個路由規則的請求,會直接通過eth0網絡卡,通過二層網路發往目的主機。而要通過二層網路到達容器2,就需要127.17.0.3對應的MAC地址。所以,容器1的網路協議棧就需要通過eth0網絡卡來發送一個ARP廣播,通過IP找到MAC地址。所謂ARP(Address Resolution Protocol),就是通過三層IP地址找到二層的MAC地址的協議。這裡說到的eth0,就是Veth Pair的一端,另一端則插在了宿主機的docker0網橋上。eth0這樣的虛擬網絡卡插在docker0上,也就意味著eth0變成docker0網橋的“從裝置”。從裝置會降級成docker0裝置的埠,而呼叫網路協議棧處理資料包的資格全部交給docker0網橋。
所以,在收到ARP請求之後,docker0就會扮演二層交換機的角色,把ARP廣播發給其它插在docker0網橋的虛擬網絡卡上,這樣,127.17.0.3就會收到這個廣播,並把其MAC地址返回給容器1。有了這個MAC地址,容器1的eth0的網絡卡就可以把資料包傳送出去。
這個資料包會經過Veth Pair在宿主機的另一端veth26cf2cc,直接交給docker0。docker0轉發的過程,就是繼續扮演二層交換機,docker0根據資料包的目標MAC地址,在CAM表查到對應的埠為veth8762ad2,然後把資料包發往這個埠。而這個埠,就是容器2的Veth Pair在宿主機的另一端,這樣,資料包就進入了容器2的Network Namespace,最終容器2將響應(Pong)返回給容器1。在真實的資料傳遞中,Linux核心Netfilter/Iptables也會參與其中,這裡不再贅述。

CAM就是交換機通過MAC地址學習維護埠和MAC地址的對應表

這裡介紹的容器間的通訊方式就是docker中最常見的bridge模式,當然此外還有host模式、container模式、none模式等,對其它模式有興趣的可以去閱讀相關資料。

跨主通訊

好了,這裡不禁問個問題,到目前為止只是單主機內部的容器間通訊,那跨主機網路呢?
在Docker預設配置下,一臺宿主機的docker0網橋是無法和其它宿主機連通的,它們之間沒有任何關聯,所以這些網橋上的容器,自然就沒辦法多主機之間互相通訊。但是無論怎麼變化,道理都是一樣的,如果我們建立一個公共的網橋,是不是叢集中所有容器都可以通過這個公共網橋去連線?
當然在正常的情況下,節點與節點的通訊往往可以通過NAT的方式,但是,這個在網際網路發展的今天,在容器化環境下未必適用。例如在向註冊中心註冊例項的時候,肯定會攜帶IP,在正常物理機內的應用當然沒有問題,但是容器化環境卻未必,容器內的IP很可能就是上文所說的172.17.0.2,多個節點都會存在這個IP,大概率這個IP是衝突的。如果我們想避免這個問題,就會攜帶宿主機的IP和對映的埠去註冊。但是這又帶來一個問題,即容器內的應用去意識到這是一個容器,而非物理機,當在容器內,應用需要去拿容器所在的物理機的IP,當在容器外,應用需要去拿當前物理機的IP。顯然,這並不是一個很好的設計,這需要應用去配合配置。所以,基於此,我們肯定要尋找其他的容器網路解決方案。

在上圖這種容器網路中,我們需要在我們已有的主機網路上,通過軟體構建一個覆蓋在多個主機之上,且能把所有容器連通的虛擬網路。這種就是Overlay Network(覆蓋網路)。
關於這些具體的網路解決方案,例如Flannel、Calico等,我會在後續篇幅繼續陳述。