k8s之flannel詳解

k8s之flannel詳解

目前k8s主流的CNI 網絡插件有很多,這裡我們主要介紹一下flannel這個用的還是比較多的

網絡原理

我們這裡假設flannel使用VXLAN協議。每臺主機都安裝有flannel。k8s定義的flannel網絡為10.0.0.0/16,各主機的flannel從這個網絡申請一個子網。pod1所在的主機的flannel子網為10.0.14.1/24,pod2所在主機的flannel子網為10.0.5.1/24。每臺主機有cni0和flannel.1虛擬網卡。cni0為在同一主機pod共用的網橋,當kubelet創建容器時,將為此容器創建虛擬網卡vethxxx,並橋接到cni0網橋。flannel.1是一個tun虛擬網卡,接收不在同一主機的POD的數據,然後將收到的數據轉發給flanneld進程。原理圖:

k8s之flannel詳解

pod1到pod2的網絡

pod1路由表:

default via 10.0.14.1 dev eth0 
10.0.0.0/16 via 10.0.14.1 dev eth0
10.0.14.0/24 dev eth0 proto kernel scope link src 10.0.14.15

host1路由表:

default via 192.168.93.254 dev eno16777984 proto static metric 100 
10.0.0.0/16 dev flannel.1
10.0.14.0/24 dev cni0 proto kernel scope link src 10.0.14.1
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1
192.168.93.0/24 dev eno16777984 proto kernel scope link src 192.168.93.212 metric 100

pod1 IP地址:10.0.14.15

pod2 IP地址:10.0.5.150

pod1與pod2不在同一臺主機

下面是從pod1 ping pod2的數據包流向

1. pod1(10.0.14.15)向pod2(10.0.5.150)發送ping,查找pod1路由表,把數據包發送到cni0(10.0.14.1)

2. cni0查找host1路由,把數據包轉發到flannel.1

3. flannel.1虛擬網卡再把數據包轉發到它的驅動程序flannel

4. flannel程序使用VXLAN協議封裝這個數據包,向api-server查詢目的IP所在的主機IP,稱為host2(不清楚什麼時候查詢)

5. flannel向查找到的host2 IP的UDP端口8472傳輸數據包

6. host2的flannel收到數據包後,解包,然後轉發給flannel.1虛擬網卡

7. flannel.1虛擬網卡查找host2路由表,把數據包轉發給cni0網橋,cni0網橋再把數據包轉發給pod2

8. pod2響應給pod1的數據包與1-7步類似

下面是這次ping數據包的wireshark解析出的協議數據:

pod1 ping請求:

k8s之flannel詳解

pod2響應:

k8s之flannel詳解

pod1與pod2在同一臺主機

pod1和pod2在同一臺主機的話,由cni0網橋直接轉發請求到pod2,不需要經過flannel。

pod到service的網絡

創建一個service時,相應會創建一個指向這個service的域名,域名規則為{服務名}.{namespace}.svc.{集群名稱}。之前service ip的轉發由iptables和kube-proxy負責,目前基於性能考慮,全部為iptables維護和轉發。iptables則由kubelet維護。service僅支持udp和tcp協議,所以像ping的icmp協議是用不了的,所以無法ping通service ip。

現在我們嘗試看看在pod1向kube-dns的service ip 10.16.0.10:53發送udp請求是如何轉發的。

我們先找出與此IP相關的iptables規則:

