背景
隨著公司業(yè)務的發(fā)展载萌,底層容器環(huán)境也需要在各個區(qū)域部署妒穴,實現(xiàn)多云架構, 使用各個云廠商提供的CNI插件是k8s多云環(huán)境下網(wǎng)絡架構的一種高效的解法靡努。我們在阿里云的方案中坪圾,便用到了阿里云提供的CNI插件terway。terway所提供的VPC互通的網(wǎng)絡方案惑朦,方便對接已有的基礎設施兽泄,同時沒有overlay網(wǎng)絡封包解包的性能損耗,簡單易用漾月,出現(xiàn)網(wǎng)絡問題方便診斷病梢。本文對該插件做簡單的代碼分析,理解其原理梁肿,以便后期診斷問題和維護蜓陌。
功能劃分
阿里云開源的terway代碼有三部分組成:
- CNI plugin: 即CNI插件,實現(xiàn)
ADD吩蔑、DEL钮热、VERSION
三個接口來供kubelet調用, 該插件將kubelet傳遞的參數(shù)進行簡單處理后烛芬,會通過gRPC調用terwayBackendServer來實現(xiàn)具體的邏輯隧期,例如申請網(wǎng)絡設備等飒责。同步調用terwayBackendServer將網(wǎng)絡設備分配完畢之后,會通過ipvlanDriver.Driver
進行pod sandbox network namespace的Setup
操作仆潮,同時還會通過TC進行流控宏蛉。該插件會通過daemonSet中initContainer安裝到所有node上。 - backend server: terway中主要的執(zhí)行邏輯性置, 會進行IPAM管理檐晕,并申請對應的網(wǎng)絡設備, 這部分是本次著重分析的對象蚌讼。該程序以daemonSet的方式運行在每個節(jié)點上辟灰。
- networkPolicy: 該部分是借助calico felix實現(xiàn), 完全與上面兩部分解耦篡石。我們看到terway創(chuàng)建的網(wǎng)絡設備是以cali為前綴的芥喇, 其實就是為了兼容calico的schema。
TerwayBackendServer
在terway的main函數(shù)中會啟動gRPC server監(jiān)聽請求凰萨,同時會創(chuàng)建一個TerwayBackendServer
继控, TerwayBackendServer封裝全部操作邏輯,在newNetworkService
函數(shù)中會依次初始化各個子模塊實例胖眷,具體包括:
- ECS client 用來操作ECS client, 所有創(chuàng)建刪除更新操作最后都會通過該client進行處理武通,簡單封裝了一層alicloud的SKD
- kubernetes pod 管理模塊,用來同步kubernetes pod信息
- resouceDB 用來存儲狀態(tài)信息珊搀,便于重啟等操作后恢復狀態(tài)
- resourceManager 管理資源分配的實例冶忱,terway會根據(jù)不同的配置生成不同的resourceManager,此處我們使用的是
ENIMultiIP
這種模式境析,對應的就是newENIIPResourceManager
ENIMultiIP模式會申請阿里云彈性網(wǎng)卡并配置多個輔助VPC的IP地址囚枪,將這些輔助IP地址映射和分配到Pod中,這些Pod的網(wǎng)段和宿主機網(wǎng)段是一致的劳淆,能夠實現(xiàn)VPC網(wǎng)絡互通链沼。
整個架構如下圖所示:
首先我們理解一下kubernetes pod管理模塊,該模塊用于獲取kubernetes pod狀態(tài)沛鸵。terway為了支持一些高級的特性括勺,例如流控等,有一些信息無法通過CNI調用傳遞過來曲掰, 還是得去kubernetes中去查詢這些信息疾捍。此外CNI調用在一些異常情況下可能無法準確回調CNI插件, 例如用戶直接kubectl delete pod --force --graceperiod=0
蜈缤,此時就需要kubernetes作為唯一的single source of truth
拾氓, 保證最后網(wǎng)絡設備在pod刪除時肯定能夠被釋放掉。 它內部主要的方法就是GetPod
與GetLocalPod
底哥。GetPod
方法會請求apiserver返回pod信息咙鞍,如果該pod已經(jīng)在apiserver中刪除房官,就會從本地的storage中獲取。該storage是用boltDB做為底層存儲的一個本地文件续滋,每個被處理過的pod都會在該storage中保存一份信息翰守,且該pod副本并不會隨著apiserver中pod的刪除而刪除,這樣后面程序如果需要該pod信息可以從該storage中獲取疲酌。同時該pod副本會通過異步清理goroutine在pod刪除一小時后刪除蜡峰。GetLocalPod
是從apiserver獲取該node上所有的pod信息,該過程是調用kubernetes最多的地方朗恳,目前兩個清理goroutine會每5min調用一次湿颅,調用量相對較小,對apiserver的負載影響不大粥诫。該模塊也會在本地DB里緩存一份數(shù)據(jù)油航,便于在kubernetes pod刪除后還可以拿到用戶信息。
其次是resourceDB模塊怀浆,該模塊是用來持久化狀態(tài)信息谊囚,該DB中記錄了當前已分配的pod及其網(wǎng)絡設備(networkResource)信息。每次請求/釋放設備都會更新該DB执赡。程序重新啟動初始化完成之后镰踏,也會從resouceDB中恢復上次運行的數(shù)據(jù)。
除了基本的分配刪除操作會更新該DB, terway還啟動異步goroutine定期清理沙合,保證異常情況下的最終一致性奠伪,該goroutine會從apiserve中獲取所有pod信息和當前DB中的信息進行對比,如果對應的pod已經(jīng)刪除會先釋放對應的網(wǎng)絡設備灌诅,然后從DB中刪除該記錄芳来。同時延遲清理可以實現(xiàn)Statefulset的Pod在更新過程中IP地址保持不變含末,
最重要的是resouceManager
模塊猜拾,該iterface封裝了具體網(wǎng)絡設備的操作,如下所示:
// ResourceManager Allocate/Release/Pool/Stick/GC pod resource
// managed pod and resource relationship
type ResourceManager interface {
Allocate(context *networkContext, prefer string) (types.NetworkResource, error)
Release(context *networkContext, resID string) error
GarbageCollection(inUseResList map[string]interface{}, expireResList map[string]interface{}) error
}
從其中三個method可以很明顯的看出可以執(zhí)行的的動作佣盒,每次CNI插件調用backendServer時挎袜, 就會調用ResoueceManager進行具體的分配釋放操作。對于ENIMultiIP
這種模式來說肥惭,具體的實現(xiàn)類是eniIPResourceManager
:
type eniIPResourceManager struct {
pool pool.ObjectPool
}
其中只有pool一個成員函數(shù)盯仪,具體的實現(xiàn)類型是simpleObjectPool
, 該pool維護了當前所有的ENI信息。當resouceManager進行分配釋放網(wǎng)絡設備的時候其實是從該pool中進行存取即可:
func (m *eniIPResourceManager) Allocate(ctx *networkContext, prefer string) (types.NetworkResource, error) {
return m.pool.Acquire(ctx, prefer)
}
func (m *eniIPResourceManager) Release(context *networkContext, resID string) error {
if context != nil && context.pod != nil {
return m.pool.ReleaseWithReverse(resID, context.pod.IPStickTime)
}
return m.pool.Release(resID)
}
func (m *eniIPResourceManager) GarbageCollection(inUseSet map[string]interface{}, expireResSet map[string]interface{}) error {
for expireRes := range expireResSet {
if err := m.pool.Stat(expireRes); err == nil {
err = m.Release(nil, expireRes)
if err != nil {
return err
}
}
}
return nil
}
由上述代碼可見蜜葱,resouceManager實際操作的都是simpleObjectPool這個對象全景。 我們看看這個pool到底做了那些操作牵囤。首先初始化該pool:
// NewSimpleObjectPool return an object pool implement
func NewSimpleObjectPool(cfg Config) (ObjectPool, error) {
if cfg.MinIdle > cfg.MaxIdle {
return nil, ErrInvalidArguments
}
if cfg.MaxIdle > cfg.Capacity {
return nil, ErrInvalidArguments
}
pool := &simpleObjectPool{
factory: cfg.Factory,
inuse: make(map[string]types.NetworkResource),
idle: newPriorityQueue(),
maxIdle: cfg.MaxIdle,
minIdle: cfg.MinIdle,
capacity: cfg.Capacity,
notifyCh: make(chan interface{}),
tokenCh: make(chan struct{}, cfg.Capacity),
}
if cfg.Initializer != nil {
if err := cfg.Initializer(pool); err != nil {
return nil, err
}
}
if err := pool.preload(); err != nil {
return nil, err
}
log.Infof("pool initial state, capacity %d, maxIdle: %d, minIdle %d, idle: %s, inuse: %s",
pool.capacity,
pool.maxIdle,
pool.minIdle,
queueKeys(pool.idle),
mapKeys(pool.inuse))
go pool.startCheckIdleTicker()
return pool, nil
}
可以看到在創(chuàng)建的時候會根據(jù)傳入的config依次初始化各成員變量爸黄, 其中
- factory 成員用來分配網(wǎng)絡設備滞伟,會調用ECS SDK進行分配資源,分配之后將信息存儲在pool之中炕贵,具體的實現(xiàn)是
eniIPFactory
梆奈。 - inuse 存儲了當前所有正在使用的networkResource
- idle 存儲了當前所有空閑的networkResource, 即已經(jīng)通過factory分配好,但是還未被某個pod實際使用称开。如果某個network resouce不再使用亩钟,也會歸還到該idle之中”詈洌 通過這種方式清酥,pool具備一定的緩充能力,避免頻繁調用factory進行分配釋放蕴侣。idle為
priorityQeueu
類型总处,即所有空閑的networkResouce通過優(yōu)先級隊列排列,優(yōu)先級隊列的比較函數(shù)會比較reverse
字段睛蛛,reverse
默認是入隊時間鹦马,也就是該networkResouce的釋放的時間,這樣做能夠盡量使一個IP釋放之后不會被立馬被復用忆肾。reverse
字段對于一些statueSet的resouce也會進行一些特殊處理荸频,因為statufulSet是有狀態(tài)workload, 對于IP的釋放也會特殊處理,保證其盡可能復用客冈。 - maxIdle, minIdle 分別表示上述idle隊列中允許的最大和最小個數(shù)旭从, minIdle是為了提供有一定的緩沖能力,但該值并不保證场仲,最大是為了防止緩存過多和悦,如果空閑的networkResouce太多沒有被使用就會釋放一部分,IP地址不止是節(jié)點級別的資源渠缕,也會占用整個vpc/vswitch/安全組的資源鸽素,太多的空閑可能會導致其他節(jié)點或者云產(chǎn)品分配不出IP。
- capacity 是該pool的容量亦鳞,最大能分配的networkResouce的個數(shù)馍忽。該值可以自己指定, 但如果超過該ECS能允許的最大個數(shù)就會被設置成允許的最大個數(shù)燕差。
- tokenCh 是個buffered channel, 容量大小即為上面capacity的值遭笋,被做token bucket。 pool初始化的時候會將其中放滿元素徒探,后面運行過程中中瓦呼,只要能從該channel中讀取到元素則意味著該pool還沒有滿。每次調用factory申請networkResouce之前會從該channel中讀取一個元素测暗, 每次調用factory釋放networkDevice會從該channel中放入一個元素央串。
成員變量初始化完成之后會調用Initializer
, 該函數(shù)會回調一個閉包函數(shù)谎替,定義在newENIIPResourceManager
中: 當程序啟動時,resouceManager通過讀取存儲在本地磁盤也就是resouceDB中的信息獲取當前正在使用的networkResouce蹋辅,然后通過ecs獲取當前所有eni設備及其ip, 依次遍歷所有ip判斷當前是否在使用钱贯,分別來初始化inuse和idle。這樣可以保證程序重啟之后可以重構內存中的pool數(shù)據(jù)信息侦另。
然后會調用preload
,該函數(shù)確保pool(idle)中有minIdle個空閑元素, 防止啟動時大量調用factory秩命。
最后會進行go pool.startCheckIdleTicker()
異步來goroutine中調用checkIdle
定期查詢pool(idle)中的元素是否超過maxIdle個元素, 如果超過則會調用factory進行釋放褒傅。同時每次調用factory也會通過notifyCh
來通知該goroutine執(zhí)行檢查操作弃锐。
pool結構初始化完成之后,resouceManager中所有對于networkResource的操作都會通過該pool進行殿托,該pool在必要條件下再調用factory進行分配釋放霹菊。
factory的具體實現(xiàn)是eniIPFactory
, 用來調用ecs SDK進行申請釋放eniIP, 并維護對應的數(shù)據(jù)結構。不同于直接使用eni設備支竹,ENIMultiIP
模式會為每個eni設備會有多個eniIP旋廷。eni設備是通過ENI
結構體標識, eniIP通過ENIIP
結構體標識礼搁。terway會為每個ENI
創(chuàng)建一個goroutine, 該ENI上所有eniIP的分配釋放都會在goroutine內進行饶碘,factory通過channel與該groutine通信, 每個goroutine對應一個接受channel ipBacklog
馒吴,用于傳遞分配請求到該goroutine扎运。 每次factory 需要創(chuàng)建(eniIPFactory.Create)一個eniIP時, 會一次遍歷當前已經(jīng)存在的ENI
設備饮戳,如果該設備還有空閑的eniIP豪治,就會通過該ipBacklog
channel發(fā)送一個元素到該ENI設備的goroutine進行請求分配, 當goroutine將eniIP分配完畢之后通過factory 的resultChan
通知factory, 這樣factory就成功完成一次分配扯罐。 如果所有的ENI的eniIP都分配完畢负拟,會首先創(chuàng)建ENI設備及其對應goroutine。因為每個ENI設備會有個主IP篮赢, 所以首次分配ENI不需要發(fā)送請求到ipBacklog
, 直接將該主ip返回即可齿椅。對應的釋放(Dispose)就是先釋放eniIP, 等到只剩最后一個eniIP(主eniIP)時會釋放整個ENI設備启泣。對于所有ecs調用都會通過buffer channel進行流控,防止瞬間調用過大示辈。
總結
總之寥茫,terway的整個實現(xiàn),邏輯比較清晰矾麻,并且擴展性也較高纱耻。后期芭梯,可以比較方便地在此基礎上做一些定制和運維支持,從而很好地融入公司的基礎架構設施弄喘。
看完三件事??
如果你覺得這篇內容對你還蠻有幫助玖喘,我想邀請你幫我三個小忙:
點贊,轉發(fā)蘑志,有你們的 『點贊和評論』累奈,才是我創(chuàng)造的動力。
關注公眾號 『 java爛豬皮 』急但,不定期分享原創(chuàng)知識澎媒。
同時可以期待后續(xù)文章ing??