比:有狀態(tài)應(yīng)用的關(guān)鍵差異)
StatefulSet 和 Deployment 的區(qū)別是我做了快十年容器平臺(tái)架構(gòu)和排障之后被問(wèn)到的頻率最高的一個(gè)問(wèn)題。這個(gè)問(wèn)題表面上是兩個(gè) Kubernetes 對(duì)象的對(duì)比實(shí)際上背后是有狀態(tài)應(yīng)用怎么上云原生這個(gè)更大的話題。我見(jiàn)過(guò)把 MySQL 直接掛在 Deployment 上跑的生產(chǎn)集群遇到節(jié)點(diǎn)宕機(jī)后數(shù)據(jù)庫(kù)連不回來(lái)最后整個(gè)項(xiàng)目組花了兩個(gè)通宵處理數(shù)據(jù)也見(jiàn)過(guò)把所有 Web 應(yīng)用都改成 StatefulSet 的團(tuán)隊(duì)結(jié)果一次發(fā)布更新要等大半天因?yàn)槊總€(gè) Pod 都在排隊(duì)。兩種方式?jīng)]有絕對(duì)的對(duì)錯(cuò)關(guān)鍵是搞清楚你到底在管理什么樣的應(yīng)用。這篇文章里我會(huì)用實(shí)際踩坑的經(jīng)驗(yàn)和完整的例子把這兩個(gè)工作負(fù)載在設(shè)計(jì)上的差異講清楚。1. 先搞清楚兩類(lèi)工作負(fù)載管理的 Pod本質(zhì)差在哪1.1 DeploymentPod 是流水線上的耗材Deployment 面向的是無(wú)狀態(tài)應(yīng)用。它底下真正干活的是 ReplicaSetDeployment 只負(fù)責(zé)聲明我要多少個(gè)副本副本數(shù)、版本、Pod 模板都由它統(tǒng)一管理。你創(chuàng)建一個(gè) Deployment實(shí)際運(yùn)行起來(lái)的 Pod 名字長(zhǎng)這樣web-7d8f9cbf74-abc12 web-7d8f9cbf74-def34 web-7d8f9cbf74-ghi56前綴的web-后面跟著 ReplicaSet 的哈希和一段隨機(jī)字符串。注意這個(gè)隨機(jī)字符串它是 Deployment 對(duì) Pod 態(tài)度的直觀體現(xiàn)Pod 只是個(gè)臨時(shí)冒出來(lái)的實(shí)例名字不重要身份不重要IP 也不重要。任意一個(gè) Pod 掛了ReplicaSet 馬上拉起一個(gè)新的名字、IP 全變了但只要總數(shù)維持正確就行。一個(gè)很典型的場(chǎng)景Deployment 管理的 Nginx 如果從節(jié)點(diǎn) A 被重新調(diào)度到節(jié)點(diǎn) B客戶端通過(guò) Service 訪問(wèn)它完全感知不到變化。因?yàn)?Service 本身是個(gè)負(fù)載均衡入口后面一堆 Pod 都是等價(jià)的請(qǐng)求打到哪個(gè) Pod 都行。這就是無(wú)狀態(tài)的核心所有副本對(duì)請(qǐng)求等價(jià)數(shù)據(jù)要么不落到本地要么落到外置共享存儲(chǔ)。所以 Deployment 下面掛一些共享 NFS 或云盤(pán)也是常見(jiàn)的但那是所有副本共用一份數(shù)據(jù)而不是每個(gè)副本各管各自的數(shù)據(jù)。從這段描述你應(yīng)該能感覺(jué)到Deployment 的設(shè)計(jì)哲學(xué)是把 Pod 用完即棄。1.2 StatefulSetPod 帶著編號(hào)和檔案StatefulSet 恰恰相反。它的 Pod 名字是固定的、有規(guī)律的mysql-0 mysql-1 mysql-2編號(hào)從 0 開(kāi)始依次遞增。每個(gè)編號(hào)不僅僅是個(gè)數(shù)字而是這個(gè) Pod 的永久身份Pod 被刪除后重新創(chuàng)建出來(lái)的 Pod 還叫mysql-0不會(huì)變成別的名字這個(gè) Pod 掛的存儲(chǔ)卷也是固定的數(shù)據(jù)不會(huì)丟集群里的其他組件可以通過(guò)固定的主機(jī)名找到它不用關(guān)心它跑到哪臺(tái)節(jié)點(diǎn)上。這個(gè)設(shè)計(jì)的意義在分布式系統(tǒng)里極其明顯。以 MySQL 主從集群來(lái)說(shuō)從節(jié)點(diǎn)初始化時(shí)需要知道主節(jié)點(diǎn)的地址節(jié)點(diǎn)重啟后集群里其他成員需要知道它還是不是原來(lái)那個(gè)節(jié)點(diǎn)復(fù)制關(guān)系建立起來(lái)了不能因?yàn)橐淮沃貑⒕蛿嗟簟H绻泄?jié)點(diǎn)都像 Deployment 那樣隨機(jī)命名、隨機(jī)調(diào)度一旦某個(gè)節(jié)點(diǎn)換了身份整個(gè)集群的拓?fù)渚蛠y了。用生活化的類(lèi)比Deployment 管理的 Pod 像是流水線上的臨時(shí)工走了一個(gè)隨便補(bǔ)人工號(hào)不需要固定StatefulSet 管理的 Pod 則是有工牌、有檔案的正式員工走了要留檔回來(lái)還是原來(lái)的工號(hào)他手里的紙質(zhì)檔案也跟著他。所以處理數(shù)據(jù)庫(kù)、消息隊(duì)列、配置中心這類(lèi)應(yīng)用時(shí)你需要的恰恰是后者這種認(rèn)身份的能力。2. 網(wǎng)絡(luò)身份那點(diǎn)事固定 IP 是謠言穩(wěn)定 DNS 才是真本事2.1 為什么說(shuō) StatefulSet 不提供固定 IP很多文章和視頻在講 StatefulSet 的時(shí)候都會(huì)說(shuō)提供穩(wěn)定的網(wǎng)絡(luò)標(biāo)識(shí)于是有人直接理解成Pod 有固定 IP。這個(gè)理解是錯(cuò)的而且會(huì)帶來(lái)兩個(gè)隱患一是排查問(wèn)題的時(shí)候總按著 IP 去找結(jié)果發(fā)現(xiàn) IP 變了二是在設(shè)計(jì)內(nèi)部通信時(shí)把 IP 硬編碼進(jìn)配置節(jié)點(diǎn)一重啟就全斷。實(shí)際上StatefulSet 里的 Pod 每次重建IP 都是重新分配的。它保證的不是 IP 不變而是主機(jī)名也就是 Pod 名不變以及基于主機(jī)名生成的 DNS 記錄不變。這一點(diǎn)我建議每個(gè)用 StatefulSet 的人都先記住能省掉后面很多排查時(shí)間。那穩(wěn)定的網(wǎng)絡(luò)標(biāo)識(shí)到底是怎么實(shí)現(xiàn)的Kubernetes 里的 Pod 默認(rèn)沒(méi)有對(duì)外可達(dá)的穩(wěn)定主機(jī)名訪問(wèn)一個(gè) Pod 一般是走 Service。普通 Service 用一個(gè) ClusterIP 做負(fù)載均衡Pod 的 IP 變化對(duì)客戶端不可見(jiàn)。但 StatefulSet 的場(chǎng)景里客戶端往往需要直連某一個(gè)具體節(jié)點(diǎn)這時(shí)候就需要另一種 Service。2.2 Headless Service 與每個(gè) Pod 獨(dú)一無(wú)二的 DNSStatefulSet 的 spec 里有個(gè)必填字段叫serviceName它要求你提供一個(gè) Service通常是一個(gè) Headless Service。所謂 Headless就是把clusterIP設(shè)為NoneapiVersion: v1 kind: Service metadata: name: mysql-headless spec: clusterIP: None selector: app: mysql ports: - port: 3306這個(gè) Service 不提供統(tǒng)一的 ClusterIP 負(fù)載均衡入口而是把域名直接解析到各個(gè) Pod 的 IP。配合 StatefulSet 的命名規(guī)則每個(gè) Pod 會(huì)得到一條獨(dú)有的 DNS 記錄mysql-0.mysql-headless.default.svc.cluster.local mysql-1.mysql-headless.default.svc.cluster.local mysql-2.mysql-headless.default.svc.cluster.local格式是pod-name.headless-service-name.namespace.svc.cluster.local。也就是說(shuō)只要通過(guò)這個(gè)域名訪問(wèn)不管 Pod 換了多少個(gè) IP都能準(zhǔn)確找到那個(gè)人。你可以在集群里隨便找個(gè) Pod 用nslookup或者dig驗(yàn)證返回的不是一個(gè) VIP而是一串 Pod 的 A 記錄。2.3 穩(wěn)定 DNS 在分布式組件里到底解決什么問(wèn)題舉幾個(gè)我實(shí)際部署過(guò)的例子。ZooKeeper 的配置里要寫(xiě)每個(gè)節(jié)點(diǎn)的地址節(jié)點(diǎn)啟動(dòng)后會(huì)根據(jù)配置里的主機(jī)名互相建立會(huì)話Etcd 集群?jiǎn)?dòng)參數(shù)里也是用 peer 地址互相發(fā)現(xiàn)Kafka 的 broker 注冊(cè)到元數(shù)據(jù)服務(wù)之后客戶端和其他 broker 要能按同一個(gè)地址找到它。這些組件如果跑在 Deployment 里Pod 每次重啟名字都變集群的一致性和成員列表就得靠額外的服務(wù)發(fā)現(xiàn)機(jī)制來(lái)維護(hù)維護(hù)成本極高。StatefulSet 的做法等于把誰(shuí)是誰(shuí)這件事固化了下來(lái)。節(jié)點(diǎn)從機(jī)器 1 搬到機(jī)器 2主機(jī)名不變DNS 指向新 IP其他成員重新連接時(shí)還是按老名字找連接關(guān)系不中斷。這是分布式系統(tǒng)最需要的一條底層能力。注意穩(wěn)定 DNS 不是說(shuō) Pod 永遠(yuǎn)不宕機(jī)而是說(shuō)它宕機(jī)重建之后身份不會(huì)變。這兩件事有本質(zhì)區(qū)別。當(dāng)然穩(wěn)定 DNS 不是沒(méi)有代價(jià)。它要求你的應(yīng)用開(kāi)發(fā)時(shí)就支持主機(jī)名配置而不是硬編碼 IP而且如果應(yīng)用自身不做成員發(fā)現(xiàn)你依然需要手寫(xiě)初始節(jié)點(diǎn)列表。StatefulSet 只是給了你地基蓋什么樣的房子還得看應(yīng)用自己。3. 存儲(chǔ)綁定volumeClaimTemplates 才是 StatefulSet 的殺手锏3.1 Deployment 為什么做不到每個(gè)副本一塊獨(dú)立盤(pán)從名字就能看出來(lái)Deployment 關(guān)心的是部署這個(gè)動(dòng)作它不關(guān)心每個(gè)副本的狀態(tài)。所以 Deployment 的 Pod 模板里存儲(chǔ)卷一般是下面兩種情況之一所有副本共享同一個(gè) PVC或者每個(gè)副本掛同一個(gè)只讀配置卷。共享一個(gè) PVC 沒(méi)問(wèn)題但你要想清楚數(shù)據(jù)語(yǔ)義如果三個(gè)副本同時(shí)往一個(gè) RWOReadWriteOnce卷上寫(xiě)數(shù)據(jù)絕大多數(shù)存儲(chǔ)后端會(huì)直接報(bào)錯(cuò)。而改成 RWXReadWriteMany共享卷又會(huì)面臨并發(fā)寫(xiě)入的一致性問(wèn)題。所以 Deployment 里跑數(shù)據(jù)庫(kù)要么單副本硬扛要么就得在應(yīng)用層面自己搞定多副本數(shù)據(jù)同步Kubernetes 層面幫不了你。StatefulSet 提供了volumeClaimTemplates翻譯過(guò)來(lái)是卷聲明模板apiVersion: apps/v1 kind: StatefulSet metadata: name: mysql spec: serviceName: mysql-headless replicas: 3 selector: matchLabels: app: mysql template: metadata: labels: app: mysql spec: containers: - name: mysql image: mysql:8.0 volumeMounts: - name: data mountPath: /var/lib/mysql volumeClaimTemplates: - metadata: name: data spec: accessModes: [ReadWriteOnce] resources: requests: storage: 10Gi這里聲明了一個(gè)名為data的 PVC 模板。StatefulSet 控制器會(huì)為每個(gè)編號(hào)的 Pod 自動(dòng)生成對(duì)應(yīng)的 PVC名字是>spec: persistentVolumeClaimRetentionPolicy: whenDeleted: Retain whenScaled: RetainwhenScaled可以設(shè)成Delete這樣縮容時(shí)對(duì)應(yīng) PVC 會(huì)被自動(dòng)清理避免殘留一堆沒(méi)人用的云盤(pán)持續(xù)收費(fèi)whenDeleted控制 StatefulSet 刪除時(shí) PVC 是否跟隨刪除。這兩個(gè)策略建議在測(cè)試環(huán)境先驗(yàn)證一遍再上生產(chǎn)因?yàn)镈elete意味著數(shù)據(jù)直接消失配置錯(cuò)了代價(jià)很大。4. 擴(kuò)縮容、更新、刪除順序性才是最有爭(zhēng)議的行為差異4.1 創(chuàng)建和縮容要排隊(duì)Deployment 擴(kuò)縮容的時(shí)候ReplicaSet 是并行創(chuàng)建或銷(xiāo)毀 Pod 的用戶幾乎感受不到先后順序。StatefulSet 默認(rèn)的podManagementPolicy是OrderedReady意思非常直白必須等序號(hào)小的 Pod 進(jìn)入 Ready 狀態(tài)才會(huì)創(chuàng)建下一個(gè)。所以創(chuàng)建一個(gè) 3 副本的 StatefulSet你執(zhí)行kubectl get pods觀察會(huì)看到mysql-0先 Running 并 Ready然后mysql-1才開(kāi)始創(chuàng)建mysql-1Ready 之后mysql-2才會(huì)出現(xiàn)??s容則是反著來(lái)先刪編號(hào)最大的一個(gè)等一個(gè)。這種排隊(duì)的意義在于很多有狀態(tài)應(yīng)用強(qiáng)依賴啟動(dòng)順序。比如一主兩從的 MySQL 集群主節(jié)點(diǎn)必須先起來(lái)客戶端和從節(jié)點(diǎn)才能知道往哪連Etcd 啟動(dòng)時(shí)也需要有初始成員先就位。如果你確定自己的應(yīng)用不依賴任何啟動(dòng)順序可以把podManagementPolicy改成Parallel讓所有 Pod 同時(shí)創(chuàng)建能省下不少等待時(shí)間。我做過(guò)一個(gè) Kafka 集群就是這樣三個(gè) broker 之間沒(méi)有嚴(yán)格的啟動(dòng)依賴改用 Parallel 策略之后整體拉起時(shí)間縮短了一半還多。4.2 滾動(dòng)更新策略和 Partition 灰度Deployment 的滾動(dòng)更新有maxSurge和maxUnavailable兩個(gè)參數(shù)控制節(jié)奏可以一次多起幾個(gè)新副本再逐步替換舊的。StatefulSet 的默認(rèn)更新策略RollingUpdate則沒(méi)有這種多副本同時(shí)切換的靈活性它按編號(hào)從大到小一次只更新一個(gè) Pod更新完一個(gè)并確認(rèn) Ready才繼續(xù)下一個(gè)。這種一次一個(gè)的行為對(duì)于數(shù)據(jù)類(lèi)應(yīng)用是必要的因?yàn)閿?shù)據(jù)庫(kù)節(jié)點(diǎn)不能同時(shí)全部替換總得有節(jié)點(diǎn)在對(duì)外服務(wù)。但也帶來(lái)了代價(jià)如果副本數(shù)多整個(gè)更新周期會(huì)很長(zhǎng)。我見(jiàn)過(guò)一個(gè) Elasticsearch 集群 15 個(gè)節(jié)點(diǎn)一次版本升級(jí)跑了接近一小時(shí)。如果你只想灰度更新部分節(jié)點(diǎn)StatefulSet 支持partition參數(shù)spec: updateStrategy: type: RollingUpdate rollingUpdate: partition: 2設(shè)置partition: 2之后控制器只會(huì)更新序號(hào)大于等于 2 的 Pod。也就是說(shuō)序號(hào) 0、1 保持舊版本序號(hào) 2 及以上的節(jié)點(diǎn)先升級(jí)。這在數(shù)據(jù)庫(kù)這類(lèi)需要先升級(jí)從庫(kù)、觀察一段時(shí)間、再升級(jí)主庫(kù)的場(chǎng)景下非常實(shí)用。等從庫(kù)驗(yàn)證沒(méi)問(wèn)題再把partition改成 1 或 0讓剩下的節(jié)點(diǎn)陸續(xù)跟上。還有OnDelete策略更新時(shí)不自動(dòng)替換 Pod只有你手動(dòng)刪除某個(gè) Pod控制器才會(huì)按新模板重建。適合那種想完全控制變更節(jié)奏的團(tuán)隊(duì)但日常用得少我一般只在需要配合外部數(shù)據(jù)遷移工具時(shí)才用。4.3 Pod 被刪、節(jié)點(diǎn)宕機(jī)后會(huì)發(fā)生什么先說(shuō)主動(dòng)刪 Pod你執(zhí)行kubectl delete pod mysql-0StatefulSet 控制器會(huì)立即創(chuàng)建一個(gè)新的mysql-0名字不變網(wǎng)絡(luò)標(biāo)識(shí)不變重新掛上原來(lái)的 PVC。這在很多場(chǎng)景下可以當(dāng)作重啟單個(gè)節(jié)點(diǎn)來(lái)用比如某個(gè)從庫(kù)連接數(shù)異常刪掉讓它重新初始化挺管用的。再說(shuō)節(jié)點(diǎn)宕機(jī)。如果承載mysql-0的節(jié)點(diǎn)失聯(lián)默認(rèn)情況下 kubelet 會(huì)對(duì)該節(jié)點(diǎn)上的 Pod 打上終止標(biāo)記但不會(huì)立刻讓 Pod 離開(kāi)節(jié)點(diǎn)因?yàn)樾枰却?jié)點(diǎn)恢復(fù)以確認(rèn) Pod 是否還活著。如果節(jié)點(diǎn)長(zhǎng)時(shí)間不恢復(fù)mysql-0會(huì)一直處于 Terminating 狀態(tài)而且由于 StatefulSet 默認(rèn)同一個(gè)編號(hào)在同一時(shí)間只能有一個(gè) Pod新 Pod 也無(wú)法創(chuàng)建整個(gè)集群的副本數(shù)就缺了。遇到這種情況常規(guī)操作是確認(rèn)節(jié)點(diǎn)確實(shí)回不來(lái)之后對(duì)這個(gè) Pod 執(zhí)行強(qiáng)制刪除kubectl delete pod mysql-0 --force --grace-period0然后控制器會(huì)用新 Pod 接管這個(gè)名字和 PVC。注意強(qiáng)制刪除有風(fēng)險(xiǎn)如果原 Pod 還活著可能產(chǎn)生兩個(gè)同名 Pod 短暫并存、同時(shí)訪問(wèn)同一塊存儲(chǔ)的情況。對(duì)數(shù)據(jù)庫(kù)來(lái)說(shuō)兩個(gè)進(jìn)程寫(xiě)同一塊數(shù)據(jù)盤(pán)是大忌所以執(zhí)行前一定要確認(rèn)那個(gè)節(jié)點(diǎn)徹底失聯(lián)或者已經(jīng)從集群中移除。這個(gè)風(fēng)險(xiǎn)我在文章開(kāi)頭提到的那個(gè) MySQL 事故里就踩過(guò)提醒各位格外小心。5. 選型判斷到底什么業(yè)務(wù)該用 StatefulSet5.1 三個(gè)判斷標(biāo)準(zhǔn)經(jīng)過(guò)前面幾輪的對(duì)比選型其實(shí)可以收斂成三個(gè)問(wèn)題數(shù)據(jù)丟了能重建嗎如果你的服務(wù)掛了之后數(shù)據(jù)可以從外部源重新同步、可以重新生成、可以從備份恢復(fù)且業(yè)務(wù)能接受短暫不可用那不一定要用 StatefulSet。每個(gè)副本需要獨(dú)立的持久化存儲(chǔ)嗎如果每個(gè)實(shí)例都必須有一塊自己的數(shù)據(jù)盤(pán)而且實(shí)例間不能共享那 Deployment 幫不了你StatefulSet 的volumeClaimTemplates幾乎是唯一直接支持的方式。實(shí)例之間是否需要穩(wěn)定的網(wǎng)絡(luò)身份分布式組件之間要互相發(fā)現(xiàn)、要建立固定連接需要每個(gè)節(jié)點(diǎn)有穩(wěn)定主機(jī)名那 StatefulSet 就很有必要。三個(gè)問(wèn)題里只要有兩個(gè)回答是基本就該認(rèn)真考慮 StatefulSet 了。如果全是否Deployment 完全夠用。5.2 典型場(chǎng)景對(duì)照表應(yīng)用場(chǎng)景有獨(dú)立存儲(chǔ)需求有穩(wěn)定身份需求推薦工作負(fù)載備注Web 前端 / API 網(wǎng)關(guān)否否Deployment無(wú)狀態(tài)隨便擴(kuò)縮容批處理一次性任務(wù)否否Job / CronJob不需要常駐MySQL 主從 / PostgreSQL 集群是是StatefulSet典型數(shù)據(jù)集群Redis 哨兵 / 集群是是StatefulSetAOF/RDB 需要獨(dú)立盤(pán)ZooKeeper / Etcd是是StatefulSet配置中心、協(xié)調(diào)服務(wù)Kafka / Pulsar是是StatefulSet分區(qū)數(shù)據(jù)需要獨(dú)立盤(pán)Mongo 副本集 / Elasticsearch是是StatefulSet分片副本各管各的盤(pán)MinIO 單節(jié)點(diǎn)是否看架構(gòu)多數(shù)建議 StatefulSet列這么多是想提醒你別光記住數(shù)據(jù)庫(kù)用 StatefulSet這一句結(jié)論。緩存、隊(duì)列、協(xié)調(diào)服務(wù)、對(duì)象存儲(chǔ)這類(lèi)中間件只要滿足有獨(dú)立存儲(chǔ) 有穩(wěn)定身份兩個(gè)條件都應(yīng)該往 StatefulSet 上靠。5.3 從 Deployment 遷到 StatefulSet 的經(jīng)驗(yàn)如果你現(xiàn)在有一個(gè)重要應(yīng)用正跑在 Deployment 上而它其實(shí)應(yīng)該有狀態(tài)怎么平滑遷移我說(shuō)幾個(gè)自己用過(guò)的思路。如果只有單副本遷移成本很低。先把應(yīng)用停掉確保數(shù)據(jù)盤(pán)沒(méi)有被占用然后把原來(lái)的 PVC 改一下標(biāo)簽裝進(jìn) StatefulSet 的 selector新建同名的 StatefulSet讓控制器認(rèn)領(lǐng)這個(gè) PVC。只要 PVC 的名字符合templateName-stsName-ordinal的規(guī)則且標(biāo)簽匹配控制器會(huì)直接接手不需要重新拷數(shù)據(jù)。這個(gè)方法我在多個(gè)單實(shí)例應(yīng)用上驗(yàn)證過(guò)。如果是多副本集群就不能只靠改名了。穩(wěn)妥的做法是新的 StatefulSet 先用一套新的 PVC 和存儲(chǔ)類(lèi)建起來(lái)通過(guò)備份恢復(fù)或從舊集群同步數(shù)據(jù)跑一段時(shí)間確認(rèn)數(shù)據(jù)一致后再把流量切過(guò)去。整個(gè)過(guò)程可以做成藍(lán)綠發(fā)布前提是兩套集群之間有足夠的網(wǎng)絡(luò)和存儲(chǔ)帶寬。把遷移時(shí)間安排在業(yè)務(wù)低峰給數(shù)據(jù)同步留足余量。最后提醒一個(gè)遷移中容易踩的坑StatefulSet 的selector是寫(xiě)在 spec 里不可變的創(chuàng)建之后不能改。所以創(chuàng)建前務(wù)必想清楚標(biāo)簽怎么寫(xiě)尤其別為了圖省事把selector寫(xiě)成空匹配規(guī)則否則控制器會(huì)把集群里一堆無(wú)關(guān)的 PVC 全認(rèn)領(lǐng)過(guò)來(lái)后面的問(wèn)題會(huì)非常難排查。我當(dāng)初就是因?yàn)檫@個(gè)吃了虧在幾個(gè)環(huán)境的共享集群里差點(diǎn)把別人的存儲(chǔ)卷?yè)屪?。最后說(shuō)一個(gè)我現(xiàn)在一直保持的習(xí)慣每次新建 StatefulSet我都會(huì)在驗(yàn)收清單里加一項(xiàng)——把 PVC 的reclaimPolicy手動(dòng)確認(rèn)一遍并在測(cè)試環(huán)境跑一次完整的縮容、擴(kuò)容、刪 StatefulSet、再重建的演練。這套流程走完心里才有底。有狀態(tài)應(yīng)用出事往往不是配置寫(xiě)錯(cuò)而是沒(méi)提前驗(yàn)證過(guò)異常路徑。有空的話建議你也找個(gè)周末把你的 StatefulSet 在測(cè)試環(huán)境里故意折騰一遍這比看十篇文檔都管用。