K8S 控制器模式

Kubernetes模型通常由以下部分組成:

TypeMeta

TypeMeta是Kubernetes對(duì)象的最基本定義,它通過(guò)引入GKV(Group,Kind,Version)定義了一個(gè)對(duì)象的類型。

Group

Kubernetes定義了非常多對(duì)象,如何歸類這些對(duì)象是一門學(xué)問(wèn),將對(duì)象依據(jù)其功能范圍歸入不同的分組,比如把支撐最基本功能的對(duì)象歸入core組,把與應(yīng)用部署有關(guān)的對(duì)象歸入apps組,會(huì)使這些對(duì)象可維護(hù)性和可理解性更高。

Kind

定義一個(gè)對(duì)象的基本類型,比如Node,Pod,Deployment等。

Version

社區(qū)每個(gè)季度會(huì)推出一個(gè)Kubernetes版本,隨著Kubernetes版本的演進(jìn),對(duì)象從創(chuàng)建之初到能夠完全生產(chǎn)化就緒的版本是不斷變化的。與軟件版本類似,通常社區(qū)提出一個(gè)模型定義以后,隨著該對(duì)象不斷成熟,其版本可能會(huì)從v1alpha1,到v1alpha2,或者到v1beta1,最終變成生產(chǎn)就緒版本v1。

Kubernetes通過(guò)Version屬性來(lái)控制版本。當(dāng)不同版本的對(duì)象定義發(fā)生變更時(shí),有可能需要涉及到數(shù)據(jù)遷移,Kubernetes API Server允許通過(guò)Conversion方法轉(zhuǎn)換不同版本的對(duì)象屬性。這是一種自動(dòng)數(shù)據(jù)遷移的機(jī)制,當(dāng)集羣版本升級(jí)以后,已經(jīng)創(chuàng)建的老版本對(duì)象會(huì)被自動(dòng)轉(zhuǎn)換為新版本。

這里所説的版本是對(duì)外版本(External Version),用戶通過(guò)API能看到的版本。事實(shí)上資源定義都有對(duì)內(nèi)版本(Internal Version),在Kubernetes API Server處先將對(duì)外版本轉(zhuǎn)換成對(duì)內(nèi)版本,然后再進(jìn)行持久化。

Metadata

TypeMeta定義了“我是什麼”,Metadata定義了“我是誰(shuí)”。為方便管理,Kubernetes將不同用戶或不同業(yè)務(wù)的對(duì)象用不同的Namespace隔離。Metadata中有兩個(gè)最重要屬性——Namespace和Name,分別定義了對(duì)象的Namespace歸屬及名字,這兩個(gè)屬性唯一定義了某個(gè)對(duì)象實(shí)例。

前面説過(guò),所有對(duì)象都會(huì)以API的形式發(fā)佈供用戶訪問(wèn),Typemeta、Namespace和Name唯一確定了該對(duì)象所在的API訪問(wèn)路徑,該路徑也會(huì)被自動(dòng)生成并保存在對(duì)象Metadata屬性的selfLink中,如下所示:

selfLink: /api/v1/namespaces/default/pods/nginx-6ccb6b48dd-zvfrj
Label

傳統(tǒng)面向?qū)ο笤O(shè)計(jì)系統(tǒng)中,對(duì)象組合的方法通常是內(nèi)嵌或引用,即將對(duì)象A內(nèi)嵌到對(duì)象B中,或者將對(duì)象A的ID內(nèi)嵌到對(duì)象B中。這種設(shè)計(jì)的弊端是這種關(guān)係是固化的,一個(gè)對(duì)象可能對(duì)多個(gè)其他對(duì)象發(fā)生關(guān)聯(lián),如果該對(duì)象發(fā)生變更,系統(tǒng)需要遍歷所有其關(guān)聯(lián)對(duì)象并做修改。

Kubernetes採(cǎi)用了更巧妙的方式管理對(duì)象和對(duì)象的松耦合關(guān)係,其依賴的就是Label和Selector。Label,顧名思義就是給對(duì)象打標(biāo)籤,一個(gè)對(duì)象可以有任意對(duì)標(biāo)籤,其存在形式是鍵值對(duì)。不像名字和UID,標(biāo)籤不需要獨(dú)一無(wú)二,多個(gè)對(duì)象可以有同一個(gè)標(biāo)籤,每個(gè)對(duì)象可以有多組標(biāo)籤。

Label定義了這些對(duì)象的可識(shí)別屬性,Kubernetes API支持以Label作為過(guò)濾條件查詢對(duì)象。因此Label通常用最簡(jiǎn)形式定義:

metadata:
  labels:
    app: web
    tier: front

其他對(duì)象只需要定義Label Selector就可按條件查詢出其需要關(guān)聯(lián)的對(duì)象。Label的查詢可以基于等式如app=web,或app!=db,或基于集合如app in (web, db)或app notin (web, db),可以只查詢Label鍵,比如app。Label對(duì)多個(gè)條件查詢只支持“與”操作,如app=web, tier=front。

Annotation

Annotation與Label一樣用鍵值對(duì)來(lái)定義,但其功能與Label不一樣,所有在用法上也有不同原則,API也不支持針用Annotation做條件過(guò)濾。雖然Kubernetes把對(duì)象做了很好的抽象,在實(shí)際運(yùn)用中特別是生產(chǎn)化落地過(guò)程中,總是需要保存一些在對(duì)象內(nèi)置屬性中無(wú)法保存的信息,Annotation就是為了滿足這類需求,事實(shí)上Annotation是對(duì)象的屬性擴(kuò)展。社區(qū)在開(kāi)發(fā)新功能,需要對(duì)象發(fā)生變更之前,往往會(huì)先把需要變更的屬性放在Annotation中,當(dāng)功能經(jīng)歷完實(shí)驗(yàn)階段再將其移至正式屬性中。

Annotation作為屬性擴(kuò)展,更多是面向系統(tǒng)管理員和開(kāi)發(fā)人員的,因此Annotation需要像其他屬性一樣做合理歸類。與Java開(kāi)發(fā)中的包名設(shè)計(jì)類似,通常需要將系統(tǒng)以不同功能規(guī)劃為不同的Annotation Namespace,其鍵應(yīng)以如下形式存在:<namespace>/key:value, 比如一個(gè)最常用場(chǎng)景,為Pod標(biāo)記如下Annotation以吿知Prometheus為其抓取系統(tǒng)指標(biāo)。

annotations:    
    prometheus.io/path: /mymetrics
    prometheus.io/port: "7355"
    prometheus.io/scrape: "true"
Finalizer

如果只看社區(qū)實(shí)現(xiàn),那麼該屬性毫無(wú)存在感,因?yàn)樵谏鐓^(qū)代碼中,很少有對(duì)Finalizer的操作。但在企業(yè)化落地過(guò)程中,它是一個(gè)十分重要,值得重點(diǎn)強(qiáng)調(diào)的屬性。因?yàn)镵ubernetes不是一個(gè)獨(dú)立存在的系統(tǒng),它最終會(huì)跟企業(yè)資源和系統(tǒng)整合,這意味著Kubernetes會(huì)操作這些集羣外部資源或系統(tǒng)。試想一個(gè)場(chǎng)景,用戶創(chuàng)建了一個(gè)Kubernetes對(duì)象,假設(shè)對(duì)應(yīng)的控制器需要從外部系統(tǒng)獲取資源,當(dāng)用戶刪除該對(duì)象時(shí),控制器接收到刪除事件后,會(huì)嘗試釋放該資源??墒侨绻藭r(shí)外部系統(tǒng)無(wú)法連通,并且同時(shí)控制器發(fā)生重啟了會(huì)有何后果?該對(duì)象永遠(yuǎn)泄露了。

Finalizer本質(zhì)上是一個(gè)資源鎖,Kubernetes在接收到某對(duì)象的刪除請(qǐng)求,會(huì)檢查Finalizer是否為空,如果為空則只對(duì)其做邏輯刪除,即只會(huì)更新對(duì)象中metadata.deletionTimestamp字段。具有Finalizer的對(duì)象,不會(huì)立刻刪除,需等到Finalizer列表中所有字段被刪除后,也就是該對(duì)象相關(guān)的所有外部資源已被刪除,這個(gè)對(duì)象才會(huì)被最終被刪除。

因此,如果控制器需要操作集羣外部資源,則一定要在操作外部資源之前為對(duì)象添加Finalizer,確保資源不會(huì)因?qū)ο髣h除而泄露。同時(shí)控制器需要監(jiān)聽(tīng)對(duì)象的更新時(shí)間,當(dāng)對(duì)象的deletionTimestamp不為空時(shí),則處理對(duì)象刪除邏輯,回收外部資源,并清空自己之前添加的Finalizer。

ResourceVersion

通常在多線程操作相同資源時(shí),為保證實(shí)物的一致性,需要在對(duì)象進(jìn)行訪問(wèn)時(shí)加鎖,以確保在一個(gè)線程訪問(wèn)該對(duì)象時(shí),其他線程無(wú)法修改該對(duì)象。排它鎖的存在確保某一對(duì)象在同一時(shí)刻只有一個(gè)線程在修改,但其排它的特性會(huì)讓其他線程等待鎖,使得系統(tǒng)整體效率顯著降低。