【PREROUTING鏈】
-m comment --comment "kubernetes service portals" -j KUBE-SERVICES
【KUBE-SERVICES鏈】
-d 10.16.0.10/32 -p udp -m comment --comment "kube-system/kube-dns:dns cluster IP" -m udp --dport 53 -j KUBE-SVC-TCOU7JCQXEZGVUNU
【KUBE-SVC-TCOU7JCQXEZGVUNU鏈】
-m comment --comment "kube-system/kube-dns:dns" -j KUBE-SEP-L5MHPWJPDKD7XIFG
【KUBE-SEP-L5MHPWJPDKD7XIFG鏈】
-p udp -m comment --comment "kube-system/kube-dns:dns" -m udp -j DNAT --to-destination 10.0.0.46:53
  1. pod1向service ip 10.16.0.10:53發送udp請求,查找路由表,把數據包轉發給網橋cni0(10.0.14.1)
  2. 在數據包進入cnio網橋時,數據包經過PREROUTING鏈,然後跳至KUBE-SERVICES鏈
  3. KUBE-SERVICES鏈中一條匹配此數據包的規則,跳至KUBE-SVC-TCOU7JCQXEZGVUNU鏈
  4. KUBE-SVC-TCOU7JCQXEZGVUNU不做任何操作,跳至KUBE-SEP-L5MHPWJPDKD7XIFG鏈
  5. KUBE-SEP-L5MHPWJPDKD7XIFG裡對此數據包作了DNAT到10.0.0.46:53,其中10.0.0.46即為kube-dns的pod ip
  6. 查找與10.0.0.46匹配的路由,轉發數據包到flannel.1
  7. 之後的數據包流向就與上面的pod1到pod2的網絡一樣了

pod到外網

  1. pod向qq.com發送請求
  2. 查找路由表,轉發數據包到宿主的網卡
  3. 宿主網卡完成qq.com路由選擇後,iptables執行MASQUERADE,把源IP更改為宿主網卡的IP
  4. 向qq.com服務器發送請求

Flannel部署常見問題

1. Node狀態顯示為“NotReady”

我的K8S環境使用kubeadm來容器化運行K8S的各個組件(除kubelet直接運行在裸機上外),當我使用kubeadm join命令加入新的Minion Node到K8S集群中後,通過kubectl get node會發現所有的node都還是not ready狀態,這是因為還沒有配置好flannel網絡。

2. 使用kube-flannel.yml無法創建DaemonSet

我使用的是K8S的1.6.4的版本,然後按照官方的說明,使用kube-flannel.yml來創建flannel deamon set,結果始終報錯。正確的姿勢是先使用kube-flannel-rbac.yml來創建flannel的ClusterRole並授權。該yml的主要作用是創建名叫flannel的ClusterRole,然後將該ClusterRole與ServiceAccount(flannel)綁定。接下來,當我們使用kube-flannel.yml來創建flannel daemon set的時候,該daemon set明確指定其Pod的ServiceAccount為flannel,於是通過它啟動起來的Pod就具有了flannel ClusterRole中指定的權限。

3.flannel Pod狀態為Running,網絡不通

我之前在我的Mac Pro上跑了三個VM,為了能夠訪問公網拉取鏡像,我為每個VM分配了一張網卡使用NAT模式,其分配到的IP地址可能重啟後發生變化。另外,為了我本機方便管理,我為每臺VM又分配了一張網卡使用Host-Only網絡模式,每個網卡都有一個固定的IP地址方便SSH。然後,奇怪的事情就這樣發生了….

原因在與在kube-flannel.yml中,kube-flannel容器的command被指定為:

command: [ "/opt/bin/flanneld", "--ip-masq", "--kube-subnet-mgr"]

可見,其並沒有指定使用哪一張網卡作為flanneld對外通信的物理網卡,於是,可能由於機器上面路由配置的差異,導致三臺機器並沒有一致通過Host-Only網絡模式的網卡來打通Overlay網絡。遇到這種情況,如果幾臺機器的配置一致,可以手動修改kube-flannel.yml文件中kube-flannel的command的值,添加參數–iface=ethX,這裡的ethX就為我們希望flanneld通信使用的網卡名稱。

4.flannel啟動異常,顯示install-cni錯誤

這個現象比較坑,遇到這種情況的第一反應就是去查看install-cni容器到底做了什麼。我們打開kube-flannel.yml可以看到,該容器的command字段只有簡單的一行Shell:

command: [ "/bin/sh", "-c", "set -e -x; cp -f /etc/kube-flannel/cni-conf.json /etc/cni/net.d/10-flannel.conf; while true; do sleep 3600; done" ]

