假如重新設計Kubernetes

最近,我和領域內的專家Vallery Lancey[1]有過一次閒聊,主題是關於Kubernetes的。具體說來,假設我們從零開始設計一個新的編排系統,不必拘泥於與現有Kubernetes的兼容性,我們可能會採取哪些不同的做法。我發現對話過程非常有意義,以至於我覺得有必要記錄下期間湧現的諸多想法,所以就有了這篇文章。

落筆前,我想強調幾點:

  • 這不是一個完全成形的設計。其中某些想法可能根本無法落地,或者需要進行大量的重構。很多章節的內容都是想到哪寫到哪。

  • 這些觀點不單純是我一個人的想法。有些是我原創的,更多的是集體思考,交流碰撞後的產物,就像Kubernetes社區中的許多設計一樣。我知道至少Vallery和Maisem Ali[2]不止一次地啓發了我的思考,還有更多我說不出名字的。如果你覺得文中有些想法很不錯,那請把它當成是集體努力的結果。如果你不太認同其中一些看法,那把它當成是我個人的小小失誤吧。

  • 文中的一些觀點是非常極端激進的。我只是嘗試把腦海的一些設計表達出來,一吐爲快。

設計原則

我過往的Kubernetes實踐經驗來自兩個截然不同的地方:一個是爲裸金屬機集羣維護MetalLB[3];另一個是在GKE SRE[4]中運維大型的集羣即服務(clusters-as-a-service)。這兩個經歷都讓我覺得,Kubernetes相當複雜,要達到目前市面上宣傳的效果,往往需要做大量的前置工作,而大多數躍躍欲試的用戶對此都沒有充分的準備。

維護MetalLB的經歷告訴我,想構建與Kubernetes集成的健壯性優異的軟件十分困難。我認爲MetalLB穩定性堪稱優秀,但是Kubernetes還是非常容易使它出現配置錯誤的情況,而調試起來也相當費勁。GKE SRE的運維經歷則教會我,即使是最出色的Kubernetes專家也無法不出差錯地運維大規模Kubernetes集羣(儘管GKE SRE藉助一些工具能做得非常出色)。

Kubernetes可以類比成編排軟件中的C ++。功能強大,特性完備,看上去似乎挺簡單,但你會不停踩坑,直到你投入大量時間和精力,去弄清它的所有原理爲止。即便如此,Kubernetes的配置和部署方式方法衆多,生態還在不停發展,以至於很難讓人覺得可以停下腳步歇口氣了。

按照這個類比,我理想的參照物是Go。如果Kubernetes是C ++,那麼編排系統中Go會是什麼樣的呢?極度簡潔,特點突出,緩慢而謹慎地拓展新特性,你可以在不到一週的時間裏上手,然後就能用它去完成你的工作。

接下來,我們就遵循上面這些原則開始了。重新設計一個Kubernetes,可以不考慮和現有的兼容,另闢蹊徑,該考慮哪些點呢?

可修改的Pod

在Kubernetes中,大部分(但不是全部)Pod在創建後是不可變的。如果你想直接修改一個Pod,不行。得重新創建一個再刪掉舊的。這與Kubernetes中的大多數資源對象的處理方式不同,在Kubernetes中的大多數對象都是可變的,並且當預期狀態出現變化時,可以優雅地將實際狀態協調到與預期一致。

因此,我不想讓Pod成爲一個例外。我打算將它設計成完全可讀寫,並讓它擁有像其它資源對象一樣的調諧邏輯。

對此我第一時間想到的方案是原地重啓。如果Pod的調度約束和需求資源前後沒有改變,猜一下如何實現?發出SIGTERM信號終止runc,使用不同的參數重新啓動runc,就已完成重啓。這樣一來,Pod有點像從前的systemd服務,必要時還可以在不同機器之間漂移。

請注意,這不需要在運行時層面操作可變性。如果用戶更改了Pod定義,仍然可以先終止容器並使用新配置重新啓動容器。Pod仍會保留在原節點上預留的資源,因此從概念上講,它等效於systemctl restart blah.service。你也可以嘗試在並在運行時層級上來執行Pod的更新操作,但其實沒有必要這樣做。不這麼做的主要好處是將調度、Pod生命週期及運行時生命週期管理解耦。

版本管理

