Kubernetes源碼解析之controller-manager deployment同步流程

基本使用

1 簡單的yaml文件

在K8s集羣上可使用Kubectl命令以指定文件方式創建一個kind=Deployment的資源對象
$ kubectl create -f nginx.yaml

apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: nginx
  
spec:
  replicas: 3
    template:
      metadata:
        labels:
          app: nginx
      spec:
        containers:
        - name: nginx
         image: nginx:1.9.1

下圖分別爲 在終端查看生成的DeployMent, ReplicaSet, pod資源,以及他們之前的拓撲關係圖(可以先忽略oldReplicaSet)
圖1
在這裏插入圖片描述
如圖,k8s根據yaml中指定的spec.replicas值爲我們創建3個pod,並在deployment整個運行週期中維護這個數量,然後根據spec.template.spec中的container數組配置,將容器組啓動在每個pod中。
這是一個簡單創建deployment任務過程。

2 更新及回滾

Deployment作爲一個大數據結構(yaml文件)控制維護我們的業務,我們通過更新這個yaml文件來更新業務部署。

上節提到spec.template下是具體pod要啓動的業務及配置,只有對spec.template進行更新纔會觸發pod重新部署(橫向擴縮容不觸發重新部署)

命令行支持兩種更新方式,更新後自動觸發deployment更新
更新結果是根據deployment中配置的replicas數和spec.template中定義的模板,生成pod。然後清理上版本的舊pod。

//直接使用 kubectl set 更新對象
$ kubectl set image deployment/nginx  nginx=nginx:1.9.1
//直接更新nginx yaml文件
$ kubectl edit deployment/nginx

此時觀察到系統中存在兩個replicaSet,如果正常發佈,Dp擁有兩個Rs,新版Rs下維護3個pod,舊版下0個,k8s默認爲我們保留更新過的版本,方便我們回滾版本使用。
以下是一個更新及回滾過程中Rs的狀態
(初次發佈後Rs狀態 -> set修改鏡像觸發更新 -> 新pod生成舊版本下pod被清理 -> 回滾 -> "舊"版本pod被重建,"新"版本pod被清理)
在這裏插入圖片描述
以上爲基本的更新/回滾流程。兩個問題:

  1. 過程描述中,回滾後的新舊版本被我加了雙引號
  2. 倒數第2次get Rs信息,發現版本之間數量的變化並非單獨的清理舊版,發起新版。
    (後面讀源碼將講到)

暫停與恢復

暫停態時,對spec.template資源的更新都不會生效。恢復狀態後,再執行更新操作。官方現在給的解釋爲:暫停態爲支持多次更新配置而不用觸發更新。
命令:

$ kubectl rollout pause deployment/nginx  //暫停
$ kubectl rollout resume  deployment/nginx  //恢復

因爲不會觸發更新,所以理論上也不支持回滾。在暫停態時,發起回滾屬於非法操作。

STATUS

Dp結構體主要包含3個部分:
* ObjectMeta 元數據
* DeploymentSpec Dp任務期望狀態
* Status 處理狀態

其中Meta由用戶指定一部分,另一部分系統維護。DpSpec基本由用戶指定,Status完全基本由系統控制,在同步過程中對此狀態進行參考修改。

我目前根據Dp配置中的condition判斷k8s在處理過程中的狀態:
在這裏插入圖片描述

以上表示兩項結果:
  • Available 服務是否達到可用狀態(可自定義livenessProbe、readinessProbe等來指定服務可用標誌,默認pod內容器正常啓動即爲可用),圖中此項status爲True,原因爲滿足用戶期望的最小可用實例數
  • Progressing 指Dp收到的最近一個更新請求是否完成。例如回滾操作,指定時間內達到用戶預期結果status將置爲true,否則爲False並設置錯誤原因。指定時間由spec.progressDeadlineSecond參數指定,Pause狀態時此值不定義超時。 (此處更新指所有對Dp的更新,包括水平擴容操作)

