深度解析Kubernetes节点维护:从Cordon到Drain的平滑之道

在任何一个严肃的生产环境中,对底层计算节点的维护(如内核升级、硬件更换或安全补丁)都是一个无法回避的、常态化的运维事件。Kubernetes作为一个面向终态的分布式编排系统,提供了一套优雅的机制来处理这一过程,即 Cordon 与 Drain。本文将面向有经验的工程师和架构师,深入剖析这两个核心操作背后的原理、实现细节、工程陷阱以及架构演进路径,确保读者能以“外科手术”般的精度来执行节点维护,而非“暴力重启”式的赌博。

现象与问题背景

想象一个运行着核心交易系统的Kubernetes集群。某个工作节点(Worker Node)上运行着多个关键业务的Pod,包括无状态的API网关、有状态的Redis缓存实例,以及一个处理异步任务的队列消费者。现在,安全团队通报该节点存在一个高危的Linux内核漏洞,要求在2小时内完成补丁并重启。一个新手工程师可能会直接SSH到节点上执行`reboot`。这将导致灾难性的后果:

  • 服务中断: 节点上的所有Pod被瞬间终止,而不是平滑退出。正在处理的HTTP请求、数据库事务或内存中的任务状态会全部丢失。
  • 流量黑洞: 在节点重启期间,集群的Service Endpoints可能还未完全更新,部分流量仍然会被路由到这个已经宕机的节点,导致客户端大量报错。
  • 数据损坏风险: 对于有状态应用,强制杀死进程可能导致数据文件损坏或状态不一致,需要复杂的手动恢复。
  • 调度混乱: 节点重启后,其上的Pod会被标记为`Unknown`状态,然后由Controller Manager在超时后进行删除和重建。这个过程是被动的、混乱的,缺乏精细的控制。

这个场景暴露了问题的核心:我们需要一种机制,能够主动、可控地将一个节点上的工作负载安全地迁移出去,然后才能对该节点进行维护操作,并在维护完成后再将其平滑地重新加入集群。这正是Kubernetes Cordon与Drain机制设计的初衷。

关键原理拆解

要真正理解Cordon与Drain,我们必须回归到分布式系统和操作系统的第一性原理。Kubernetes本质上是一个巨大的、基于状态机的分布式控制平面。

大学教授视角:原理剖析

  • Cordon:修改调度策略的“软隔离”
    Cordon(警戒线)操作的本质,是在分布式系统的状态存储(etcd)中,修改特定Node对象的一个属性:`spec.unschedulable`。从调度器的视角看,这是一个经典的谓词(Predicate)算法的应用。Kubernetes调度器(kube-scheduler)在为新Pod寻找合适的节点时,会执行一系列谓词函数进行过滤。其中一个核心的谓词就是`NodeUnschedulablePredicate`,它会检查节点的`spec.unschedulable`字段。如果为`true`,该节点就会被直接从候选节点列表中排除。因此,Cordon操作是一个非常轻量级的状态变更,它仅仅是通知调度器:“请不要再往这个节点上调度新的Pod了”。它不会影响任何已经运行在该节点上的Pod,实现了对节点的“软隔离”。
  • Drain:精心编排的“优雅驱逐”
    Drain(排空)则复杂得多,它是一个客户端(`kubectl`)和服务端(API Server, Kubelet)协同完成的主动驱逐流程。其核心是“优雅终止”(Graceful Termination),这个概念源于操作系统对进程生命周期的管理。

    1. 信号机制: 当一个Pod被驱逐时,Kubelet并不会直接发送`SIGKILL`(信号9,强制杀死)给容器内的进程。它会首先发送`SIGTERM`(信号15,通知终止)。这给予了应用程序一个清理现场的机会,比如完成当前请求、保存内存数据到磁盘、关闭数据库连接等。
    2. 优雅终止宽限期(Termination Grace Period): Kubelet在发送`SIGTERM`后,会等待一段预设的时间(`terminationGracePeriodSeconds`,默认为30秒)。如果在此期间应用程序正常退出(进程返回码为0),则流程结束。如果超时后进程仍未退出,Kubelet才会发送`SIGKILL`强制终结。这个宽限期是应用与OS之间的一个重要“契约”。
    3. preStop Hook: Kubernetes在Pod生命周期中提供了`preStop`钩子。它在`SIGTERM`信号发送之前被同步调用。这为那些不方便直接处理`SIGTERM`信号的复杂应用(例如需要执行一个脚本来通知集群内其他组件自己即将下线)提供了一个更可靠的清理入口。
  • 分布式环境下的网络挑战:Endpoints更新与Pod终止的竞态条件
    在分布式系统中,状态同步永远存在延迟。当一个Pod被删除时,会触发两个并行的流程:Kubelet在本机终止Pod,以及Endpoint Controller(或EndpointSlice Controller)从关联的Service中移除该Pod的IP地址。这两个流程通过API Server解耦,它们的完成时间是不确定的。这就可能导致一个经典的竞态条件:Kubelet已经用`SIGKILL`杀死了Pod进程,但集群中其他节点上的kube-proxy还没来得及更新其iptables/IPVS规则,新的流量依然可能被转发到这个已经不存在的Pod IP上,造成连接拒绝(Connection Refused)。优雅终止期间的`readinessProbe`失败可以将Pod标记为`Unready`状态,这有助于缓解该问题,但根本性的解决需要依赖服务网格(Service Mesh)等更先进的流量管理机制,实现离线前的流量摘除。
  • 可用性契约:PodDisruptionBudget (PDB)
    Drain操作可能会同时驱逐多个Pod。如果这些Pod同属于一个应用,可能会导致服务中断。PodDisruptionBudget(PDB)是应用所有者与集群管理者之间的一个API化契约。它声明了一个应用在“自愿中断”(如节点维护)期间,最少需要保持可用的副本数(`minAvailable`)或最多允许不可用的副本数(`maxUnavailable`)。在执行驱逐(Eviction API)时,API Server会检查相关的PDB。如果驱逐这个Pod会导致违反PDB的约定,API Server会拒绝该驱逐请求(返回HTTP 429 Too Many Requests),`kubectl drain`命令会卡住,直到其他副本恢复或PDB被调整。这是保障高可用的核心机制。

系统架构总览

一次完整的`kubectl drain`操作,是多个Kubernetes核心组件协同工作的交响乐。我们可以通过描述一个简化的信息流来理解其架构:

  1. 用户/客户端 (`kubectl`): 用户在终端执行`kubectl drain my-node-01`。`kubectl`二进制文件作为客户端,开始执行一系列预定义的逻辑。
  2. API Server: 所有通信的中枢。`kubectl`首先会向API Server发送一个PATCH请求,将`my-node-01`节点的`spec.unschedulable`设置为`true`(Cordon操作)。
  3. Scheduler: 在此之后,Scheduler在处理新的Pod调度请求时,会因为节点被Cordon而自动过滤掉`my–node-01`。
  4. `kubectl` 客户端循环: `kubectl`接着向API Server请求`my-node-01`上的所有Pod列表。它在客户端内存中对列表进行过滤,排除掉由DaemonSet控制器管理的Pod(因为它们应该在每个符合条件的节点上都有一个副本,驱逐它们没有意义)和本地存储的静态Pod。
  5. 驱逐请求: 对于剩余的每个Pod,`kubectl`会向API Server发送一个Eviction API请求(`policy/v1beta1`或`v1/eviction`)。这并非一个简单的DELETE请求。
  6. API Server (PDB检查): API Server在收到Eviction请求后,会查找该Pod关联的PDB。它会计算如果该Pod被驱逐,是否会违反PDB的`minAvailable`或`maxUnavailable`约束。如果违反,则拒绝请求;否则,接受请求。
  7. Pod删除: Eviction请求成功后,API Server会像处理普通`DELETE`请求一样,更新etcd中该Pod的状态,标记其为正在删除(设置`deletionTimestamp`)。
  8. Kubelet (执行者): 节点`my-node-01`上的Kubelet通过Watch机制监听到自己节点上的一个Pod被标记为删除。它立即启动该Pod的优雅终止流程:执行`preStop`钩子,发送`SIGTERM`,等待宽限期,最后发送`SIGKILL`。
  9. Controller Manager: 与此同时,负责该Pod的控制器(如ReplicaSet、StatefulSet Controller)也监听到Pod被删除。它会发现当前应用的实际副本数少于期望副本数,于是触发调度流程,创建一个新的Pod来替代被删除的Pod。由于`my-node-01`已被Cordon,新Pod会被调度到其他可用节点上。
  10. `kubectl` 等待与完成: `kubectl`客户端会持续轮询,直到节点上所有目标Pod都被成功删除,然后命令执行完毕。

这个流程清晰地展示了Kubernetes声明式API和控制器模式的强大之处。用户只需声明“排空节点”的意图,整个复杂的、涉及多组件的、保障高可用的执行过程由系统自动完成。

核心模块设计与实现

极客工程师视角:代码与命令的细节

理论讲完了,我们来点硬核的。实际操作中,细节决定成败。

Cordon的本质

`kubectl cordon my-node-01` 实际上只是一个语法糖,它等价于以下这个命令:


kubectl patch node my-node-01 -p '{"spec":{"unschedulable":true}}'

你可以通过`kubectl get node my-node-01 -o yaml`看到`spec.unschedulable: true`这个字段。恢复节点(`uncordon`)则是将其设置回`false`或直接移除该字段。

Drain命令的“魔鬼”参数

基础的`kubectl drain `命令背后隐藏着一系列参数,错误使用它们会导致严重后果。


kubectl drain my-node-01 \
  --ignore-daemonsets \
  --delete-local-data \
  --force \
  --grace-period=-1 \
  --timeout=5m
  • --ignore-daemonsets: 这是最常用的参数之一。如原理所述,DaemonSet的Pod不应该被驱逐。如果不加此参数,drain会因发现无法删除DaemonSet Pod而失败退出。
  • --delete-local-data: 这是一个极其危险的参数! 它会强制删除那些使用了`emptyDir`卷的Pod。如果你的应用使用`emptyDir`作为临时缓存,这没问题。但如果它被用作某些重要数据的临时存储(一种不好的实践),使用此参数将导致数据丢失。对于使用了本地持久卷(Local Persistent Volume)的Pod,此参数同样会使其被删除。务必在确认无数据丢失风险时才使用。
  • --force: 这个参数会强制删除那些不受控制器(如ReplicaSet, Job等)管理的“裸Pod”。在正常的Kubernetes实践中,几乎所有的应用Pod都应该由控制器管理以保证其高可用。如果存在裸Pod,drain会默认失败,因为删除它们后不会有任何组件负责重建它们,这等同于应用下线。`–force`绕过了这个安全检查。
  • --grace-period: 覆盖Pod自身定义的`terminationGracePeriodSeconds`。设置为`0`意味着不等待,直接发送`SIGTERM`然后`SIGKILL`。设置为`-1`则使用Pod spec中定义的值。滥用`–grace-period=0`等同于放弃优雅终止。
  • --timeout: 整个drain操作的超时时间。如果因为PDB卡住或其他原因导致驱逐过程过长,此参数可以防止命令无限期挂起。

PodDisruptionBudget (PDB) 的实践

一个配置良好的PDB是节点维护平滑进行的关键。假设我们有一个3副本的`frontend`应用:


apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: frontend-pdb
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app: frontend

或者等价地,使用`maxUnavailable`:


apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: frontend-pdb
spec:
  maxUnavailable: 1
  selector:
    matchLabels:
      app: frontend

当`kubectl drain`试图驱逐一个`frontend` Pod时:

  • 如果当前有3个`frontend` Pod健康运行,API Server检查PDB,发现驱逐1个后还剩2个,满足`minAvailable: 2`,于是允许驱逐。
  • 假设其中一个`frontend` Pod恰好因为其他原因正在重启,此时只有2个Pod健康。`drain`再尝试驱逐一个,API Server计算后发现只剩下1个健康Pod,违反了`minAvailable: 2`的约定,于是拒绝驱逐请求。`kubectl drain`会在此Pod上卡住,并反复重试,直到健康的副本数恢复到3。

坑点: 如果将`minAvailable`设置为副本数(例如3副本应用设置`minAvailable: 3`),或者`maxUnavailable`设置为0,那么drain操作将永远无法驱逐该应用的任何一个Pod,导致维护流程完全卡死。这是一个常见的配置错误。

性能优化与高可用设计

在真实复杂的环境中,Cordon/Drain的考量远不止执行命令本身。这涉及到应用设计、可用性策略和风险权衡。

Trade-off分析:速度 vs. 安全

维护窗口通常是有限的。一个缓慢的drain过程可能会导致超时,迫使工程师采取更激进的手段。

  • 长优雅终止时间 (`terminationGracePeriodSeconds`): 对于需要长时间进行清理操作的应用(如大型数据批处理任务),设置较长的宽限期是必要的。但这会显著拉长整个drain的时间。如果一个节点上有数十个这样的Pod,drain过程可能长达数小时。
  • 严格的PDB: 如前所述,非常保守的PDB(如`maxUnavailable: 1`对于一个10副本的应用)虽然安全性高,但也意味着drain必须串行地、一个一个地等待Pod在新节点上启动并就绪后,才能驱逐下一个。
  • 有状态应用 (StatefulSets): 驱逐有状态应用的Pod(如数据库)尤其棘手。它们的Pod通常与特定的持久卷(PersistentVolume)绑定。驱逐一个Pod后,新的Pod必须被调度到可以挂载原PV的可用区(如果存储是区域性的),或者等待存储卷的重新挂载。如果是有主从关系的应用,还需要考虑主从切换的耗时和数据同步状态。对于有状态应用,通常需要结合应用层面的维护模式或主从切换操作来配合drain。

