1. Docker網絡模型
標准的Docker支持以下4類網絡模式。
- host模式:使用--net=host指定。
- container模式:使用--net=container:NAME_or_ID指定。
- none模式:使用--net=none指定。
- bridge模式:使用--net=bridge指定,為默認設置。
在Kubernetes管理模式下通常只會使用bridge模式,所以本節重點介紹在bridge模式下Docker是如何支持網絡的,其他3種模式則僅作簡單介紹。
1.1 host 模式
使用 host 驅動的時候,不會為容器創建網絡協議棧,即不會創建獨立的 network namespace。Docker 容器中的進程處於宿主機的網絡環境中,相當於容器和宿主機共用同一個 network namespace,容器共享使用宿主機的網卡、IP 和端口等資源。其網絡模型如下:
在host 模式下,容器內的服務可以直接使用宿主機的端口,也可以直接使用宿主機的 IP 進行通信,不存在虛擬化網絡帶來的開銷,性能上有了很大的提升。但是 host 驅動也降低了容器與容器之間、容器與宿主機之間網絡的隔離性,引起網絡資源的競爭和沖突。
1.2 none 模式
在這種模式下,容器有獨立的網絡棧,但不包含任何網絡配置,只具有lo這個loopback網卡用於進程通信。也就是說,該模式為容器做了最少的網絡設置。可通過第三方工具的方式,開發任意定制容器的網絡,提供了最高的靈活性。
1.3 container 模式
在這個模式下的容器,會使用其他容器的網絡命名空間,其網絡隔離性會處於bridge橋接模式與host模式之間。當容器共享其他容器的網絡命名空間,則在這兩個容器之間不存在網絡隔離,而它們又與宿主機以及除此之外其他的容器存在網絡隔離。其網絡模型可以參考下圖:
在一些特殊的場景中非常有用,例如,kubernetes的pod,kubernetes為pod創建一個基礎設施容器,同一pod下的其他容器都以container模式共享這個基礎設施容器的網絡命名空間,相互之間以localhost訪問,構成一個統一的整體。
下面重點介紹bridge模式。
2. Docker網絡實現之bridge模式
在Docker網絡基礎中,我們已經知道了容器虛擬化技術中與linux網絡相關的幾個技術點。這里再重復一遍,所謂的“網絡棧”,包括了網卡(network interface)、回環設備(loopback device)、路由表(routing table)和iptables規則等。對一個進程而言,這些要素其實就構成了它發起和響應網絡請求的基本環境。
我們可以把每一個容器看做一個獨立的網絡實體(或者說,就簡單的認為它是一台主機),它們都有一套獨立的“網絡棧”。在真實的物理機上,如果你想要實現兩台主機之間的通信,最直接的辦法,就是把它們用一根網線連接起來;而如果你想要實現多台主機之間的通信,那就需要用網線,把它們連接在一台交換機上。
而在 Linux 中,能夠起到虛擬交換機作用的網絡設備,是網橋(Bridge)。它是一個工作在數據鏈路層(Data Link)的設備,主要功能是根據 MAC 地址將數據包轉發到網橋的不同端口上。為了實現上述目的,Docker 項目會默認在宿主機上創建一個名叫 docker0 的網橋,凡是連接在 docker0 網橋上的容器,就可以通過它來進行通信。那么該如何把這些容器“連接”到 docker0 網橋上呢?——此時就需要使用一種名叫 Veth Pair 的虛擬設備了。可以把 Veth Pair 理解為連接不同network namespace 的“網線”。
(1) 同一宿主機上不同容器之間的通信
首先啟動了一個叫作 nginx-1 的容器:
$ docker run –d --name nginx-1 nginx
然后進入到這個容器中查看一下它的網絡設備:
# 在宿主機上
$ docker exec -it nginx-1 /bin/bash
# 在容器里
root@2b3c181aecf1:/# ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 172.17.0.2 netmask 255.255.0.0 broadcast 0.0.0.0
inet6 fe80::42:acff:fe11:2 prefixlen 64 scopeid 0x20<link>
ether 02:42:ac:11:00:02 txqueuelen 0 (Ethernet)
RX packets 364 bytes 8137175 (7.7 MiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 281 bytes 21161 (20.6 KiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536
inet 127.0.0.1 netmask 255.0.0.0
inet6 ::1 prefixlen 128 scopeid 0x10<host>
loop txqueuelen 1000 (Local Loopback)
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
root@2b3c181aecf1:/# 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 0.0.0.0 255.255.0.0 U 0 0 0 eth0
注:在nginx容器里並沒有ifconfig命令,可通過如下命令先安裝:
// nginx 內的ifconfig可以在容器內安裝。
root@2b3c181aecf1:/# apt-get update
root@2b3c181aecf1:/# apt install net-tools
可以看到,這個容器里有一張叫作 eth0 的網卡,它正是一個 Veth Pair 設備在容器里的這一端。通過 route 命令查看 nginx-1 容器的路由表,我們可以看到,這個 eth0 網卡是這個容器里的默認路由設備;所有對 172.17.0.0/16 網段的請求,也會被交給 eth0 來處理(第二條 172.17.0.0 路由規則)。
而這個 Veth Pair 設備的另一端,則在宿主機上。你可以通過查看宿主機的網絡設備看到它,如下所示:
# 在宿主機上
$ ifconfig
...
docker0 Link encap:Ethernet HWaddr 02:42:d8:e4:df:c1
inet addr:172.17.0.1 Bcast:0.0.0.0 Mask:255.255.0.0
inet6 addr: fe80::42:d8ff:fee4:dfc1/64 Scope:Link
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:309 errors:0 dropped:0 overruns:0 frame:0
TX packets:372 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:18944 (18.9 KB) TX bytes:8137789 (8.1 MB)
veth9c02e56 Link encap:Ethernet HWaddr 52:81:0b:24:3d:da
inet6 addr: fe80::5081:bff:fe24:3dda/64 Scope:Link
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:288 errors:0 dropped:0 overruns:0 frame:0
TX packets:371 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:21608 (21.6 KB) TX bytes:8137719 (8.1 MB)
$ brctl show
bridge name bridge id STP enabled interfaces
docker0 8000.0242d8e4dfc1 no veth9c02e56
通過 ifconfig 命令的輸出,你可以看到,nginx-1 容器對應的 Veth Pair 設備,在宿主機上是一張虛擬網卡。它的名字叫作 veth9c02e56。並且,通過 brctl show (該命令用來查詢網橋信息) 的輸出,你可以看到這張網卡被“插”在了 docker0 網橋上。
如果我們再在這台宿主機上啟動另一個 Docker 容器,比如 nginx-2:
$ docker run –d --name nginx-2 nginx
$ brctl show
bridge name bridge id STP enabled interfaces
docker0 8000.0242d8e4dfc1 no veth9c02e56 vethb4963f3
會發現一個新的名叫 vethb4963f3 的虛擬網卡,也被“插”在了 docker0 網橋上。這時候,如果你在 nginx-1 容器里 ping 一下 nginx-2 容器的 IP 地址,就會發現同一宿主機上的兩個容器默認就是相互連通的。
這其中的原理如下圖所示:
結合上圖,描述一遍nginx-1容器訪問nginx-2容器的數據流過程:
當你在 nginx-1 容器里訪問 nginx-2 容器的 IP 地址(比如 ping 172.17.0.3)的時候,這個目的 IP 地址會匹配到 nginx-1 容器里的第二條路由規則。即:
Destination Gateway Genmask Flags Metric Ref Use Iface
...
172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 eth0
可以看到,這條路由規則的網關(Gateway)是 0.0.0.0,這就意味着這是一條直連規則,即:凡是匹配到這條規則的 IP 包,應該經過本機的 eth0 網卡,通過二層網絡(數據鏈路層)直接發往目的主機。而要通過二層網絡到達 nginx-2 容器,就需要有 172.17.0.3 這個 IP 地址對應的 MAC 地址。所以 nginx-1 容器的網絡協議棧,就需要通過 eth0 網卡發送一個 ARP 廣播,來通過 IP 地址查找對應的 MAC 地址。
備注:ARP(Address Resolution Protocol),是通過網絡層的 IP 地址找到對應的二層 MAC 地址的協議。
我們前面提到過,這個 eth0 網卡,是一個 Veth Pair,它的一端在這個 nginx-1 容器的 Network Namespace 里,而另一端則位於宿主機上(Host Namespace),並且被“插”在了宿主機的 docker0 網橋上。
一旦一張虛擬網卡被“插”在網橋上,它就會變成該網橋的“從設備”。從設備會被“剝奪”調用網絡協議棧處理數據包的資格,從而“降級”成為網橋上的一個端口。而這個端口唯一的作用,就是接收流入的數據包,然后把這些數據包的“生殺大權”(比如轉發或者丟棄),全部交給對應的網橋。
所以,在收到這些 ARP 請求之后,docker0 網橋就會扮演二層交換機的角色,把 ARP 廣播轉發到其他被“插”在 docker0 上的虛擬網卡上。這樣,同樣連接在 docker0 上的 nginx-2 容器的網絡協議棧就會收到這個 ARP 請求,從而將 172.17.0.3 所對應的 MAC 地址回復給 nginx-1 容器。有了這個目的 MAC 地址,nginx-1 容器的 eth0 網卡就可以將數據包發出去。而根據 Veth Pair 設備的原理,這個數據包會立刻出現在宿主機上的 veth9c02e56 虛擬網卡上。不過,此時這個 veth9c02e56 網卡的網絡協議棧的資格已經被“剝奪”,所以這個數據包就直接流入到了 docker0 網橋里。
docker0 處理轉發的過程,則繼續扮演二層交換機的角色。此時,docker0 網橋根據數據包的目的 MAC 地址(也就是 nginx-2 容器的 MAC 地址),在它的 CAM 表(即交換機通過 MAC 地址學習維護的端口和 MAC 地址的對應表)里查到對應的端口(Port)為:vethb4963f3,然后把數據包發往這個端口。
而這個端口,正是 nginx-2 容器“插”在 docker0 網橋上的另一塊虛擬網卡,當然,它也是一個 Veth Pair 設備。這樣,數據包就進入到了 nginx-2 容器的 Network Namespace 里。所以,nginx-2 容器看到的情況是,它自己的 eth0 網卡上出現了流入的數據包。這樣,nginx-2 的網絡協議棧就會對請求進行處理,最后將響應返回到 nginx-1。
熟悉了 docker0 網橋的工作方式,你就可以理解,在默認情況下,被限制在 Network Namespace 里的容器進程,實際上是通過 Veth Pair 設備 + 宿主機網橋的方式,實現了跟同其他容器的數據交換。
(2) 宿主機與該宿主機上的容器之間的通信
與之類似地,當你在一台宿主機上,訪問該宿主機上的容器的 IP 地址時,這個請求的數據包,也是先根據路由規則到達 docker0 網橋,然后被轉發到對應的 Veth Pair 設備,最后出現在容器里。這個過程的示意圖,如下所示:
(3) 容器與另外一個宿主機通信
當一個容器試圖連接到另外一個宿主機時,比如:ping 10.168.0.3,它發出的請求數據包,首先經過 docker0 網橋出現在宿主機上。然后根據宿主機的路由表里的直連路由規則(10.168.0.0/24 via eth0)),對 10.168.0.3 的訪問請求就會交給宿主機的 eth0 處理。
接下來,這個數據包就會經宿主機的 eth0 網卡轉發到宿主機網絡上,最終到達 10.168.0.3 對應的宿主機上。當然,這個過程的實現要求這兩台宿主機本身是連通的。這個過程的示意圖,如下所示:
所以說,當遇到容器連不通“外網”的時候,你都應該先試試 docker0 網橋能不能 ping 通,然后查看一下跟 docker0 和 Veth Pair 設備相關的 iptables 規則是不是有異常,往往就能夠找到問題的答案了。
(4) 跨主機間的容器通信
如果在另外一台宿主機(比如:10.168.0.3)上,也有一個 Docker 容器。那么,我們的 nginx-1 容器又該如何訪問它呢?這個問題,其實就是容器的“跨主通信”問題。
在 Docker 的默認配置下,一台宿主機上的 docker0 網橋,和其他宿主機上的 docker0 網橋,沒有任何關聯,它們互相之間也沒辦法連通。所以,連接在這些網橋上的容器,自然也沒辦法進行通信了。不過,萬變不離其宗。如果我們通過軟件的方式,創建一個整個集群“公用”的網橋,然后把集群里的所有容器都連接到這個網橋上,不就可以相互通信了嗎?這樣一來,我們整個集群里的容器網絡就會類似於下圖所示的樣子:
可以看到,構建這種容器網絡的核心在於:我們需要在已有的宿主機網絡上,再通過軟件構建一個覆蓋在已有宿主機網絡之上的、可以把所有容器連通在一起的虛擬網絡。所以,這種技術就被稱為:Overlay Network(覆蓋網絡)。
而這個 Overlay Network 本身,可以由每台宿主機上的一個“特殊網橋”共同組成。比如,當 Node 1 上的 Container 1 要訪問 Node 2 上的 Container 3 的時候,Node 1 上的“特殊網橋”在收到數據包之后,能夠通過某種方式,把數據包發送到正確的宿主機,比如 Node 2 上。而 Node 2 上的“特殊網橋”在收到數據包后,也能夠通過某種方式,把數據包轉發給正確的容器,比如 Container 3。甚至,每台宿主機上,都不需要有一個這種特殊的網橋,而僅僅通過某種方式配置宿主機的路由表,就能夠把數據包轉發到正確的宿主機上。這也就是“容器跨主機網絡”的內容,單獨再詳細分析。
補充:
怎么找 docker 和 宿主機上 veth 設備的關系?學完后我也有這個疑問,查了一下,結論是沒有命令可以直接查到。但是可以查看 container 里的 eth0 網卡的 iflink 找到對應關系。
// 宿主機上
$ ip link
......
9: veth0e9cd8d@if8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP mode DEFAULT group default
link/ether 6a:fb:59:e5:7e:da brd ff:ff:ff:ff:ff:ff link-netnsid 1
// 容器內
$ sudo docker exec -it e151 bash
root@e1517e9d9e1a:/# cat /sys/class/net/eth0/iflink
9
這樣就可以確定 container e1517e9d9e1a 在物理機上對應的 veth pair 是 veth0e9cd8d 了。
這種方式需要登錄到 docker 里執行命令,不是所有的容器都能這么做,不過 github 上有人專門做了個腳本來用實現這個功能,可以參考一下:
https://github.com/micahculpepper/dockerveth
(全文完)
參考: