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,沒法從跨宿主機通訊,因此容器的網絡互通,須要承載到宿主機的網絡之上。linux

flannel支持多種網絡模式,經常使用的是vxlan、UDP、hostgw、ipip以及gce和阿里雲等,vxlan和UDP的區別是:vxlan是內核封包,而UDP是flanneld用戶態程序封包,因此UDP的方式性能會稍差;hostgw模式是一種主機網關模式,容器到另一個主機上容器的網關設置成所在主機的網卡地址,這個和calico很是類似,只不過calico是經過BGP聲明,而hostgw是經過中心的etcd分發,因此hostgw是直連模式,不須要經過overlay封包和拆包,性能比較高,但hostgw模式最大的缺點是必須是在一個二層網絡中,畢竟下一跳的路由須要在鄰居表中,不然沒法通行。docker

在實際的生產環境中,最經常使用的仍是vxlan模式,咱們先看工做原理,而後經過源碼解析實現過程。後端

安裝的過程很是簡單,主要分爲兩步:網絡

第一步安裝flannelapp

yum install flannel 或者經過kubernetes的daemonset方式啓動,配置flannel用的etcd地址curl

第二步配置集羣網絡性能

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、工做原理url

一、容器的地址如何分配插件

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

二、容器如何通訊

上面介紹了爲每一個容器分配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配置的網絡模式,設定啓用對應的網絡管理器。

三、註冊網絡

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裏面任何主機的添加和刪除均可以被其它節點所感知到,從而更新本地內核轉發表。

做者:陳曉宇

來源:宜信技術學院

相關文章
相關標籤/搜索