极客建议: 在设计应用时,就应该考虑“快速死亡”的能力。尽量减少启动和关闭时的清理工作,依赖外部存储和消息队列来持久化状态,让Pod本身尽可能无状态,这样`terminationGracePeriodSeconds`可以设置得更短,drain速度更快。

对抗网络竞态条件

为了解决Endpoints更新延迟的问题,除了依赖`readinessProbe`之外,一个更健壮的模式是在`preStop`钩子中加入一段延时。


lifecycle:
  preStop:
    exec:
      command: ["/bin/sh", "-c", "sleep 10"]

这个简单的`sleep 10`的目的是,在Kubelet开始发送`SIGTERM`之前,主动等待10秒。这10秒的时间窗口,给了Endpoint Controller和kube-proxy足够的时间将该Pod从服务发现和负载均衡规则中移除。这样,当应用进程真正开始关闭时,新的流量已经不会再进来了。这个简单的技巧能极大地提高服务在节点维护期间的平滑度。

架构演进与落地路径

对于不同规模和成熟度的团队,节点维护的自动化程度和策略也应循序渐进。

  1. 阶段一:标准化手动流程
    在团队初期,最重要的是建立一个标准操作流程(SOP)文档。明确规定维护前必须执行`kubectl cordon`,然后使用带有特定参数的`kubectl drain`,并检查所有Pod是否已成功迁移。整个过程由人工执行,但有章可循,避免了“牛仔式”的随意操作。
  2. 阶段二:脚本化与半自动化
    随着集群规模扩大,手动操作变得繁琐且容易出错。可以编写Shell或Python脚本,将cordon、drain、执行维护、uncordon等一系列步骤封装起来。脚本可以加入更多的检查和确认环节,例如,在drain之前检查集群中是否存在不满足PDB的Pod,或者集成到内部的发布和变更系统中,实现审批后的一键执行。
  3. 阶段三:引入集群级自动化工具
    对于公有云环境,可以利用云厂商提供的节点组自动修复或滚动更新功能,这些功能底层就是封装了Cordon/Drain逻辑。对于更通用的场景,可以引入开源工具:

    • Cluster Autoscaler: 在缩容(scale-down)时,它会自动选择一个利用率低的节点,执行drain操作,然后将其从云提供商处终止,以节省成本。
    • Descheduler: 它可以根据策略(如驱逐长时间未就绪的Pod、整合碎片化的节点资源)来主动驱逐Pod,其底层同样依赖Eviction API。
  4. 阶段四:全自动化的节点生命周期管理
    这是最理想的状态。可以使用更先进的节点管理工具,如Karpenter(用于AWS)或自定义的Kubernetes Operator。这些工具可以实现基于工作负载的、即时的节点供应和回收。例如,当一个高优先级任务需要特定类型的GPU节点时,Karpenter会自动创建该节点;当任务完成后,它会自动drain并删除该节点。整个节点的生命周期管理变得自动化和智能化,节点维护的概念被“滚动替换”的常态化操作所取代,无缝融入到GitOps的流程中。

总而言之,Kubernetes的Cordon与Drain机制不仅仅是两个简单的命令,它们是构建在Kubernetes声明式API、控制器模型和健壮的分布式系统原理之上的一套精密的工作流。深刻理解其背后的原理、熟练掌握其实现细节,并根据业务场景和团队成熟度规划其演进路径,是每一位高级Kubernetes使用者和架构师的必备技能。

延伸阅读与相关资源

  • 想系统性规划股票、期货、外汇或数字币等多资产的交易系统建设,可以参考我们的
    交易系统整体解决方案
  • 如果你正在评估撮合引擎、风控系统、清结算、账户体系等模块的落地方式,可以浏览
    产品与服务
    中关于交易系统搭建与定制开发的介绍。
  • 需要针对现有架构做评估、重构或从零规划,可以通过
    联系我们
    和架构顾问沟通细节,获取定制化的技术方案建议。
滚动至顶部