背景
SOFAStack 是螞蟻集團的商業化金融級雲原生架構產品,基於 SOFAStack 可快速搭建雲原生微服務體系,快速開發更具可靠性和擴充套件性、更加易於維護的雲原生應用。在巨集觀架構層面,提供單機房向同城雙活、兩地三中心、異地多活架構演進路線,使系統容量能在多個資料中心內任意擴充套件和排程,充分利用伺服器資源,提供機房級容災能力,保證業務連續性。
在應用生命週期管理層面,SOFAStack 提供了一個多模應用 PaaS 平臺——SOFAStack CAFE (Cloud Application Fabric Engine) 雲應用引擎。它提供應用管理、流程編排、應用部署、叢集運維等全生命週期管理的 PaaS 平臺能力,滿足金融場景中經典和雲原生架構的運維需求,幫助傳統架構平滑過渡、保障金融技術風險。
在雲原生架構運維上,SOFAStack CAFE 通過單元化混合雲產品 LHC (LDC Hybrid Cloud) 提供單元化應用的雲原生多叢集釋出運維能力,實現應用的多地域、多機房、多雲混合部署。本文將揭開 LHC 的神祕面紗,來詳細談談我們在其底層 Kubernetes 多叢集釋出體系中的一些實踐。
挑戰
在 LHC 產品誕生之初,我們首要面臨的問題便是為其選擇一個合適的底層 Kubernetes 多叢集框架。彼時 Kubernetes 社群剛剛完成了其官方多叢集專案 KubeFed,其提供了多叢集的納管、Kubernetes 資源的多叢集分發與狀態迴流等一系列多叢集基礎能力,自然成為了我們當時的最佳選擇。
但正如前面所說,社群的多叢集框架提供的僅僅是“基礎能力”,這些能力對於我們的單元化混合雲產品來說存在著很多不滿足甚至有衝突的點。其中,最突出的一個問題就是社群沒有“單元化”的概念,其多叢集就是純粹的多 Kubernetes 叢集,對任何一個多叢集 Kubernetes 資源(在 KubeFed 中我們稱其為聯邦資源)來說,它的分發拓撲只能是按叢集。但在單元化模型中,一個應用服務的資源是分佈在多個部署單元中的,而部署單元和叢集之間的關係的靈活的——在我們目前的模型中,叢集和部署單元之間的關係是 1:n,即一個 Kubernetes 叢集可以包含多個部署單元。這時候,我們便遇到了和社群框架的分歧點,也是最大的挑戰:上層業務需要按部署單元維度來進行 Kubernetes 資源的治理,底層社群框架則只認叢集。
除此之外,KubeFed 自身所涵蓋的基礎能力也還不足以滿足我們的所有需求,比如缺乏叢集的租戶隔離能力、不支援資源 annotation 的下發、主叢集和子叢集之間的網路連通性要求高等等。由此,解決衝突並補齊能力便成為了我們在建設 LHC 產品底層多叢集能力上的重點課題。
實踐
下面我們就來分模組談談建設 LHC 產品底層 Kubernetes 多叢集能力中的一些具體實踐。
多拓撲聯邦 CRD
在社群 KubeFed 框架中,我們通過聯邦 CR 來進行 Kubernetes 資源的多叢集分發。一個典型的聯邦 CR 的 spec 如下所示:
可以看到其主要包含三個欄位,其中 placement 用於指定所需分發的叢集,template 包含了該聯邦資源的單叢集資源體,overrides 用於指定每個子叢集中對 template 中資源體的自定義部分。
前面我們提到,對於單元化應用的 Kubernetes 資源而言,它需要按部署單元維度而非叢集維度進行分發,因此上面的社群 CRD 顯然是無法滿足要求的,需要對其進行修改。經過修改後,新的聯邦 CR 的 spec 如下所示:
可以看到,我們沒有完全摒棄社群的 CRD,而是對其進行了“升級”,通過將具體的“叢集”轉變為抽象的“拓撲”,將聯邦資源的分發拓撲完全自定義化,打破了單一叢集維度的限制。如上面的 spec 中我們將 topologyType 設定為 cell 即指定了該資源以部署單元維度進行分發,反之指定為 cluster 則能完全相容社群原生的叢集維度分發模式。
當然,僅僅定義一個新的 CRD 無法解決問題,我們還需要修改其對應的實現,才能讓其工作起來。但是,如果要讓社群的 KubeFed controller 感知多拓撲模型,勢必會對其底層實現進行大量修改,最終很可能就變成了半重寫,研發成本高,且無法再繼續迴流社群上游的修改,帶來的維護成本也比較高。我們需要尋求更好方法,把我們的多拓撲模型和 KubeFed 的原生多叢集能力解耦開來。
獨立並擴充套件聯邦層 ApiServer
既然我們不想對社群 KubeFed controller 進行過多侵入式修改,那麼我們必然需要一個轉換層,來將上述的多拓撲聯邦 CRD 轉換成相應的社群 CRD。對於某種特定拓撲來說,其轉換邏輯也是確定的,因此最簡單高效的轉換便是直接通過 Kubernetes ApiServer 來進行處理,而 ApiServer 對於 CRD 的 Conversion Webhook 能力則剛好能夠滿足這一轉換層的實現需求。
因此,我們給 KubeFed controller 搭配了一個專屬的 Kubernetes ApiServer 形成了一個獨立的 Kubernetes 控制面,我們稱其為“聯邦層”。這一獨立的控制面僅包含聯邦多叢集相關的資料,確保不會與其他 Kubernetes 資源互相干擾,同時也避免了部署時對外部 Kubernetes 叢集的強依賴。
那麼,聯邦層的 ApiServer 有何特別之處?其實它的主體還是 Kubernetes 原生的 ApiServer,提供 ApiServer 所能提供的所有能力,我們所做的則是對其進行了“包裝”,將我們需要擴充套件到聯邦層的能力裝了進去。下面具體介紹幾項關鍵擴充套件能力。
內建多拓撲聯邦 CRD 轉換能力
正如上文所說,這是聯邦層 ApiServer 提供的最重要的能力。藉助 Kubernetes CRD 的多版本能力,我們將自己的多拓撲聯邦 CRD 和社群 CRD 定義為了同一個 CRD 的兩個版本,然後通過在聯邦層 ApiServer 中整合針對該 CRD 的 Conversion Webhook,即可自定義二者的轉換實現了。這樣一來,在聯邦層控制面上,任何聯邦 CR 都能同時以兩種形式進行讀取和寫入,做到了上層業務僅關心部署單元(或者其他業務拓撲),而底層 KubeFed controller 仍僅關心叢集,實現了其對多拓撲聯邦 CRD 模型的無感支援。
下面以部署單元拓撲為例,簡單介紹下其與叢集拓撲之間的轉換實現。在 KubeFed 中,我們通過建立包含了子叢集訪問配置的 KubeFedCluster 物件來使其納管這一子叢集,隨後我們就可以在聯邦 CRD 的 placement 中通過 KubeFedCluster 物件的名稱來指定需要分發的子叢集。那麼,我們的轉換邏輯所要做的就是將多拓撲聯邦 CRD 中的部署單元名稱轉換為其所對應叢集的 KubeFedCluster 物件名稱。由於叢集和部署單元是 1:n 的關係,因此我們只需為每個部署單元額外建立包含其所在叢集訪問配置的 KubeFedCluster 物件,並通過統一的命名規則為其生成能夠通過部署單元所在名稱空間(即租戶與工作空間組名稱)與名稱定址到的名稱即可。
以此類推,我們可以通過類似的方式很容易地支援更多拓撲型別,極大地提高了我們在聯邦模型使用上的靈活度。
支援直接使用 MySQL/OB 作為 etcd 資料庫
對於任何一個 Kubernetes ApiServer 來說,etcd 資料庫都是必不可少的依賴。在螞蟻主站,我們有豐富的物理機資源和強大的 DBA 團隊來提供持續高可用的 etcd,但對於 SOFAStack 產品複雜繁多的對外輸出場景而言則不是這樣。在域外,運維 etcd 的成本要比運維 MySQL 要高得多,此外,SOFAStack 常常會搭配 OceanBase 一起輸出,我們也希望能夠充分利用 OB 提供的成熟的多機房容災能力解決資料庫高可用的問題。
因此,在一番調研和嘗試後,我們將 k3s 社群開源的 etcd on MySQL 介面卡 Kine 整合進了聯邦層 ApiServer,使其能夠直接使用通用的 MySQL 資料庫作為 etcd 後端,省去了單獨維護一個 etcd 的煩惱。此外,我們也針對 OB 與 MySQL 的一些差異化行為(如切主自增序跳變)進行了適配,使其也能完美相容 OB,以享受到 OB 帶來的資料高可用與強一致性。
此外,我們還在聯邦層 ApiServer 內整合了一些用於校驗與初始化聯邦相關資源的 Admission Plugin 等,由於大多和產品業務語義相關,這裡不再贅述。
值得一提的是,我們所做的這些擴充套件都具備拆解為獨立的元件與 Webhook 的能力,因此也能適用於社群原生外掛化安裝的形式,不強依賴獨立的 ApiServer。目前我們將 ApiServer 獨立出來主要是為了隔離聯邦層的資料,同時便於獨立部署和維護所需。
總結一下,從架構層面來講,聯邦層 ApiServer 主要起到了一個聯邦資源南北向橋接器的作用,如下圖所示,其通過 CRD 多版本的能力,南向為 KubeFed controller 提供了承載社群聯邦資源的 ApiServer 能力,北向則為上層業務產品提供了從業務拓撲(部署單元)到叢集拓撲的對映轉換能力。
KubeFed Controller 能力增強
前面提到,除了聯邦模型外,社群 KubeFed controller 在自身底層能力上也無法滿足我們的全部需求,因此,我們在產品化過程中對其進行了一些能力增強。其中一些通用的增強我們也貢獻給了社群,如支援設定 controller worker 併發數與多叢集 informer cache 同步超時、支援 Service 特殊欄位保留等。而一些高階能力我們都通過 Feature Gate 的形式進行了可插拔的增強,做到了與 code base 與社群上游的實時同步。下面我們就來介紹其中幾個有代表性的增強能力。
支援子叢集多租戶隔離
在 SOFAStack 產品中,無論是在公有云還是專有云,所有資源都是按租戶與工作空間(組)的粒度進行隔離的,以確保各個使用者及其下屬的各個環境之間不會互相影響。對於 KubeFed 而言,其所關心的主要資源是 Kubernetes 叢集,而社群的實現對其是不做任何隔離的,這一點從聯邦資源的刪除邏輯上就可看出:在聯邦資源被刪除時,KubeFed 會檢查其控制面中納管的所有叢集以確保該資源在所有子叢集中的單叢集資源都被刪除。在 SOFAStack 產品語義下,這麼做顯然是不合理的,會產生不同環境之間互相影響的風險。
因此,我們對聯邦資源與 KubeFed 中代表納管子叢集的 KubeFedCluster 物件進行了一些無侵入的擴充套件,通過為其注入一些 well known labels 的方式使其持有了業務層的一些後設資料,如租戶與工作空間組資訊等。藉助這些資料,我們在 KubeFed controller 處理聯邦資源時對子叢集加入了一次預選,使其對聯邦資源的任何處理都只會將讀寫範圍限制在其所隸屬的租戶與工作空間組內,做到了多租戶多環境的完全隔離。
支援灰度分發能力
對於 SOFAStack CAFE 這樣一個金融生產級釋出部署平臺而言,灰度釋出是一項必不可少的能力。對於任意的應用服務變更,我們都希望其能夠以使用者可控的方式灰度釋出到指定的部署單元中。這就對底層的多叢集資源管理框架也提出了相應要求。
從上文聯邦 CRD 的介紹中可以看到,我們通過 placement 為聯邦資源指定需要分發的部署單元(或其他拓撲)。在初次下發一個聯邦資源時,我們可以通過逐步向 placement 中新增欲釋出的部署單元來實現灰度釋出,但當我們要更新該資源時,就沒有辦法進行灰度了——此時 placement 中已經包含了所有部署單元,對聯邦資源的任何修改都會立刻同步到所有部署單元,同時,我們也不能通過將 placement 重新設定為欲釋出的部署單元來做灰度,因為這會導致其他部署單元內的資源被立即刪除。此時,為了支援灰度釋出,我們就需要一項能力支援我們指定 placement 中的哪些部署單元是要被更新的,其餘則需保持不變。
為此,我們引入了 placement mask 的概念。如其名所示,它就像是 placement 的一個掩碼一樣,當 KubeFed controller 處理聯邦資源時,它所更新的拓撲範圍就變成了 placement 和 placement mask 的交集。此時,我們只需要在更新聯邦資源時同時指定它的 placement mask,即可精細化地控制本次變更影響的部署單元範圍,實現了完全自主可控的灰度釋出能力。
如下圖所示,我們為該聯邦資源新增了僅包含部署單元 rz00a 的 placement mask,此時可以看到位於 rz00a 子資源被成功更新(更新後子資源的 generation 為 2),而 rz01a 的資源則不做處理(因而沒有產生更新後的 generation),實現了灰度釋出的效果。
值得一提的是,placement mask 的引入不僅解決了灰度釋出的問題,也解決了容災釋出的問題。在因機房災難導致部分叢集(部署單元)不可用的情況下,我們可以通過 placement mask 繼續正常釋出其他可用的部署單元,不會因區域性異常而阻塞整個多叢集的釋出。而在叢集恢復後,placement mask 的存在能夠防止對剛恢復部署單元的預期外自動變更,保證了釋出變更的強管控性。
支援自定義 Annotation 下發策略
KubeFed 對於資源的下發有一項原則,即只下發 spec 類的屬性,不下發 status 類的屬性。這一原則的出發點很簡單:我們需要保證子叢集資源的 spec 被聯邦層強管控,但又要保持其 status 各自獨立。對於任何 Kubernetes 物件而言,其絕大多數屬性都是非 spec 即 status 的——spec 和 status 屬性本身就不用說了,像 metadata 中的 name、labels 等就屬於 spec,而 creationTimestamp、resourceVersion 之流則屬於 status。但是,凡事皆有例外,這其中有一個屬性是既能充當 spec 又能充當 status 的,它就是 annotations。
很多時候,我們無法把一個 Kubernetes 物件的所有 spec 和 status 都收斂在真正的 spec 和 status 屬性內,一個最典型的例子就是 Service。對 Service 應用有所瞭解的同學應該知道,我們可以使用 LoadBalancer 型別的 Service 並搭配不同雲廠商提供的 CCM(Cloud Controller Manager)來實現不同雲平臺下負載均衡的管理。由於 Service 是 Kubernetes 內建物件,其 spec 和 status 都是固定不可擴充套件的,而不同雲廠商 CCM 支援的引數都各不相同,因此 Service 的 annotations 便自然地承載起了這些配置,起到了 spec 的作用。與此同時,部分 CCM 還支援迴流負載均衡的一些具體狀態到 annotations,比如建立負載均衡過程中的一些中間狀態包括錯誤資訊等,這時候 Service 的 annotations 又起到了 status 的作用。這時候 KubeFed 就面臨了一個問題——究竟要不要下發 annotations 欄位?社群選擇了完全不下發,這固然不會影響 annotations 作為 status 的能力,但也失去了 annotations 作為 spec 時對其的管控能力。
那麼,有沒有可能做到兩者兼得呢?答案自然是有的。在 KubeFed 中,對於每一種需要多叢集化的 Kubernetes 資源型別,都需要在聯邦層為其建立一個 FederatedTypeConfig 物件,用於指定聯邦型別與單叢集型別的 GVK 等資訊。既然 annotations 欄位的 spec/status 特性也是和具體資源型別有關的,那麼我們便可以在這個物件中做文章,為其加入了一項 propagating annotations 的配置。在該配置中,我們可以顯式地指定(支援萬用字元)該資源型別的 annotations 中,哪些 key 是用作 spec 的,對這些 key,KubeFed controller 會進行下發和管控,而其餘的 key 則作為 status 對待,不會對子叢集資源上的值進行覆蓋。藉助這項擴充套件能力,我們便能靈活地自定義任意 Kubernetes 資源型別的 annotation 下發策略,實現完整的多叢集資源 spec 管控能力。
以 Service 為例,我們對該型別的 FederatedTypeConfig 物件進行了如下配置:
下面第一張圖為 FederatedService 的 template 所指定的下發模板,第二張圖為實際子叢集中被管控 Service 的情況。可以看到,我們在聯邦資源中指定的 spec 類 annotation(如 service.beta.kubernetes.io/antcloud-loadbalancer-listener-protocol)被成功下發到了子資源上,而屬於子資源自身的 status 類 annotation(如 status.cafe.sofastack.io/loadbalancer)則被正常保留,沒有因 annotations 欄位的強管控而被刪去或覆蓋。
此外,我們還增強了 KubeFed controller 的狀態迴流能力,使其能夠實時迴流所有聯邦資源型別的 status 類欄位;支援了聯邦層子叢集訪問配置的 KMS 加密儲存以滿足金融級安全合規需求等等,受篇幅所限不再一一展開介紹。
到這裡,聯邦層已經滿足了上層單元化應用釋出運維產品的絕大多數需求,但正如前文所提到的,我們做的是一款“混合雲”的產品,在混合雲場景下,異構叢集與網路連通性的限制是我們運維 Kubernetes 叢集會遇到的最典型的問題。對聯邦層而言,由於其主要關注 Kubernetes 應用資源管理,因此叢集的異構性不會帶來太多影響,只要是符合一定版本範圍內 Kubernetes 規範的叢集理論上都能直接納管;而網路連通性的限制則是致命的:由於 KubeFed 採用推模式進行子叢集管控,它要求 KubeFed controller 需要能直接訪問到每個子叢集的 ApiServer,這對於網路連通性的要求是相當高的,很多情況下的網路環境都無法滿足這樣的要求,就算有辦法滿足也需要很高的成本(比如打通中樞叢集與所有使用者叢集之間的網路)。因此,我們勢必要尋求一種解法來降低聯邦層對叢集間網路連通性的要求,以使我們的產品能夠相容更多的網路拓撲。
整合 ApiServer Network Proxy
既然 KubeFed controller 到子叢集 ApiServer 的直接正向連線可能受限,那麼我們就需要在這兩者之間架設一個能夠反向建連的代理,通過該代理建立的長連線通道進行正向訪問。ApiServer Network Proxy(ANP)是社群為了解決 Kubernetes 叢集內部網路隔離問題而開發的 ApiServer 代理,它剛好提供了我們所需要的反向長連線代理的能力,使得我們無需正向的網路打通也能正常訪問到子叢集的 ApiServer。
但是,ANP 主要解決的是單叢集內部訪問 ApiServer 的問題,其連線模型是多個 client 訪問一個 ApiServer,但對於聯邦層這樣的多叢集管控而言,它的連線模型是一個 client 訪問多個 ApiServer。為此,我們對 ANP 的後端連線模型進行了擴充套件,使其支援了按叢集名稱進行動態選址——通過在於 ANP 服務端進行建連時告知本次連線需要訪問的叢集,ANP 服務端就會將後續請求路由到其與上報了該叢集名稱的 agent 所建立的長連線通道上。最終的架構如下圖所示,通過整合這一“多叢集擴充套件版”的 ANP,我們就能夠在更為嚴苛的網路環境下輕鬆地進行多叢集管理了。
總結
最後,讓我們通過具體產品能力來進行總結,以簡要地體現 SOFAStack CAFE 多叢集產品相較於社群版 KubeFed 的一些亮點:
● 藉助可擴充套件的多拓撲聯邦模型原生支援了 LDC 部署單元維度的多叢集應用釋出,遮蔽了底層 Kubernetes 基礎設施
● 支援多租戶,能夠在底層對 Kubernetes 叢集等資源進行租戶、工作空間級別的隔離
● 打破宣告式的掣肘,支援精細化的多叢集灰度釋出,同時支援容災釋出
● 支援自定義 annotation 下發、完整狀態迴流、叢集訪問憑證 KMS 加密等高階能力
● 藉助 ANP 支援在異構混合雲等網路連通性受限的情況下繼續以推模式管理所有使用者叢集
● 多叢集控制面可不依賴 Kubernetes 叢集獨立部署,且支援直接使用 MySQL/OB 作為後端資料庫
目前,SOFAStack 已經應用在國內外 50 多家金融機構,其中浙江農信、四川農信等企業正是藉助 CAFE 的單元化混合雲架構來進行容器應用的全生命週期管理,構建多地域、高可用的多叢集管理平臺。
未來規劃
從上文的實踐部分中可以看出,目前我們對於底層多叢集框架的應用主要還是集中在 Kubernetes 叢集納管和多叢集資源治理上,但多叢集的應用還有著更廣闊的可能性。未來,我們也會逐步演進包括但不限於如下的能力:
● 多叢集資源動態排程能力
● 多叢集 HPA 能力
● 多叢集 Kubernetes API 代理能力
● 直接使用單叢集原生資源作為模板的輕 CRD 能力
後續,我們也會繼續分享對這些能力的思考與實踐,歡迎大家持續關注我們的多叢集產品,也隨時期盼任何意見與交流。