5.深入k8s:StatefulSet控制器及源碼分析


轉載請聲明出處哦~,本篇文章發布於luozhiyun的博客:https://www.luozhiyun.com

image-20200807220814361

在上一篇中,講解了容器持久化存儲,從中我們知道什么是PV和PVC,這一篇我們講通過StatefulSet來使用它們。

這一篇中我重新按照源碼重新擼了一遍,希望能加深對k8s的理解,源碼版本是1.19,在閱讀源碼的時候可以參照着一起看會比較便於理解。

StatefulSet概念

我們在第三篇講的Deployment控制器是應用於無狀態的應用的,所有的Pod啟動之間沒有順序,Deployment可以任意的kill一個Pod不會影響到業務數據,但是這到了有狀態的應用中就不管用了。

而StatefulSet就是用來對有狀態應用提供支持的控制器。

StatefulSet創建的pod具有唯一的標識和創建和刪除順序的保障,從而主要做到了兩件事情:

  1. 提供穩定的網絡標識。一個StatefulSet創建的每個pod都有一個從零開始的順序索引。這樣可以方便通過主機名來定位pod,例如我們可以創建一個headless Service,通過Service記錄每個pod的獨立DNS記錄來定位到不同的pod,由於pod主機名固定,所以DNS記錄也不會變。如下:

StatefulSet A

  1. 提供穩定的專屬存儲。一個StatefulSet在創建的時候也可以聲明需要一個或多個PVC,然后pvc會在創建pod前綁定到pod上。StatefulSet在縮容的時候依然會保留pvc,這樣不會導致數據的丟失,在擴容的時候也可以讓pvc掛載到相同的pod上。

StatefulSet 的核心功能,就是通過某種方式記錄這些狀態,然后在 Pod 被重新創建時,能夠為新 Pod 恢復這些狀態。

提供穩定的網絡標識

在k8s中,Service是用來將一組 Pod 暴露給外界訪問的一種機制。Service可以通過DNS的方式,代理到某一個Pod,然后通過DNS記錄的方式解析出被代理 Pod 的 IP 地址。

如下:


apiVersion: v1
kind: Service
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  ports:
  - port: 80
    name: web
  clusterIP: None
  selector:
    app: nginx

這個Service會通過Label Selector選擇所有攜帶了 app=nginx 標簽的 Pod,都會被這個 Service 代理起來。

它所代理的所有 Pod 的 IP 地址,都會被綁定一個這樣格式的 DNS 記錄,如下所示:

<pod-name>.<svc-name>.<namespace>.svc.cluster.local

所以通過這個DNS記錄,StatefulSet就可以使用到DNS 記錄來維持 Pod 的網絡狀態。

如下:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web
spec:
  serviceName: "nginx"
  replicas: 2  # by default is 1
  selector:
    matchLabels:
      app: nginx  # has to match .spec.template.metadata.labels
  template:
    metadata:
      labels:
        app: nginx # has to match .spec.selector.matchLabels
    spec:
      containers:
      - name: nginx
        image: nginx:1.9.1
        ports:
        - containerPort: 80
          name: web

這里使用了serviceName=nginx,表明StatefulSet 控制器會使用nginx 這個Service來進行網絡代理。

我們可以如下創建:

$ kubectl create -f svc.yaml
$ kubectl get service nginx
NAME      TYPE         CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
nginx     ClusterIP    None         <none>        80/TCP    10s

$ kubectl create -f statefulset.yaml
$ kubectl get statefulset web
NAME      DESIRED   CURRENT   AGE
web       2         1         19s

然后我們可以觀察pod的創建情況:

$ kubectl get pods -w -l app=nginx

NAME    READY   STATUS    RESTARTS   AGE
web-0   1/1     Running   0          76m
web-1   1/1     Running   0          76m

我們通過-w命令可以看到pod創建情況,StatefulSet所創建的pod編號都是從0開始累加,在 web-0 進入到 Running 狀態、並且細分狀態(Conditions)成為 Ready 之前,web-1 會一直處於 Pending 狀態。

然后我們使用exec查看pod的hostname:

$ kubectl exec web-0 -- sh -c 'hostname'
web-0
$ kubectl exec web-1 -- sh -c 'hostname'
web-1

然后我們可以啟動一個一次性的pod用 nslookup 命令,解析一下 Pod 對應的 Headless Service:

$ kubectl run -i --tty --image busybox:1.28.4 dns-test --restart=Never --rm /bin/sh
$ nslookup web-0.nginx
Server:    10.68.0.2
Address 1: 10.68.0.2 kube-dns.kube-system.svc.cluster.local

Name:      web-0.nginx
Address 1: 172.20.0.56 web-0.nginx.default.svc.cluster.local

$ nslookup web-1.nginx
Server:    10.68.0.2
Address 1: 10.68.0.2 kube-dns.kube-system.svc.cluster.local

Name:      web-1.nginx
Address 1: 172.20.0.57 web-1.nginx.default.svc.cluster.local

如果我們刪除了這兩個pod,然后觀察pod情況:

$ kubectl delete pod -l app=nginx

$ kubectl get pod -w -l app=nginx
web-0   1/1     Terminating   0          83m
web-1   1/1     Terminating   0          83m
web-0   0/1     Pending       0          0s
web-1   0/1     Terminating   0          83m
web-0   0/1     ContainerCreating   0          0s
web-0   1/1     Running             0          1s
web-1   0/1     Pending             0          0s
web-1   0/1     ContainerCreating   0          0s
web-1   1/1     Running             0          1s

當我們把這兩個 Pod 刪除之后,Kubernetes 會按照原先編號的順序,創建出了兩個新的 Pod。並且,Kubernetes 依然為它們分配了與原來相同的“網絡身份”:web-0.nginx 和 web-1.nginx。

發生故障

但是網絡結構雖然沒變,但是pod對應的ip是改變了的,我們再進入到pod進行DNS解析:

$ nslookup web-0.nginx
Server:    10.68.0.2
Address 1: 10.68.0.2 kube-dns.kube-system.svc.cluster.local

Name:      web-0.nginx
Address 1: 172.20.0.59 web-0.nginx.default.svc.cluster.local

$ nslookup web-1.nginx
Server:    10.68.0.2
Address 1: 10.68.0.2 kube-dns.kube-system.svc.cluster.local

Name:      web-1.nginx
Address 1: 172.20.0.60 web-1.nginx.default.svc.cluster.local

提供穩定的專屬存儲

在講存儲狀態的時候,需要大家掌握上一節有關pv和pvc的知識才好往下繼續,建議大家看完再來看本節。

在上一節中,我們了解到Kubernetes 中 PVC 和 PV 的設計,實際上類似於“接口”和“實現”的思想。而 PVC、PV 的設計,也使得 StatefulSet 對存儲狀態的管理成為了可能。

比如我們聲明一個如下的StatefulSet:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web
spec:
  serviceName: "nginx"
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.9.1
        ports:
        - containerPort: 80
          name: web
        volumeMounts:
        - name: local-volume-a
          mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
  - metadata:
      name: local-volume-a
    spec:
      accessModes:
      - ReadWriteMany
      storageClassName: "local-volume"
      resources:
        requests:
          storage: 512Mi
      selector:
        matchLabels:
          key: local-volume-a-0

在這個StatefulSet中添加了volumeClaimTemplates字段,用來聲明對應的PVC的定義;也就是說這個PVC中使用的storageClass必須是local-volume,需要的存儲空間是512Mi,並且這個pvc對應的pv的標簽必須是key: local-volume-a-0。

然后我們准備一個PV:

apiVersion: v1
kind: PersistentVolume
metadata:
  name: local-volume-pv-0
  labels:
    key: local-volume-a-0
spec:
  capacity:
    storage: 0.5Gi
  volumeMode: Filesystem
  accessModes:
  - ReadWriteMany
  persistentVolumeReclaimPolicy: Retain
  storageClassName: local-volume
  local:
    path: /mnt/disks/vol1
  nodeAffinity:
    required:
      nodeSelectorTerms:
      - matchExpressions:
        - key: kubernetes.io/hostname
          operator: In
          values:
          - node1

我把這個PV創建在node1節點上,並且將本地磁盤掛載聲明為PV。

然后我們創建這個PV:

$ kubectl apply -f local-pv-web-0.yaml

$ kubectl get pv
NAME                CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM
               STORAGECLASS   REASON   AGE
local-volume-pv-0   512Mi      RWX            Retain           Available       default/local-vo

然后我們在創建這個StatefulSet的時候,會自動創建PVC:

$ kubectl apply -f statefulset2.yaml

$ kubectl get pvc
NAME                   STATUS   VOLUME              CAPACITY   ACCESS MODES   STORAGECLASS   AGE
local-volume-a-web-0   Bound    local-volume-pv-0   512Mi      RWX            local-volume   15m

創建的PVC名字都是由:<PVC 名字 >-<StatefulSet 名字 >-< 編號 >構成,編號從0開始,並且我們可以看到上面的PV已經處於Bound狀態

這個時候我們進入到Pod中,寫入一個文件:

$ kubectl exec -it web-0  -- /bin/bash

$ echo helloword >/usr/share/nginx/html/index.html

這樣就會在Pod 的 Volume 目錄里寫入一個文件。

StatefulSet的縮容與擴容

如果我們把StatefulSet進行縮容,那么StatefulSet會刪除將pod的順序由大到小刪除。在刪除完相應的pod之后,對應的PVC並不會被刪除,如果需要釋放特定的持久卷時,需要手動刪除對應的持久卷聲明。

如果我們再把StatefulSet進行擴容,新創建的pod還是會和原來的PVC相互綁定,新的pod實例會運行到與之前完全一致的狀態。

更新策略

在 Kubernetes 1.7 及之后的版本中,可以為 StatefulSet 設定 .spec.updateStrategy 字段。

OnDelete

如果 StatefulSet 的 .spec.updateStrategy.type 字段被設置為 OnDelete,當您修改 .spec.template 的內容時,StatefulSet Controller 將不會自動更新其 Pod。您必須手工刪除 Pod,此時 StatefulSet Controller 在重新創建 Pod 時,使用修改過的 .spec.template 的內容創建新 Pod。

例如我們執行下面的語句更新上面例子中創建的web:

$ kubectl set image statefulset web nginx=nginx:1.18.0

$ kubectl describe pod web-0
....
Containers:
  nginx:
    Container ID:   docker://7e45cd509db74a96b4f6ca4d9f7424b3c4794f56e28bfc3fbf615525cd2ecadb
    Image:          nginx:1.9.1
....

然后我們發現pod的nginx版本並沒有發生改變,需要我們手動刪除pod之后才能生效。

$ kubectl delete pod web-0
pod "web-0" deleted

$ kubectl describe pod web-0
...
Containers:
  nginx:
    Container ID:   docker://0f58b112601a39f3186480aa97e72767b05fdfa6f9ca02182d3fb3b75c159ec0
    Image:          nginx:1.18.0
...

Rolling Updates

.spec.updateStrategy.type 字段的默認值是 RollingUpdate,該策略為 StatefulSet 實現了 Pod 的自動滾動更新。在更新完.spec.tempalte 字段后StatefulSet Controller 將自動地刪除並重建 StatefulSet 中的每一個 Pod。

刪除和重建的順序也是有講究的:

  • 刪除的時候從序號最大的開始刪,每刪除一個會更新一個。
  • 只有更新完的pod已經是ready狀態了才往下繼續更新。

為 RollingUpdate 進行分區

當為StatefulSet 的 RollingUpdate 字段的指定 partition 字段的時候,則所有序號大於或等於 partition 值的 Pod 都會更新。序號小於 partition 值的所有 Pod 都不會更新,即使它們被刪除,在重新創建時也會使用以前的版本。

如果 partition 值大於其 replicas 數,則更新不會傳播到其 Pod。這樣可以實現金絲雀發布Canary Deploy或者灰度發布。

如下,因為我們的web是2個pod組成,所以可以將partition設置為1:

$ kubectl patch statefulset web -p '{"spec":{"updateStrategy":{"type":"RollingUpdate","rollingUpdate":{"partition":1}}}}'

在這里,我使用了 kubectl patch 命令。它的意思是,以“補丁”的方式(JSON 格式的)修改一個 API 對象的指定字段。

下面我們執行更新:

$ kubectl set image statefulset  web nginx=nginx:1.19.1
statefulset.apps/web image updated

並在另一個終端中watch pod的變化:

$ kubectl get pods -l app=nginx -w
NAME    READY   STATUS    RESTARTS   AGE
web-0   1/1     Running   0          13m
web-1   1/1     Running   0          93s
web-1   0/1     Terminating   0          2m16s
web-1   0/1     Pending       0          0s
web-1   0/1     ContainerCreating   0          0s
web-1   1/1     Running             0          16s

可見上面只有一個web-1進行了版本的發布。

源碼分析

在k8s中,有三個文件stateful_pod_control.go、stateful_set.go、stateful_set_control.go共同完成了對statefulset的實現。主要實現是stateful_pod_control.go中的realStatefulPodControl執行pod具體創建、刪除、更新等操作;stateful_set_control.go的defaultStatefulSetControl實現了StatefulSet各個策略邏輯的處理;stateful_set.go的StatefulSetController是StatefulSet的執行入口。

調用次序是:StatefulSetController#sync-->StatefulSetController#syncStatefulSet-->defaultStatefulSetControl#UpdateStatefulSet-->defaultStatefulSetControl#performUpdate-->defaultStatefulSetControl#updateStatefulSet-->realStatefulPodControl中的各個pod操作方法

stateful_set.go中的StatefulSetController是statefulset啟動初始化的地方,所有的controller都是會執行到核心sync方法中,然后才對相應的pod進行操作,所以我們直接先看這個方法。

StatefulSetController#sync

func (ssc *StatefulSetController) sync(key string) error {
	...
	//獲取選擇器
	selector, err := metav1.LabelSelectorAsSelector(set.Spec.Selector)
	if err != nil {
		utilruntime.HandleError(fmt.Errorf("error converting StatefulSet %v selector: %v", key, err))
		// This is a non-transient error, so don't retry.
		return nil
	}

	if err := ssc.adoptOrphanRevisions(set); err != nil {
		return err
	}
	//根據選擇器拿到對應的pod列表
	pods, err := ssc.getPodsForStatefulSet(set, selector)
	if err != nil {
		return err
	}
	//往下執行sync操作
	return ssc.syncStatefulSet(set, pods)
}

然后我們接着往下看:

func (ssc *StatefulSetController) syncStatefulSet(set *apps.StatefulSet, pods []*v1.Pod) error {
	klog.V(4).Infof("Syncing StatefulSet %v/%v with %d pods", set.Namespace, set.Name, len(pods)) 
	//這里會調用到StatefulSetControlInterface的實現的UpdateStatefulSet方法中
	if err := ssc.control.UpdateStatefulSet(set.DeepCopy(), pods); err != nil {
		return err
	}
	klog.V(4).Infof("Successfully synced StatefulSet %s/%s successful", set.Namespace, set.Name)
	return nil
}

這里會調用到stateful_set_control.go文件中的StatefulSetControlInterface的實現defaultStatefulSetControl類中的UpdateStatefulSet方法中。

defaultStatefulSetControl#UpdateStatefulSet

func (ssc *defaultStatefulSetControl) UpdateStatefulSet(set *apps.StatefulSet, pods []*v1.Pod) error {

	// list all revisions and sort them
	revisions, err := ssc.ListRevisions(set)
	if err != nil {
		return err
	}
	history.SortControllerRevisions(revisions)
	//StatefulSet主要的更新邏輯
	currentRevision, updateRevision, err := ssc.performUpdate(set, pods, revisions)
	if err != nil {
		return utilerrors.NewAggregate([]error{err, ssc.truncateHistory(set, pods, revisions, currentRevision, updateRevision)})
	}

	// maintain the set's revision history limit
	return ssc.truncateHistory(set, pods, revisions, currentRevision, updateRevision)
}

在UpdateStatefulSet方法中具體的邏輯都放在了performUpdate中,繼續往下走:

defaultStatefulSetControl#performUpdate

func (ssc *defaultStatefulSetControl) performUpdate(
	set *apps.StatefulSet, pods []*v1.Pod, revisions []*apps.ControllerRevision) (*apps.ControllerRevision, *apps.ControllerRevision, error) {

	// get the current, and update revisions
	//獲取各個Revision,通過不同的Revision來進行版本的控制
	currentRevision, updateRevision, collisionCount, err := ssc.getStatefulSetRevisions(set, revisions)
	if err != nil {
		return currentRevision, updateRevision, err
	}

	// perform the main update function and get the status
	//主要執行更新操作,包括pod的創建、更新、刪除,並返回最后的StatefulSet執行狀態
	status, err := ssc.updateStatefulSet(set, currentRevision, updateRevision, collisionCount, pods)
	if err != nil {
		return currentRevision, updateRevision, err
	}

	// update the set's status
	//最后更新StatefulSet的狀態
	err = ssc.updateStatefulSetStatus(set, status)
	if err != nil {
		return currentRevision, updateRevision, err
	}
	...
}

這個方法主要分三步:

  1. 獲取目前StatefulSet各個Revision的情況;
  2. 執行具體的更新操作;
  3. 最后將StatefulSet的運行狀態進行更新;

接下來進入到核心的方法中,這個方法很長,會分成幾段進行說明:

defaultStatefulSetControl#updateStatefulSet

func (ssc *defaultStatefulSetControl) updateStatefulSet(
	set *apps.StatefulSet,
	currentRevision *apps.ControllerRevision,
	updateRevision *apps.ControllerRevision,
	collisionCount int32,
	pods []*v1.Pod) (*apps.StatefulSetStatus, error) {
    ...
	//將pod列表區分為有效的和失效的列表
	for i := range pods {
		status.Replicas++

		// count the number of running and ready replicas
		//如果已經ready了,那么計數加一
		if isRunningAndReady(pods[i]) {
			status.ReadyReplicas++
		}

		// count the number of current and update replicas
		//為需要更新的pod計數
		if isCreated(pods[i]) && !isTerminating(pods[i]) {
			if getPodRevision(pods[i]) == currentRevision.Name {
				status.CurrentReplicas++
			}
			if getPodRevision(pods[i]) == updateRevision.Name {
				status.UpdatedReplicas++
			}
		}
		//getOrdinal是獲取pod的序號
		if ord := getOrdinal(pods[i]); 0 <= ord && ord < replicaCount {
			// if the ordinal of the pod is within the range of the current number of replicas,
			// insert it at the indirection of its ordinal
			replicas[ord] = pods[i]
		//	如果序號大於statefulset設置的副本數,那么放入到condemned集合中,等待銷毀
		} else if ord >= replicaCount {
			// if the ordinal is greater than the number of replicas add it to the condemned list
			condemned = append(condemned, pods[i])
		}
		// If the ordinal could not be parsed (ord < 0), ignore the Pod.
	}

	// for any empty indices in the sequence [0,set.Spec.Replicas) create a new Pod at the correct revision
	//如果對應的序號中沒有對應的pod,那么需要創建新的pod
	for ord := 0; ord < replicaCount; ord++ {
		if replicas[ord] == nil {
			replicas[ord] = newVersionedStatefulSetPod(
				currentSet,
				updateSet,
				currentRevision.Name,
				updateRevision.Name, ord)
		}
	}

	// sort the condemned Pods by their ordinals
	sort.Sort(ascendingOrdinal(condemned))

	// find the first unhealthy Pod
	//找到副本集合中狀態不正常的pod
	for i := range replicas {
		if !isHealthy(replicas[i]) {
			unhealthy++
			//找到第一個不正常的pod的序號
			if ord := getOrdinal(replicas[i]); ord < firstUnhealthyOrdinal {
				firstUnhealthyOrdinal = ord
				firstUnhealthyPod = replicas[i]
			}
		}
	}
	//從失效pod集合中找到第一個不正常pod的序號
	for i := range condemned {
		if !isHealthy(condemned[i]) {
			unhealthy++
			if ord := getOrdinal(condemned[i]); ord < firstUnhealthyOrdinal {
				firstUnhealthyOrdinal = ord
				firstUnhealthyPod = condemned[i]
			}
		}
	}
    ...
}

這段代碼會遍歷pod列表,然后將pod分表存到replicas列表和condemned列表中,在condemned列表中的pod表示這些pod是多余的,超過了statefulset設置的副本數,需要被刪除掉的;

然后會繼續遍歷replicas列表和condemned列表,找到pod中序號最小的不健康的pod,不健康的pod定義如下:

func isHealthy(pod *v1.Pod) bool {
	return isRunningAndReady(pod) && !isTerminating(pod)
}

然后我們繼續往下:

func (ssc *defaultStatefulSetControl) updateStatefulSet(
	set *apps.StatefulSet,
	currentRevision *apps.ControllerRevision,
	updateRevision *apps.ControllerRevision,
	collisionCount int32,
	pods []*v1.Pod) (*apps.StatefulSetStatus, error) {
	...
	//檢查StatefulSet是否已經被刪除
	if set.DeletionTimestamp != nil {
		return &status, nil
	}
	//我們默認的狀態是OrderedReady,所以monotonic是true
	//也就是說在擴縮容的時候會等待pod狀態為ready才會繼續
	monotonic := !allowsBurst(set)

	// Examine each replica with respect to its ordinal
	//檢查副本集合里面是不是所有的pod都遵循序號遞增原則
	for i := range replicas {
		// delete and recreate failed pods
		//刪除然后創新創建 fail狀態的pod
		if isFailed(replicas[i]) {
			ssc.recorder.Eventf(set, v1.EventTypeWarning, "RecreatingFailedPod",
				"StatefulSet %s/%s is recreating failed Pod %s",
				set.Namespace,
				set.Name,
				replicas[i].Name)
			if err := ssc.podControl.DeleteStatefulPod(set, replicas[i]); err != nil {
				return &status, err
			}
			if getPodRevision(replicas[i]) == currentRevision.Name {
				status.CurrentReplicas--
			}
			if getPodRevision(replicas[i]) == updateRevision.Name {
				status.UpdatedReplicas--
			}
			status.Replicas--
			replicas[i] = newVersionedStatefulSetPod(
				currentSet,
				updateSet,
				currentRevision.Name,
				updateRevision.Name,
				i)
		}
		// If we find a Pod that has not been created we create the Pod
		//如果發現一個pod還沒被創建,那么創建一下這個pod
		if !isCreated(replicas[i]) {
			if err := ssc.podControl.CreateStatefulPod(set, replicas[i]); err != nil {
				return &status, err
			}
			status.Replicas++
			if getPodRevision(replicas[i]) == currentRevision.Name {
				status.CurrentReplicas++
			}
			if getPodRevision(replicas[i]) == updateRevision.Name {
				status.UpdatedReplicas++
			}

			// if the set does not allow bursting, return immediately
			if monotonic {
				return &status, nil
			}
			// pod created, no more work possible for this round
			continue
		}
		// If we find a Pod that is currently terminating, we must wait until graceful deletion
		// completes before we continue to make progress.
		//如果發現這個pod處於terminating狀態,需要等到這個pod被優雅的刪除后才繼續執行,所以先return
		if isTerminating(replicas[i]) && monotonic {
			klog.V(4).Infof(
				"StatefulSet %s/%s is waiting for Pod %s to Terminate",
				set.Namespace,
				set.Name,
				replicas[i].Name)
			return &status, nil
		}
		// If we have a Pod that has been created but is not running and ready we can not make progress.
		// We must ensure that all for each Pod, when we create it, all of its predecessors, with respect to its
		// ordinal, are Running and Ready.
		//如果一個pod不是處於running和ready中動態,那么也不能繼續
		if !isRunningAndReady(replicas[i]) && monotonic {
			klog.V(4).Infof(
				"StatefulSet %s/%s is waiting for Pod %s to be Running and Ready",
				set.Namespace,
				set.Name,
				replicas[i].Name)
			return &status, nil
		}
		// Enforce the StatefulSet invariants
		if identityMatches(set, replicas[i]) && storageMatches(set, replicas[i]) {
			continue
		}
		// Make a deep copy so we don't mutate the shared cache
		replica := replicas[i].DeepCopy()
		//指定更新操作
		if err := ssc.podControl.UpdateStatefulPod(updateSet, replica); err != nil {
			return &status, err
		}
	}
    ...
}

首先會檢查StatefulSet是否已經被刪除,如果被刪除了直接返回就好了;

在遍歷replicas之前會獲取一個monotonic參數,表示是否串行更新,默認是OrderedReady,表示串行執行,也就是說如果是那么在擴縮容的時候,如果發現有pod不是處於ready狀態都會等待。

在遍歷replicas的時候如果發現pod處於fail狀態,那么會刪除之后重新創建;

如果該pod還沒有創建,那么會直接創建,如果pod處於Terminating,那么需要等待直到這個pod被優雅的刪除后才繼續執行,所以先return,等待下一次的syncLoop繼續處理;

如果一個pod不是處於running和ready中動態,那么也不能繼續,先return,等待下一次的syncLoop繼續處理;

繼續往下:

func (ssc *defaultStatefulSetControl) updateStatefulSet(
	set *apps.StatefulSet,
	currentRevision *apps.ControllerRevision,
	updateRevision *apps.ControllerRevision,
	collisionCount int32,
	pods []*v1.Pod) (*apps.StatefulSetStatus, error) {
	...
	//遍歷condemned列表的時候是從后往前遍歷的,擴容將優於更新
	for target := len(condemned) - 1; target >= 0; target-- {
		// wait for terminating pods to expire
		//等待處於Terminating的pod終止
		if isTerminating(condemned[target]) {
			klog.V(4).Infof(
				"StatefulSet %s/%s is waiting for Pod %s to Terminate prior to scale down",
				set.Namespace,
				set.Name,
				condemned[target].Name)
			// block if we are in monotonic mode
			if monotonic {
				return &status, nil
			}
			continue
		}
		// if we are in monotonic mode and the condemned target is not the first unhealthy Pod block
		//如果pod沒有處於Running 或Ready狀態,並且這個pod不是第一個不正常的pod,那么等待此pod運行
		if !isRunningAndReady(condemned[target]) && monotonic && condemned[target] != firstUnhealthyPod {
			klog.V(4).Infof(
				"StatefulSet %s/%s is waiting for Pod %s to be Running and Ready prior to scale down",
				set.Namespace,
				set.Name,
				firstUnhealthyPod.Name)
			return &status, nil
		}
		klog.V(2).Infof("StatefulSet %s/%s terminating Pod %s for scale down",
			set.Namespace,
			set.Name,
			condemned[target].Name)
		//刪除此pod
		if err := ssc.podControl.DeleteStatefulPod(set, condemned[target]); err != nil {
			return &status, err
		}
		if getPodRevision(condemned[target]) == currentRevision.Name {
			status.CurrentReplicas--
		}
		if getPodRevision(condemned[target]) == updateRevision.Name {
			status.UpdatedReplicas--
		}
		if monotonic {
			return &status, nil
		}
	}
    ...
}

遍歷condemned的時候是從后往前遍歷,然后校驗pod的狀態;

如果pod處於處於Terminating,那么需要等待pod終止,先return;

如果pod沒有處於Running 或Ready狀態,並且這個pod不是第一個不正常的pod,那么等待此pod運行;

狀態沒有異常之后刪除該pod,然后return,等待下一次的syncLoop 。

繼續:

func (ssc *defaultStatefulSetControl) updateStatefulSet(
	set *apps.StatefulSet,
	currentRevision *apps.ControllerRevision,
	updateRevision *apps.ControllerRevision,
	collisionCount int32,
	pods []*v1.Pod) (*apps.StatefulSetStatus, error) {
	...
	//如果UpdateStrategy是OnDelete,那么pod需要手動刪除,所以直接返回
	if set.Spec.UpdateStrategy.Type == apps.OnDeleteStatefulSetStrategyType {
		return &status, nil
	}

	// we compute the minimum ordinal of the target sequence for a destructive update based on the strategy.
	updateMin := 0
	//滾動更新策略,沒有設置Partition,那么默認是0
	if set.Spec.UpdateStrategy.RollingUpdate != nil {
		updateMin = int(*set.Spec.UpdateStrategy.RollingUpdate.Partition)
	}
	// we terminate the Pod with the largest ordinal that does not match the update revision.
	//只會更新序號大於updateMin的pod,並且是倒序更新
	for target := len(replicas) - 1; target >= updateMin; target-- {

		// delete the Pod if it is not already terminating and does not match the update revision.、
		//如果該pod狀態不是terminating,並且該pod沒有被更新,那么刪除該pod
		if getPodRevision(replicas[target]) != updateRevision.Name && !isTerminating(replicas[target]) {
			klog.V(2).Infof("StatefulSet %s/%s terminating Pod %s for update",
				set.Namespace,
				set.Name,
				replicas[target].Name)
			err := ssc.podControl.DeleteStatefulPod(set, replicas[target])
			status.CurrentReplicas--
			return &status, err
		}

		// wait for unhealthy Pods on update
		if !isHealthy(replicas[target]) {
			klog.V(4).Infof(
				"StatefulSet %s/%s is waiting for Pod %s to update",
				set.Namespace,
				set.Name,
				replicas[target].Name)
			return &status, nil
		}

	}
	return &status, nil
}

到這里的時候校驗UpdateStrategy策略是不是OnDelete,如果是,那么pod需要手動刪除,所以直接返回;

然后校驗是不是滾動更新,並且查看有沒有設置Partition,Partition沒有設置默認為0;

然后遍歷更新replicas,順序也是從后往前進行更新,但是會只會更新序號大於Partition的pod。滾動更新的時候會都會判斷當前的狀態是不是terminating,然后刪除該pod,而不會再去看monotonic這個值,這里需要注意一下。

總結

StatefulSet把有狀態的應用抽象為兩種情況:拓撲狀態和存儲狀態。

拓撲狀態指的是應用的多個實例之間不是完全對等的關系,包含啟動的順序、創建之后的網絡標識等必須保證。

存儲狀態指的是不同的實例綁定了不同的存儲,如Pod A在它的生命周期中讀取的數據必須是一致的,哪怕是重啟之后還是需要讀取到同一個存儲。

然后講解了一下StatefulSet發布更新該如何做,updateStrategy策略以及通過partition如果實現金絲雀發布等。

最后通過源碼,我們更清晰的了解到了statefulset中的創建、更新、刪除等操作是如何實現的。

Reference

https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/

https://github.com/kubernetes/kubernetes/tree/release-1.19

https://draveness.me/kubernetes-statefulset/

https://blog.tianfeiyu.com/source-code-reading-notes/kubernetes/statefulset_controller.html

《 K8s in Action》

《深入理解k8s》


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM