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

在上一篇中,講解了容器持久化存儲,從中我們知道什么是PV和PVC,這一篇我們講通過StatefulSet來使用它們。
這一篇中我重新按照源碼重新擼了一遍,希望能加深對k8s的理解,源碼版本是1.19,在閱讀源碼的時候可以參照着一起看會比較便於理解。
StatefulSet概念
我們在第三篇講的Deployment控制器是應用於無狀態的應用的,所有的Pod啟動之間沒有順序,Deployment可以任意的kill一個Pod不會影響到業務數據,但是這到了有狀態的應用中就不管用了。
而StatefulSet就是用來對有狀態應用提供支持的控制器。
StatefulSet創建的pod具有唯一的標識和創建和刪除順序的保障,從而主要做到了兩件事情:
- 提供穩定的網絡標識。一個StatefulSet創建的每個pod都有一個從零開始的順序索引。這樣可以方便通過主機名來定位pod,例如我們可以創建一個headless Service,通過Service記錄每個pod的獨立DNS記錄來定位到不同的pod,由於pod主機名固定,所以DNS記錄也不會變。如下:

- 提供穩定的專屬存儲。一個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
}
...
}
這個方法主要分三步:
- 獲取目前StatefulSet各個Revision的情況;
- 執行具體的更新操作;
- 最后將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》