來繼續討論Pod的設計:既然它現在是可變的了,那麼我接下來考慮的事情自然而然就是Pod回滾。爲此,讓我們保留Pod舊版本的定義,如此一來“回滾到特定舊版本”就輕而易舉了。

現在,Pod更新流程如下:編寫文件更新Pod定義,並進行更新以符合預期定義。更新出錯?回滾上一個版本,流程結束。

上述流程的好處是:無需依賴所謂的GitOps,即可輕鬆瞭解到集羣中應用的版本迭代。如無必要就不用引入GitOps,儘管它有不少優點。如果,你只希望解決一個很基本的“集羣發生了什麼變化?”的問題,僅僅使用集羣中的數據就夠了。

其中還涉及到更多設計細節。尤其是我想將外部更改(用戶提交Pod的變更)與系統變更(Kubernetes內部觸發的Pod定義變更)這兩者區分開。我還沒有考慮清楚如何對這兩種變更的歷史信息進行編碼,並使得運維人員和其他系統組件都可以獲取到這些變更。也許可以設計成完全通用的,“修改人”在提交新版本時會標識自己。然後用戶可以指定特定修改人(或排除特定修改人)以查詢某類變更(類似於標籤查詢的工作原理)。同樣的,這裏還需要更多的設計考量,我確定的是我想要一個具有版本管理特性的Pod對象,可以查詢它的歷史版本記錄。

最後,還需要考慮垃圾回收。具體說就是,對每個Pod的更改應該可以很好地進行增量壓縮。默認設置是保留所有變更內容,積累到一定數據量後,在此基礎上進行一次壓縮。保留所有變更內容也會對系統產生一定壓力,但可避免“因頻繁提交更改”而給系統的其它部分帶來影響。這裏用戶要注意,爲方便聚合,應該進行次數更少同時更有意義的變更,而不是每次改動一個字段進而產生一系列版本。

一旦有了歷史版本這個功能後,我們還可以整一些其它的小功能。例如,節點可以將最近的若干個版本的容器鏡像保留在節點上,從而使回滾更快。原來的垃圾回收超過一定期限就觸發,有了歷史版本記錄,可以更精確地保留需要的版本數。概括而言,所有編排軟件都將舊版本用作各種資源對象的GC roots,以加快回滾速度。回滾是避免服務中斷的基本方式,這是非常有價值的事情。

用PinnedDeployment替換Deployment

這是部分內容比較簡短,主要是受Vallery啓發。他的PinnedDeployment[5]設計非常讓人驚歎。PinnedDeployment使運維人員可以通過跟蹤兩個版本的Deployment狀態來控制應用發佈。這是由SRE設計的部署對象。設計人員非常清楚SRE在部署中的關注的焦點。我個人很喜歡這個設計。

這可以和上面的可版本管理、可原地更新的Pod結合得非常好,真想不到還有什麼可以添加的了。它非常清晰的解釋了多個Pod時的工作流程。要從Kubernetes各項約束中脫離來適應這一個全新的流程,可能需要做些調整,但是大體設計是非常不錯的。

顯式的編排流程

我認爲Kubernetes的“API machinery”機制最大問題是編排,即一系列獨立控制循環的鬆散集合所構成工作流程。從表面上看,這似乎是一個好主意:你有好幾十個微小的控制循環,每個控制循環只負責一個小功能。當它們被整合到一個集羣時,它們彼此互相協作以調諧資源對象狀態並收斂至符合預期的最終狀態。所以這其中有何問題?

問題就在於出現錯誤時幾乎不可能去進行調試。Kubernetes中典型的出錯,就是用戶將變更提交給集羣,然後反覆刷新以等待資源對象符合預期,如果遲遲沒有符合……那麼,來問題了。Kubernetes分辨不出“對象已符合預期”和“控制循環被中斷並阻塞了其他事物”之間的區別。你或許希望有問題的控制循環會發布它所管理對象的一些事件來幫你排錯,但總的來說它們發揮不了多少作用。

此時,你唯一的可行選擇是收集可能涉及的每個控制循環的日誌,尋找被中斷的循環。如果你對所有控制循環的工作機制都有深入的瞭解,則定位錯誤的速度可以更快一些,豐富的經驗可以讓你從資源對象的當前狀態,推斷出是哪個控制循環出錯,並正嘗試恢復運行。

