阿里terway源碼分析

背景

隨著公司業(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)絡互通链沼。

整個架構如下圖所示:

image

首先我們理解一下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刪除時肯定能夠被釋放掉。 它內部主要的方法就是GetPodGetLocalPod底哥。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??

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市波桩,隨后出現(xiàn)的幾起案子戒努,更是在濱河造成了極大的恐慌,老刑警劉巖镐躲,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件储玫,死亡現(xiàn)場離奇詭異,居然都是意外死亡萤皂,警方通過查閱死者的電腦和手機缘缚,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來敌蚜,“玉大人桥滨,你說我怎么就攤上這事〕诔担” “怎么了齐媒?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長纷跛。 經(jīng)常有香客問我喻括,道長,這世上最難降的妖魔是什么贫奠? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任唬血,我火速辦了婚禮,結果婚禮上唤崭,老公的妹妹穿的比我還像新娘拷恨。我一直安慰自己,他們只是感情好谢肾,可當我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布腕侄。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪冕杠。 梳的紋絲不亂的頭發(fā)上微姊,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天,我揣著相機與錄音分预,去河邊找鬼兢交。 笑死,一個胖子當著我的面吹牛笼痹,可吹牛的內容都是我干的配喳。 我是一名探鬼主播,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼与倡,長吁一口氣:“原來是場噩夢啊……” “哼界逛!你這毒婦竟也來了?” 一聲冷哼從身側響起纺座,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤息拜,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后净响,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體少欺,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年馋贤,在試婚紗的時候發(fā)現(xiàn)自己被綠了赞别。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡配乓,死狀恐怖仿滔,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情犹芹,我是刑警寧澤崎页,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站腰埂,受9級特大地震影響飒焦,放射性物質發(fā)生泄漏。R本人自食惡果不足惜屿笼,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一牺荠、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧驴一,春花似錦休雌、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽例朱。三九已至孝情,卻和暖如春鱼蝉,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背箫荡。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工魁亦, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人羔挡。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓洁奈,卻偏偏與公主長得像,于是被迫代替她去往敵國和親绞灼。 傳聞我的和親對象是個殘疾皇子利术,可洞房花燭夜當晚...
    茶點故事閱讀 44,713評論 2 354