一文讀懂 K8S StatefulSet 原理
爲什麼需要 StatefulSet
在使用 Kubernetes 部署服務時,通常會選擇 Deployment
資源對象。由 Deployment 創建的 Pod 實例是完全相同的,它們在啓動時沒有順序要求,並且通常掛載相同的存儲卷。因此,客戶端訪問任意一個 Pod,返回的結果是一致的。這類服務被稱爲 無狀態服務。
然而,如果嘗試使用 Deployment 部署如 MySQL、Redis 等數據庫服務時,僅能滿足單節點部署的需求。在集羣模式下,Deployment 的特性顯然無法滿足複雜場景的要求。以 MySQL 一主多從集羣爲例,主節點和從節點的數據是各自獨立的,顯然不能共用同一個存儲卷。同時,客戶端在訪問 MySQL 時,通常將寫操作路由到主節點,而讀操作則可以分發至任意從節點,這就要求客戶端能識別並連接多個具體的 MySQL 實例。
Deployment 由於無法提供固定的網絡標識、穩定的存儲綁定和有序的 Pod 啓動機制,難以滿足上述需求。因此,這類具備狀態、節點間職責區分的數據服務,通常被稱爲 有狀態服務。
Kubernetes 提供的 StatefulSet
資源,正是專門爲有狀態服務設計的。它可以爲每個 Pod 分配固定的名稱、唯一的存儲卷,並確保 Pod 的啓動、縮容和終止過程具有可預測的順序,是部署數據庫、消息隊列、分佈式緩存等有狀態應用的首選方案。
StatefulSet 特徵
StatefulSet
資源具有一下特徵:
-
• 有序創建 Pod
-
• 穩定的、唯一的網絡標識符
-
• 穩定的、持久化的存儲
-
• 有序的、自動滾動更新
-
• 有序的、優雅的刪除和終止
順序創建 Pod
StatefulSet
默認以嚴格的順序創建其 Pod,因爲有些服務 Pod 之間啓動具有先後順序,例如 Mysql 從節點需要在主節點啓動完成之後再啓動。我們可以通過配置 podManagementPolicy
來管理啓動策略。可以是 OrderedReady
或 Parallel
。
-
• OrderedReady:表示按順序啓動和終止 Pod,保證有序性和穩定性;
-
• Parallel:表示可以並行啓動和終止 Pod,適用於無依賴關係的應用場景。
apiVersion: apps/v1
kind:StatefulSet
metadata:
name:web
spec:
serviceName:"nginx"
podManagementPolicy:"Parallel"
replicas:2
selector:
matchLabels:
app:nginx
template:
metadata:
labels:
app:nginx
spec:
containers:
-name:nginx
image:registry.k8s.io/nginx-slim:0.21
ports:
-containerPort:80
name: web
對於一個擁有 n 個副本的 StatefulSet,Pod name 是固定的,且按照 <statefulset name>-<ordinal index>
規則命令。Pod 被部署時是按照 {0..n-1} 的序號順序創建的。
# watch pod 從創建到 running 的過程
kubectl get pods --watch -l app=nginx
NAME READY STATUS RESTARTS AGE
web-0 0/1 Pending 0 0s
web-0 0/1 Pending 0 0s
web-0 0/1 ContainerCreating 0 0s
web-0 1/1 Running 0 19s
web-1 0/1 Pending 0 0s
web-1 0/1 Pending 0 0s
web-1 0/1 ContainerCreating 0 0s
web-1 1/1 Running 0 18s
唯一的網絡
在部署 MySQL 一主多從集羣時,客戶端服務在連接數據庫時通常有讀寫分離的需求:寫請求需連接主節點,而 讀請求則可以連接任意從節點。由於 Kubernetes 中 Pod 的 IP 地址在重啓後可能發生變化,因此直接使用 Pod IP 進行連接顯然不可行。
此時,我們自然會想到使用 Kubernetes 的 Service 資源。然而,Kubernetes 中 Service 是通過 LabelSelector
機制將一組具有相同標籤的 Pod 聚合在一起的。同一個 StatefulSet 下的 Pod 通常共享相同的標籤,因此無法通過兩個不同的 Service 去精確綁定主節點和從節點。
一種可行的方案是將主節點和從節點分別部署爲兩個獨立的 StatefulSet,並分別創建對應的 Service。但這種方式雖然可以實現功能,結構上卻顯得 冗餘且不優雅,增加了維護和部署的複雜度。
那麼有沒有更合理的方式來爲每個 Pod 提供獨立的訪問入口呢?答案是可以通過 Kubernetes 的 DNS 系統 —— CoreDNS,爲每個 Pod 提供穩定的域名解析。StatefulSet 正是基於這種設計理念,引入了 Headless Service(無頭服務)的機制。
Headless Service 是一種特殊類型的 Service,它不會分配 ClusterIP,也不會做流量負載均衡。而是將請求通過 DNS 直接解析到 StatefulSet 中具體的 Pod IP 地址。每個 Pod 都擁有一個唯一且穩定的 DNS 域名(例如:mysql-0.mysql-headless.default.svc.cluster.local
),即便 Pod 重啓或調度到其他節點,只要 Pod 名保持不變,其域名也不會改變。DNS 記錄會隨着 Pod 的 IP 變更自動更新,從而保障客戶端始終可以通過固定域名訪問對應的數據庫節點。
這種方式既實現了服務的可發現性,又滿足了有狀態服務對節點身份的精確識別需求,結構更清晰、運維更高效。
每個 Headless service 背後的 Pod 對應域名格式如下:
<pod-name>.<svc-name>.<namespace>.svc.cluster.local
這個 DNS 記錄正是 Kubernetes 集羣爲 Pod 分配的一個唯一標識,只要我們知道 Pod 的名字,以及它對應的 Service 名字,就可以組裝出這樣一條 DNS 記錄訪問到 Pod 的 IP 地址,這個能力是非常重要的,接下來我們就來看下 StatefulSet 資源對象是如何結合 Headless Service 提供服務的。下面具體看 HeadlessService 如何提供服務的:
apiVersion: v1
kind:Service
metadata:
name:mysql
labels:
app:mysql
spec:
# Headless Service
clusterIP:None
selector:
app:mysql
ports:
-port:3306
name:mysql
---
apiVersion:apps/v1
kind:StatefulSet
metadata:
name:mysql
spec:
serviceName:mysql
replicas:3
selector:
matchLabels:
app:mysql
template:
metadata:
labels:
app:mysql
spec:
containers:
-name:mysql
image:mysql:8.0
ports:
-containerPort:3306
name:mysql
......
StatefulSet 中
serviceName
字段必須和 Service 中name
一致,StatefulSet Controller 就是通過serviceName
字段去查找對應的 Service
這樣三個 Mysql Pod 實例對應的 DNS 域名爲:
mysql-0.mysql.default.svc.cluster.local
mysql-1.mysql.default.svc.cluster.local
mysql-2.mysql.default.svc.cluster.local
進入某個客戶端 Pod 去訪問這些域名,可以發現解析後始終是對應的 Pod 的 IP 地址。
$ / # nslookup mysql-0.mysql
Server: 10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local
Name: mysql-0.mysql
Address 1: 10.244.4.83 mysql-0.mysql.default.svc.cluster.local
$/ # nslookup mysql-1.mysql
Server: 10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local
Name: mysql-1.mysql
Address 1: 10.244.1.175 mysql-1.mysql.default.svc.cluster.local
$/ # nslookup mysql-2.mysql
Server: 10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local
Name: mysql-2.mysql
Address 1: 10.244.2.75 mysql-2.mysql.default.svc.cluster.local
穩定的存儲
Mysql 這種有狀態服務,每個實例對應的存儲肯定是不同的,而且 Pod 重啓後還能夠重新掛載到之前的存儲上。爲了實現這種穩定存儲功能,StatefulSet 中 volumeClaimTemplates
字段定義了 Pod 的存儲配置定義。
apiVersion: v1
kind:Service
metadata:
name:mysql
labels:
app:mysql
spec:
# Headless Service
clusterIP:None
selector:
app:mysql
ports:
-port:3306
name:mysql
---
apiVersion:apps/v1
kind:StatefulSet
metadata:
name:mysql
spec:
serviceName:mysql
replicas:3
selector:
matchLabels:
app:mysql
template:
metadata:
labels:
app:mysql
spec:
containers:
-name:mysql
image:mysql:8.0
ports:
-containerPort:3306
name:mysql
volumeMounts:
-name:data
mountPath:/var/lib/mysql
subPath:mysql
volumeClaimTemplates:
-metadata:
name:mysql-data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 1Gi
volumeClaimTemplates
屬性會自動給每一個 Pod 創建一個 PVC 對象,然後會在集羣中綁定適合的 PV 資源。volumeClaimTemplates
其實這裏就是一個 PVC 的模板,而且 PVC Name 是有規則的,按照 <volumeClaimTemplatesName.podName>
,以上 Yaml 會創建三個 PVC,如下:
$ kubectl get pvc -l app=mysql
NAME STATUS VOLUME CAPACITY ACCESSMODES AGE
mysql-data.mysql-0 Bound pvc-15c268c7-b507-11e6-932f-42010a800002 1Gi RWO 48s
mysql-data.mysql-1 Bound pvc-15c79307-b507-11e6-932f-42010a800002 1Gi RWO 48s
由於 PVC name 是固定的,即使 Pod 重啓後也會掛載到之前的 PVC 上。
更新策略
由於有狀態服務的多個實例之間通常存在數據同步、複製等機制,因此在更新這類服務時,需要遵循一定的順序,以保證系統的一致性與穩定性。
Kubernetes 中,StatefulSet
通過 spec.updateStrategy
字段定義了更新策略,支持以下兩種策略:
OnDelete
OnDelete
策略意味着,當更新了 StatefulSet
的 Pod 模板(如鏡像版本或配置)後,控制器不會自動重啓或更新 Pod。只有在手動刪除舊的 Pod 時,新的 Pod 纔會根據更新後的模板重新創建。這種方式適用於對更新節奏有嚴格控制要求的場景,運維人員可以手動掌控每個節點的替換時機。
RollingUpdate
RollingUpdate
是 StatefulSet 默認的更新策略。它會按照 序號索引的逆序(即從編號最大的 Pod 開始)依次進行更新,例如按照 mysql-2
、mysql-1
、mysql-0
的順序依次重啓。
在更新過程中,StatefulSet 控制器會:
-
• 逐個終止當前 Pod;
-
• 等待 Pod 恢復到
Running
和Ready
狀態; -
• 再繼續更新下一個 Pod。
這種順序性確保了服務集羣的穩定性,尤其適合有主從、複製關係的應用場景,如數據庫、分佈式存儲系統等。
通過設置 .spec.updateStrategy.rollingUpdate.partition
,可以指定一個分區序號。更新過程中,只有序號大於或等於該分區值的 Pod 纔會被滾動更新,而序號小於 partition 的 Pod 則保持不變。
這使得在需要分批次、逐步更新集羣節點時,可以更靈活地控制更新範圍,進一步降低風險。
刪除
在使用 StatefulSet 管理有狀態服務時,通常會爲每個 Pod 創建獨立的持久化卷(PVC)。當 StatefulSet 被刪除或縮容時,默認情況下這些 PVC 會被保留。這意味着在下次重新創建同名 StatefulSet 時,原先的 Pod 將會重新掛載到原有的數據捲上,從而保留了之前的應用狀態。
然而,在某些場景下,我們希望能夠在刪除 StatefulSet 或縮容時自動清理對應的 PVC,避免資源浪費。在 Kubernetes v1.27 中引入了這一能力,並在 v1.32 中變爲穩定特性。
Kubernetes 通過 StatefulSet 的可選字段 .spec.persistentVolumeClaimRetentionPolicy
來控制 PVC 的保留策略。啓用該功能需確保 API Server 和 Controller Manager 開啓了 StatefulSetAutoDeletePVC
特性門控。
該字段支持在兩類場景下配置 PVC 的刪除行爲:
-
• whenDeleted:刪除整個 StatefulSet 時觸發;
-
• whenScaled:縮小副本數,刪除多餘 Pod 時觸發。
每種場景都支持兩種策略:
Retain(默認)
表示即使 Pod 被刪除,來自 volumeClaimTemplates
的 PVC 也不會被移除。
這正是之前 Kubernetes 的默認行爲,確保數據不被意外清除。
Delete
-
• 在
whenDeleted: Delete
下,刪除 StatefulSet 時,其控制器會自動刪除所有與之相關聯的 PVC。 -
• 在
whenScaled: Delete
下,縮容過程中,只有對應被縮減的 Pod 所綁定的 PVC 會被清理。
通過這種方式,用戶可以靈活地選擇 PVC 的保留或清理策略。
注意:這兩種策略僅適用於 StatefulSet 自身刪除或縮容所引發的 Pod 刪除操作。
如果 Pod 是由於節點故障、驅逐或重啓等原因被替換,這些 PVC 仍會被保留,並由新創建的 Pod 自動重新掛載。
以下是 persistentVolumeClaimRetentionPolicy
的四種可能配置:
# 默認行爲:全部保留 PVC
apiVersion:v1
kind:StatefulSet
metadata:
name:mysql
spec:
persistentVolumeClaimRetentionPolicy:
whenDeleted:Retain
whenScaled:Retain
---
# 刪除 StatefulSet 時清理 PVC,縮容時保留
apiVersion:v1
kind:StatefulSet
metadata:
name:mysql
spec:
persistentVolumeClaimRetentionPolicy:
whenDeleted:Delete
whenScaled:Retain
---
# 刪除 StatefulSet 和縮容時均清理 PVC
apiVersion:v1
kind:StatefulSet
metadata:
name:mysql
spec:
persistentVolumeClaimRetentionPolicy:
whenDeleted:Delete
whenScaled:Delete
---
# 刪除 StatefulSet 時保留 PVC,縮容時清理多餘 PVC
apiVersion:v1
kind:StatefulSet
metadata:
name:mysql
spec:
persistentVolumeClaimRetentionPolicy:
whenDeleted:Retain
whenScaled: Delete
背後機制
-
• 當配置
whenDeleted: Delete
時,StatefulSet 控制器會爲每個 PVC 設置ownerReferences
,指向 StatefulSet 本身。刪除 StatefulSet 時,Kubernetes 通過 級聯刪除機制,自動清理這些 PVC。 -
• 若設置
whenScaled: Delete
,在縮容前,控制器會將對應 PVC 的ownerReferences
更新爲已刪除的 Pod。待 Pod 正常終止後,PVC 將被 Kubernetes 的垃圾回收機制自動移除。
這種方式確保了數據在被刪除前能被幹淨地卸載,避免資源泄漏或狀態異常,同時保持整個過程的可控性和可追溯性。
總結
StatefulSet 相比於 Deployment,更加智能且具備更強的場景適配能力,因此需要結合深入的業務場景才能真正發揮其優勢。通常在部署 MySQL、Redis、MongoDB 等有狀態服務時,會優先選擇使用 StatefulSet。然而,對於這些關鍵的中間件服務,單純地部署往往遠遠不夠,還需要具備如數據恢復、故障自動轉移等高級功能。
爲了實現更智能、自動化的管理,通常會引入 Operator 模式,例如 etcd-operator、prometheus-operator 等。這些 Operator 不僅能夠自動部署和維護有狀態應用,還能實現自愈、擴縮容、備份恢復等複雜運維邏輯。通過編寫自定義 Operator,我們可以根據具體業務需求,構建更加智能化、自動化的有狀態服務管理體系,從而提升系統的穩定性和運維效率。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/zoR3hbZn6qcpV3nPWcO_fQ