通常,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等,我會在后續篇幅繼續陳述。