這裏要注意關鍵一點,我們看待複雜度的視角已經從控制循環的設計者轉換到到了集羣運維人員。設計一個可以獨立執行單一任務的控制循環很容易(並非是說其不重要)。但是,要在集羣中維護數十個這樣的控制循環,就需要運維人員非常熟悉這些控制循環的操作,以及它們之間的交互,並嘗試理解這樣一個組織鬆散的系統。這是必須認真考慮的問題,通常設計者編寫控制循環代碼驗證其功能這樣的工作是一次性的,但是運維人員可能要終日和它們打交道,並反覆處理控制循環出現的問題。簡化那些你只需要做一次的事情對運維人員來說不公平。

爲了解決這個問題,我會參照systemd的做法。它解決了類似的生命週期管理問題:給定當前狀態A和目標狀態B,如何從A變爲B?區別是,在systemd中,操作步驟及其依賴是顯式的。你告訴systemd,你的服務單元是multi-user.target服務組的一部分,則它必須在掛載文件系統之後聯網之前啓動運行。您還可以依賴系統的其他具體組件,例如說只要sshd運行,你的服務就需要運行(聽起來像邊車,是吧?)。

這樣做的好處是systemd可以準確地告訴用戶,是系統的哪一部分發生故障,哪部分仍在運行,或是哪個前置條件沒通過。它甚至還可以打印出系統啓動的執行過程,以供分析定位問題,例如“哪個服務的啓動耗時最長”。

我想批量的照搬這些設計到我的集羣編排系統中。不過也確實需要一些微調,但大致來說:控制循環必須聲明它們對其他控制循環的依賴性,必鬚生成結構化日誌,以便用戶可以輕鬆搜索到“有關Pod X的所有控制循環的操作日誌”,並且編排系統處理生命週期事件,可以採取像systemd那樣的做法,逐個排查定位到出問題的服務組單元。

這在實踐起來會是怎麼樣的?先結合Pod的生命週期說起。可能我們將定義一個抽象的“運行”target,這是我們要達到的狀態——Pod已經啓動並且一切正常。容器運行時將添加一個任務到“運行”之前,以啓動容器。但它可能要到存儲系統完成網絡設備掛載後才能運行,因此它將在“存儲”target之後自行啓動。同樣地,對於網絡,容器希望在“網絡”target之後啓動。

現在,你的Ceph控制循環將自己安排在“存儲”target之前運行,因爲它負責啓動存儲。其他存儲控制循環也是相同的執行流程(local bind mount,NFS等)。請注意,這裏的執行流程可以是併發執行,因爲它們都聲明要在存儲準備就緒之前執行,但是並不在意在其他存儲插件的控制循環之前還是之後執行。也有可能存在例外情況!比如你編寫了一個很棒的存儲插件,它功能出色,但是必須先進行NFS掛載,然後才能運行。好了,我們只需要在nfs-mounts步驟中添加一個依賴項,就可以完成了。這就和systemd類似,我們既規定了順序,又規定了“還需要其他組件才能正常工作”這樣的硬性要求,因此用戶可以輕鬆定義服務的啓動步驟。

(此處的討論我稍微簡化了一下,並假設各項操作步驟沒有太多的循環依賴。要深入的話,這可以展開出更復雜的流程。請參閱下文進一步探討,這裏先不討論太過於複雜的流程。)

有了這些設計,編排系統可以回答用戶“爲什麼Pod沒有啓動?”用戶可以dump下Pod的啓動流程圖,並查看哪些步驟已完成,哪些步驟失敗,哪些已在運行。NFS掛載已經進行了5分鐘?會不會有可能是NFS服務器已掛掉,但控制循環沒報超時?服務的各項配置和可能的狀態,疊加出來的結果矩陣是非常龐大的:如果有了我們設計的這樣一個輔助調試的工具,這也不算個大問題。Systemd允許用戶以任意順序、任意約束往服務的啓動過程添加內容。但是當出現問題時,我仍然可以輕鬆對其進行故障排查,根據約束條件,在調試工具的輔助下,我可以第一時間定位到問題的關鍵所在。

和Systemd給系統啓動帶來的好處類似,這讓系統可以儘可能地並行執行生命週期操作,但也僅此而已。而且由於工作流程是顯式的,它還可以擴展。你的集羣是否存在這種情況:在每個Pod上都有企業定製的操作,且必須在某個生命週期階段內執行的?可以定義一個新的中間target,使其依賴於於正確的前置或後置條件,然後將控制循環掛接(hook)到這裏接收回調。編排系統將確保控制循環在生命週期的各階段發揮作用。

