1 概述
根據官網的描述,flannel是一個專為kubernetes定制的三層網絡解決方案。它主要用於解決容器的跨主機通信問題。首先我們來簡單看一下,它是如何工作的。
首先,flannel會利用Kubernetes API或者etcd用於存儲整個集群的網絡配置,其中最主要的內容為設置集群的網絡地址空間,例如,設定整個集群內所有容器的IP都取自網段“10.1.0.0/16”。接着,flannel會在每個主機中運行flanneld作為agent,它會為所在主機從集群的網絡地址空間中,獲取一個小的網段subnet,本主機內所有容器的IP地址都將從中分配。然后,flanneld再將本主機獲取的subnet以及用於主機間通信的Public IP,同樣通過kubernetes API或者etcd存儲起來。最后,flannel利用各種backend mechanism,例如udp,vxlan等等,跨主機轉發容器間的網絡流量,完成容器間的跨主機通信。下面,我們以一個具體的例子來描述在flannel中,跨主機的容器間通信是如何進行的。
如下圖所示,集群范圍內的網絡地址空間為10.1.0.0/16,Machine A獲取的subnet為10.1.15.0/24,並且其中的兩個容器的IP分別為10.1.15.2/24和10.1.15.3/24,兩者都在10.1.15.0/24這一子網范圍內,對於下方的Machine B同理。
現在,我們來簡單看一下,如果上方Machine A中IP地址為10.1.15.2/24的容器要與下方Machine B中IP地址為10.1.16.2/24的容器進行通信,封包是如何進行轉發的。從上文可知,每個主機的flanneld會將自己與所獲取subnet的關聯信息存入etcd中,例如,subnet 10.1.15.0/24所在主機可通過IP 192.168.0.100訪問,subnet 10.1.16.0/24可通過IP 192.168.0.200訪問。反之,每台主機上的flanneld通過監聽etcd,也能夠知道其他的subnet與哪些主機相關聯。如下圖,Machine A上的flanneld通過監聽etcd已經知道subnet 10.1.16.0/24所在的主機可以通過Public 192.168.0.200訪問,而且熟悉docker橋接模式的同學肯定知道,目的地址為10.1.16.2/24的封包一旦到達Machine B,就能通過cni0網橋轉發到相應的pod,從而達到跨宿主機通信的目的。
因此,flanneld只要想辦法將封包從Machine A轉發到Machine B就OK了,而上文中的backend就是用於完成這一任務。不過,達到這個目的的方法是多種多樣的,所以我們也就有了很多種backend。在這里我們舉例介紹的是最簡單的一種方式hostgw:因為Machine A和Machine B處於同一個子網內,它們原本就能直接互相訪問。因此最簡單的方法是:在Machine A中的容器要訪問Machine B的容器時,我們可以將Machine B看成是網關,當有封包的目的地址在subnet 10.1.16.0/24范圍內時,就將其直接轉發至B即可。而這通過下圖中那條紅色標記的路由就能完成,對於Machine B同理可得。由此,在滿足仍有subnet可以分配的條件下,我們可以將上述方法擴展到任意數目位於同一子網內的主機。而任意主機如果想要訪問主機X中subnet為S的容器,只要在本主機上添加一條目的地址為R,網關為X的路由即可。
下面,我將以問題驅動的方式,來詳細分析flannel是如何運作的
2 節點初始化
首先,我們最感興趣的是,當一個新的節點加入集群時,它是如何初始化的。對此,我們可能會有以下幾個疑問:
1) 若主機有多張網卡和多個IP,如何選擇其中的一張網卡和一個IP用於集群主機間的通信
2) 主機如何獲取屬於自己的subnet並維護
3) 我們如何在集群中有新的節點加入時,獲取對應的subnet和Public IP,並通過配置backend進行訪問
2.1 網卡及對外IP選擇
對於第一個問題,事實上我們可以在flanneld的啟動參數中通過"--iface"或者"--iface-regex"進行指定。其中"--iface"的內容可以是完整的網卡名或IP地址,而"--iface-regex"則是用正則表達式表示的網卡名或IP地址,並且兩個參數都能指定多個實例。flannel將以如下的優先級順序來選取:
1) 如果"--iface"和"----iface-regex"都未指定時,則直接選取默認路由所使用的輸出網卡
2) 如果"--iface"參數不為空,則依次遍歷其中的各個實例,直到找到和該網卡名或IP匹配的實例為止
3) 如果"--iface-regex"參數不為空,操作方式和2)相同,唯一不同的是使用正則表達式去匹配
最后,對於集群間交互的Public IP,我們同樣可以通過啟動參數"--public-ip"進行指定。否則,將使用上文中獲取的網卡的IP作為Public IP。
2.2 獲取subnet
在獲取subnet之前,我們首先要創建一個SubnetManager,它在具體的代碼實現中,表現為一個接口,如下所示:
type Manager interface { GetNetworkConfig(ctx context.Context) (*Config, error) AcquireLease(ctx context.Context, attrs *LeaseAttrs) (*Lease, error) RenewLease(ctx context.Context, lease *Lease) error WatchLease(ctx context.Context, sn ip.IP4Net, cursor interface{}) (LeaseWatchResult, error) WatchLeases(ctx context.Context, cursor interface{}) (LeaseWatchResult, error) Name() string }
從接口中各個函數的名字,我們大概就能猜出SubnetManager的作用是什么了。但是,為什么獲取subnet的函數叫AcquireLease,而不叫AcquireSubnet呢?實際上,每台主機都是租借了一個subnet,如果到了一定時間不進行更新,那么該subnet就會過期從而重新分配給其他的主機,即主機和subnet的關聯信息會從etcd中消失(在本文中我們將默認選擇etcd作為SubnetManager的后端存儲)。因此,lease就是一條subnet和所屬主機的關聯信息,並且具有時效性,需要定期更新。下面我們來看看,每台主機都是如何獲取lease的:
1) 首先,我們調用GetNetworkConfig(),它會訪問etcd獲取集群網絡配置並封裝在結構Config中返回,Config結構如下所示。其中的Network字段對應的集群網絡地址空間是在flannel啟動前,必須寫入etcd中的,例如"10.1.0.0/16"。
type Config struct { Network ip.IP4Net SubnetMin ip.IP4 SubnetMax ip.IP4 SubnetLen uint BackendType string `json:"-"` Backend json.RawMessage `json:",omitempty"` }
對於其他字段的含義及默認值如下:
(1) SubnetLen表示每個主機分配的subnet大小,我們可以在初始化時對其指定,否則使用默認配置。在默認配置的情況下,如果集群的網絡地址空間大於/24,則SubnetLen配置為24,否則它比集群網絡地址空間小1,例如集群的大小為/25,則SubnetLen的大小為/26
(2) SubnetMin是集群網絡地址空間中最小的可分配的subnet,可以手動指定,否則默認配置為集群網絡地址空間中第一個可分配的subnet。例如對於"10.1.0.0/16",當SubnetLen為24時,第一個可分配的subnet為"10.1.1.0/24"。
(3) SubnetMax表示最大可分配的subnet,對於"10.1.0.0/16",當subnetLen為24時,SubnetMax為"10.1.255.0/24"
(4) BackendType為使用的backend的類型,如未指定,則默認為“udp”
(5) Backend中會包含backend的附加信息,例如backend為vxlan時,其中會存儲vtep設備的mac地址
2) 在獲取了集群的網絡配置之后,接下來我們就調用SubnetManager中的AcquireLease()獲取本主機的subnet。其中的參數類型LeaseAttrs如下所示:
type LeaseAttrs struct { PublicIP ip.IP4 BackendType string `json:",omitempty"` BackendData json.RawMessage `json:",omitempty"` }
顯然,其中最重要的字段就是PublicIP,它實質上是標識了一台主機。在獲取subnet之前,我們先要從etcd中獲取當前所有已經存在的lease信息----leases,以備后用。下面我們將對不同情況下lease的獲取進行討論:
(1) 事實上,這可能並不是我們第一次在這台機器上啟動flannel,因此,很有可能,在此之前,這台機器已經獲取了lease。已知一台主機其實是由它的Public IP標識的,所以我們可以用Public IP作為關鍵字匹配leases中所有lease的Public IP。若匹配成功,則檢查相應的lease是否和當前的集群網絡配置兼容:檢查的內容包括IP是否落在SubnetMin和SubnetMax內,以及subnet大小是否和SubnetLen相等。若兼容,則用新的LeaseAttrs和ttl更新該lease,表示成功獲取本機的lease,否則只能將該lease刪除。
(2) 當初始化SubnetManager時,會先試圖解析之前flannel獲取了lease后留下的配置文件(該文件的創建,會在下文描述),從中讀取出之前獲取的subnet。如果讀取到的subnet不為空,則同樣利用該subnet去匹配leases中所有lease的subnet。若匹配成功,則同樣檢查lease是否和當前的集群網絡配置兼容。若兼容則更新lease,表示成功獲取本機的lease,否則將其刪除。如果該subnet並不能從leases中找到,但是它和當前的集群網絡配置兼容的話,可以直接將它和LeaseAttrs封裝為lease,寫入etcd。
(3) 若此時還未獲取到lease,那么我們有必要自己創建一個新的了。創建的方法很簡單,從SubnetMin遍歷到SubnetMax,將其中和leases中已有的subnet都不重合者加入一個集合中。再從該集合隨機選擇一個,作為本主機的subnet即可。最后,將subnet和LeaseAttrs封裝為一個lease寫入etcd。由此,該主機獲取了自己的subnet。
最后,我們將有關的集群網絡和subnet的配置信息寫入文件/run/flannel/subnet.env(可通過命令行參數"--subnet-file"手動指定)中,寫入的信息如下所示,包括:集群網絡地址空間FLANNEL_NETWORK,獲取的子網信息FLANNEL_SUBNET等等
cat /var/run/flannel/subnet.env FLANNEL_NETWORK=10.1.0.0/16 FLANNEL_SUBNET=10.1.16.1/24 FLANNEL_MTU=1450 FLANNEL_IPMASQ=false
2.3 維護subnet
當SubnetManager的后端存儲使用的是etcd時,各個主機還需要對自己的lease進行維護,在租期即將到期時,需要對etcd中的lease進行更新,調用SubnetManager中的RenewLease()方法,防止它到期后被自動刪除。另外,我們可以在flanneld的命令行啟動參數中用"--subnet-lease-renew-margin"指定在租期到期前多久進行更新。默認值為1小時,即每23小時更新一次lease,重新獲取一次24小時的租期。
2.4 發現新節點
現在,初始化已經完成了,我們需要面對如下兩個問題:
1、當本主機的flanneld啟動時,如果集群中已經存在了其他主機,我們如何通過backend進行配置,使得封包能夠到達它們
2、如果之后集群中又添加了新的主機,我們如何獲取這一事件,並通過backend對配置進行調整,對於刪除主機這類事件同理
當然上述兩個問題,都是通過etcd解決的。backend會一邊通過上文中的WatchLeases()方法對etcd進行監聽,從中獲取各類事件,另一邊會啟動一個事件處理引擎,不斷地對監聽到的事件進行處理。對於問題1,我們首先要從etcd中獲取當前所有的lease信息,並將其轉化為一系列的event,將它交於事件處理引擎進行處理,從而讓封包能夠到達這些主機。對於問題2,直接對etcd中的事件進行監聽,將獲取的事件轉換為事件處理引擎能夠處理的形式,並進行處理即可。事件的類型也很簡單,總共就只有EventAdded和EventRemoved兩種,分別表示新增了lease以及一個lease過期。因為不同backend的配置方式是完全不同的,下面我們就將對各種backend的基本原理進行解析,並說明它們如何處理EventAdded和EventRemoved這兩類事件。
3 backend原理解析
在本節中,我將對hostgw,udp和vxlan三種backend進行解析
3.1 hostgw
hostgw是最簡單的backend,它的原理非常簡單,直接添加路由,將目的主機當做網關,直接路由原始封包。例如,我們從etcd中監聽到一個EventAdded事件:subnet為10.1.15.0/24被分配給主機Public IP 192.168.0.100,hostgw要做的工作非常簡單,在本主機上添加一條目的地址為10.1.15.0/24,網關地址為192.168.0.100,輸出設備為上文中選擇的集群間交互的網卡即可。對於EventRemoved事件,刪除對應的路由即可。
3.2 udp
我們知道當backend為hostgw時,主機之間傳輸的就是原始的容器網絡封包,封包中的源IP地址和目的IP地址都為容器所有。這種方法有一定的限制,就是要求所有的主機都在一個子網內,即二層可達,否則就無法將目的主機當做網關,直接路由。
而udp類型backend的基本思想是:既然主機之間是可以相互通信的(並不要求主機在一個子網中),那么我們為什么不能將容器的網絡封包作為負載數據在集群的主機之間進行傳輸呢?這就是所謂的overlay。具體過程如下所示:
當容器10.1.15.2/24要和容器10.1.20.2/24通信時,因為該封包的目的地不在本主機是subnet內,因此封包會首先通過網橋轉發到主機中。最終在主機上經過路由匹配,進入網卡flannel0。需要注意的是flannel0是一個tun設備,它是一種工作在三層的虛擬網絡設備,而flanneld是一個proxy,它會監聽flannel0並轉發流量。當封包進入flannel0時,flanneld就可以從flannel0中將封包讀出,由於flannel0是三層設備,所以讀出的封包僅僅包含IP層的報頭及其負載。最后flanneld會將獲取的封包作為負載數據,通過udp socket發往目的主機。同時,在目的主機的flanneld會監聽Public IP所在的設備,從中讀取udp封包的負載,並將其放入flannel0設備內。由此,容器網絡封包到達目的主機,之后就可以通過網橋轉發到目的容器了。
最后和hostgw不同的是,udp backend並不會將從etcd中監聽到的事件中包含的lease信息作為路由寫入主機中。每當收到一個EventAdded事件,flanneld都會將其中的subnet和Public IP保存在一個數組中,用於轉發封包時進行查詢,找到目的主機的Public IP作為udp封包的目的地址。
3.3 vxlan
首先,我們對vxlan的基本原理進行簡單的敘述。從下圖所示的封包結構來看,vxlan和上文提到的udp backend的封包結構是非常類似的,不同之處是多了一個vxlan header,以及原始報文中多了個二層的報頭。
下面讓我們來看看,當有一個EventAdded到來時,flanneld如何進行配置的,以及封包是如何在flannel網絡中流動的。
如上圖所示,當主機B加入flannel網絡時,和其他所有backend一樣,它會將自己的subnet 10.1.16.0/24和Public IP 192.168.0.101寫入etcd中,和其他backend不一樣的是,它還會將vtep設備flannel.1的mac地址也寫入etcd中。
之后,主機A會得到EventAdded事件,並從中獲取上文中B添加至etcd的各種信息。這個時候,它會在本機上添加三條信息:
1) 路由信息:所有通往目的地址10.1.16.0/24的封包都通過vtep設備flannel.1設備發出,發往的網關地址為10.1.16.0,即主機B中的flannel.1設備。
2) fdb信息:MAC地址為MAC B的封包,都將通過vxlan首先發往目的地址192.168.0.101,即主機B
3) arp信息:網關地址10.1.16.0的地址為MAC B
現在有一個容器網絡封包要從A發往容器B,和其他backend中的場景一樣,封包首先通過網橋轉發到主機A中。此時通過,查找路由表,該封包應當通過設備flannel.1發往網關10.1.16.0。通過進一步查找arp表,我們知道目的地址10.1.16.0的mac地址為MAC B。到現在為止,vxlan負載部分的數據已經封裝完成。由於flannel.1是vtep設備,會對通過它發出的數據進行vxlan封裝(這一步是由內核完成的,相當於udp backend中的proxy),那么該vxlan封包外層的目的地址IP地址該如何獲取呢?事實上,對於目的mac地址為MAC B的封包,通過查詢fdb,我們就能知道目的主機的IP地址為192.168.0.101。
最后,封包到達主機B的eth0,通過內核的vxlan模塊解包,容器數據封包將到達vxlan設備flannel.1,封包的目的以太網地址和flannel.1的以太網地址相等,三層封包最終將進入主機B並通過路由轉發達到目的容器。
事實上,flannel只使用了vxlan的部分功能,由於VNI被固定為1,本質上工作方式和udp backend是類似的,區別無非是將udp的proxy換成了內核中的vxlan處理模塊。而原始負載由三層擴展到了二層,但是這對三層網絡方案flannel是沒有意義的,這么也做僅僅只是為了適配vxlan的模型。vxlan詳細的原理參見文后的參考文獻,其中的分析更為具體,也更易理解。
4 總結
總的來說,flannel更像是經典的橋接模式的擴展。我們知道,在橋接模式中,每台主機的容器都將使用一個默認的網段,容器與容器之間,主機與容器之間都能互相通信。要是,我們能手動配置每台主機的網段,使它們互不沖突。接着再想點辦法,將目的地址為非本機容器的流量送到相應主機:如果集群的主機都在一個子網內,就搞一條路由轉發過去;若是不在一個子網內,就搞一條隧道轉發過去。這樣以來,容器的跨網絡通信問題不就解決了么?而flannel做的,其實就是將這些工作自動化了而已。
參考文獻
1、flannel源碼:https://github.com/coreos/flannel
2、《vxlan協議原理解析》:http://cizixs.com/2017/09/25/vxlan-protocol-introduction
3、《linux上實現vxlan網絡》:http://cizixs.com/2017/09/28/linux-vxlan
4、《VxLAN和VTEP》:http://maoxiaomeng.com/2017/07/31/vxlan%E5%92%8Cvtep/