有状态应用的排水远比无状态应用复杂,核心差距在于数据是否允许丢失、是否允许中断、是否允许重新调度到其他节点。无状态应用排水时可以随手拔掉电源,有状态应用排水则必须完成优雅退出、数据落盘、网络断开、存储解绑这一整套流程,任何一步出错,轻则数据损坏,重则整个集群不可用。
近年来,随着Kubernetes在各大中型企业普及,越来越多团队开始用kubectl drain命令做节点维护,但真正把排水玩明白的人并不多,本文围绕有状态应用与无状态应用的排水,从工作原理、操作手法、常见坑点三个维度拆解。
无状态应用和有状态应用的区别是什么
先给两者定性,无状态应用不持久化数据,Pod重建后状态归零,典型代表是Nginx、业务API服务,有状态应用必须依赖持久化数据,Pod重建后需要恢复原有身份和数据,典型代表是MySQL、Redis、ZooKeeper、Kafka。
生命周期管理逻辑截然不同
无状态应用的Pod由一个控制器统一调度,创建、删除、扩容、缩容都是批量操作,节点故障时,Pod被重新调度到任意健康节点,IP地址变了无所谓,数据丢了也无所谓。
有状态应用则使用StatefulSet控制器,每个Pod拥有固定的主机名、稳定的网络标识和独立的持久化存储卷,Pod的创建和删除有严格的顺序,比如MySQL的主从集群,必须先启动主节点,等主节点Ready后再启动从节点。
存储依赖是两者最根本的分界线
拿Kubernetes集群里的实际运维场景来说:
- 无状态应用一般不挂PVC,或者挂载的PVC也是只读配置文件,Pod删除后PVC不跟随删除
- 有状态应用几乎全部使用StatefulSet + PVC模板,每个Pod绑定一个专属PVC,Pod重建后要重新挂载同一块卷
这决定了排水时的处理优先级完全不同,无状态应用要解决的是流量排空问题,有状态应用要解决的是数据一致性和存储Association问题。
排水到底在排什么
排水的本质是让节点上的Pod优雅撤退,把资源占用、流量接入、数据读写全部归零,然后将节点标记为不可调度(SchedulingDisabled),K8s里对应命令是kubectl drain。
排水流程逐步拆解
从触发到完成,整个过程分四步走:
- 标记节点不可调度,新的Pod不会再被调度到这台节点
- 驱逐节点上的Pod,先发SIGTERM信号给容器主进程,触发优雅终止流程
- 等待终止窗口,默认30秒,容器内的应用需要在这段时间完成收尾工作
- 节点进入维护状态,等运维完成硬件维修、内核升级、安全补丁等操作后,再通过kubectl uncordon恢复调度
这套流程对无状态应用来说非常友好,Nginx收到SIGTERM后直接退出连接,新请求由Service自动切换到其他节点上的副本,整个过程用户无感知。
有状态应用的排水远比想象中麻烦
有状态应用挂在PVC上时,kubectl drain默认会拒绝驱逐,原因很简单K8s担心你把这台节点的Pod赶走之后,新的Pod被调度到别的节点,PVC无法跨节点漂移(除非使用共享存储),最终导致Pod卡在Pending状态。
想强制驱逐也可以,给drain命令加上–force参数,但代价是跳过优雅终止流程,直接删除Pod,MySQL这种强一致性的应用根本受不了这种操作,进程突然被杀,binlog和redolog可能未完全落盘,重启后需要走崩溃恢复流程,严重时直接丢事务。
业内专家指出,有状态应用的排水本质上是有计划的故障切换,需要提前做好数据一致性检查和容灾切换演练,不能把排水的成功概率寄托在运气上。
K8s滚动更新场景下的排水差异
滚动更新和排水经常同时出现,比如你需要升级集群节点内核,或者替换物理服务器,操作顺序通常是:先排干节点A,升级完成后再排干节点B,过程中应用不能停服,不能丢数据。
无状态应用的排水操作路径
步骤非常直接:
- 先确认集群可用副本数足够,比如总副本数8个,节点上跑3个,排水要求节点上的副本数归零,但其他节点需要提前扩容托住流量
- 执行
kubectl drain <node-name> --ignore-daemonsets --delete-emptydir-data - 等待Pod全部终止,Nginx或后端API的Pod在SIGTERM后3秒内退出(取决于配置的terminationGracePeriodSeconds)
- 维护完成后执行
kubectl uncordon <node-name>
需要注意--delete-emptydir-data参数,虽然名字看起来像是会删除数据,但实际上只是允许驱逐使用emptyDir临时存储的Pod,这种数据本身就是临时的,删除无影响。
有状态应用排水操作路径
以常见的三节点Redis集群为例,步骤明显更长:
- 提前收缩副本,把集群内某节点的数据迁移到其他节点,Redis集群用
CLUSTER DELSLOTS和MIGRATE命令,Cassandra用nodetool decommission,Kafka用kafka-reassign-partitions.sh - 确认数据迁移完成,比如Redis使用
CLUSTER INFO检查节点角色和槽位分布,确保待排水节点持有0个槽位 - 执行Pod优雅终止,用
kubectl drain命令,如果PVC卡住驱逐,需要结合StatefulSet的有序调度策略手动处理 - 处理存储卷的归属问题,这是最容易被忽略的环节,云上的云硬盘、EBS或Azure Disk跟着Pod走,如果不显式解绑,节点下线后卷无法被其他节点挂载
即使排水命令成功执行,数据安全性也不是百分之百保证的,有状态应用持有的PVC数据在重启后是否完好,取决于应用本身是否实现了优雅关闭逻辑,一个写操作刚提交一半就收到SIGTERM的MySQL实例,丢数据只是概率问题。
必备的PodDisruptionBudget配置
针对有状态应用,行业共识是必须配置PDB,PodDisruptionBudget,来限制自愿中断时可以驱逐的副本数。
一个典型的MySQL主从集群PDB配置:
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: mysql-pdb
spec:
minAvailable: 2
selector:
matchLabels:
app: mysql
这个配置确保排水时至少有2副MySQL副本存活,如果集群只有3副本,那你最多只能驱逐1个Pod,避免排水过程中整个数据库不可写。
无状态应用同样可以配置PDB来保证服务可用性,但实际的资源要求没那么严格多一个少一个副本,业务侧几乎没有感知,所以不少团队直接忽略PDB配置,省事归省事,生产环境还是建议补上,防止节点维护期间Pod被驱逐太多导致整体毛刺率上升。
排水过程中最容易踩的坑
有状态应用和无状态应用的排水,实操中各有各的坑,提前了解能省去不少维护时间。
三个有状态应用的高频事故场景
- Pod被删后PVC无法跨节点迁移,使用本地SSD存储时,PVC绑定在节点本地,排水后Pod被调度到其他节点,但PVC仍然绑定在原节点上,新的Pod创建时报错
volume node affinity conflict,一直卡Pending,解决思路是先删除原Pod对应的PVC定义,再用重命名方式让新Pod绑定新卷,但这就意味着数据不丢只能靠多副本 - 优雅终止窗口不够用,K8s默认的30秒对普通Web服务够用,但MySQL刷脏页、Kafka落消费位点、Cassandra做compaction,动不动就超过60秒,提前调大
terminationGracePeriodSeconds,否则K8s强制用SIGKILL干掉进程,数据损坏概率大幅上升 - 集群拓扑感知失效,很多有状态应用自带节点亲缘性,比如Solr配置了rack awareness,Pod重新调度后拓扑打散,性能反而下降,排水前需要人工调整拓扑配置或拓扑亲和策略
两个无状态应用容易忽略的问题
- Connection Drain没有实现,Spring Boot应用默认优雅停机只关HTTP端口,不等待处理中的请求完成,Nginx反代倒是自带优雅关闭,但业务代码层面的长连接(如WebSocket)不处理,会导致用户端闪断
- preStop钩子没写,理想情况下,应用收到SIGTERM后立刻把节点标记为不健康,让Service停止转发新流量,但很多无状态应用重不care这一步,导致排水过程中相当比例请求打到正在关闭的Pod上,触发5xx错误
关于有状态应用是否需要停止写流量再做排水,答案很明确:必须停写,至少也要切到只读模式,K8s本身没有感知业务数据一致性的能力,它只负责管理Pod生命周期,不负责理解MySQL的复制协议。
有状态应用排水时写流量怎么处理
顺带说一个高频操作细节,排水有状态应用前,需要先把业务写流量切到其他节点或集群:
- 在接入层(比如VIP、SLB、Gateway)摘掉待排水实例
- 等待实例上的存量连接自然耗尽,或者主动调用管理API触发连接迁移
- 确认读写延迟恢复正常值
- 执行kubectl drain
没有这步操作,直接强杀Pod,大概率出现数据回滚或者双写冲突,在MySQL主从同步场景里尤其严重从节点被排水前还有积压未执行的relay log,新从节点抢占主节点角色后,数据对不上,主从撕裂。
Q&A
有状态应用在不停止服务的情况下能排水吗?
能,但前提是集群架构支持在线迁移,Cassandra、Kafka这类无中心化或分布式的有状态应用,数据天然打散在多个节点,上线一个节点排干一个节点,副本迁移完成后即可安全下线,MySQL单主架构做不到,必须先把流量切换到备节点。
无状态应用排水时要不要停服?
不用停服,但需要保证其他节点有足够冗余承载全部流量,操作前确认Service的endpoint中存活Pod数量多于最大容忍故障数,执行drain后K8s会自动将新连接转发到健康Pod,应用本身感知不到节点变化。
华为云CCE、简米云ACK这类托管K8s产品里,排水操作封装得更加图形化,控制台直接就有节点排水按钮,原理和kubectl drain完全一致,底层仍然走驱逐Pod、等待终止、标记调度的流程。
首发原创文章,作者:王坚,如若转载,请注明出处:https://idctop.com/article/776052.html


