深入理解 Kubernetes 容器網絡

Kubernetes 中要保證容器之間網絡互通,網絡至關重要。而 Kubernetes 本身並沒有自己實現容器網絡,而是通過插件化的方式自由接入進來。在容器網絡接入進來需要滿足如下基本原則:

容器網絡基礎

一個 Linux 容器的網絡棧是被隔離在它自己的 Network Namespace 中,Network Namespace 包括了:網卡(Network Interface),迴環設備(Lookback Device),路由表(Routing Table)和 iptables 規則,對於服務進程來講這些就構建了它發起請求和相應的基本環境。而要實現一個容器網絡,離不開以下 Linux 網絡功能:

基於以上的基礎,同宿主機的容器時間如何通信呢?

我們可以簡單把他們理解成兩臺主機,主機之間通過網線連接起來,如果要多臺主機通信,我們通過交換機就可以實現彼此互通,在 Linux 中,我們可以通過網橋來轉發數據。

在容器中,以上的實現是通過 docker0 網橋,凡是連接到 docker0 的容器,就可以通過它來進行通信。要想容器能夠連接到 docker0 網橋,我們也需要類似網線的虛擬設備 Veth Pair 來把容器連接到網橋上。

我們啓動一個容器:

docker run -d --name c1 hub.pri.ibanyu.com/devops/alpine:v3.8 /bin/sh

然後查看網卡設備:

docker exec -it c1  /bin/sh
/ # 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:14 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:1172 (1.1 KiB)  TX bytes:0 (0.0 B)

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)# route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         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

可以看到其中有一張 eth0 的網卡,它就是 veth peer 其中的一端的虛擬網卡。然後通過 route -n 查看容器中的路由表,eth0 也正是默認路由出口。所有對 172.17.0.0/16 網段的請求都會從 eth0 出去。

我們再來看 Veth peer 的另一端,我們查看宿主機的網絡設備:

ifconfig
docker0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 172.17.0.1  netmask 255.255.0.0  broadcast 172.17.255.255
        inet6 fe80::42:6aff:fe46:93d2  prefixlen 64  scopeid 0x20<link>
        ether 02:42:6a:46:93:d2  txqueuelen 0  (Ethernet)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 8  bytes 656 (656.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 10.100.0.2  netmask 255.255.255.0  broadcast 10.100.0.255
        inet6 fe80::5400:2ff:fea3:4b44  prefixlen 64  scopeid 0x20<link>
        ether 56:00:02:a3:4b:44  txqueuelen 1000  (Ethernet)
        RX packets 7788093  bytes 9899954680 (9.2 GiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 5512037  bytes 9512685850 (8.8 GiB)
        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 32  bytes 2592 (2.5 KiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 32  bytes 2592 (2.5 KiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

veth20b3dac: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet6 fe80::30e2:9cff:fe45:329  prefixlen 64  scopeid 0x20<link>
        ether 32:e2:9c:45:03:29  txqueuelen 0  (Ethernet)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 8  bytes 656 (656.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

我們可以看到,容器對應的 Veth peer 另一端是宿主機上的一塊虛擬網卡叫 veth20b3dac,並且可以通過 brctl 查看網橋信息看到這張網卡是在 docker0 上。

# brctl show
docker0  8000.02426a4693d2 no  veth20b3dac

然後我們再啓動一個容器,從第一個容器是否能 ping 通第二個容器。

docker run -d --name c2 -it hub.pri.ibanyu.com/devops/alpine:v3.8 /bin/sh
 docker exec -it c1 /bin/sh
/ # 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=ttl=64 time=0.291 ms
64 bytes from 172.17.0.3: seq=ttl=64 time=0.129 ms
64 bytes from 172.17.0.3: seq=ttl=64 time=0.142 ms
64 bytes from 172.17.0.3: seq=ttl=64 time=0.169 ms
64 bytes from 172.17.0.3: seq=ttl=64 time=0.194 ms
^C
--- 172.17.0.3 ping statistics ---
5 packets transmitted, 5 packets received, 0% packet loss
round-trip min/avg/max = 0.129/0.185/0.291 ms

可以看到,能夠 ping 通,其原理就是我們 ping 目標 IP172.17.0.3 時,會匹配到我們的路由表第二條規則,網關爲 0.0.0.0,這就意味着是一條直連路由,通過二層轉發到目的地。要通過二層網絡到達 172.17.0.3,我們需要知道它的 Mac 地址,此時就需要第一個容器發送一個 ARP 廣播,來通過 IP 地址查找 Mac。此時 Veth peer 另外一段是 docker0 網橋,它會廣播到所有連接它的 veth peer 虛擬網卡去,然後正確的虛擬網卡收到後會響應這個 ARP 報文,然後網橋再回給第一個容器。

以上就是同宿主機不同容器通過 docker0 通信,如下圖所示:

默認情況下,通過 network namespace 限制的容器進程,本質上是通過 Veth peer 設備和宿主機網橋的方式,實現了不同 network namespace 的數據交換。

與之類似,當你在一臺宿主機上,訪問該宿主機上的容器的 IP 地址時,這個請求的數據包,也是先根據路由規則到達 docker0 網橋,然後被轉發到對應的 Veth Pair 設備,最後出現在容器裏。

跨主機網絡通信

在 Docker 的默認配置下,不同宿主機上的容器通過 IP 地址進行互相訪問是根本做不到的。爲了解決這個問題,社區中出現了很多網絡方案。同時 Kubernetes 爲了更好的控制網絡的接入,推出了 CNI 即容器網絡的 API 接口。它是 Kubernetes 中標準的一個調用網絡實現的接口,kubelet 通過這個 API 來調用不同的網絡插件以實現不同的網絡配置,實現了這個接口的就是 CNI 插件,它實現了一系列的 CNI API 接口。

目前已經有的包括 FlannelCalico、**Kube-OVN、**Weave、**Contiv **等等。

實際上 CNI 的容器網絡通信流程跟前面的基礎網絡一樣,只是 CNI 維護了一個單獨的網橋來代替 docker0。這個網橋的名字就叫作:CNI 網橋,它在宿主機上的設備名稱默認是:cni0。cni 的設計思想,就是:Kubernetes 在啓動 Infra 容器之後,就可以直接調用 CNI 網絡插件,爲這個 Infra 容器的 Network Namespace,配置符合預期的網絡棧。

CNI 插件三種網絡實現模式

我們看下路由模式的一種實現 flannel Host-gw

如圖可以看到當 node1 上 container-1 要發數據給 node2 上的 container2 時, 會匹配到如下的路由表規則:

10.244.1.0/24 via 10.168.0.3 dev eth0

表示前往目標網段 10.244.1.0/24 的 IP 包,需要經過本機 eth0 出去發往的下一跳 IP 地址爲 10.168.0.3(node2),然後到達 10.168.0.3 以後再通過路由錶轉發 CNI 網橋,進而進入到 container2。

以上可以看到 host-gw 工作原理,其實就是在每個 Node 節點配置到每個 Pod 網段的下一跳爲 Pod 網段所在的 Node 節點 IP,Pod 網段和 Node 節點 IP 的映射關係,Flannel 保存在 etcd 或者 Kubernetes 中。Flannel 只需要 watch 這些數據的變化來動態更新路由表即可。

這種網絡模式最大的好處就是避免了額外的封包和解包帶來的網絡性能損耗。缺點我們也能看見主要就是容器 IP 包通過下一跳出去時,必須要二層通信封裝成數據幀發送到下一跳。如果不在同個二層局域網,那麼就要交給三層網關,而此時網關是不知道目標容器網絡的(也可以靜態在每個網關配置 Pod 網段路由)。所以 flannel host-gw 必須要求集羣宿主機是二層互通的。

而爲了解決二層互通的限制性,Calico 提供的網絡方案就可以更好的實現,Calico 大三層網絡模式與 Flannel 提供的類似,也會在每臺宿主機添加如下格式的路由規則:

<目標容器IP網段> via <網關的IP地址> dev eth0

其中網關的 IP 地址不通場景有不同的意思,如果宿主機是二層可達那麼就是目的容器所在的宿主機的 IP 地址,如果是三層不同局域網那麼就是本機宿主機的網關 IP(交換機或者路由器地址)。

不同於 Flannel 通過 Kubernetes 或者 etcd 存儲的數據來維護本機路由信息的做法,Calico 是通過 BGP 動態路由協議來分發整個集羣路由信息。

BGP 全稱是 Border Gateway Protocol 邊界網關協議,Linxu 原生支持的、專門用於在大規模數據中心爲不同的自治系統之間傳遞路由信息。只要記住 BGP 簡單理解其實就是實現大規模網絡中節點路由信息同步共享的一種協議。而 BGP 這種協議就能代替 Flannel 維護主機路由表功能。

Calico 主要由三個部分組成:

除此之外,Calico 還和 flannel host-gw 不同之處在於,它不會創建網橋設備,而是通過路由表來維護每個 Pod 的通信,如下圖所示:

可以看到 Calico 的 CNI 插件會爲每個容器設置一個 veth pair 設備,然後把另一端接入到宿主機網絡空間,由於沒有網橋,CNI 插件還需要在宿主機上爲每個容器的 veth pair 設備配置一條路由規則,用於接收傳入的 IP 包,路由規則如下:

10.92.77.163 dev cali93a8a799fe1 scope link

以上表示發送 10.92.77.163 的 IP 包應該發給 cali93a8a799fe1 設備,然後到達另外一段容器中。

有了這樣的 veth pair 設備以後,容器發出的 IP 包就會通過 veth pair 設備到達宿主機,然後宿主機根據路有規則的下一條地址,發送給正確的網關(10.100.1.3),然後到達目標宿主機,在到達目標容器。

10.92.160.0/23 via 10.106.65.2 dev bond0 proto bird

這些路由規則都是 Felix 維護配置的,而路由信息則是 calico bird 組件基於 BGP 分發而來。Calico 實際上是將集羣裏所有的節點都當做邊界路由器來處理,他們一起組成了一個全互聯的網絡,彼此之間通過 BGP 交換路由,這些節點我們叫做 BGP Peer。

需要注意的是 Calico 維護網絡的默認模式是 node-to-node mesh,這種模式下,每臺宿主機的 BGP client 都會跟集羣所有的節點 BGP client 進行通信交換路由。這樣一來,隨着節點規模數量 N 的增加,連接會以 N 的 2 次方增長,會集羣網絡本身帶來巨大壓力。

所以一般這種模式推薦的集羣規模在 50 節點左右,超過 50 節點推薦使用另外一種 **RR(Router Reflector)**模式,這種模式下,Calico 可以指定幾個節點作爲 RR,他們負責跟所有節點 BGP client 建立通信來學習集羣所有的路由,其他節點只需要跟 RR 節點交換路由即可。這樣大大降低了連接數量,同時爲了集羣網絡穩定性,建議 RR>=2。

以上的工作原理依然是在二層通信,當我們有兩臺宿主機,一臺是 10.100.0.2/24,節點上容器網絡是 10.92.204.0/24;另外一臺是 10.100.1.2/24,節點上容器網絡是 10.92.203.0/24,此時兩臺機器因爲不在同個二層所以需要三層路由通信,這時 Calico 就會在節點上生成如下路由表:

10.92.203.0/23 via 10.100.1.2 dev eth0 proto bird

這時候問題就來了,因爲 10.100.1.2 跟我們 10.100.0.2 不在同個子網,是不能二層通信的。這之後就需要使用 Calico IPIP 模式,當宿主機不在同個二層網絡時就是用 Overlay 網絡封裝以後再發出去。如下圖所示:

IPIP 模式下在非二層通信時,Calico 會在 Node 節點添加如下路由規則:

10.92.203.0/24 via 10.100.1.2 dev tunnel0

可以看到儘管下一條任然是 Node 的 IP 地址,但是出口設備卻是 tunnel0,其是一個 IP 隧道設備,主要有 Linux 內核的 IPIP 驅動實現。會將容器的 IP 包直接封裝宿主機網絡的 IP 包中,這樣到達 node2 以後再經過 IPIP 驅動拆包拿到原始容器 IP 包,然後通過路由規則發送給 veth pair 設備到達目標容器。

以上儘管可以解決非二層網絡通信,但是仍然會因爲封包和解包導致性能下降。如果 Calico 能夠讓宿主機之間的 router 設備也學習到容器路由規則,這樣就可以直接三層通信了。比如在路由器添加如下的路由表:

10.92.203.0/24 via 10.100.1.2 dev interface1

而 node1 添加如下的路由表:

10.92.203.0/24 via 10.100.1.1 dev tunnel0

那麼 node1 上的容器發出的 IP 包,基於本地路由表發送給 10.100.1.1 網關路由器,然後路由器收到 IP 包查看目的 IP,通過本地路由表找到下一跳地址發送到 node2,最終到達目的容器。這種方案,我們是可以基於 underlay 網絡來實現,只要底層支持 BGP 網絡,可以和我們 RR 節點建立 EBGP 關係來交換集羣內的路由信息。

Kube-OVN 實現的網絡模型

Flannel 和很多網絡的實現,都是一個 Node 一個子網,這種子網模型很不靈活,而且也很難擴展。Kube-OVN 裏採用了一個 Namespace 一個子網的模型,子網是可以跨節點的這樣比較符合用戶的預期和管理。每個子網對應着 OVN 裏的一個虛擬交換機,LB、DNS 和 ACL 等流量規則現在也是應用在虛擬交換機上,這樣方便我們之後做更細粒度的權限控制,例如實現 VPC,多租戶這樣的功能

所有的虛擬交換機目前會接在一個全局的虛擬路由器上,這樣可以保證默認的容器網絡互通,未來要做隔離也可以很方便的在路由層面進行控制。此外還有一個特殊的 Node 子網,會在每個宿主機上添加一塊 OVS 的網卡,這個網絡主要是把 Node 接入容器網絡,使得主機和容器之間網絡可以互通。需要注意的是這裏的虛擬交換機和虛擬路由器都是邏輯上的,實現中是通過流表分佈在所有的節點上的,因此並不存在單點的問題。

網關負責訪問集羣外部的網絡,目前有兩種實現,一種是分佈式的,每臺主機都可以作爲運行在自己上面的 Ood 的出網節點。另一種是集中式的,可以一個 Namespace 配置一個網關節點,作爲當前 Namespace 裏的 Pod 出網所使用的網關,這種方式所有出網流量用的都是特定的 IP nat 出去的,方便外部的審計和防火牆控制。當然網關節點也可以不做 nat 這樣就可以把容器 IP 直接暴露給外網,達到內外網絡的直連。

以上就是 Kubernetes 常用的幾種網絡方案了,在公有云場景下一般用雲廠商提供的或者使用 flannel host-gw 這種更簡單,而私有物理機房環境中,Kube-OVN、Calico 項目更加適合。根據自己的實際場景,再選擇合適的網絡方案。

原文鏈接: https://tech.ipalfish.com/blog/2020/03/06/kubernetes_container_network/

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/E147nwJTY0vmL6lM_V3WNg