ResourceVersion可以被看做是一種樂(lè)觀鎖,每個(gè)對(duì)象在任意時(shí)刻都有其ResourceVersion,當(dāng)Kubernetes對(duì)象被客戶端讀取以后,ResourceVersion信息也被一併讀取??蛻舳烁膶?duì)象并回寫(xiě)APIServer時(shí),ResourceVersion會(huì)被增加,同時(shí)APIServer需要確?;貙?xiě)的版本比服務(wù)器端當(dāng)前版本高,在回寫(xiě)成功后服務(wù)器端的版本會(huì)更新為新的ResourceVersion。因此當(dāng)兩個(gè)線程同時(shí)訪問(wèn)某對(duì)象時(shí),假設(shè)它們獲取的對(duì)象ResourceVersion為1。緊接著第一個(gè)線程修改了對(duì)象,資源版本會(huì)變?yōu)?,回寫(xiě)至APIServer以后,該對(duì)象服務(wù)器端ResourceVersion會(huì)被更新為2。此時(shí)如果第二個(gè)線程對(duì)該對(duì)象在1的版本基礎(chǔ)上做了更改,回寫(xiě)APIServer時(shí),所帶的新的版本信息也為2,APIServer校驗(yàn)會(huì)發(fā)現(xiàn)第二個(gè)線程新寫(xiě)入的對(duì)象ResourceVersion與服務(wù)器端ResourceVersion衝突,寫(xiě)入失敗,需要第二個(gè)線程讀取最新版本重新更新。

此機(jī)制確保了分佈式系統(tǒng)中,任意多線程無(wú)鎖併發(fā)訪問(wèn)對(duì)象,極大提升系統(tǒng)整體效率。

Spec和Status

Spec和Status才是對(duì)象的核心,Spec是用戶的期望狀態(tài),由創(chuàng)建對(duì)象的用戶端定義。Status是對(duì)象的實(shí)際狀態(tài),由對(duì)應(yīng)的控制器收集實(shí)際狀態(tài)并更新。與TypeMeta和Metadata等通用屬性不同,Spec和Status是每個(gè)對(duì)象獨(dú)有的,后續(xù)的章節(jié)會(huì)通過(guò)介紹一些核心對(duì)象來(lái)深入理解。

為方便對(duì)Kubernetes對(duì)象的理解,下圖展示了按照業(yè)務(wù)目的歸類的常用Kubernetes對(duì)象和其分組。Kubernetes對(duì)象設(shè)計(jì)完全遵循互補(bǔ)的原則。鼓勵(lì)A(yù)PI對(duì)象儘量實(shí)現(xiàn)面向?qū)ο笤O(shè)計(jì)時(shí)的要求,即“高內(nèi)聚,松耦合”,對(duì)業(yè)務(wù)相關(guān)的概念有一個(gè)合適的分解,提高分解出來(lái)的對(duì)象的可重用性。高層API對(duì)象設(shè)計(jì)一定是從業(yè)務(wù)出發(fā)的,低層API對(duì)象能夠被高層API對(duì)象所使用,從而實(shí)現(xiàn)減少宂馀、提高重用性的目的。

核心對(duì)象概覽

常用Kubernetes對(duì)象和其分組

核心對(duì)象概覽

Kubernetes的對(duì)象設(shè)計(jì)避免了簡(jiǎn)單封裝和內(nèi)部隱藏機(jī)制。簡(jiǎn)單地封裝,A對(duì)象封裝了B對(duì)象的定義,實(shí)際沒(méi)有提供新的功能,反而增加了對(duì)所封裝API的依賴性。內(nèi)部隱藏的機(jī)制也非常不利于系統(tǒng)維護(hù)的設(shè)計(jì)方式。例如StatefulSet、ReplicaSet和DaemonSet,如圖所示,本來(lái)就是三種Pod集合,那麼Kubernetes就用不同API對(duì)象來(lái)定義它們,而不會(huì)説將它們封裝在同一個(gè)資源對(duì)象,內(nèi)部再通過(guò)特殊的隱藏算法再來(lái)區(qū)分這個(gè)資源對(duì)象是有狀態(tài)的、無(wú)狀態(tài)的還是節(jié)點(diǎn)服務(wù)。Pod是Kubernetes應(yīng)用程序的基本執(zhí)行單元,即它是Kubernetes對(duì)象模型中創(chuàng)建或部署的最小和最簡(jiǎn)單的單元。多數(shù)核心對(duì)象都為Pod對(duì)象服務(wù)的,但是它們都從Pod對(duì)象中所剝離出來(lái)的,有自己的API定義。Secret、ConfigMap和PVC是不同的資源對(duì)象定義,都可以作為存儲(chǔ)卷在Pod中使用。而在Pod中使用時(shí),只需要指定該對(duì)象的名稱即可,無(wú)需將其具體信息在Pod資源對(duì)象中擴(kuò)展。

核心對(duì)象關(guān)系圖
Namespace