請注意,這還解決了諸如Istio之類的存在奇葩問題。在Istio中,它們必須注入一些額外的開發者提供的定義才能起作用。沒必要!提供對應的控制循環介入到生命週期管理中,並根據實際需要在進行調整。只要你可以向系統表示,在生命週期中某個特定階段需要執行操作的,就無需考慮通過向運維人員提供額外的資源對象去操作。

這部分內容很長,但想表達的意思很簡短。這和Kubernetes的原來的API machinery大相徑庭,因此需要大量新的設計工作才能實現。最突出的變化,控制循環不再只是簡單地觀察集羣對象的狀態並做出修正,還必須等待編排器(Orchestrator)完成對特定對象的調用,當這些對象達到符合預期的狀態時,控制循環再進一步響應。你現在可以通過註解和約定,將其在Kubernetes實現上。但除非對工作機制的細枝末節的都瞭解得一清二楚,否則就可觀察性和可調試性來說,沒什麼幫助。

有趣的是,Kubernetes已經有其中一些想法的原型實現:Initializers和Finalizers 。它們分別是生命週期兩個不同階段裏執行預操作的鉤子。它使您可以將控制循環掛接到兩個硬編碼的“target”上。他們將控制循環分爲三個部分:初始,“默認”和終結。雖然是硬編碼,這是顯式工作流程圖的雛形。我打算把這個設計推廣到更一般的情況。

顯式的字段歸屬

承接上一部分設計的適度擴展:使資源對象的每個字段都被特定的控制循環顯式擁有。該循環是唯一允許寫入該字段的循環。如果未定義所有者,則該字段可被集羣運維人員寫入,但運維人員不能寫其他任何內容。這是由API machinery(而非約定)強制執行的。

這已經是大多數情況,但還是存在字段所有權模糊不清的時候。這導致兩個問題:如果字段錯誤,則很難弄清誰負責;而且一不小心就會進入到兩個控制器修改一個字段的情況,陷入循環。

後者是MetalLB存在的大麻煩,它與其他一些負載均衡器實現方式發生了衝突。不應該出現這樣的情況。Orchestrator應該拒絕MetalLB添加到集羣中,因爲與LB相關的字段將有兩個所有者。

可能需要留個後門,讓用戶處理一個字段有多個歸屬者的情況。但簡單起見,我在這裏先不考慮,然後看看設計是不是經得起考驗。除非有充分證明支持,否則共享所有權就是一個會帶來潛在隱患的設計。

如果你還要求控制循環顯式註冊讀取的字段(並把那些沒有註冊的字段剝離出來——不準作弊),這也可以讓你做一些有意義的事情,比如證明系統收斂(沒有讀->寫->讀的循環),或是幫你照出拖慢系統響應速度的調用環節。

有且只有IPv6

我對Kubernetes網絡部分非常熟悉,它是我最想全盤推倒重來的一個部分。有很多原因造成了網絡模塊發展成今天這個局面,我並不是想說Kubernetes網絡設計得一無是處。但網絡不在我的編排體系裏。這部分內容很長,請帶點耐心。

首先,讓我們先徹底拋開Kubernetes現有的網絡。覆蓋網絡,Service, CNI,kube-proxy,網絡插件,這些統統都不要了。

(順便提一句,爲什麼網絡插件是不應該出現Kubernetes理想的設計中的。目前,已經有不少企業開始兜售他們的網絡插件了,你最好不要相信他們能保持客觀中立,讓我來列出反駁他們的理由。無論是自然界還是軟件界,所有生態系統的第一要務都是確保其繼續存在。你不能要求一個生態系統自我進化到滅亡,你必須從外部觸發滅亡。)

回到正題,現在一切歸零了。我們有容器,它們需要互相通信,和外部通信。那該做什麼?

讓我們賦予每個Pod一個IPv6地址。是的,只有一個IPv6地址。它們從哪裏來?這裏要求局域網支持IPv6(假定具備這樣的條件,畢竟我們的設計要面向未來),IP地址就從這來。你幾乎都不需要做IP地址衝突檢測,2^64足夠大,隨機生成的IP地址基本上就滿足需求了。我們需要一個機制,好讓每個節點上之間能互相發現,這樣就可以找到其他 Pod 在哪個節點上。這應該不難實現,這麼做的理由很簡單:對集羣網絡內的其他部分而言,一個 Pod 看起來就像是在運行其中的某個節點。

或者我們乾脆組一個全是唯一本地地址的網絡,然後手動在每個節點上做路由。這實現起來非常容易,而且地址分配基本上就是“隨便選一個數字就可以了”。可能還需要設計一個子集,這樣節點到節點的路由纔會更有效率,但這都是不太複雜的東西。

有個麻煩是雲服務商喜歡介入到網絡的基礎部分。所以IPAM模塊要保持可插拔性(在上文所提到工作流模型之內),這樣我們就可以做一些事情,比如向AWS解釋流量是如何轉發的。不過,使用IPv6可能就無法在GCP上運行了。

不管怎麼說,有許多備選的方案來做這部分的設計。就其根本而言,我只想在節點之間使用IPv6和配置一些基本的、簡單的路由就可以了。這樣就可以在接近零配置的情況下,解決Pod之間的連接問題,因爲IPv6是有足夠大的地址空間,我們選一些隨機數字就能完事。

如果你有更復雜的連接需求,你就把這些作爲額外的網絡接口和我設計的簡單、可預測的IPv6路由接上。需要保證節點間的通信安全?引入wireguard隧道,添加路由,通過wireguard隧道推送節點IP,完事。編排系統不需要知道這些細枝末節,除了可能會在節點生命週期管理中添加一個小小的控制循環,這樣在隧道建立好之後,才讓節點處於就緒狀態。

好了,Pod之間的互聯互通,Pod和外部網絡的連接,這兩個問題都解決了。考慮到現在只有IPv6,我們如何處理流入集羣的IPv4流量呢?

首先,我們規定IPv4只適用於Pod和Internet連通的這種情況。在集羣內必須強制使用IPv6。

我們可以用幾種方法來應對這個限制。簡單來說,我們可以讓每個節點封裝IPv4流量,爲Pod預留一小塊符合RFC 1918規範的地址空間(所有節點上預留的地址都從屬於這個空間)。這樣就可以讓它們到達IPv4互聯網,但這都是每個節點的靜態配置,根本不需要集羣可見。你甚至可以將IPv4的東西完全隱藏在控制平面中,這只是每臺機器運行時的一個實現細節。

我們也可以用NAT64和CLAT來找點樂子:讓整個網絡只用IPv6,但用CLAT來欺騙Pods,讓它們以爲自己有IPv4連接。然後在Pod內進行IPv4到IPv6的地址翻譯,並將流量發送到NAT64網關。可以是每臺機器的NAT64,也可以是集羣內的部署的。如果你需要處理大量的NAT64流量,甚至可以用一個集羣外部類似CGNAT這樣的東西。在這一點上,CLAT和NAT64已經有很好的應用:你的手機可能正是通過這樣的方式來讓你獲得IPv4地址接入互聯網。

我可能會從簡單的IPv4僞裝開始(第一種方案),因爲所需的配置量極少,都可以由每臺機器在本地處理,不會有任何交叉影響,讓我們更容易着手實現。另外,後期改起來也很方便,因爲在Pod看來都是一樣的,而且我們也不希望通過一個網絡插件來處理任何東西。

到這我們已經處理了出站方面的問題,我們有雙棧上網。接下來怎麼處理入站端呢?負載均衡器。不考慮把它構建在覈心編排系統中。編排系統應該專注於一件事:如果一個數據包的目的IP是Pod IP,就把這個數據包交付給Pod。

正好,這應該主要適合公有云的場景。廠商們也傾向於這樣的模型,這樣就可以把他們的負載均衡器產品賣給你了。好吧,你贏了,姑且先採取這樣的設計模型。不過我想要一個控制循環來控制負載均衡器,並將其與IPAM集成,這樣VPC就能明白如何將數據包路由到Pod IP。

這忽略了由物理機搭建集羣的場景。但這也不是一件壞事,因爲沒有一個放之四海而皆準的負載均衡器。如果我試圖給你一個負載均衡器,但它沒有完全按照你預想的工作。這說不定還會讓你抓狂,一氣之下裝起了Istio,這時我所討論到降低複雜性都是無用功了。

