天天看點

編寫自定義Kubernetes控制器:小心狀态漂移

Kubernetes已經取得了成功,協調了容器,也影響了雲計算。

它的控制平面的設計模式基于一個聲明式API(允許使用者表達其所需狀态)和一組控制循環(也稱為控制器),這些控制循環驅動“真實世界”達到使用者所需狀态,已被證明是足夠通用的,可管理的不僅僅是容器。

這帶來社群編寫的自定義Kubernetes控制器和API的蔓延,用于管理各種資源,如虛拟機、資料服務、軟體定義的網絡等。

然而,編寫一個玩具自定義控制器相對容易,但編寫一個生産級控制器有很多挑戰。

我們正在基于自定義Kubernetes控制器建構一個完整的控制平面,以完全自動化資料服務的生命周期管理。在這樣做的過程中,我們意識到了與生産級控制器相關的許多挑戰。本文描述了一個很少讨論的挑戰:“真實世界”可以自主地偏離使用者期望的狀态,是以它必須像Kubernetes API一樣由控制器監控。

首先,我們先概述可能的解決方案。

問題

人們通常給出的Kubernetes控制器的定義是:“一個程序,它監控某些(自定義)Kubernetes API對象上的通知,并通過修改對象描述的資源來處理每個此類對象,以使資源與該對象的所需狀态相比對。”使API對象描述的資源與所需狀态比對的行為有時稱為協調。

控制器修改哪些資源來協調API對象取決于API對象表示的内容,是以每個控制器都不同。一些控制器通過建立、更新和/或删除其他依賴的API對象來協調API對象。将API對象轉換為較低級别的API對象可以進行多個級别的轉換,但在某個時候,它會停止,因為控制器通過将副作用應用到非Kubernetes API的對象來協調API對象。

例如,如果你建立一個StatefulSet API對象,它将被轉換為一些pod API對象,然後為每個pod啟動一個容器;這些容器是Kubernetes API之外的副作用。

根據本節開頭的定義,控制器所做的所有工作都是由API對象上的通知驅動的:如果因為API對象的填充沒有變化而沒有收到新的通知,那麼控制器什麼也不做。事實上,這就是寫了多少控制器。如果控制器隻建立/更新/删除API對象,并且不直接修改Kubernetes API之外的任何内容,那麼這沒有問題。

如果控制器修改Kubernetes API之外的一些資源,那麼這樣的設計可能有缺陷。原因是它忽略了這樣一個事實,即這些資源的狀态——“真實”狀态可能會自動偏離API對象中描述的所需狀态。在這種情況下,控制器将不會收到任何通知,是以無法将資源的狀态驅動回所需狀态,并且系統無法自我修複。

讓我們看一個例子,如圖1所示。假設我們有一個控制器,負責直接管理容器化應用程式。(當然,你不想編寫這樣一個自定義控制器,因為已經有Kubernetes解決了這個問題,但是為了這個示例,請耐心聽我們說)。如果建立了一個API對象來描述具有一個副本的新應用,則控制器将生成一個運作該應用的容器。如果容器由于代碼中的錯誤、錯誤的輸入等而崩潰,控制器将永遠不會收到關于API對象的通知。然而,它必須了解容器崩潰是為了盡快産生一個替代者來自我修複!

編寫自定義Kubernetes控制器:小心狀态漂移

圖1:支援API對象的真實狀态可以自動偏離所需狀态。

是以,對于直接修改Kubernetes API外部資源的自定義Kubernetes控制器,訂閱Kubernetes API對象上的通知是不夠的。它還必須監控支援這些API對象的資源,即使真實狀态消失,也必須觸發期望狀态和真實狀态之間的協調,如圖2所示。有一個例外,即隻處理API對象的控制器也會遇到相同的問題:當控制器不是直接而是通過Kubernetes作業修改Kubernetes API之外的資源時。

編寫自定義Kubernetes控制器:小心狀态漂移

圖2:真實狀态偏離期望狀态,但控制器收到通知并做出反應。

如果你正在建構一個簡單的自定義控制器,那麼可能隻需要将協調的API對象轉換為依賴的Kubernetes内置API對象,就不會遇到我們描述的問題。但是,如果您要基于自定義控制器建構一個完整的控制平面,該控制器必須支援複雜的用例,那麼您可能需要編寫一個控制器來修改Kubernetes API之外的内容,然後我們描述的問題可能會出現。

不幸的是,不可能有通用的解決方案,因為它在很大程度上取決于自定義API對象所描述的資源的性質。在下一節中,我們将概述一些可以作為建構解決方案基礎的想法。

解決方案的想法

完全避免這個問題:

如果自定義控制器隻能通過建立/更新/删除依賴的Kubernetes API對象來完成其任務,則采用這種方法;它更簡單,完全避免了問題。控制器仍必須監控支援其實作的API對象的狀态。盡管如此,由于該狀态由其他API對象組成,控制器可以重用其用于通知其實作的API對象的相同Kubernetes機制,是以Kubernetes自動解決了該問題。

以幂等方式定期重新協調API對象:

假設修改Kubernetes API之外的資源的操作是幂等的。在這種情況下,控制器可以定期重新協調每個API對象,即使沒有收到新的通知(Kubernetes對此有内置支援),并重新執行幂等操作。如果實際狀态偏離所需狀态,則在下一次協調時,實際狀态是固定的。否則,由于應用的操作是幂等的,是以不會發生任何更改,如圖3所示。

編寫自定義Kubernetes控制器:小心狀态漂移

圖3:通過周期性地對真實狀态應用幂等操作來解決真實狀态和期望狀态之間的不比對。

這種方法會浪費資源,因為即使沒有必要,也會定期進行協調,而且還存在調整協調周期的問題。

輪詢真實狀态:

如果對支援API對象的資源的真實狀态的檢查已經是協調的一部分,并且協調不會消耗太多資源,那麼控制器可以定期重新協調每個API對象。标準協調邏輯将輪詢真實狀态,并僅在必要時進行糾正,如圖4所示。

編寫自定義Kubernetes控制器:小心狀态漂移

圖4:作為正常協調的一部分,通過定期輪詢真實狀态來解決真實狀态和期望狀态之間的不比對。

否則,如果一個完整的協調占用了太多的資源,你可以編寫隻定期輪詢每個API對象的真實狀态的代碼,并在真實狀态與所需狀态不同時觸發協調,如圖5所示。此類代碼将在控制器程序内運作,但它将與完全協調API對象的主要制循環分開。

編寫自定義Kubernetes控制器:小心狀态漂移

圖5:通過定期輪詢專用邏輯中的真實狀态并在檢測到不比對時觸發協調,解決了真實狀态和期望狀态之間的不比對。

這兩種方法的問題是:

資源消耗(可能存在大量不必要的協調/輪詢),

設定協調/輪詢的合理期限,

控制器開發人員必須編寫執行輪詢的額外邏輯(對于第二種方法)。

實時監控通知:

如果支援API對象的真實狀态内置了對其更改的流式通知的支援(與控制器使用相同的二進制),則可以包括偵聽此類通知并在收到此類通知時觸發相關API對象協調的邏輯。我們有一個真實的例子,其中每個API對象表示PostgreSQL資料庫中的一個角色。由于PostgreSQL支援通知(例如,通過觸發器),控制器可以訂閱有關角色建立/更新/删除的PostgreSQL通知,并在收到此類通知後觸發相關API對象的新協調。這相當于使控制器不僅由Kubernetes API通知驅動,還由“真實狀态通知”驅動,如圖6所示。

編寫自定義Kubernetes控制器:小心狀态漂移

圖6:由Kubernetes API通知和真實狀态通知驅動的協調。

不幸的是,有些情況下這是不可能的,因為真實狀态不支援更改通知。

如果您遇到這種情況,您仍然可以通過輪詢從實際狀态的更改中合成通知,而代價是編寫、維護和操作其他元件。你可以編寫并部署一個“輪詢器”,它輪詢真實狀态,并在發現真實狀态和所需狀态之間的差異時更新相關API對象的狀态。此類狀态更新将導緻控制器收到API對象的通知,控制器将繼續再次協調,修複真實狀态。

繼續閱讀