概念

Label、Seletor and OwnerReference

觀察本文圖1,發現資源名的特點:
創建Dp時,我們定義nginx爲name;Dp生成的Rs名均爲nginx-hashstr;Rs又創建多個pod,pod名爲rs-hashstr

假定Dp->Rs->Pod是一個從上到下的關係,那k8s通過上層selector和下層labels來確認下屬於上,同時下層會保存上層的metaUid信息,用於所屬確認和垃圾回收。

我通過–show-labels 來查看三項資源的lebels信息:
在這裏插入圖片描述
如上,Dp通過 app=roll 來確定Rs,但是不同版本Rs之間必須有差別,所以創建Rs時引入pod-template-hash作爲selector 和 labels,並將其複製給pod.labels,這樣在dp下同時存在兩個版本時,多個Rs可以接管各自的pod

在這裏插入圖片描述

Rs和Pod中,都保存了ownerReferences信息。uid爲所屬Dp.uid。有兩項用處(以獲取dp下擁有的rs爲例):
  • 遍歷檢查rs.labels,首先檢查並確認dp.selector需要是它的子集。然後檢查rs.ownerReferences,確認爲Dp信息時,表示此Rs屬於Dp
  • 刪除Deployment時,僅操作Dp資源。檢查Rs時,通過確認其uid標識的owner已被刪除,確認是不是清理當前Rs資源

ControllerManager源碼閱讀

簡單介紹一下事件處理前的如何獲取事件集:
爲了減輕對apiserver的壓力,客戶端存在一個Informer,它負責從apiserver端同步發生變更的數據到store,然後從store中讀取需要處理的事件調用相應的Handler。
deploymentController會啓動多個worker去接收store中的deployment-key,Handler處理函數爲syncDeployment

syncDeployment

func (dc *DeploymentController) syncDeployment(key string) error {
	//由key值獲取Dp的namespaces和name
	namespace, name, err := cache.SplitMetaNamespaceKey(key)
	
	//根據ns、name從系統中獲取deployment當前信息(此時有可能已被delete,在處理同步中會不斷檢查刪除狀態)
	deployment, err := dc.dLister.Deployments(namespace).Get(name)
	
	//deepcopy信息,更新狀態時更新拷貝信息然後將副本更新到server
	d := deployment.DeepCopy()

	//行爲:獲取屬於當前Dp的所有Rs,同時進行一些adopt和release操作
	rsList, err := dc.getReplicaSetsForDeployment(d)
	
	//獲取rs列表下的所有pod,返回Map(key爲Rs.UID  value爲PodList)
	podMap, err := dc.getPodMapForDeployment(d, rsList)
	
	//如果Pod已經被delete,調用getAllReplicaSetsAndSyncRevision更新版本信息,並同步狀態信息。不明白這裏,爲什麼已刪除還要同步狀態
	if d.DeletionTimestamp != nil {
		return dc.syncStatusOnly(d, rsList)
	}

	//檢查是否爲暫停或恢復事件。
	//暫停時將condition中Progressing中 status=Unknown reason=DpPaused,此時不對其進行處理超時等檢查
	//檢查爲恢復請求並且當前爲暫停時,更新Progressing爲 status=Unknown reason=DpResume
	if err = dc.checkPausedConditions(d)

	//暫停態時,執行sync同步狀態(本節會單獨分析函數)
	if d.Spec.Paused {
		return dc.sync(d, rsList)
	}

	//檢查有回滾事件時,回滾版本(下節會分析此函數)
	if getRollbackTo(d) != nil {
		return dc.rollback(d, rsList)
	}

	//發現desire與dp.replicas不符時,確定爲正在進行擴縮容事件,調用sync同步
	scalingEvent, err := dc.isScalingEvent(d, rsList)
	if scalingEvent {
		return dc.sync(d, rsList)
	}

	//根據兩種發佈策略檢查並更新deployment到最新狀態(下節會分析處理函數)
	switch d.Spec.Strategy.Type {
	case apps.RecreateDeploymentStrategyType:
		return dc.rolloutRecreate(d, rsList, podMap)
	case apps.RollingUpdateDeploymentStrategyType:
		return dc.rolloutRolling(d, rsList)
	}
}
getReplicaSetForDeployment行爲: 輪詢檢查Dp所在ns下所有Rs並返回Dp控制的RsList。
  • Dp.Selector與Rs.labels匹配(前者必須爲後者的子集),並且rs.ownerRefrence必須爲Dp的信息,則認爲此Rs屬於Dp,加入RsList
  • 如果Rs.owner爲空,並且Dp.Selector匹配Rs.labels, controller將爲Rs.owner添加此Dp的信息,稱爲adopt,並加入RsList
  • 如果Rs.Owner爲此Dp信息, 但是selector不匹配,controller將刪除此Rs的Owner信息,稱爲release,此時Rs將稱爲孤兒直到有匹配labels的Dp出現收養它
  • 返回RsList
    在這裏插入圖片描述
    上圖爲Rs.Owner信息。uid爲所屬Dp的元數據,此值全局唯一。
sync函數 在Dp暫停時協調Dp狀態以及擴縮容狀態
  • 更新Dp下Rs revision信息 遍歷所有Rs獲取當前最大revision號maxId,檢查maxId的Rs.spec是否等於Dp.spec.template.spec(檢查是否爲newRs的唯一方法),如果存在newRs將其revision更新爲maxId+1,如果不存在,將根據Dp.spec.template.spec創建newRs並設置revision號爲maxid+1(注意:暫停態時不會創建newRs)。更新後將revision和history-revision信息都加入Annotations
  • 如果正在進行scale,對activeRs進行scale up/down;如果activeRs不止一個,比如當前正在進行滾動升級,按比例進行擴縮容,計算公式如下:
    newRs.replicas / (newRs.replicas+oldRs.replicas) * needScaleNumber
    檢查isScale標誌:滾動升級時,rs.replicas可能隨着滾動過程逐次上升,但annotion中有一項" deployment.kubernetes.io/desired-replicas=10"會指定最終的期望狀態,如果此值!=Dp.replicas,則判斷爲isScale狀態
  • 如果newRs已經完成更新,將Dp下所有oldRs.replicas調整爲0
  • 根據d.spec.RevisionHistoryLimit參數保留最新n個Rs版本,清理其他
  • 根據allRs信息計算Dp當前狀態,並更新到server,信息包括:
status := apps.DeploymentStatus{
		Replicas:    //Dp下所有Rs的期望實例數
		UpdatedReplicas:    //新版本Rs期望實例數(不論running與否)
		ReadyReplicas:      //Dp下所有Rs擁有的ReadyPod數 
		AvailableReplicas:   ///Dp下所有Rs擁有的AvaliblePod數
		UnavailableReplicas: //Dp下所有Rs擁有的unavailablePod數
}
//更新condition中type=AvaliblePod狀態,下面是avaliblePod個數是否達標時設置的conditon
deploymentutil.NewDeploymentCondition("Available", "True", "MinimumReplicasAvailable", "Deployment has minimum availability.")
deploymentutil.NewDeploymentCondition("Available", "False", "MinimumReplicasAvailable", "Deployment does not have minimum availability")

如上,是被加入queue中的每個deployment被處理的過程,deployment通過更新Rs-yaml信息來同步狀態。

stracy 更新策略

deployment目前支持兩種更新策略:
  • Recreate 刪除所有舊pod,然後創建新Pod。一般用於開發環境
  • RollingUpdate 滾動更新,刪除一部分oldPod,創建一部分newPod,重複此步驟直到達到Dp預期
RollingUpdate

滾動更新涉及兩個重要參數,配置在deployment.yaml文件中如下:

replicas: 5 				#deployment期望實例數
strategy:			 		#升級策略提示符 位於yaml中 .spec下
	rollingUpdate:   
		maxSurge: 1        	#更新中允許存在的最大pod數
		maxUnavailable: 1  	#更新中允許存在的最大不可用pod數, dp.replicas-maxUnava爲最小可用數
	type: RollingUpdate  	#升級策略

更新版本時觸發rollingUpdate,5b69爲新版,85bb爲舊版本。這次更新設置了錯誤的鏡像,所以更新停止在以下狀態:
在這裏插入圖片描述

更新過程:
	對5b69版本scaleUp 1個pod  
    對85bb版本scaleDown 1個pod
    對5b69版本scaleUp 1個pod
    !對85bb版本scaleDown 1個pod  此時因爲maxUnavaluble限制,此版本不能再縮容,又因爲maxSurge限制,新版本pod不能再發起,由此stuck在上圖狀態
//升級過程中發生的scale up/down,實際上是操作Rs.replicas
func (dc *DeploymentController) rolloutRolling(d *apps.Deployment, rsList []*apps.ReplicaSet) error {
	//從rslist中獲取newRs、oldRs,並更新revision信息。參數爲true時,如果不存在newRs就創建它,檢查存在newRs與否的標準是:rs.template = dp.spec.template 
	newRS, oldRSs, err := dc.getAllReplicaSetsAndSyncRevision(d, rsList, true)
	allRSs := append(oldRSs, newRS)

	//調整newRs-yaml信息,通常爲擴容事件。如果操作了rs.replicas, scaledUp=true.
	scaledUp, err := dc.reconcileNewReplicaSet(allRSs, newRS, d)
	if scaledUp {
		return dc.syncRolloutStatus(allRSs, newRS, d)
	}
	//調整oldRs-yaml信息,通常爲縮容事件。如果操作了rs.replicas,scaledDown=false
	scaledDown, err := dc.reconcileOldReplicaSets(allRSs, controller.FilterActiveReplicaSets(oldRSs), newRS, d)
	if scaledDown {
		return dc.syncRolloutStatus(allRSs, newRS, d)
	}
	//清理deployment下Rs版本信息
	if deploymentutil.DeploymentComplete(d, &d.Status) {
		if err := dc.cleanupDeployment(oldRSs, d); err != nil {
			return err
		}
	}
	//同步rs狀態
	return dc.syncRolloutStatus(allRSs, newRS, d)
}
reconcileNewReplicaSet 檢查newRs.replicas,確定本次滾動新版本需要新建的數目並更新rs-yaml
  • 檢查 new.replicas = dp.replicas,等於時返回false,無需更新
  • newRs.replicas > dp.replicas時,new版本實例過多,直接scale down至dp.replicas。如果沒達到dp.replicas, 則根據公式計算本次需要scale up的數量,公式如下:
    Min ( (maxSurge + dp.replicas - dp.currentPod), dp.replicas - newRs.replicas )
  • 檢查同步dpStatus,爲Rs設置Annotations, 添加Event
reconcileOldReplicaSets 檢查oldRs.replicas,確定本次滾動舊版本需要清理的數目並更新rs-yaml
  • 獲取所有activeRs下pod總數量,爲0時,返回false,無需更新
  • 清理oldRs列表中 UnAvalible狀態的pod,代碼如下:
//最大可清理舊版本pod數量,縮容時,RS下列表pod是經過排序的,保證優先清理Unhealthy Pod
maxCleanCount = allPodsCount - minAvailable - newRSUnavailablePodCount  
//遍歷每個ActiveoldRs
totalScaledDown=0    //記錄已清理副本數,不能超過maxCleanCount值
for _, targetRs := range oldRsList {
	scaledDownCount := Min(maxCleanCount - totalScaledDown, targetRS.Spec.Replicas-targetRS.Status.AvailableReplicas)   //當前Rs縮容數
	//向server發送縮容請求
	totalScaledDown+=scaledDownCount 
}
  • 根據配置最小可用數計算本次需要scaleDown的pod,代碼如下:
totalScaleDownCount := availablePodCount - minAvailable
totalScaledDown := int32(0)  //記錄本次總scaleDown數目,不能超過totalScaleDownCount值
for _, targetRs := range oldRsList {
	scaleDownCount := Min(targetRS.Spec.Replicas, totalScaleDownCount-totalScaledDown)
	totalScaledDown += scaleDownCount
}
  • 檢查同步dpStatus,爲Rs設置Annotations, 添加Event

以上爲rollingUpdate過程中,deployment-controller通過控制其下rs.replicas值來控制pod更新的過程,簡單流程爲:根據deployment.spec確定newRs和oldRs,通過maxSurge和maxUnavalible限制來不斷添加新版本Pod並刪除舊版本pod,最終達到newRs.replicas=dp.replicas 並且oldRs.replicas=0,標誌progressing正常結束。

Recreate
func (dc *DeploymentController) rolloutRecreate(d *apps.Deployment, rsList []*apps.ReplicaSet, podMap map[types.UID]*v1.PodList) error {
	//getRs並更新版本信息,false參數表示如果沒有新版本則不創建
	newRS, oldRSs, err := dc.getAllReplicaSetsAndSyncRevision(d, rsList, false)
	if err != nil {
		return err
	}
	allRSs := append(oldRSs, newRS)
	activeOldRSs := controller.FilterActiveReplicaSets(oldRSs)

	//縮容所有active的舊實例(其下有pod即爲active),如果有縮容操作,則更新狀態
	scaledDown, err := dc.scaleDownOldReplicaSetsForRecreate(activeOldRSs, d)
	if scaledDown {
		return dc.syncRolloutStatus(allRSs, newRS, d)
	}

	//確認Rs下已經沒有 running/pending/unknown 狀態的pod
	if oldPodsRunning(newRS, oldRSs, podMap) {
		return dc.syncRolloutStatus(allRSs, newRS, d)
	}

	//創建newRs並擴容
	if newRS == nil {
		newRS, oldRSs, err = dc.getAllReplicaSetsAndSyncRevision(d, rsList, true)
		allRSs = append(oldRSs, newRS)
	}
	dc.scaleUpNewReplicaSetForRecreate(newRS, d)

	if util.DeploymentComplete(d, &d.Status) {
		if err := dc.cleanupDeployment(oldRSs, d); err != nil {
			return err
		}
	}
	return dc.syncRolloutStatus(allRSs, newRS, d)
Rollback
func (dc *DeploymentController) rollback(d *apps.Deployment, rsList []*apps.ReplicaSet) error {
	//getRs並更新版本號
	newRS, allOldRSs, err := dc.getAllReplicaSetsAndSyncRevision(d, rsList, true)
	allRSs := append(allOldRSs, newRS)

	rollbackTo := getRollbackTo(d)
	if rollbackTo.Revision == 0 {
		//如果未指定Revision,遍歷rs找到第二max的revision
		rollbackTo.Revision = deploymentutil.LastRevision(allRSs)}
	//根據revision遍歷RsList找到對應rs,將rs.spec.template複製給dp.spec.template,並更新此版本爲最新版,deployment-controller將在下次調用getAllReplicaSetsAndSyncRevision時創建newRs	
	for _, rs := range allRSs {
		v, err := deploymentutil.Revision(rs)
		if v == rollbackTo.Revision {
			performedRollback, err := dc.rollbackToTemplate(d, rs)
			return err
		}
	}
	//清理一些anntition相關的信息
	return dc.updateDeploymentAndClearRollbackTo(d)
}

如上,回滾相當於一次更新操作,更新dp.spec.template,在同步時,根據此內容生成新的Rs版本,繼而控制產生期望Pod。

syncReplicaSet

func (rsc *ReplicaSetController) syncReplicaSet(key string) error {
	startTime := time.Now()
	namespace, name, err := cache.SplitMetaNamespaceKey(key)
	
	rs, err := rsc.rsLister.ReplicaSets(namespace).Get(name)
	//rs是否需要同步
	rsNeedsSync := rsc.expectations.SatisfiedExpectations(key)
	
	//獲取Rs所在ns下所有Pod
	//過濾非success和已刪除pod
	//獲取ns下所有pod,獲取過程參考Dp獲取Rs
	selector, err := metav1.LabelSelectorAsSelector(rs.Spec.Selector)
	allPods, err := rsc.podLister.Pods(rs.Namespace).List(labels.Everything())
	filteredPods := controller.FilterActivePods(allPods)
	filteredPods, err = rsc.claimPods(rs, selector, filteredPods)
	
	var manageReplicasErr error
	//管理此rs下pod, 增刪pod(具體實現下面講?)
	if rsNeedsSync && rs.DeletionTimestamp == nil {
		manageReplicasErr = rsc.manageReplicas(filteredPods, rs)
	}
	
	//返回rs下關於pod的running/avalidble等狀態數量彙總和status相關的東西
	rs = rs.DeepCopy()
	newStatus := calculateStatus(rs, filteredPods, manageReplicasErr)
	updatedRS, err := updateReplicaSetStatus(rsc.kubeClient.AppsV1().ReplicaSets(rs.Namespace), rs, newStatus)

	//狀態還需要同步時,加入隊列
	if manageReplicasErr == nil && updatedRS.Spec.MinReadySeconds > 0 &&
		updatedRS.Status.ReadyReplicas == *(updatedRS.Spec.Replicas) &&
		updatedRS.Status.AvailableReplicas != *(updatedRS.Spec.Replicas) {
		rsc.enqueueReplicaSetAfter(updatedRS, time.Duration(updatedRS.Spec.MinReadySeconds)*time.Second)
	}
	return manageReplicasErr
}

ManageReplicas函數 管理rs下pod,使符合預期

func (rsc *ReplicaSetController) manageReplicas(filteredPods []*v1.Pod, rs *apps.ReplicaSet) error {
	diff := len(filteredPods) - int(*(rs.Spec.Replicas))
	rsKey, err := controller.KeyFunc(rs)
	
	//avaliblePod數未達到期望值,需要擴容。 burstReplicas爲單次創建Pod限制
	if diff < 0 {
		diff *= -1
		if diff > rsc.burstReplicas {
			diff = rsc.burstReplicas
		}
		//批量創建Pod,批量數字從1開始double增加,這樣可以防止出現相同錯誤的pod大量失敗的情況
		//例如,一個嘗試創建大量Pod的低quota的任務將在第一個Pod創建失敗時被停止,返回成功創建數量
		//successfulCreations表示成功調用創建pod函數的次數
		successfulCreations, err := slowStartBatch(diff, controller.SlowStartInitialBatchSize, func() error {
			err := rsc.podControl.CreatePodsWithControllerRef(rs.Namespace, &rs.Spec.Template, rs, metav1.NewControllerRef(rs, rsc.GroupVersionKind))
		
		//重新調起pod數量未達到diff值,這裏應該是一個記錄此次調用失敗,提醒informer下次繼續調用的過程
		if skippedPods := diff - successfulCreations; skippedPods > 0 {
			for i := 0; i < skippedPods; i++ {
				rsc.expectations.CreationObserved(rsKey)
			}
		}
	} else if diff > 0 {
		//存在pod超過期望值,getPodsToDelete中對pod按照狀態進行排序,根據數量返回需要刪除pod數組,優先刪除unhealthy等unAvailible態
		if diff > rsc.burstReplicas {
			diff = rsc.burstReplicas
		}
		podsToDelete := getPodsToDelete(filteredPods, diff)

		for _, pod := range podsToDelete {
			go func(targetPod *v1.Pod) {
				defer wg.Done()
				rsc.podControl.DeletePod(rs.Namespace, targetPod.Name, rs)
			}(pod)
		}
		wg.Wait()
	}
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章