也就是將鏡像中做好的cni-conf.json拷貝到cni的netconf目錄下。由於容器的/etc/cni/net.d是掛載主機的對應的目錄,所以該操作主要目的是為CNI準備flannel環境,便於啟動容器的時候正確從netconf目錄中加載到flannel,從而使用flannel網絡。

我發現進入主機的netconf目錄中能夠看到10-flannel.conf:

#ls /etc/cni/net.d/10-flannel.conf 

/etc/cni/net.d/10-flannel.conf
# cat /etc/cni/net.d/10-flannel.conf
cat: /etc/cni/net.d/10-flannel.conf: No such file or directory

無法打出其內容,而且文件顯示為紅色,說明其內容並沒有正確從容器中拷貝過來。

之前我懷疑該異常是因為kubelet所帶的文件系統參數為systemd,而docker的文件系統參數為cgroupfs所致,結果發現並非如此。當前能夠繞開該異常的workaround為手動進入到主機的netconf目錄,創建10-flannel.conf目錄,並寫入以下數據:

{
"name": "cbr0",
"type": "flannel",
"delegate": {
"isDefaultGateway": true
}
}

5.flannel網絡啟動正常,能夠創建pod,但是網絡不通

出現該現象一般會想到debug,而debug的思路當然是基於官方的那張網絡拓撲圖。比如在我的機器上,通過參看網卡IP地址有如下信息:

6: flannel.1: <broadcast> mtu 1450 qdisc noqueue state UNKNOWN
link/ether 22:a0:ce:3c:bf:1f brd ff:ff:ff:ff:ff:ff
inet 10.244.1.0/32 scope global flannel.1
valid_lft forever preferred_lft forever
inet 10.244.2.0/32 scope global flannel.1
valid_lft forever preferred_lft forever
7:cni0: <no-carrier> mtu 1450 qdisc noqueue state DOWN qlen 1000
link/ether 0a:58:0a:f4:01:01 brd ff:ff:ff:ff:ff:ff
inet 10.244.1.1/24 scope global cni0
valid_lft forever preferred_lft forever

inet6 fe80::4cb6:7fff:fedb:2106/64 scope link
valid_lft forever preferred_lft forever
/<no-carrier>/<broadcast>

很明顯,cni0為10.244.1.1/24網段,說明該主機應該位於10.244.1.0/16子網內;但是我們看flannel.1網卡,確有兩個IP地址,分別為於兩個不同的”/16”子網。所以,可以肯定的是,我們一定是在該太主機上部署了多次kubeadm,而kubeadm reset並不會清理flannel創建的flannel.1和cni0接口,這就導致環境上遺留下了上一次部署分配到的IP地址。這些IP地址會導致IP地址衝突,子網混亂,網絡通信異常。

解決的方法就是每次執行kubeadm reset的時候,手動執行以下命令來清楚對應的殘餘網卡信息:

ip link del cni0
ip link del flannel.1

K8S如何使用Flannel網絡

Flannel打通Overlay網絡

當使用kubeadm來部署k8s,網絡組件(如flannel)是通過Add-ons的方式來部署的。我們使用這個yml文件來部署的flannel網絡。

我截除了關鍵的一段內容(containers spec):

containers:
- name: kube-flannel
image: quay.io/coreos/flannel:v0.7.1-amd64
command: [ "/opt/bin/flanneld", "--ip-masq", "--kube-subnet-mgr" ]
securityContext:
privileged: true
env:

- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
volumeMounts:
- name: run
mountPath: /run
- name: flannel-cfg
mountPath: /etc/kube-flannel/
- name: install-cni
image: quay.io/coreos/flannel:v0.7.1-amd64
command: [ "/bin/sh", "-c", "set -e -x; cp -f /etc/kube-flannel/cni-conf.json /etc/cni/net.d/10-flannel.conf; while true; do sleep 3600; done" ]
volumeMounts:
- name: cni
mountPath: /etc/cni/net.d
- name: flannel-cfg
mountPath: /etc/kube-flannel/

通過分析該yaml文件可以知道:該pod內有兩個container,分別為install-cni和 kube-flannel。

  • install-cni

主要負責將config-map中適用於該flannel的CNI netconf配置拷貝到主機的CNI netconf 路徑下,從而使得K8S在創建pod的時候可以遵照標準的CNI流程掛載網卡。

  • kube-flannel

主要啟動flanneld,該command中為flanneld的啟動指定了兩個參數: –ip-masq, –kube-subnet-mgr。第一個參數–ip-masq代表需要為其配置SNAT;第二個參數–kube-subnet-mgr代表其使用kube類型的subnet-manager。該類型有別於使用etcd的local-subnet-mgr類型,使用kube類型後,flannel上各Node的IP子網分配均基於K8S Node的spec.podCIDR屬性。可以參考下圖的方式來查看(該示例中,k8s為node-1節點分配的podCIDR為:10.244.8.0/24):

#kubectl edit node node-1
apiVersion: v1
kind: Node
...
name: dev-1
resourceVersion: "2452057"
selfLink: /api/v1/nodesdev-1
uid: 31f6e4c3-57b6-11e7-a0a5-00163e122a49
spec:
externalID: dev-1
podCIDR: 10.244.8.0/24

另外,flannel的subnet-manager通過監測K8S的node變化來維護了一張路由表,這張表裡面描述了要到達某個Pod子網需要先到達哪個EndPoint。

CNI掛載容器到隧道端點

如果說flannel為Pod打通了一張跨node的大網,那麼CNI就是將各個終端Pod掛載到這張大網上的安裝工人。

在剛部署好flannel網絡並未在該Node上啟動任何Pod時,通過ip link我們只能夠看到flannel.1這張網卡,卻無法看到cni0。難道是flannel網絡運行異常嗎?我們接下來就來分析flannel的CNI實現原理,就知道答案了。

通過傳統方式來部署flannel都需要通過腳本來修改dockerd的參數,從而使得通過docker創建的容器能夠掛載到指定的網橋上。但是flannel的CNI實現並沒有採用這種方式。通過分析CNI代碼,

我們可以瞭解flannel CNI的流程:

  • 讀取netconf配置文件,並加載/run/flannel/subnet.env環境變量信息。
  • 基於加載的信息,生成適用於其delegate的ipam和CNI bridge的netconf文件;其中指定ipam使用host-local,CNI bridge type為bridge。
  • 調用deletgate(CNI bridge type)對應的二進制文件來掛載容器到網橋上。

這裡的環境變量文件/run/flannel/subnet.env是由flanneld生成的,裡面包含了該主機所能夠使用的IP子網網段,具體內容如下:

# cat /run/flannel/subnet.env
FLANNEL_NETWORK=10.244.0.0/16
FLANNEL_SUBNET=10.244.8.1/24
FLANNEL_MTU=1450
FLANNEL_IPMASQ=true

這些數據生成ipam的配置文件的依據,另外,flannel CNI插件的代碼中,默認指定delegate為bridge:

if !hasKey(n.Delegate, "type") {
n.Delegate["type"] = "bridge"
}

所以,當flannel CNI插件調用delegate,本質上就是調用bridge CNI插件來將容器掛載到網橋上。分析bridge CNI 插件的過程我們可以看到其指定了默認網橋名稱為cni0:

const defaultBrName = "cni0"
func loadNetConf(bytes []byte) (*NetConf, error) {
n := &NetConf{
BrName: defaultBrName,
}
...

return n, nil
}

因此,現在我們可以將整個流程連起來了:flannel CNI插件首先讀取netconf配置和subnet.env信息,生成適用於bridge CNI插件的netconf文件和ipam(host-local)配置,並設置其delegate為bridge CNI插件。然後調用走bridge CNI插件掛載容器到bridge的流程。由於各個Pod的IP地址分配是基於host-local的Ipam,因此整個流程完全是分佈式的,不會對API-Server造成太大的負擔。

至此,基於kubeadm的flannel分析就大致結束了,希望對您有幫助!


分享到:


相關文章: