Kubernetes網路分析之Flannel
Flannel是cereos開源的CNI網路外掛,下圖flannel官網提供的一個數據包經過封包、傳輸以及拆包的示意圖,從這個圖片中可以看出兩臺機器的docker0分別處於不同的段:10.1.20.1/24 和 10.1.15.1/24 ,如果從Web App Frontend1 pod(10.1.15.2)去連線另一臺主機上的Backend Service2 pod(10.1.20.3),網路包從宿主機192.168.0.100發往192.168.0.200,內層容器的資料包被封裝到宿主機的UDP裡面,並且在外層包裝了宿主機的IP和mac地址。這就是一個經典的overlay網路,因為容器的IP是一個內部IP,無法從跨宿主機通訊,所以容器的網路互通,需要承載到宿主機的網路之上。
flannel支援多種網路模式,常用的是vxlan、UDP、hostgw、ipip以及gce和阿里雲等,vxlan和UDP的區別是:vxlan是核心封包,而UDP是flanneld使用者態程式封包,所以UDP的方式效能會稍差;hostgw模式是一種主機閘道器模式,容器到另外一個主機上容器的閘道器設定成所在主機的網絡卡地址,這個和calico非常相似,只不過calico是通過BGP宣告,而hostgw是通過中心的etcd分發,所以hostgw是直連模式,不需要通過overlay封包和拆包,效能比較高,但hostgw模式最大的缺點是必須是在一個二層網路中,畢竟下一跳的路由需要在鄰居表中,否則無法通行。
在實際的生產環境中,最常用的還是vxlan模式,我們先看工作原理,然後通過原始碼解析實現過程。
安裝的過程非常簡單,主要分為兩步:
第一步安裝flannel
yum install flannel 或者通過kubernetes的daemonset方式啟動,配置flannel用的etcd地址
第二步配置叢集網路
curl -L http://etcdurl:2379/v2/keys/flannel/network/config -XPUT -d value="{\"Network\":\"172.16.0.0/16\",\"SubnetLen\":24,\"Backend\":{\"Type\":\"vxlan\",\"VNI\":1}}"
然後啟動每個節點的flanned程式。
一、工作原理
1、容器的地址如何分配
Docker容器啟動時通過docker0分配IP地址,flannel為每個機器分配一個IP段,配置在docker0上,容器啟動後就在本段內選擇一個未佔用的IP,那麼flannel如何修改docker0網段呢?
先看一下 flannel的啟動檔案 /usr/lib/systemd/system/flanneld.service
[Service] Type=notify EnvironmentFile=/etc/sysconfig/flanneld ExecStart=/usr/bin/flanneld-start $FLANNEL_OPTIONS ExecStartPost=/opt/flannel/mk-docker-opts.sh -k DOCKER_NETWORK_OPTIONS -d /run/flannel/docker
檔案裡面指定了flannel環境變數和啟動指令碼和啟動後執行指令碼 ExecStartPost 設定的mk-docker-opts.sh,這個指令碼的作用是生成/run/flannel/docker,檔案內容如下:
DOCKER_OPT_BIP="--bip=10.251.81.1/24" DOCKER_OPT_IPMASQ="--ip-masq=false" DOCKER_OPT_MTU="--mtu=1450" DOCKER_NETWORK_OPTIONS=" --bip=10.251.81.1/24 --ip-masq=false --mtu=1450"
而這個檔案又被docker啟動檔案/usr/lib/systemd/system/docker.service所關聯,
[Service] Type=notify NotifyAccess=all EnvironmentFile=-/run/flannel/docker EnvironmentFile=-/etc/sysconfig/docker
這樣便可以設定docker0的網橋了。
在開發環境中,有三臺機器,分別分配了如下網段:
host-139.245 10.254.44.1/24
host-139.246 10.254.60.1/24
host-139.247 10.254.50.1/24
2、容器如何通訊
上面介紹了為每個容器分配IP,那麼不同主機上的容器如何通訊呢,我們用最常見的vxlan舉例,這裡有三個關鍵點,一個路由,一個arp,一個FDB。我們按照容器發包的過程,逐一分析上面三個元素的作用,首先容器出來的資料包會經過docker0,那麼下面是直接從主機網路出去,還是通過vxlan封包轉發呢?這是每個機器上面路由設定的。
#ip route show dev flannel.1 10.254.50.0/24 via 10.254.50.0 onlink 10.254.60.0/24 via 10.254.60.0 onlink
可以看到每個主機上面都有到另外兩臺機器的路由,這個路由是onlink路由,onlink引數表明強制此閘道器是“在鏈路上”的(雖然並沒有鏈路層路由),否則linux上面是沒法新增不同網段的路由。這樣資料包就能知道,如果是容器直接的訪問則交給flannel.1裝置處理。
flannel.1這個虛擬網路裝置將會對資料封包,但下面一個問題又來了,這個閘道器的mac地址是多少呢?因為這個閘道器是通過onlink設定的,flannel會下發這個mac地址,檢視一下arp表
# ip neig show dev flannel.1 10.254.50.0 lladdr ba:10:0e:7b:74:89 PERMANENT 10.254.60.0 lladdr 92:f3:c8:b2:6e:f0 PERMANENT
可以看到這個閘道器對應的mac地址,這樣內層的資料包就封裝好了
還是最後一個問題,外出的資料包的目的IP是多少呢?換句話說,這個封裝後的資料包應該發往那一臺機器呢?難不成每個資料包都廣播。vxlan預設實現第一次確實是通過廣播的方式,但flannel再次採用一種hack方式直接下發了這個轉發表FDB
# bridge fdb show dev flannel.1 92:f3:c8:b2:6e:f0 dst 10.100.139.246 self permanent ba:10:0e:7b:74:89 dst 10.100.139.247 self permanent
這樣對應mac地址轉發目標IP便可以獲取到了。
這裡還有個地方需要注意,無論是arp表還是FDB表都是permanent,它表明寫記錄是手動維護的,傳統的arp獲取鄰居的方式是通過廣播獲取,如果收到對端的arp相應則會標記對端為reachable,在超過reachable設定時間後,如果發現對端失效會標記為stale,之後會轉入的delay以及probe進入探測的狀態,如果探測失敗會標記為Failed狀態。之所以介紹arp的基礎內容,是因為老版本的flannel並非使用本文上面的方式,而是採用一種臨時的arp方案,此時下發的arp表示reachable狀態,這就意味著,如果在flannel宕機超過reachable超時時間的話,那麼這臺機器上面的容器的網路將會中斷,我們簡單回顧試一下之前(0.7.x)版本的做法,容器為了為了能夠獲取到對端arp地址,核心會首先發送arp徵詢,如果嘗試
/proc/sys/net/ipv4/neigh/$NIC/ucast_solicit
此時後會向用戶空間傳送arp徵詢
/proc/sys/net/ipv4/neigh/$NIC/app_solicit
之前版本的flannel正是利用這個特性,設定
# cat /proc/sys/net/ipv4/neigh/flannel.1/app_solicit 3
從而flanneld便可以獲取到核心傳送到使用者空間的L3MISS,並且配合etcd返回這個IP地址對應的mac地址,設定為reachable。從分析可以看出,如果flanneld程式如果退出後,容器之間的通訊將會中斷,這裡需要注意。Flannel的啟動流程如下圖所示:
Flannel啟動執行newSubnetManager,通過他建立後臺資料儲存,當前有支援兩種後端,預設是etcd儲存,如果flannel啟動指定“kube-subnet-mgr”引數則使用kubernetes的介面儲存資料。
具體程式碼如下:
func newSubnetManager() (subnet.Manager, error) { if opts.kubeSubnetMgr { return kube.NewSubnetManager(opts.kubeApiUrl, opts.kubeConfigFile) } cfg := &etcdv2.EtcdConfig{ Endpoints: strings.Split(opts.etcdEndpoints, ","), Keyfile: opts.etcdKeyfile, Certfile: opts.etcdCertfile, CAFile: opts.etcdCAFile, Prefix: opts.etcdPrefix, Username: opts.etcdUsername, Password: opts.etcdPassword, } // Attempt to renew the lease for the subnet specified in the subnetFile prevSubnet := ReadCIDRFromSubnetFile(opts.subnetFile, "FLANNEL_SUBNET") return etcdv2.NewLocalManager(cfg, prevSubnet) }
通過SubnetManager,結合上面介紹部署的時候配置的etcd的資料,可以獲得網路配置資訊,主要指backend和網段資訊,如果是vxlan,通過NewManager建立對應的網路管理器,這裡用到簡單工程模式,首先每種網路模式管理器都會通過init初始化註冊,
如vxlan
func init() { backend.Register("vxlan", New)
如果是udp
func init() { backend.Register("udp", New) }
其它也是類似,將構建方法都註冊到一個map裡面,從而根據etcd配置的網路模式,設定啟用對應的網路管理器。
3、註冊網路
RegisterNetwork,首先會建立flannel.vxlanID的網絡卡,預設vxlanID是1.然後就是向etcd註冊租約並且獲取相應的網段資訊,這樣有個細節,老版的flannel每次啟動都是去獲取新的網段,新版的flannel會遍歷etcd裡面已經註冊的etcd資訊,從而獲取之前分配的網段,繼續使用。
最後通過WriteSubnetFile寫本地子網檔案,
# cat /run/flannel/subnet.env FLANNEL_NETWORK=10.254.0.0/16 FLANNEL_SUBNET=10.254.44.1/24 FLANNEL_MTU=1450 FLANNEL_IPMASQ=true
通過這個檔案設定docker的網路。細心的讀者可能發現這裡的MTU並不是乙太網規定的1500,這是因為外層的vxlan封包還要佔據50 Byte。
當然flannel啟動後還需要持續的watch etcd裡面的資料,這是當有新的flannel節點加入,或者變更的時候,其他flannel節點能夠動態更新的那三張表。主要的處理方法都在handleSubnetEvents裡面
func (nw *network) handleSubnetEvents(batch []subnet.Event) { . . . switch event.Type {//如果是有新的網段加入(新的主機加入) case subnet.EventAdded: . . .//更新路由表 if err := netlink.RouteReplace(&directRoute); err != nil { log.Errorf("Error adding route to %v via %v: %v", sn, attrs.PublicIP, err) continue } //新增arp表 log.V(2).Infof("adding subnet: %s PublicIP: %s VtepMAC: %s", sn, attrs.PublicIP, net.HardwareAddr(vxlanAttrs.VtepMAC)) if err := nw.dev.AddARP(neighbor{IP: sn.IP, MAC: net.HardwareAddr(vxlanAttrs.VtepMAC)}); err != nil { log.Error("AddARP failed: ", err) continue } //新增FDB表 if err := nw.dev.AddFDB(neighbor{IP: attrs.PublicIP, MAC: net.HardwareAddr(vxlanAttrs.VtepMAC)}); err != nil { log.Error("AddFDB failed: ", err) if err := nw.dev.DelARP(neighbor{IP: event.Lease.Subnet.IP, MAC: net.HardwareAddr(vxlanAttrs.VtepMAC)}); err != nil { log.Error("DelARP failed: ", err) } continue }//如果是刪除實踐 case subnet.EventRemoved: //刪除路由 if err := netlink.RouteDel(&directRoute); err != nil { log.Errorf("Error deleting route to %v via %v: %v", sn, attrs.PublicIP, err) } else { log.V(2).Infof("removing subnet: %s PublicIP: %s VtepMAC: %s", sn, attrs.PublicIP, net.HardwareAddr(vxlanAttrs.VtepMAC)) //刪除arp if err := nw.dev.DelARP(neighbor{IP: sn.IP, MAC: net.HardwareAddr(vxlanAttrs.VtepMAC)}); err != nil { log.Error("DelARP failed: ", err) } //刪除FDB if err := nw.dev.DelFDB(neighbor{IP: attrs.PublicIP, MAC: net.HardwareAddr(vxlanAttrs.VtepMAC)}); err != nil { log.Error("DelFDB failed: ", err) } if err := netlink.RouteDel(&vxlanRoute); err != nil { log.Errorf("failed to delete vxlanRoute (%s -> %s): %v", vxlanRoute.Dst, vxlanRoute.Gw, err) } } default: log.Error("internal error: unknown event type: ", int(event.Type)) } } }
這樣flannel裡面任何主機的新增和刪除都可以被其它節點所感知到,從而更新本地核心轉發表。
作者:陳曉宇
來源:宜信技術學院