介紹
kubernetes operator是通過連接主API并watch時間的一組進(jìn)程,一般會watch有限的資源類型。
當(dāng)相關(guān)(所watch的)event觸發(fā)的時候,operator做出響應(yīng)并執(zhí)行具體的動作。這可能僅限于與主 API 交互,但通常會涉及在其他一些系統(tǒng)上執(zhí)行某些操作(可以是集群中或集群外資源)。

Operators是控制器的集合,并且每個控制器watch了指定的資源類型。當(dāng)被watched的資源時間觸發(fā)的時候,reconcile cycle(直譯:調(diào)諧循環(huán),下文均使用reconcile cycle)也將隨之啟動。
在執(zhí)行reconcile cycle期間,控制器有責(zé)任檢查當(dāng)前狀態(tài)是否與被watched資源描述的期望狀態(tài)相匹配。有趣的是,根據(jù)設(shè)計(jì),時間并不會傳遞到reconcile cycle中,這將會強(qiáng)制地讓你去考慮實(shí)例的整個狀態(tài)。這種方法被稱為基于水平觸發(fā)而不是基于邊緣觸發(fā)(level-based, as opposed to edge-based)。這源自于電子電路的設(shè)計(jì),水平觸發(fā)是接收event(例如中斷)并對狀態(tài)做出反應(yīng)的理念,而基于邊緣的觸發(fā)是接收event并對狀態(tài)變化做出反應(yīng)的理念。
水平觸發(fā)雖說效率較低,因?yàn)樗鼜?qiáng)制重新評估完整的狀態(tài),而不是僅僅關(guān)注改變了什么,但在信號可能丟失或多次重復(fù)傳輸?shù)膹?fù)雜不可靠環(huán)境中,這種方式被認(rèn)為是更適用的。
這種設(shè)計(jì)的選擇會影響我們編寫控制器代碼的方式。
與此討論相關(guān)的還有對 API 請求生命周期的理解。下圖提供了一個高層次的總結(jié):

當(dāng)向API服務(wù)器發(fā)送請求時,特別是對于創(chuàng)建和刪除請求,它們會經(jīng)歷上圖所示的階段。需要注意的是,也可以指定webhook來執(zhí)行請求的更改和驗(yàn)證。如果operator引入了CRD(custom resource definition),我們可能還必須定義這些webhook。一般來說,operator進(jìn)程會開放一個端口來來實(shí)現(xiàn)webhook endpoint。
本文介紹了一系列在使用Operator SDK來設(shè)計(jì)和開發(fā)Operator時需要牢記的最佳實(shí)踐。
如果你的operator引入了一個新的CRD,Operator SDK將會協(xié)助你來搭建。為確保您的 CRD 符合 Kubernetes 擴(kuò)展 API 的最佳實(shí)踐,請遵循這些約定。
文中所提到的所有的最佳實(shí)踐都在operator-utils代碼庫中,并以可運(yùn)行的例子體現(xiàn)。在你的operator項(xiàng)目中,也可以將operator-utils以library的方式導(dǎo)入,以此提供給你一些有用的工具。
最后,這組編寫operator的最佳實(shí)踐僅代表我的個人觀點(diǎn),不應(yīng)被視為Red Hat的官方最佳實(shí)踐。
創(chuàng)建 watches
正如我們所說,控制器watch著資源events。
watch是一種接收某種類型(核心類型或CRD)的機(jī)制。一般通過指定以下內(nèi)容來創(chuàng)建watch機(jī)制:
想要watch的資源類型
handler。handler將被監(jiān)視類型上的events映射到一個或多個調(diào)用協(xié)調(diào)周期的實(shí)例。監(jiān)視類型和實(shí)例類型不必相同。
predicate。predicate是一組能夠過濾我們感興趣的events且可自定義的函數(shù)。
下圖記錄了以上提及的內(nèi)容:

通常來說,同一類型(kind)開啟多個watch是可行的,因?yàn)閣atch是多路復(fù)用的。
你也應(yīng)該盡可能多地嘗試過濾event。這邊有個predicate例子,用來過濾secret資源上的event。這里你只對類型為TLS的secret資源event感興趣:
isAnnotatedSecret:=predicate.Funcs{
UpdateFunc:func(eevent.UpdateEvent)bool{
oldSecret,ok:=e.ObjectOld.(*corev1.Secret)
if!ok{
returnfalse
}
newSecret,ok:=e.ObjectNew.(*corev1.Secret)
if!ok{
returnfalse
}
ifnewSecret.Type!=util.TLSSecret{
returnfalse
}
oldValue,_:=e.MetaOld.GetAnnotations()[certInfoAnnotation]
newValue,_:=e.MetaNew.GetAnnotations()[certInfoAnnotation]
old:=oldValue=="true"
new:=newValue=="true"
//ifthecontenthaschangedwetriggeriftheannotationisthere
if!reflect.DeepEqual(newSecret.Data[util.Cert],oldSecret.Data[util.Cert])||
!reflect.DeepEqual(newSecret.Data[util.CA],oldSecret.Data[util.CA]){
returnnew
}
//otherwisewetriggeriftheannotationhaschanged
returnold!=new
},
CreateFunc:func(eevent.CreateEvent)bool{
secret,ok:=e.Object.(*corev1.Secret)
if!ok{
returnfalse
}
ifsecret.Type!=util.TLSSecret{
returnfalse
}
value,_:=e.Meta.GetAnnotations()[certInfoAnnotation]
returnvalue=="true"
},
}
一個非常常見的模式是觀察我們創(chuàng)建(和我們擁有)資源上的events,并且定期在擁有這些資源的CR上執(zhí)行reconcile cycle。為此,你可以使用EnqueueRequestForOwner handler,按照如下方式完成:
err=c.Watch(&source.Kind{Type:&examplev1alpha1.MyControlledType{}},&handler.EnqueueRequestForOwner{})
另一種不太常用的情況是將一個events傳播到多個資源上??紤]一種情況,一個控制器注入了TLS secret的路由。同一個命名空間中的多個路由可以指向同一個secret。如果secret發(fā)生了改變,我們需要更新所有路由。因此,我們需要在secret類型上創(chuàng)建一種watch機(jī)制,處理程序如下所示:
typeenqueueRequestForReferecingRoutesstruct{
client.Client
}
//triggerarouterreconcileeventforthoseroutesthatreferencethissecret
func(e*enqueueRequestForReferecingRoutes)Create(evtevent.CreateEvent,qworkqueue.RateLimitingInterface){
routes,_:=matchSecret(e.Client,types.NamespacedName{
Name:evt.Meta.GetName(),
Namespace:evt.Meta.GetNamespace(),
})
for_,route:=rangeroutes{
q.Add(reconcile.Request{NamespacedName:types.NamespacedName{
Namespace:route.GetNamespace(),
Name:route.GetName(),
}})
}
}
//UpdateimplementsEventHandler
//triggerarouterreconcileeventforthoseroutesthatreferencethissecret
func(e*enqueueRequestForReferecingRoutes)Update(evtevent.UpdateEvent,qworkqueue.RateLimitingInterface){
routes,_:=matchSecret(e.Client,types.NamespacedName{
Name:evt.MetaNew.GetName(),
Namespace:evt.MetaNew.GetNamespace(),
})
for_,route:=rangeroutes{
q.Add(reconcile.Request{NamespacedName:types.NamespacedName{
Namespace:route.GetNamespace(),
Name:route.GetName(),
}})
}
}
資源 Reconciliation Cycle
reconcile cycle是在被watch的event傳遞后框架將控制權(quán)轉(zhuǎn)交給我們地方。正如之前所解釋的,在該reconcile cycle中我們沒有獲得相關(guān)時間類型的信息,是因?yàn)槲覀兪腔谒接|發(fā)的方式來工作。
下面是一個管理CRD控制器的常見reconcile cycle的模型。和其他任何一個模型一樣,它不會反映任何特定用例,但我希望它將有助于解決你在編寫operator時遇到的問題。

從圖中我們可以看到,主要步驟是:
檢索你感興趣的CR實(shí)例
確認(rèn)實(shí)例的有效性。我們不會在不合法實(shí)例上做任何事情。
初始化實(shí)例。如果實(shí)例的某些值沒有被初始化,我們會在這一步進(jìn)行處理。
判斷實(shí)例的deletion狀態(tài)。如果實(shí)例正在被刪除,我們也需要做一些特殊的清理。
管理控制器的業(yè)務(wù)邏輯。如果以上步驟均通過,我們最終可以管理和執(zhí)行該實(shí)例的reconcile邏輯。這個邏輯每個控制器都不盡相同。
在本節(jié)的剩余部分,你可以找到有關(guān)每個步驟的更深入的注意事項(xiàng)。
資源驗(yàn)證
這里存在兩種類型的校驗(yàn):語法校驗(yàn)和語義校驗(yàn)。
語法校驗(yàn):通過定義OpenAPI規(guī)則來驗(yàn)證。
語義校驗(yàn):可以通過創(chuàng)建 ValidatingAdmissionConfiguration 來完成。
注意:在控制器中不能校驗(yàn)CR合法性。一旦CR被APIServer接受了,它就會存在Etcd中。CR存在Etcd之后,管理該CR資源的控制器就無法拒絕它,如果這個CR是不合法的,控制器在嘗試使用或處理它的時候?qū)l(fā)生錯誤。
推薦:但是由于我們不能保證ValidatingAdmissionConfiguration被創(chuàng)建或正常工作,我們還是應(yīng)該在控制器內(nèi)部去驗(yàn)證CR,如果CR不合法,應(yīng)該避免創(chuàng)建無限錯誤循環(huán)。
語法校驗(yàn)
可以按照這個鏈接的描述添加OpenAPI驗(yàn)證規(guī)則
推薦:盡可能多地為你的自定義資源模型進(jìn)行語法校驗(yàn)。你應(yīng)該盡量使用語法校驗(yàn),因?yàn)樗鄬唵?,并且可以防止格式錯誤的 CR 存儲在 etcd 中。
語義校驗(yàn)
語義校驗(yàn)是為了確保字段具有合理的值,從而使整個資源記錄是有意義的。語義驗(yàn)證業(yè)務(wù)邏輯取決于 CR 所代表的概念,并且必須由operator的開發(fā)人員進(jìn)行編碼實(shí)現(xiàn)。
如果給定的CR需要語義校驗(yàn),那么operator需要暴露一個webhook,作為operator deploymen的一部分,ValidatingAdmissionConfiguration也應(yīng)該被創(chuàng)建。
以下是目前存在的局限性:
在OpenShift 3.11中,ValidatingAdmissionConfigurations還處于技術(shù)預(yù)覽階段(將從4.1開始支持)
Operator SDK不支持腳手架形式的webhook。可以使用kubebuilder來進(jìn)行實(shí)現(xiàn):
kubebuilderwebhook--groupcrew--versionv1--kindFirstMate--type=mutating--operations=create,update
驗(yàn)證控制器中的資源
最好的方式是直接拒絕一個無效的CR,而不是接受并保存在Etcd中,然后對它進(jìn)行錯誤條件處理。當(dāng)然也有可能的情況是,ValidatingAdmissionConfiguration并沒有被部署或者根本不可用。所以我認(rèn)為在控制器代碼中進(jìn)行語義校驗(yàn)仍然是一個很好的做法。你應(yīng)該做到的是,可以在ValidatingAdmissionConfiguration和控制器之間共享這部分結(jié)構(gòu)化的代碼。
控制器中調(diào)用驗(yàn)證方法的代碼如下所示:
ifok,err:=r.IsValid(instance);!ok{
returnr.ManageError(instance,err)
}
請注意,如果驗(yàn)證失敗,我們按照錯誤管理部分中的描述來管理這個錯誤。
IsValid函數(shù)如下:
func(r*ReconcileMyCRD)IsValid(objmetav1.Object)(bool,error){
mycrd,ok:=obj.(*examplev1alpha1.MyCRD)
//validationlogic
}
資源初始化
Kubernetes的一個很好的慣例是用戶只初始化他所需要的資源字段,其他的可以省略。以上是用戶的視點(diǎn),但從編碼人員和調(diào)試者的角度來說,實(shí)際上最好將所有的字段都初始化。這允許在編碼的時候不必總是去校驗(yàn)字段是否被定義了,并且可以輕松地排除錯誤情況。為了初始化資源,這里有兩個選項(xiàng):
在控制器中定義初始化方法
定義一個 MutatingAdmissionConfiguration(類似于ValidatingAdmissionConfiguration的程序)
建議:在控制器中定義一個初始化方法。代碼應(yīng)類似于此示例:
ifok:=r.IsInitialized(instance);!ok{
err:=r.GetClient().Update(context.TODO(),instance)
iferr!=nil{
log.Error(err,"unabletoupdateinstance","instance",instance)
returnr.ManageError(instance,err)
}
returnreconcile.Result{},nil
}
注意,如果IsInitialized方法的結(jié)果返回true,我們更新instance并return。這將會立即出發(fā)另一個reconcile cycle。第二次調(diào)用IsInitialized方法將會返回false,代碼邏輯將會執(zhí)行到下一部分。
資源 Finalization
如果資源不屬于您的操作員控制的 CR,但在刪除該 CR 時需要采取措施,您必須使用finalizer。
終結(jié)器提供了一種機(jī)制來通知 Kubernetes 控制平面,在執(zhí)行標(biāo)準(zhǔn) Kubernetes 垃圾收集邏輯之前需要執(zhí)行一個操作。
資源可以有一個或多個finalizers。每一個控制器應(yīng)該管理自己的finalizer并且忽略其他的。
這是管理finalizers的偽代碼算法:
如果需要,在初始化方法中添加finalizer。
當(dāng)資源被刪除,檢查此控制器擁有的finalizer是否存在。
清理成功,移除finalizer并更新CR
如果失敗決定是重試還是放棄并可能留下垃圾(在某些情況下這是可以接受的)
如果不存在,直接return
如果存在,執(zhí)行如下清理邏輯:
如果你的清理邏輯需要添加額外的資源,需要記住的是,無法在正在刪除的命名空間中創(chuàng)建其他資源。刪除命名空間將會觸發(fā)finalizer并刪除其下所有資源。
看如下的代碼例子:
ifutil.IsBeingDeleted(instance){
if!util.HasFinalizer(instance,controllerName){
returnreconcile.Result{},nil
}
err:=r.manageCleanUpLogic(instance)
iferr!=nil{
log.Error(err,"unabletodeleteinstance","instance",instance)
returnr.ManageError(instance,err)
}
util.RemoveFinalizer(instance,controllerName)
err=r.GetClient().Update(context.TODO(),instance)
iferr!=nil{
log.Error(err,"unabletoupdateinstance","instance",instance)
returnr.ManageError(instance,err)
}
returnreconcile.Result{},nil
}
資源所有權(quán)
資源所有權(quán)是Kubernetes中的原生概念,它決定了資源如何被刪除。默認(rèn)情況下,當(dāng)一個資源被刪除的時候,它的子資源也也會被刪除(你可以設(shè)置cascade=false來關(guān)閉這種行為)
這種行為有助于確保資源的正確垃圾收集,尤其是當(dāng)資源控制多級層次結(jié)構(gòu)中的其他資源時(deployment-> repilcaset->pod)
建議:如果你的控制器創(chuàng)建資源并且它的生命周期與其他資源(kubernetes核心資源或其他CR)有關(guān)聯(lián),那么您應(yīng)該將此資源設(shè)置為其他資源的所有者,如下所示:
controllerutil.SetControllerReference(owner,obj,r.GetScheme())
有關(guān)所有權(quán)的其他規(guī)則如下:
父子資源必須位于同一命名空間中
命名空間資源可以擁有集群資源。但我們必須小心處理。一個對象可以有一個所有者列表。如果多個命名空間對象擁有相同的集群資源,則每個對象都應(yīng)聲明所有權(quán),而不會覆蓋其他對象的所有權(quán)
集群資源不能擁有命名空間資源
集群資源可以擁有另外一個集群資源
狀態(tài)管理
Status是資源的一個標(biāo)準(zhǔn)部分。Status被用于報(bào)告資源的狀態(tài)。在本文檔中,我們將使用 status 報(bào)告最后一次執(zhí)行協(xié)調(diào)循環(huán)的結(jié)果。你也可以在Status中添加更多的信息。
在正常情況下,如果我們每次執(zhí)行reconcile cycle的時候都要更新資源,這將觸發(fā)更新時間,進(jìn)而導(dǎo)致無限觸發(fā)reconcile cycle。
因此,正如上面描述的那樣,我們應(yīng)該把Status作為子資源。
使用這種方法,我們能夠不增加ResourceGeneration元數(shù)據(jù)域的情況下更新資源的狀態(tài)。使用如下命令更新狀態(tài):
err=r.Status().Update(context.Background(),instance)
現(xiàn)在我們需要為我們的watch機(jī)制寫一個predicate(有關(guān)這些概念的更多詳細(xì)信息,請參閱有關(guān)watches的部分)用來丟棄不增加ResourceGeneration的更新事件??梢允褂肎enerationChangePredicate來完成此功能。
如果你還記得的話,上文提到過,在使用finalizer的時候,應(yīng)該在初始化的時候設(shè)置。如果finalizer是初始化的唯一項(xiàng),由于它是元數(shù)據(jù)項(xiàng)的一部分,所以ResourceGeneration不會遞增。為了說明該用例,以下是predicate的修改版本:
typeresourceGenerationOrFinalizerChangedPredicatestruct{ predicate.Funcs } //UpdateimplementsdefaultUpdateEventfilterforvalidatingresourceversionchange func(resourceGenerationOrFinalizerChangedPredicate)Update(eevent.UpdateEvent)bool{ ife.MetaNew.GetGeneration()==e.MetaOld.GetGeneration()&&reflect.DeepEqual(e.MetaNew.GetFinalizers(),e.MetaOld.GetFinalizers()){ returnfalse } returntrue }
現(xiàn)在假設(shè)你的status如下所示:
typeMyCRStatusstruct{
//+kubebuilderEnum=Success,Failure
Statusstring`json:"status,omitempty"`
LastUpdatemetav1.Time`json:"lastUpdate,omitempty"`
Reasonstring`json:"reason,omitempty"`
}
你可以寫一個函數(shù)來管理并保證reconcile cycle成功執(zhí)行:
func(r*ReconcilerBase)ManageSuccess(objmetav1.Object)(reconcile.Result,error){
runtimeObj,ok:=(obj).(runtime.Object)
if!ok{
log.Error(errors.New("notaruntime.Object"),"passedobjectwasnotaruntime.Object","object",obj)
returnreconcile.Result{},nil
}
ifreconcileStatusAware,updateStatus:=(obj).(apis.ReconcileStatusAware);updateStatus{
status:=apis.ReconcileStatus{
LastUpdate:metav1.Now(),
Reason:"",
Status:"Success",
}
reconcileStatusAware.SetReconcileStatus(status)
err:=r.GetClient().Status().Update(context.Background(),runtimeObj)
iferr!=nil{
log.Error(err,"unabletoupdatestatus")
returnreconcile.Result{
RequeueAfter:time.Second,
Requeue:true,
},nil
}
}else{
log.Info("objectisnotRecocileStatusAware,notsettingstatus")
}
returnreconcile.Result{},nil
}
錯誤管理
如果控制器進(jìn)入了一個錯誤條件,并且在reconcile方法中返回了一個錯誤。operator將會打印錯誤日志到標(biāo)準(zhǔn)輸出,reconlie event將會立即再次調(diào)度(默認(rèn)的調(diào)度器實(shí)際上應(yīng)該檢測是否一遍又一遍地出現(xiàn)相同的錯誤,并增加相應(yīng)的調(diào)度時間,但在我的經(jīng)驗(yàn)看來,這并沒有發(fā)生)。如果錯誤一直存在,那么也將永遠(yuǎn)存在錯誤循環(huán)。而且,這個錯誤條件對用戶來說是不可見的。
有兩種方法可以通知用戶發(fā)生了錯誤,它們可以同時使用:
在對象的status字段中返回錯誤
生成一個event描述錯誤
此外,如果你認(rèn)為錯誤能夠自解決,你應(yīng)該在一段周期時間后重新調(diào)度reconcile cycle。通常來說,周期時間是呈指數(shù)增長的,因此在每次迭代中,reconcile event周期會越來越長(例如每次增長時間量的兩倍)。
我們現(xiàn)在構(gòu)建狀態(tài)管理來處理錯誤條件:
func(r*ReconcilerBase)ManageError(objmetav1.Object,issueerror)(reconcile.Result,error){
runtimeObj,ok:=(obj).(runtime.Object)
if!ok{
log.Error(errors.New("notaruntime.Object"),"passedobjectwasnotaruntime.Object","object",obj)
returnreconcile.Result{},nil
}
varretryIntervaltime.Duration
r.GetRecorder().Event(runtimeObj,"Warning","ProcessingError",issue.Error())
ifreconcileStatusAware,updateStatus:=(obj).(apis.ReconcileStatusAware);updateStatus{
lastUpdate:=reconcileStatusAware.GetReconcileStatus().LastUpdate.Time
lastStatus:=reconcileStatusAware.GetReconcileStatus().Status
status:=apis.ReconcileStatus{
LastUpdate:metav1.Now(),
Reason:issue.Error(),
Status:"Failure",
}
reconcileStatusAware.SetReconcileStatus(status)
err:=r.GetClient().Status().Update(context.Background(),runtimeObj)
iferr!=nil{
log.Error(err,"unabletoupdatestatus")
returnreconcile.Result{
RequeueAfter:time.Second,
Requeue:true,
},nil
}
iflastUpdate.IsZero()||lastStatus=="Success"{
retryInterval=time.Second
}else{
retryInterval=status.LastUpdate.Sub(lastUpdate).Round(time.Second)
}
}else{
log.Info("objectisnotRecocileStatusAware,notsettingstatus")
retryInterval=time.Second
}
returnreconcile.Result{
RequeueAfter:time.Duration(math.Min(float64(retryInterval.Nanoseconds()*2),float64(time.Hour.Nanoseconds()*6))),
Requeue:true,
},nil
}
注意,此函數(shù)會立即發(fā)送一個event,然后使用錯誤條件更新狀態(tài)。最后,計(jì)算何時重新安排下一次reconcile。該算法嘗試將每個循環(huán)的時間加倍,最多到六個小時為止。
六個小時是一個很好的上限時間,因?yàn)閑vent大約持續(xù)6個小時,所以這應(yīng)該確保始終有一個活動event描述當(dāng)前的錯誤情況。
總結(jié)
本博客中介紹的實(shí)踐涉及Kubernetes Operator時最常見的問題,且讓你可以編寫一個有信心投入生產(chǎn)環(huán)境的operator。當(dāng)然很有可能,這僅僅是一個開始,我們很容易預(yù)見將會有更多的框架和工具的出現(xiàn)來幫助你編寫operator。
審核編輯:劉清
-
控制器
+關(guān)注
關(guān)注
114文章
17886瀏覽量
195301 -
API接口
+關(guān)注
關(guān)注
1文章
115瀏覽量
11290 -
CRD
+關(guān)注
關(guān)注
0文章
14瀏覽量
4257 -
TLS
+關(guān)注
關(guān)注
0文章
54瀏覽量
5007
原文標(biāo)題:Kubernetes Operator 最佳實(shí)踐
文章出處:【微信號:magedu-Linux,微信公眾號:馬哥Linux運(yùn)維】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
leader選舉在kubernetes controller中是如何實(shí)現(xiàn)的
Kubernetes Ingress 高可靠部署最佳實(shí)踐
Kubernetes Dashboard實(shí)踐學(xué)習(xí)
虛幻引擎的紋理最佳實(shí)踐
華為云在Kubernetes大規(guī)模場景下的Service性能優(yōu)化實(shí)踐
阿里巴巴 Kubernetes 應(yīng)用管理實(shí)踐中的經(jīng)驗(yàn)與教訓(xùn)
教你們Kubernetes五層的安全的最佳實(shí)踐
最常用的11款Kubernetes工具
Kubernetes是什么,一文了解Kubernetes
Kubernetes Operator最佳實(shí)踐介紹
評論