K8s高可用实战:Deployment和StatefulSet如何选型,以Redis集群为例
发布时间:2026/10/9 8:36:27 锦皓数字建站

前阵子帮一个朋友看生产环境一个用Deployment部署的Redis主从架构频繁出问题每次发布或者节点重启主从关系就乱套数据还没完全同步就被切走。我看了半天配置最后告诉他把这组Redis换到StatefulSet上——问题的根不在Redis参数而在K8s没给它“身份”。这句话基本能概括今天这篇的核心K8s高可用实战Deployment和StatefulSet你得分开用。正好这是K8s系列第九篇。前面八篇我们从Pod、Service、Namespace一路讲到Ingress和存储经常有朋友问“Deployment已经这么方便了什么场景还要碰StatefulSet”“为什么我的Redis集群用Deployment管理就老是埋雷”。这篇就把两个控制器放到同一张桌上对比着讲顺便给出一套可以直接抄的Redis集群StatefulSet配置。如果说前几篇是在教“怎么把容器跑起来”那这一篇就是教“怎么让容器在故障面前保持体面”。我会先讲高可用设计的账怎么算再拆Deployment的进阶玩法接着把StatefulSet的机制彻底拆开最后落一个完整实战。适不适合你看到选型表应该心里就有数了。1. 先把高可用的账算明白Deployment和StatefulSet各守什么1.1 无状态高可用的核心套路很多刚接触K8s的人有个误区高可用就是多副本。一听这话我就想摇头。无状态服务确实靠多副本解决问题Nginx、后端API、定时任务worker这些组件不保存自己的业务数据或者数据都在Redis、MySQL这类独立存储里。这种情况想做高可用只需要保证副本数大于1把流量交给Service或Ingress负载均衡任何一个Pod挂掉控制器立刻重建一个用户几乎无感知。无状态场景下Deployment就是最趁手的工具。它底层的ReplicaSet会持续保证副本数滚动更新时先启动新Pod、确认可用后再干掉旧Pod天然把发布风险控制在最小。你把maxUnavailable调小甚至能做到发布过程零中断。所以无状态高可用的公式很简单副本数≥2加readiness探针准确加Service流量收敛加控制器自动重建。Deployment就是公式里“自动重建”和“滚动更新”这两环的化身。1.2 有状态应用到底缺什么但一旦涉及有状态应用公式就失灵了。还是拿Redis举例三个节点组成主从结构对外提供读写。一个副本挂了K8s虽然能拉起新容器但新容器是全新的——它不知道自己是master还是slave也不知道该找谁同步数据。如果状态信息都保存在容器内部比如Redis的AOF文件写在Pod本地磁盘Pod一重建就全没了。这时候就算Deployment帮你把Pod拉起来也只是一具空壳。高可用要求“挂了能自动恢复”而有状态应用的恢复依赖两样东西历史数据和稳定身份。历史数据靠持久卷解决稳定身份靠的是Pod的“名字”。没有身份主从关系、集群节点互相发现、故障恢复后的归属判定全部都会乱。1.3 一个简单的选型决策列表我把这两类控制器的选型逻辑总结成一个决策列表遇到问题直接对号入座应用实例之间完全不共享数据谁处理请求都一样用Deployment。应用需要固定唯一的网络标识比如主从识别、集群成员发现用StatefulSet。应用需要每个实例都挂持久卷且数据独立用StatefulSet配合volumeClaimTemplates。应用启动有依赖顺序比如某些数据库节点必须按编号初始化用StatefulSet。只是API网关、前端、定时任务这类无状态负载别为酷炫用StatefulSet徒增复杂度。我见过不少团队把MySQL、Redis、Kafka全部硬塞进Deployment流量一大就出各种灵异问题。其实K8s社区早就把边界划得很清楚Deployment是给“无状态平民”用的StatefulSet才是给“有身份的角色”用的。2. Deployment进阶滚动更新、回滚、暂停编排我把每个参数都讲透2.1 maxSurge和maxUnavailable的计算逻辑可能很多人在写Deployment时看到过这两个字段strategy: type: RollingUpdate rollingUpdate: maxSurge: 25% maxUnavailable: 25%默认值两个都是25%。这个百分比的算法不是“四舍五入”而是向上取整而且它们可以同时作用在一个发布过程中。我拿3副本的Deployment举例期望副本数等于3maxSurge计算结果是向上取整等于1意味着发布期间最多允许1个额外Pod总数不能超过4。maxUnavailable计算结果也等于1意味着发布期间允许有1个Pod处于不可用状态也就是最少要保证2个Pod可用。发布时控制器会新建一个ReplicaSet先把新Pod拉起来等它通过readiness探针变成Ready再停掉旧Pod。整个过程会在“新RS扩容、旧RS缩容”之间来回直到流量全部切到新版镜像。如果我想让发布更快一点可以把maxSurge调大到50%甚至100%让新副本一次性多起几个。如果我想让发布更保守一点可以把maxUnavailable设为0再配合PodDisruptionBudget保证任意时刻都有足额可用副本。世界上没有“最安全配置”只有适合你业务的配置。2.2 minReadySeconds和探针为什么不能省滚动更新里最容易翻车的一个环节是新Pod还没真正就绪就被判成Ready。很多服务启动后会有一段“自检”时间接口能通不代表内部状态已经就绪。如果readinessProbe只配一个简单的TCP端口检查结果就是新Pod在流量进来后才开始报错。建议至少配置两个东西readinessProbe用真实业务接口或者命令做检查。Redis就用redis-cli pingHTTP服务就用httpGet路径判断返回码。minReadySeconds让Pod在变成Ready之后再“观察”一段时间才算数。比如设成30控制器会等30秒确认稳定性后才继续滚动更新。这两个参数一配合发布过程的假Ready就基本被堵死了。否则你加再多副本也是在给故障扩散批通行证。2.3 回滚和revisionHistoryLimit发布失败很正常关键是能不能快速回滚。Deployment每次配置变更都会生成一个新的ReplicaSet版本这些版本默认保留最近10个。回滚操作其实很简单kubectl rollout history deployment/my-app kubectl rollout undo deployment/my-app --to-revision3这里有个细节如果发布失败是因为镜像tag没变只是把代码推到同一个tag里Deployment可能不会识别为新版本回滚也不会生效。正确做法是给每次发布换一个明确的tag比如v1.2.3而不是一直用latest。生产环境用latest等于把回滚的路亲手拆了。revisionHistoryLimit也不要盲目调大。保留太多版本会堆积一堆旧ReplicaSet虽然不占Pod但占etcd资源。我一般只在需要频繁回滚的窗口期临时调大稳定后再改回10。2.4 暂停发布把上线的胆子练大Deployment还有一个容易被忽略的能力——暂停发布。当你配置spec: paused: trueDeployment就不会继续执行滚动更新而是停在当前状态。这很适合“先发一部分观察再放量”的场景。操作流程也很简单启动发布后立刻执行kubectl rollout pause新RS只承担很少流量观察监控和日志没问题再执行kubectl rollout resume把剩余更新继续走完。相当于把每一步发布都变成可控的分支而不是一把梭。我自己的习惯是涉及核心服务的镜像升级先pause再resume全程盯着监控告警。这套操作练熟了上线恐慌症能治好一大半。3. StatefulSet拆解稳定标识、创建顺序、存储模板是怎么配合的3.1 为什么必须有Headless ServiceStatefulSet的第一个设计目标是让每个Pod有“固定的名字”。普通Deployment的Pod名是随机生成的比如my-app-xxx-abc12重建后名字就变了。对无状态服务无所谓但有状态应用不行——节点A要能找到节点B主从关系一旦绑定名字就不能乱动。StatefulSet的Pod命名规则是“StatefulSet名-序号”第一个叫xxx-0第二个叫xxx-1依次递增。不管怎么重建名字都保持不变。但光有名字不够还要让集群内的其他Pod能通过DNS解析到这个名字这里就需要Headless Service。Headless Service和普通Service的差别只有一个字段spec: clusterIP: None设置成NoneK8s就不会给它分配VIP而是把DNS记录直接解析到每个Pod的IP。于是每个StatefulSet Pod都会获得一条类似这样的A记录redis-0.redis-hs.default.svc.cluster.local。别的服务只要知道这个域名就能永远定位到同一个实例。3.2 从0开始的序号有序创建和缩容的规则StatefulSet默认的podManagementPolicy是OrderedReady。创建一个3副本的StatefulSet时控制器会严格按顺序执行先创建redis-0等待它Running且Ready。再创建redis-1等待Ready。最后创建redis-2等待Ready。之所以这么严格是因为很多有状态应用要求“第一个节点先就绪后面的节点才能加入”。比如数据库集群通常要先有主节点副本节点才能同步Redis哨兵要先有一个可用master后面节点才知道往哪汇报。缩容的顺序正好反过来要删的时候从最大的序号开始删。也就是说如果从3个缩到1个被删的是redis-2和redis-1redis-0始终保留。这也是故意的——主节点通常是0号按顺序缩容不至于把主节点先干掉。如果你用Parallel策略控制器会忽略顺序一次性并行创建或删除。只有你明确知道应用不需要顺序启动时才用否则别碰。3.3 volumeClaimTemplates存储跟着副本走StatefulSet最厉害的是volumeClaimTemplates。它相当于一个“PVC模板”每创建一个Pod就自动绑定一块属于它的PVC。以我下面的Redis清单为例模板里声明name: data创建出来的PVC名字就是data-redis-0、data-redis-1、data-redis-2。这块PVC会绑定到对应Pod上。哪怕Pod被删除重建PVC还在数据还在新Pod会直接挂载同一块PVC。这就解决了“有状态应用需要历史数据”的核心诉求。这个机制很像房产证Deployment模式下Pod像住酒店退房后房间就重置了StatefulSet模式下Pod像住自己的房子就算搬走再回来家里的东西还在。3.4 更新策略、partition和OnDeleteStatefulSet的更新策略默认也是RollingUpdate但顺序和Deployment完全相反从序号最大的Pod开始一个一个更新更新完最大的才动下一个。为什么还是那句话主从结构里主节点通常编号小先把从节点更新完最后再动主节点风险最小。partition参数是很多人没搞懂的点。简单说只有序号大于等于partition的Pod才会更新。比如有5个节点设置partition3控制器只会更新序号3和4序号0、1、2保持旧版本。这个能力用来做金丝雀发布非常合适——先让最后两个节点用新版跑一段时间没问题再把partition调成0全部更新完。OnDelete策略则更“佛系”改了Pod模板也不会主动重建只有你手动删除某个Pod控制器才会用新版配置把它重新拉起来。适合那些对更新时机极度敏感、必须由人控制节奏的应用。StatefulSet并不是神但它在有状态业务里把“身份、顺序、数据、更新节奏”四件事都接管了这才是我敢在生产环境用它托管Redis和数据库类服务的原因。4. 实战用StatefulSet部署一个3节点Redis集群4.1 全貌headless Service加StatefulSet加ConfigMap纸上谈兵没意思下面这套配置是我在测试集群里验证过的。目标很单纯3个Redis节点任意节点挂掉后Pod能原地重建并找回数据同时每个节点通过DNS拥有稳定身份后续接哨兵也好、接运维脚本也好都有固定入口。完整组成三块一个Headless Service让redis-0/1/2有可解析的稳定域名一个ConfigMap放redis.conf最后是StatefulSet负责管Pod和PVC。4.2 完整的YAML清单先创建ConfigMapapiVersion: v1 kind: ConfigMap metadata: name: redis-config data: redis.conf: | appendonly yes save 900 1 save 300 10再创建Headless ServiceapiVersion: v1 kind: Service metadata: name: redis-hs labels: app: redis spec: clusterIP: None selector: app: redis ports: - name: redis port: 6379 targetPort: 6379然后是核心的StatefulSetapiVersion: apps/v1 kind: StatefulSet metadata: name: redis spec: serviceName: redis-hs replicas: 3 podManagementPolicy: OrderedReady updateStrategy: type: RollingUpdate rollingUpdate: partition: 0 selector: matchLabels: app: redis template: metadata: labels: app: redis spec: terminationGracePeriodSeconds: 30 containers: - name: redis image: redis:7.2 command: - redis-server - /etc/redis/redis.conf ports: - containerPort: 6379 readinessProbe: exec: command: - sh - -c - redis-cli ping | grep PONG initialDelaySeconds: 5 periodSeconds: 5 volumeMounts: - name: data mountPath: /data - name: config-volume mountPath: /etc/redis/redis.conf subPath: redis.conf volumes: - name: config-volume configMap: name: redis-config volumeClaimTemplates: - metadata: name: data spec: accessModes: - ReadWriteOnce storageClassName: local-path resources: requests: storage: 2Gi这里有几个关键点spec.serviceName必须和前面的Headless Service名字完全一致否则Pod拿不到DNS记录。volumeClaimTemplates里的storageClassName要改成你集群实际存在的存储类。我在本地测试环境用的是local-path云上一般用云厂商的SSD类。readinessProbe用redis-cli ping检查比TCP端口检查靠谱。因为Redis可能TCP虽然通着但本身还在持久化或者恢复状态。4.3 启动后的验证清单应用完这套YAML不要马上说“部署完成”。建议按下面顺序验证查看Pod名和启动顺序kubectl get pods -l appredis应该看到redis-0先变成Running且1/1 READY然后redis-1、redis-2依次跟上。如果redis-0卡在ContainerCreating多半是PVC没绑上用kubectl describe pod redis-0看事件。验证DNS解析kubectl run test-pod --imagebusybox --rm -it -- nslookup redis-1.redis-hs.default.svc.cluster.local能正常返回IP说明Headless Service接入成功。验证持久化。往redis-0写一个key然后删除这个Podkubectl exec redis-0 -- redis-cli set hello k8s kubectl delete pod redis-0等新的redis-0重建并变成Ready后再查询kubectl exec redis-0 -- redis-cli get hello能返回k8s说明AOF持久化和PVC绑定都生效了。这一步是整个实验的灵魂过了就有底气把Redis托管在K8s里。4.4 故障切换实验观察“身份”和“数据”是否稳持久化验证完再做一次故障模拟。实际操作中我会验证两个层面一是进程级故障执行kubectl exec redis-1 -- redis-cli debug segfault让Redis进程崩掉。注意不要在线上环境玩这个命令测试集群里随便。Pod会变成CrashLoopBackOff或者重启但StatefulSet会把同名Pod拉起PVC原样挂上数据不丢。二是节点级故障可选直接到宿主机上把某个节点关机或者kubectl drain某台Node。Pod会被驱逐然后调度到其他可用节点。因为StatefulSet的PVC大多绑定节点上的本地路径如果是local-path存储类新Pod会卡在跨节点漂移上——这也暴露一个现实问题部分本地存储方案并不适合跨节点恢复。如果要追求节点级高可用最好用分布式存储或者云盘类存储。做完这两个实验基本就能理解StatefulSet的边界在哪里它能保证名字不变、数据不丢但数据的可迁移性取决于底层存储实现。选存储类这件事做得好就是一半的高可用。5. 躲不开的坑缩容、数据安全与环境差异5.1 StatefulSet缩容引发的“删了又建”问题如果你在缩容后马上又扩容可能会遇到诡异现象本来3节点缩到1节点再扩回3节点redis-1和redis-2没有像第一次部署那样全新初始化而是把老PVC又挂回去了。原因很简单StatefulSet缩容默认不会删除PVCPVC还在Pod按序号重建时自然会重新绑定。这个机制本身是保护数据的但也会带来困惑。比如我想彻底清掉某个节点数据光删StatefulSet没用还要把所有data-redis-*的PVC一起删掉否则数据残留会反复出现。我的建议是凡是涉及有状态服务的“销毁重建”先列一个清理清单——删Pod检查PVC确认数据不再需要再删PVC最后再删StatefulSet。顺序颠倒了数据可能就真没了。5.2 PVC生命周期与数据备份StatefulSet只是保证数据在Pod崩溃时还在不等于数据永远安全。PVC所在的存储卷损坏、整个Namespace被误删、存储节点物理故障这些事StatefulSet全都管不了。所以真实生产环境我坚持两条原则对Redis这类内存型数据库K8s之外的备份机制必须存在。比如定时执行BGSAVE后把RDB文件同步到对象存储或者直接用云厂商的备份方案。对MySQL这类强一致数据库StatefulSet只是交付载体真正的可靠还是靠binlog备份和恢复演练。不要把“控制器”当成“灾备”。还有个小技巧StatefulSet销毁重建时如果PVC用了Delete回收策略删除PVC等于删数据。如果你暂时不确定要不要保留历史数据先把PVC的回收策略改成Retain再操作给恢复留一条退路。5.3 环境差异Rocky Linux上装K8s 1.36的几个注意点很多朋友装了K8s之后发现StatefulSet的PVC一直Pending第一反应是配置写错。其实很可能是环境问题。我最近在Rocky Linux上搭K8s 1.36测试集群就踩了几个典型坑存储类缺失。裸金属或者本地虚拟机默认没有云厂商的StorageClass需要自己装local-path-provisioner或者手动创建PV。否则PVC一直Pending根本到不了Pod创建那一步。节点防火墙和SELinux。Rocky默认的SELinux策略可能会拦截Pod挂载目录的读写容器启动时报PermissionDenied。要么在测试环境临时调整要么正经配置SELinux布尔值。测试环境可以放宽生产环境的安全策略还是要留着。内核参数。K8s网络组件对内核模块和系统参数要求比较多装完先跑一遍kubeadm init的预检重点看交换分区是否关闭、内核模块是否加载。这些细节不是StatefulSet本身的问题但恰恰是它们决定了你的PVC和Pod能不能正常走完整个生命周期。环境没夯实控制器设计得再好也白搭。5.4 StatefulSet控制器行为的几个隐藏细节最后补充几个我在排障过程中总结的细节每一个都对应过一次真实教训StatefulSet的serviceName如果指向一个不存在的ServicePod会创建失败报错信息很隐晦。先检查Service是否存在再查别的。修改StatefulSet的replicas等于触发一次扩容或缩容但如果Pod的readinessProbe太宽松控制器会把未就绪的Pod当成Ready顺序启动就失效了。探针必须真实反映可用状态。更新StatefulSet镜像时如果镜像tag没变比如总是latest控制器不会触发更新。这和Deployment一样但StatefulSet更坑的是OnDelete策略下你得手动删除旧Pod才能触发重建。删除Namespace会自动删除其中的StatefulSet和PVC这是不可逆的。所以在做任何涉及Namespace删除的操作前先把有状态服务的备份做到K8s之外。这些坑不会写在官方文档里但每一个都能让你在故障现场省下至少半小时。我每次迁移有状态服务到K8s都会先按这个清单自检一遍基本能把坑提前踩掉。我从这套实践中最大的体会是Deployment和StatefulSet本身没有谁更高级只是分管的工作不同。Deployment让无状态服务跑得又轻又快StatefulSet用一串固定序号换来数据与身份的稳定性。选错了控制器再漂亮的YAML也只是把故障包装得晚一点出现选对了高可用才算是真正落地。希望这篇第九篇能帮你在下一次架构设计时少走一点我走过的弯路。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。