讓負載均衡器集中精力把一件事做好:如果要把數據包轉發給Pod,那就把數據包轉發給Pod。在遵循這一原則的前提下,你可以基於LVS、Nginx、無狀態、雲廠商負載均衡服務、F5等來構建負載均衡器,你可以自由發揮。這裏也許可以考慮提供幾個“默認”實現。對於負載均衡器這部分,我確實有很多想法,也許我設計的方案就挺合適。這裏的關鍵是編排系統對負載均衡器如何實現毫不關心,只要能把數據包轉發到Pod上。

我沒有觸及IPv4 ingress的問題,主要是我認爲這是負載均衡器該做的事情,讓它們各自用最合適的方式來解決問題。像Nginx這樣的代理型負載均衡器,只需要通過IPv6轉發後端就可以了,沒什麼問題。無狀態的負載均衡器可以很容易地將IPv4地址轉換成IPv6,其間有個轉換標準。源地址爲::fffff:1.2.3.4數據包到達Pod時,Pod可以將其轉回IPv4。或者乾脆將其視爲IPv6直接處理,這樣的處理方式就假定網絡中的地址都是IPv6。如果使用了無狀態翻譯方式,出站的時候需要有狀態的跟蹤機制,來映射回IPv4。但這也比原先在IPv4下采取的層層封裝方式來得好。從節點的視角來看,這完全可以通過一條額外的::fffff:0000:0000/96的路由來處理。

將極簡貫徹到底

作爲上述所有網絡問題的替代方案,我們乾脆都不要了。回到Borg式的端口分配,所有服務都在主機的網絡命名空間中運行,並且必須請求分配端口。不是監聽:80,而是監聽:%port%,然後編排系統會用一個未使用的端口號來代替。例如,最終會變成監聽主機上的:53928。

這樣的設計真的非常非常簡單。簡單到基本沒什麼需要額外做的。在分配端口時,需要做一些煩人的檢查,以避免端口衝突,這倒是一個令人頭疼的問題。還有一個端口耗盡的問題,因爲如果你的客戶端非常活躍且數量不少,65000個端口其實並不算太多。但這個真的非常非常簡潔。我個人崇尚簡潔。

我們也可以採用經典的Docker方式,將其和上面的設計結合起來:容器在自己的網路命名空間中運行,使用一些臨時的私有IP。你可以使用任意的端口,但對其他Pod和外部可見的只有那些告知運行時要暴露的端口。而且你只能聲明要暴露的容器端口,映射到主機上的端口是由容器運行時選擇的。(這裏也可以留一些後門來應對特例,你可以告訴系統你非要80端口不可,然後通過調度約束來起作用,調度到80端口沒被佔用的機器——類似於當前Kubernetes在這方面的處理。)

上面論述的關鍵點是,這些設計極大地簡化了網絡層。以至於我可以在短短几分鐘內向別人解釋清楚,確保他們能夠了解其工作機制。

缺點是這把複雜性推給了服務發現。你不能使用“純粹的DNS”作爲發現機制,因爲大多數DNS客戶端不解析SRV記錄,因此不會動態發現隨機端口。

搞笑的是,服務網格的逐漸普及讓這個問題不再是個問題。因爲人們現在假設存在一個本地智能代理,它可以做任何服務發現能做的事情,並將其映射到一個網絡視圖上,而這個視圖只被需要它的Pod看到。不過,我不太願意接受這種做法,因爲服務網格增加了太多的複雜性和維護成本,所以我不想採用它們……至少在有實踐方案表明能使它們良好運行之前,我維持這樣的觀點。

所以,我們不妨做一些類似服務網格的東西,但更簡單點。在源主機上做一些自動的IP端口轉換……不過這看起來很像kube-proxy,這隨之而來的就是複雜性和調試困難(這不是一個通過在不同的地方執行tcpdump就能解決的問題,因爲流量會在不同的跳數之間變化)。

所以,這個方案也表明顯式主機端口映射可能也算個解決方案,但仍存在很多隱藏的複雜性(我相信這就是爲什麼Kubernetes一開始就採用單Pod單IP的原因)。Borg通過強制規定解決了這種複雜性,它規定了應用都必須用自家設計的依賴庫和框架。所以這裏有個顯而易見的缺點,不能隨意更換的服務發現和負載平衡的實現框架。除非我們採用真正的服務網格,否則做不到這點。

本節描述的方案還有可改善之處,但我更傾向於上一節的實現。它的設計時要考慮的東西更多一些,但可以得到是一個可組合、可調試、可理解的系統,不需要無限制地增加功能以滿足新需求。

安全同樣重要

長篇大論的探討完網絡之後,來簡單說一下安全問題。容器默認應該被最大限度的沙盒化,並需要顯式的雙重確認步驟。

我們可以直接應用Jessie Frazelle[6]在容器安全方面出色的工作成果。打開默認的apparmor和seccomp配置文件。不允許在容器中以root身份運行。使用user命名空間進行隔離,這樣哪怕有人設法在容器中升級爲root,那也不是系統root。默認阻止所有設備掛載。不允許主機綁定掛載。爲了達到效果,寫一個你能想的的最嚴格的Pod安全策略,然後把他們作爲默認值,並且讓它們很難背離默認值。

Pod安全策略與此相當接近,因爲它們強制執行雙重確認:集羣運維人員確認允許用戶做不安全的操作,而用戶必須顯式申請權限執行不安全的操作。遺憾的是,Kubernetes現有的設計並沒有這麼考慮。這裏我們先不關心向下的兼容性,把默認值做得儘可能安全。

(溫馨提示:從這裏開始,章節內容開始有點天馬行空。這些都是我想要的設計,不過我強烈意識到,很多細節沒有考慮清楚。)

gVisor?Firecracker?

說到默認情況下的最高級別的安全,我覺得不妨採取更激進的沙盒化措施。可以考慮將gVisor或Firecracker作爲默認容器沙箱,並開啓雙重確認機制,最終達到“與主機共享內核的最安全的容器環境”這目的?

這裏需要再斟酌斟酌。一方面,這些工具所承諾的極度安全非常吸引我。另一方面,這也不可避免地要運行一大堆額外的代碼,也帶來潛在漏洞和複雜性。而且這些沙盒對你能做的事情進行了極度的限制。甚至,任何與存儲有關的事情都會演變成“不,你不能有任何存儲”。這對於某些場景而言來說是不錯,但把它變成默認值就限制得太過分了。

至少在存儲方面,virtio-fs[7]的成熟會解決很多這樣的問題,能讓這些沙盒在不破壞安全模型的前提下,執行有效地綁定和掛載操作。可能我們應該在那個時候再重新審視這個決定?

去中心化集羣

我猜這個時髦的術語應該是“邊緣計算”,但我真正的意思是,我想讓我所有的服務器都在一個編排系統下,把它們作爲一個單元來運作。這意味着我家裏服務器機架上的計算機,我在DigitalOcean上的虛擬機,以及在互聯網上的其他幾臺服務器。這些都應該是集羣內基本等效的一部分,實際上也具備這樣的能力。

這就導致了幾個與Kubernetes不一樣的地方。首先,工作節點應該設計得比當前更加獨立,對控制節點的依賴更少。可以在沒有控制節點的情況下長時間(極端情況下是幾周)運行,只要沒有機器故障,導致需要Pod重新調度。

我認爲主要的轉變是將更多的數據同步節點上,並存到持久化存儲中。這樣節點自身就有了恢復正常運行所需要的數據,和主節點失聯後也能從冷啓動中恢復到可響應狀態。理論上,我希望集羣編排系統在每個節點上填充一組systemd單元,在節點的運行過程中扮演一個被動管理的角色。它在本地擁有它需要的一切,除非這些東西需要改變,否則節點是獨立於管理節點的。

這確實導致瞭如何處理節點失聯的問題。在“中心化”的集羣中,這是觸發工作負載重新調度的關鍵信號,但在去中心化的情況下,我更有可能會說“別擔心,這可能是短暫的失聯,節點很快就會回來”。所以,節點生命週期以及它與Pod生命週期的關係將不得不改變。也許你必須顯式聲明你是否希望Pod是“高可用”(即當節點失聯時主動地重新調度)或“盡力”(只有當系統確定一個節點已經掛了並無法恢復時才重新調度)。

一種說法是,在我設計的“去中心化”集羣中,Pod的表現更像是“獨一無二的寵物”而不是“牧場裏的羊羣”。我會考慮設計類似無狀態應用水平擴展的機制,但與Kubernetes不同,在這個場景下,當應用副本數縮小到一個時,我可以干預這一個應用運行在哪個節點上。這是Kubernetes不鼓勵的做法,所以此處我們不得不背離Kubernetes的某些做法。

另一種觀點是,將集羣聯邦視爲一級對象。實際上可以把分散的機器看成是單獨的集羣,各自有自己的控制平面,然後將它們整合作爲一個超大型集羣來使用。這當然可以,並且回答了一些關於如何將節點與控制平面解耦的問題(我個人的答案:不要嘗試這麼做,應當將控制平面的功能儘可能地下放到數量龐大的工作節點)。在這種情況下,我希望控制平面是極其精簡的,否則在Kubernetes中這樣做的開銷會很大,我個人希望避免這種情況。

這也提高了網絡部分的難度,因爲我們現在必須跨網連接。我的做法是以某種方式去和Tailscale[8]集成,這剛好解決我們需求。也可以選擇需要一些更定製化的、組件更少的方案(不要進行多餘的NAT轉換)。

納管虛擬機

注意:當我在這裏說虛擬機的時候,我並不是指“用戶在Kubernetes上運行的Pod”。我指的是管理員自己創建的hypervisor的服務器虛擬機。類似Proxmox或ESXi創建出來的,但不是EC2這種託管的。

我希望我的編排系統能夠無縫地管理容器和虛擬機。否則,在實踐中,我將需要一個單獨的hypervisor,那樣一來我將有兩套的管理系統。

我不確定這究竟會成爲一種怎樣的設計,只是一個粗略的想法。kubevirt提供的功能應該內置到系統中,併成爲系統關鍵的一部分,就像容器一樣。這是一個相當龐大的問題,因爲這可能意味着從“讓我運行一個帶有虛擬軟盤的系統”到“運行一個看起來和感覺都有點像EC2的管理程序”,這是非常不同的兩件事。我唯一確定的是,我不希望運行同時運行Proxmox和這套編排系統,但我確實需要同時擁有虛擬機和容器。

如何存儲?

在我當前的設想中,存儲是一個巨大未知數。我缺乏充足的經驗,沒有太多獨到的見解。我覺得CSI太複雜了,應當精簡,但除了上面提到的,與生命週期工作流程有關的那一小部分,我也沒有好的想法可以提出來。存儲是我目前唯一個想保留目前Kubernetes插件化設計的模塊,不過一旦我對這方面的知識瞭解到位,我可能會有不同想法。

最後

寫到這裏,文章的內容很多,我相信我可能遺漏了一些我一開始想解決的問題或是一些古怪的想法。但是,如果我明天就要着手替換Kubernetes,上面列的幾點應該是我一定要改的地方。

我沒有過多提及這個行業內的其他玩家——Hashicorp的Nomad,Facebook的Twine,Google的Borg和Omega,Twitter的Mesos。除了Borg之外,我還沒有實踐過其它方案,無法對其有深刻見解。如果要着手開發一個全新的Kubernetes,我一定先投入更多的時間去了解清楚這些競品,這樣我就可以取其精華,去其糟粕。我也會對Nix[9]進行深入的思考,好好想想如何把它糅合到我的設計中。

老實說,我可能也只是想想而已,什麼也沒實踐。我從Borg上學到了很多關於雲計算理念的精髓,而Kubernetes也促使我進行了反思。我目前依舊相信,最好的容器編排系統就是沒有容器編排系統,而這種努力將不惜一切代價避免Kubernetes各種坑。顯然,這個想法與構建容器編排系統是格格不入的。

相關鏈接:

  1. https://timewitch.net/

  2. https://twitter.com/maisem_ali

  3. https://www.metallb.org/

  4. https://cloud.google.com/kubernetes-engine

  5. https://timewitch.net/post/2019-12-30-pinneddeployments/

  6. https://blog.jessfraz.com/

  7. https://virtio-fs.gitlab.io/

  8. https://www.tailscale.com/

  9. https://nixos.org/

原文鏈接:https://blog.dave.tf/post/new-kubernetes/

Kubernetes實戰培訓

Kubernetes實戰培訓將於2020年12月25日在深圳開課,3天時間帶你係統掌握Kubernetes,學習效果不好可以繼續學習。本次培訓包括:雲原生介紹、微服務;Docker基礎、Docker工作原理、鏡像、網絡、存儲、數據卷、安全;Kubernetes架構、核心組件、常用對象、網絡、存儲、認證、服務發現、調度和服務質量保證、日誌、監控、告警、Helm、實踐案例等,點擊下方圖片或者閱讀原文鏈接查看詳情。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章