前言
在介紹k8s的網絡通信機制前,先介紹一下docker的網絡通信機制,不同節點pod間網絡通信,在docker里是這樣訪問的
即: Container1 snat-> 節點A (docker0網橋) -> 節點B(docker0網橋) ->dnat -> pod2
這樣的訪問方式會產生很大的系統開銷,因為需要產生兩級NAT轉換,container1訪問出去時需要經過eth0的SNAT,報文達到對端節點的eth0后,做一次DNAT,當Container2發送響應報文時,又需要本節點物理網卡做一次SNAT,對端節點物理網卡做一次DNAT。
同樣還會產生一個問題,Container1始終不知道它真正和誰在通信,它明明是訪問Container2的地址,當目標地址指向的確是對端節點的物理IP。
k8s網絡通信
下面再看看k8s是怎么解決網絡通信的。
k8s要解決的網絡通信模型有以下幾種場景:
- 容器間通信,同一個pod內多個容器的通信
- pod之間通信,又分為同節點pod之間的通信,和不同節點pod之間的通信
- pod與Service通信
- service與集群外部進行通信
(2)pod之間通信,k8s要求pod和pod通信時,從一個pod IP到另一個pod IP不需要經過任何的NAT轉換,要能夠直達 ,雙方所見的地址就是通信時的地址。
這里又分為同節點pod之間的通信,和不同節點pod之間的通信。
(3)pod與Service通信 podIP <--> clusterIP
(4)service與集群外部進行通信
Pod內部通信
pod內部給容器之間共享網絡命名空間,就好比一台機器上有多個進程,他們之間通信可以通過lo口來實現。
同節點Pod之間通信
同一個節點內Pod之間是通過cni0網橋直接進行通信,和docker的通信方式一樣。
不同節點Pod之間通信
Flannel網絡插件
這里先要介紹下CNI,k8s自身沒有提供網絡解決方案,但它定義了一種容器網絡接口,任何第三方的程序只要能解決上面這4種通信模型,那它就可以拿來做k8s的網絡接口。有點類似golang里接口的概念。
目前比較常用的網絡插件有flannel和calico,flannel的功能比較簡單,不具備復雜網絡的配置能力,calico是比較出色的網絡管理插件,單具備復雜網絡配置能力的同時,往往意味着本身的配置比較復雜,所以相對而言,比較小而簡單的集群使用flannel,考慮到日后擴容,未來網絡可能需要加入更多設備,配置更多策略,則使用calico更好。
kubelet會調用/etc/cni/net.d目錄下的網絡插件配置,由網絡插件實現地址分配、接口創建、網絡創建等功能 。
flannel有三種模式:
-
host gw
把節點作為網關,那每個節點的pod網絡就應該都不一樣,A節點的pod要訪問B節點的pod時,先會發到本地網關,本地網關存有所有pod網絡的路由信息,會匹配到B節點的pod網絡並把數據發給B節點的網關,B節點網關也會檢查本地路由信息,發現是發給本地pod的請求就會把請求發給本地pod。
此種方式需要各節點在同一個網絡中,數據包直接以路由形式發給對方而不要像vxlan一樣進行數據包的封裝,這種方式有缺點,節點數量很多時,每個節點維護的路由信息會很大,而且由於節點在同一個網絡,容易受到廣播包的影響。 -
vxlan
當節點在同一個網絡中時,會自動降級為host gw模式通信,其中vxlan又分為兩種模式:
1、vxlan(默認)
2、directrouting,適用於宿主機同網段,不同網段時自動降級為vxlan
參見kube-flannel的配置:
kubectl edit configmap kube-flannel-cfg -n kube-system
- UDP: 這種方式性能最差的方式,這源於早期flannel剛出現時,Linux內核還不支持VxLAN,即沒有VxLAN核心模塊,因此flannel采用了這種方式,來實現隧道封裝。
這里只介紹Vxlan的實現方式
在k8s中,網橋變為cni0,Pod的數據包出去時,先經過cni0,再到flannel.1接口時會將數據包封裝成vxlan協議的數據包發給對端,對端flannel.1解包后發給本地Pod,最終的效果就是pod間通信就像在二層網絡一樣,容器直接可以直接使用pod ip進行通訊。
在跨節點的pod通信時,在物理網卡上抓包時可以看到OTV類型的包的,表示的是overlay數據包。
在同節點的pod通信時,不會產生overlay數據包。
flannel.1網卡:
pod跨節點通信時,物理網卡要做成疊加網絡的形式時,兩個物理網卡之間應各自有個疊加報文封裝的隧道,隧道的兩端通常稱為flannel.0/1,其地址很獨特,ip地址為10.244.0.0(舉例),掩碼為255.255.255.255, 而且它的mtu是1450,不像物理網卡一樣是1500,因為隧道要做疊加封裝,要有額外的開銷,所以要留出50給這個開銷。如下圖所示:
Pod網卡:
生成Pod時,會像docker一樣創建一對虛擬接口/網卡,也就是veth pair,一端連接到容器中,另一端橋接到vethxxxx網卡並橋接到cni0網橋,如下圖所示:
可以看到宿主機上的vethxxxx網卡都是橋接在cni0網卡上的。
通過ethtool -S (網卡名),可以看到peer的index,一對peer的index是相連的
測試
先在兩個node節點上分別創建一個PodA。PodB,模擬不同不同節點不同網絡的測試環境,然后在PodA上一直ping PodB的地址,然后在PodA所在的節點上抓包分析。
先抓取flannel.1網卡上的包:
可以看到兩個Pod是直接通過podIP訪問的。
再抓取物理網卡上的包:
tcpdump -i ens32 -nn host (PodB所在節點物理網卡IP)
可以看到兩個物理網卡之間通信有vxlan的包,並且包里可以看到是兩個pod之間的icmp包
再查看主機路由:
可以看到Pod跨節點訪問時的默認網關是fannel.1
如果改成directrouting模式,再次從物理網卡上抓包,可以看到pod之間不會走vxlan進行包封裝了,直接通過物理網卡通信
查看宿主機路由
Pod與Service通信
Pod與Service通信是通過Keube-Proxy模塊來實現的,它會監聽service的變化並管理sevice的Endpoints,該service對外暴露一個Virtual IP,也成為Cluster IP, 集群內通過訪問這個Cluster IP:Port就能訪問到集群內對應的serivce下的Pod。
Pod和Service是兩種不同的網絡,那么怎么實現Service和Pod的互通呢?kube-proxy提供了三種模式,iptables、ipvs、userspace,這里我們只介紹iptable的實現。
因為service地址就是iptables里的一個虛擬IP,而iptables在每個節點上都會存在,所以外部請求轉發到任意一個物理節點,都能通過iptables規則把請求轉發到后端Endpoints。
Service與集群外部進行通信
可以通過nodePort獲取ingress將服務暴露出去。