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