Namespace是Kubernetes進(jìn)行歸類的對(duì)象,當(dāng)一個(gè)集羣有多個(gè)用戶或一個(gè)用戶有多個(gè)應(yīng)用需要管理時(shí),有時(shí)需要將所有被管理的對(duì)象進(jìn)行一定的隔離。Kubernetes引入了Namespace對(duì)象,類似文件目錄,不同對(duì)象被劃分到不同Namespace以后,可以通過(guò)權(quán)限控制來(lái)限制哪些用戶以何種權(quán)限訪問(wèn)哪些Namespace的哪些對(duì)象,進(jìn)而構(gòu)建一個(gè)多租戶、彼此隔離的通用集羣。

Pod

容器云平臺(tái)需要解決的最核心問(wèn)題是應(yīng)用運(yùn)行,Kubernetes將容器化應(yīng)用運(yùn)行的實(shí)體抽象為Pod,Pod類似豆莢,它是一個(gè)或者多個(gè)容器鏡像的組合。當(dāng)應(yīng)用啟動(dòng)以后,每一個(gè)容器鏡像對(duì)應(yīng)一組進(jìn)程,而同一個(gè)Pod的所有容器中的進(jìn)程默認(rèn)公用同一網(wǎng)絡(luò)Namespace,并且共用同一網(wǎng)絡(luò)標(biāo)識(shí)。Pod具有基本的自恢復(fù)能力,當(dāng)某個(gè)副本出現(xiàn)問(wèn)題時(shí),它會(huì)按照預(yù)定策略被重啟。

當(dāng)然,應(yīng)用運(yùn)行通常需要配置文件,這些配置文件又有可以明文讀寫(xiě)的配置,也包含需要加密和嚴(yán)格權(quán)限控制的密碼證書(shū)等配置,Kubernetes為這些配置分別定義了Configmap和Secret。Configmap和Secret,和PersistVolumeClaim類似,都可以作為卷加載給運(yùn)行的Pod,Pod中運(yùn)行的進(jìn)程可以像訪問(wèn)本地文件一樣訪問(wèn)它們。Configmap和Secret沒(méi)有本質(zhì)區(qū)別,Secret只是將內(nèi)容進(jìn)行base64編碼,我們知道base64編碼是一種對(duì)稱加密,可以輕鬆解密,事實(shí)上沒(méi)有太多安全性可言。但Kubneretes支持Secret在持久化時(shí)的加密存儲(chǔ),這樣保存在硬盤的Secret數(shù)據(jù)是無(wú)法解密的。其次,Kubernetes可以通過(guò)權(quán)限嚴(yán)格控制能夠訪問(wèn)Secret的用戶,以保證密碼和證書(shū)信息的安全。

Pod除了包含用戶希望運(yùn)行的容器鏡像和配置文件,還允許用戶定義其運(yùn)行所需的資源,用戶創(chuàng)建Pod以后,Kubernetes會(huì)為其選擇一個(gè)最佳節(jié)點(diǎn)運(yùn)行。計(jì)算節(jié)點(diǎn)被抽象成Node對(duì)象,節(jié)點(diǎn)數(shù)量和每個(gè)節(jié)點(diǎn)的資源彙總起來(lái)就是整個(gè)集羣能提供的算力。每個(gè)計(jì)算節(jié)點(diǎn)負(fù)責(zé)彙報(bào)自己的心跳信息,并上報(bào)節(jié)點(diǎn)的資源總量和可用資源。

ServiceAccount

Pod中運(yùn)行的進(jìn)程有時(shí)需要與Kubernetes API通信,在啟用了安全配置的集羣后,Pod一定要以某種身份與Kubernetes通信,這個(gè)身份就是系統(tǒng)賬戶(ServiceAccount)。Kubernetes會(huì)默認(rèn)為每個(gè)Namespace創(chuàng)建一個(gè)default ServiceAccount,并且為每個(gè)ServiceAccount生成一個(gè)JWT Token,這個(gè)Token保存在Secret中。用戶可以在其Pod定義中指定ServiceAccount(默認(rèn)為default),其對(duì)應(yīng)的Token會(huì)被掛載在Pod中,Pod中的進(jìn)程可以帶著該Token與Kubernetes通信以標(biāo)識(shí)其身份。

ReplicaSet

Pod只是單個(gè)應(yīng)用實(shí)例的抽象,要構(gòu)建高可用應(yīng)用,通常需要構(gòu)建多個(gè)同樣的副本,提供同一個(gè)服務(wù)。Kubernetes為此抽象出副本集ReplicaSet,其允許用戶定義Pod的副本數(shù),每一個(gè)Pod都會(huì)被當(dāng)作一個(gè)無(wú)狀態(tài)的成員管理,Kubernetes保證總是有用戶期望的數(shù)量的Pod正常運(yùn)行。當(dāng)某個(gè)副本宕機(jī)以后,控制器將會(huì)創(chuàng)建一個(gè)新的副本。當(dāng)因業(yè)務(wù)負(fù)載發(fā)生變更而需要調(diào)整擴(kuò)縮容時(shí),可以方便地調(diào)整副本數(shù)量。

Deployment

對(duì)于無(wú)狀態(tài)在線應(yīng)用,Kubernetes提供了更高級(jí)的版本變更控制。版本變更是一個(gè)日常頻繁發(fā)生的關(guān)鍵操作,如何在不中斷業(yè)務(wù)的前提下更新版本,一直是業(yè)界努力解決的問(wèn)題。Deployment就是一個(gè)用來(lái)描述發(fā)佈過(guò)程的對(duì)象,其實(shí)現(xiàn)機(jī)制是,當(dāng)某個(gè)應(yīng)用有新版本發(fā)佈時(shí),Deployment會(huì)同時(shí)操作兩個(gè)版本的ReplicaSet。其內(nèi)置多種滾動(dòng)升級(jí)策略,會(huì)按照既定策略降低老版本的Pod數(shù)量,同時(shí)創(chuàng)建新版本的Pod,并且總是保證正在運(yùn)行的Pod總數(shù)與用戶期望副本數(shù)一致,并依次將該Deployment中的所有副本都更新至新版本。下圖展示了基于Deployment進(jìn)行版本發(fā)佈的一箇中間狀態(tài)。

Deployment的滾動(dòng)升級(jí)

因?yàn)镈eployment會(huì)維護(hù)ReplicaSet,ReplicaSet會(huì)創(chuàng)建Pod,因此通過(guò)Deployment維護(hù)針對(duì)無(wú)狀態(tài)的應(yīng)用是第一選擇,它可以滿足諸多需求,縮短應(yīng)用上線的時(shí)間,在不造成停機(jī)的情況下創(chuàng)建彈性部署,能夠使用戶更快或更頻繁地發(fā)佈應(yīng)用和功能。

  • 創(chuàng)建并保證目標(biāo)數(shù)量的Pod在運(yùn)行狀態(tài)。

  • 按既定策略滾動(dòng)升級(jí),同時(shí)支持升級(jí)暫停、恢復(fù)和回滾。選擇滾動(dòng)升級(jí)策略非常靈活,正確的策略對(duì)于交付彈性應(yīng)用程序和基礎(chǔ)架構(gòu)都是至關(guān)重要的。

  • 便利的擴(kuò)容和縮容。

Service和Ingress

即使在傳統(tǒng)平臺(tái)中,為支持應(yīng)用的高可用,都需要在應(yīng)用實(shí)例之上構(gòu)建負(fù)載均衡。Service和Ingress就是描述負(fù)載均衡配置的對(duì)象,它允許用戶定義發(fā)佈服務(wù)的協(xié)議和端口,并定義Selector選擇后端服務(wù)的Pod。Selector本身是一個(gè)Label過(guò)濾器,它會(huì)選擇所有Label與該Selector匹配的Pod作為目標(biāo)。Kubernetes會(huì)為Service和其選擇出來(lái)的Pod創(chuàng)建一個(gè)關(guān)聯(lián)對(duì)象,Endpoint里面記錄了所有Pod的IP,以及就緒狀態(tài),這些信息會(huì)被相應(yīng)組件作為期望狀態(tài)進(jìn)行負(fù)載均衡配置。Ingress是在服務(wù)的基礎(chǔ)上,定義API網(wǎng)關(guān)的對(duì)象。通過(guò)Ingress,用戶可以定義七層轉(zhuǎn)發(fā)規(guī)則、網(wǎng)關(guān)證書(shū)等高級(jí)路由功能。

PersistentVolume和PersistentVolumeClaim

PersistentVolume(PV)是集羣中的一塊存儲(chǔ)卷,可由管理員手動(dòng)設(shè)置,或當(dāng)用戶創(chuàng)建PersistentVolumeClaim(PVC)時(shí)根據(jù)StorageClass動(dòng)態(tài)設(shè)置。PV和PVC與Pod生命週期無(wú)關(guān)。也就是説當(dāng)Pod中的容器重新啟動(dòng)、Pod重新調(diào)度或者刪除時(shí),PV和PVC不會(huì)受到影響,Pod存儲(chǔ)于PV里的數(shù)據(jù)得以保留。對(duì)于不同的使用場(chǎng)景,用戶通常需要不同屬性(例如性能、訪問(wèn)模式等)的PV。所以集羣一般需要提供各種類型的PV,由StorageClass來(lái)區(qū)分。一般集羣環(huán)境都設(shè)置了默認(rèn)的StorageClass。如果在PersistentVolumeClaim中未指定StorageClass,則使用羣集的默認(rèn)StorageClass。

CustomResourceDefinition

自定義資源定義(CRD)是Kubernetes 1.7中引入的一項(xiàng)強(qiáng)大功能,它允許用戶將自己的自定義對(duì)象添加到Kubernetes集羣中,當(dāng)創(chuàng)建新CRD的定義時(shí),APIServer將為指定的每個(gè)版本創(chuàng)建一個(gè)新的RESTful資源路徑。當(dāng)集羣中成功地創(chuàng)建了CRD,就可以像Kubernetes原生的資源一樣使用它,利用Kubernetes的所有功能,例如其CLI、安全性、API服務(wù)、RBAC等。CRD的定義是集羣范圍內(nèi)的,CRD的資源對(duì)象的作用域可以是命名空間(Namespaced)或者集羣范圍(Cluster-wide)的。與現(xiàn)有的內(nèi)置對(duì)象一樣,刪除Namespace也會(huì)刪除該Namespace中所有自定義的對(duì)象,但不會(huì)刪除CRD的定義。Kubernetes還提供一系列Codegen工具(deepcopy-gen、client-gen、lister-gen、informer-gen等),能夠自動(dòng)生成該CRD資源的Golang版本的Clientset、Lister及Informer,這為該資源編寫(xiě)控制器提供了很大便利。

CRD就像數(shù)據(jù)庫(kù)的開(kāi)放式表結(jié)構(gòu),允許用戶自定義Schema。有了這種開(kāi)放式設(shè)計(jì),使得用戶可以基于CRD定義一切需要的模型,滿足不同業(yè)務(wù)的需求。社區(qū)鼓勵(lì)基于CRD的業(yè)務(wù)抽象,眾多主流的擴(kuò)展應(yīng)用都是基于CRD構(gòu)建的,比如Istio,比如Knative。甚至基于CRD推出了Operator Mode和Operator SDK,可以以極低的開(kāi)發(fā)成本定義新對(duì)象,并構(gòu)建新對(duì)象的控制器。

控制器模式

聲明式系統(tǒng)的工作原理是什麼?當(dāng)用戶定義了對(duì)象的期望狀態(tài),Kubernetes通過(guò)何種機(jī)制確保實(shí)際狀態(tài)與期望狀態(tài)最終保持一致?定義瞭如此多的對(duì)象,那麼這些對(duì)象是如何聯(lián)動(dòng)起來(lái),完成一個(gè)個(gè)業(yè)務(wù)流的呢?祕(mì)密就是控制器模式,Kubernetes定義了一系列的控制器,事實(shí)上幾乎所有的Kubernetes對(duì)象都被一個(gè)或數(shù)個(gè)控制器監(jiān)聽(tīng),當(dāng)對(duì)象發(fā)生變化時(shí),控制器會(huì)捕獲對(duì)象變化并完成配置操作。

Kubernetes的功能組件會(huì)在后面章節(jié)中展開(kāi),但本節(jié)深入理解控制器模式有助于理解Kubernetes的運(yùn)作機(jī)制。APIServer是Kubernetes的大腦,保存了所有對(duì)象和其狀態(tài)。開(kāi)源項(xiàng)目client-go對(duì)控制器的編寫(xiě)提供了完備的自動(dòng)化支持,任何Kubernetes對(duì)象都可以由client-go創(chuàng)建供控制器使用的Informer()和Lister()接口。如圖所示,控制器的工作流程就是圍繞著Informer()和Lister()的。

  • Informer()是用來(lái)接收資源對(duì)象的變化的Event,針對(duì)Add、Update和Delete的事件,可注冊(cè)相應(yīng)的EventHandler。在EventHandler內(nèi),根據(jù)傳入的object調(diào)用controller.KeyFunc計(jì)算出字符串key,并把它加入控制器的隊(duì)列中。

  • Lister()是給控制器提供主動(dòng)查詢資源對(duì)象的接口,根據(jù)labels.Selector去指定篩選條件。

控制器模式是一個(gè)標(biāo)準(zhǔn)的生產(chǎn)者消費(fèi)者模式,一方面控制器在啟動(dòng)后,Informer會(huì)監(jiān)聽(tīng)其所關(guān)注的對(duì)象變化。一旦對(duì)象發(fā)生了創(chuàng)建,更新和刪除等事件,這些事件會(huì)由核心組件APIServer推送給控制器。控制器會(huì)將對(duì)象保存在本地緩存,并將對(duì)象的主鍵推送至消息隊(duì)列,此為生產(chǎn)者。

另一方面,控制器會(huì)啟動(dòng)多個(gè)工作子線程(Worker),從隊(duì)列中依次獲取對(duì)象主鍵,并從緩存中讀取完整狀態(tài),按照期望狀態(tài)完成配置更改并將最終狀態(tài)回寫(xiě)至APIServer,此為消費(fèi)者。

Kubernetes就是基于此模式保證了整個(gè)系統(tǒng)的最終一致性。

控制器工作流程

Kubernetes運(yùn)行一組控制器,以使資源的當(dāng)前狀態(tài)與所需狀態(tài)保持匹配?;谑录捏w系結(jié)構(gòu),控制器利用事件去觸發(fā)相應(yīng)的自定義代碼,這部分都是由SharedInformer完成。例如創(chuàng)建Deployment的控制器,其核心代碼如下:

kubeInformerFactory := kubeinformers.NewSharedInformerFactory(kubeClient, resyncPeriod)
deploymentInformer := kubeInformerFactory.Apps().V1().Deployments()
deploymentInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
  AddFunc: controller.handleObject,
  UpdateFunc: func(old, new interface{}) {
     newDepl := new.(*appsv1.Deployment)
     oldDepl := old.(*appsv1.Deployment)
     if newDepl.ResourceVersion == oldDepl.ResourceVersion {
        return
     }
     controller.handleObject(new)
  },
  DeleteFunc: controller.handleObject,
})
kubeInformerFactory.Start(stopCh)

具體地,如圖所示,SharedInformer有Reflector、Informer、Indexer和Store四個(gè)組件。

inform 內(nèi)部機(jī)制

Reflector是用來(lái)監(jiān)聽(tīng)特定的Kubernetes API資源對(duì)象,可以是Kubernetes內(nèi)建的或者是自定義的資源。具體的實(shí)現(xiàn)是通過(guò)ListAndWatch的方法。Reflector首先會(huì)將資源版本號(hào)設(shè)置為0,使用List操作獲得指定資源對(duì)象,可能會(huì)導(dǎo)致本地的緩存相對(duì)于etcd里面的內(nèi)容存在延遲。Reflector再通過(guò)Watch操作監(jiān)聽(tīng)到APIServer處資源對(duì)象的版本號(hào)變化,并將最新的數(shù)據(jù)放入到Delta FIFO隊(duì)列中,使得本地的緩存數(shù)據(jù)與etcd的數(shù)據(jù)保持一致。如果resyncPeriod不為零,那麼Reflector會(huì)以resyncPeriod為週期定期執(zhí)行Delta FIFO的Resync函數(shù),這樣就可以使Informer定期處理所有的對(duì)象。

Informer是從Delta FIFO隊(duì)列中彈出對(duì)象,一方面將對(duì)象存入本地存儲(chǔ)以供檢索,另一方面觸發(fā)事件以調(diào)用資源事件回調(diào)函數(shù)??刂破骱罄m(xù)的典型模式是獲取資源對(duì)象的key,并將該key排入工作隊(duì)列以進(jìn)行進(jìn)一步處理。Indexer提供對(duì)象的索引功能。

Indexer可以根據(jù)多個(gè)索引函數(shù)維護(hù)索引。Indexer使用線程安全的數(shù)據(jù)存儲(chǔ)來(lái)存儲(chǔ)對(duì)象及其鍵。在Store中定義了一個(gè)名為MetaNamespaceKeyFunc的默認(rèn)函數(shù),該函數(shù)生成對(duì)象的鍵的格式是<namespace>/<name>的組合。

控制器的協(xié)同工作原理

單個(gè)Kubernetes資源對(duì)象的變更,觸發(fā)多個(gè)控制器對(duì)該資源對(duì)象的變更進(jìn)行響應(yīng),繼而還能引發(fā)其相關(guān)的其他對(duì)象發(fā)生變更,從而觸發(fā)其他對(duì)象控制器的配置邏輯,這一模式使得整個(gè)系統(tǒng)成為聲明式。下圖簡(jiǎn)要描述了用戶創(chuàng)建一個(gè)Deployment對(duì)象時(shí)各個(gè)控制器是如何協(xié)同工作的。

協(xié)同工作流程示例

除APIServer和etcd外,所有Kubernetes組件,不論其名稱是Scheduler,Controller Manager、或是Kubelet,其本質(zhì)都是一致的,都可以被稱為控制器,因?yàn)檫@些組件中都有一個(gè)控制循環(huán)。他們監(jiān)聽(tīng)APIServer中的對(duì)象變更,并在自己關(guān)注的對(duì)象發(fā)生變更后完成既定的邏輯控制,并將控制邏輯執(zhí)行完成后的結(jié)果更新回APIServer,并持久化到etcd中。

APIServer作為集羣的API網(wǎng)關(guān),接收所有來(lái)自用戶的請(qǐng)求。用戶發(fā)創(chuàng)建Deployment之后,該請(qǐng)求被髮送至APIServer,經(jīng)過(guò)認(rèn)證鑑權(quán)和準(zhǔn)入三個(gè)環(huán)節(jié),該Deployment對(duì)象被保存至etcd。

Controller Manager中的Deployment Controller監(jiān)聽(tīng)APIServer中所有Deployment的變更事件,此時(shí)其捕獲了Deployment的創(chuàng)建事件,并開(kāi)始執(zhí)行控制邏輯。Deployment Controller讀取Deployment對(duì)象的Selector定義,并通過(guò)該屬性過(guò)濾當(dāng)前Namespace中所有ReplicaSet對(duì)象,并判斷是否有任何ReplicaSet對(duì)象的OwnerReference屬性為此Deployment。因?yàn)榇薉eployment剛剛創(chuàng)建,因此沒(méi)有滿足此查詢條件的ReplicaSet,于是Deployment Controller會(huì)讀取Deployment中定義的podTemplate,并將其做哈希計(jì)算,并依照如下約定創(chuàng)建新的ReplicaSet:

  • 創(chuàng)建新的ReplicaSet,將其命名為[deployment-name]-[pod-template-hash]。

  • 更新ReplicaSet,為ReplicaSet添加label,記pod-template-hash值為[計(jì)算出的哈希值]。

  • 將Deployment設(shè)置為ReplicaSet的OwnerReference。

Deployment Controller將新的ReplicaSet創(chuàng)建請(qǐng)求發(fā)送至APIServer,APIServer同樣的經(jīng)過(guò)認(rèn)證授權(quán)和準(zhǔn)入步驟,將該對(duì)象保存至etcd。

ReplicaSet Controller監(jiān)聽(tīng)APIServer中所有ReplicaSet對(duì)象的變更,新對(duì)象的創(chuàng)建令其喚醒并開(kāi)始執(zhí)行控制邏輯。ReplicaSet Controller讀取ReplicaSet對(duì)象的Selector定義,并通過(guò)該屬性過(guò)濾當(dāng)前Namespace中所有Pod對(duì)象,并判斷是否有任何Pod對(duì)象的OwnerReference為該ReplicaSet。因?yàn)榇薘eplicaSet剛剛創(chuàng)建,因此沒(méi)有滿足此查詢條件的Pod,于是ReplicaSet會(huì)按照如下約定創(chuàng)建Pod:

  • 讀取Replicas定義,Replicas的數(shù)量代表需要?jiǎng)?chuàng)建Pod的數(shù)量。

  • 以ReplicaSet名作為Pod的GenerateName,該屬性會(huì)作為Pod名的前綴,Kubernetes在此基礎(chǔ)上加一個(gè)隨機(jī)字符串作為Pod名。

  • 該ReplicaSet作為Pod的OwnerReference。

ReplicaSet Controller將新建Pod的請(qǐng)求發(fā)送至APIServer,APIServer將Pod悉數(shù)保存。

此時(shí)調(diào)度器被喚醒,其監(jiān)聽(tīng)APIServer中所有nodeName為空的Pod,即未經(jīng)過(guò)調(diào)度的Pod。經(jīng)過(guò)一系列的調(diào)度算法,不滿足Pod需求的節(jié)點(diǎn)被過(guò)濾,符合的節(jié)點(diǎn)按照空閒資源,端口占用情況,實(shí)際資源利用率等信息被排序,評(píng)分最高的節(jié)點(diǎn)名被更新至nodeName屬性,該同樣經(jīng)APIServer保存至etcd。

最后,運(yùn)行在Pod被調(diào)度節(jié)點(diǎn)的Kubelet監(jiān)聽(tīng)到有歸屬于自己節(jié)點(diǎn)的新Pod,則開(kāi)始加載Pod清單,下載Pod所需的配置信息,調(diào)用容器運(yùn)行時(shí)接口啟動(dòng)容器,調(diào)用容器網(wǎng)絡(luò)接口加載網(wǎng)絡(luò),調(diào)用容器存儲(chǔ)接口掛載存儲(chǔ),并完成Pod的啟動(dòng)。

Kubernetes就是依靠這樣的聯(lián)動(dòng)機(jī)制,通過(guò)分散的業(yè)務(wù)控制邏輯滿足用戶需求。從用戶的角度看,只是發(fā)送了一個(gè)Deployment創(chuàng)建請(qǐng)求,但事實(shí)上,為滿足該需求,可能會(huì)牽扯到數(shù)個(gè)甚至更多Kubernetes組件。此架構(gòu)模式的優(yōu)勢(shì)是每個(gè)組件各司其職,巧妙而靈活,代碼易維護(hù),但帶來(lái)的運(yùn)維複雜度相對(duì)較高,此業(yè)務(wù)流中有任何組件出現(xiàn)故障,對(duì)用戶感受來(lái)講,都是Kubernetes不可用。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容