Skip to content

NodePort 服务下的流量摘除:kube-proxy 的工作机制


🏷 后端   🕒 4/5/2025

在 Kubernetes 集群进行滚动更新或缩容时,我们经常会遇到一个棘手的问题:尽管配置了优雅停机,NodePort 类型的服务在 Pod 停止瞬间仍会出现短暂的连接失败(如 502 Bad Gateway 或 Connection Refused)。

这个问题的核心在于**流量摘除(Traffic Draining)**的滞后性。当 Pod 停止时,Kubernetes 的网络层是否能足够快地切断流向该 Pod 的流量?本文将深入 kube-proxy 的黑盒,从原理到实践,剖析 NodePort 服务下的流量摘除机制及其优化方案。

一、基石:NodePort 流量路径与 kube-proxy

要理解流量为何无法及时摘除,首先需要理解流量是如何到达 Pod 的。

kube-proxy 是运行在每个节点上的网络代理,它通过监听 API Server 来维护节点上的网络规则。对于 NodePort 服务,kube-proxy 负责在所有节点上开放端口,并将流量转发到后端的 Pod。

流量转发架构

当外部流量访问 <节点IP>:<NodePort> 时,数据包的流向如下:

在这个过程中,kube-proxy 是被动的。它必须等待 API Server 通知它“某个 Pod 已经不存在了”,然后才会去修改节点上的 iptables 或 IPVS 规则。

二、动态过程:Pod 删除时的时序竞态

流量丢失通常发生在 Pod 被删除的瞬间。这是一个典型的分布式系统一致性问题:

kubectl delete pod 时, Pod 会在 API Server 被标记为 Terminating 状态(可以通过 kubectl describe 查看到),然后在容器侧和网络侧会同时进行下面2个流程:

应用流(容器侧)

  1. kubelet 开始本地优雅关闭
    • 节点上的 kubelet 发现 Pod 被标记为 terminating 后,开始关闭流程。
  2. 执行 preStop(如果配置且 grace period 不为 0)
    • kubelet 在容器内执行 preStop hook。
    • 同时开始计算优雅停止等待开始时间 terminationGracePeriodSeconds , 因此 preStop 不应该大于 terminationGracePeriodSeconds。如果确实需要更长的 preStop, 需要同时调大 terminationGracePeriodSeconds
  3. 发送 SIGTERM
    • preStop 执行结束后,kubelet 让容器运行时给每个容器的 PID 1 发送 SIGTERM。
  4. 优雅期结束:强制终止
    • terminationGracePeriodSeconds 到期仍有容器在运行, 对仍存活进程发送 SIGKILL。

控制流(网络侧)

  1. 控制面并行进行“摘流/下线”评估
    • kubelet 开始优雅关闭的同时,控制面会评估是否把该 Pod 从对应 Service 的 EndpointSlice 中移除。
    • 工作负载控制器(如 ReplicaSet)不再把该 Pod 视为“可用副本”。
  2. EndpointSlice 中的 terminating 端点表现
    • terminating 的 endpoints 不会立刻从 EndpointSlice 消失,而是暴露“正在终止”的状态。
    • 这些 terminating endpoints 的 ready 会被置为 false(兼容 1.26 之前逻辑),因此负载均衡/Service 通常不会再把“常规流量”转发给它。
    • 若需要更精细的连接/会话 draining,可看 serving 等条件,并参考官方教程实现 connection draining。

参考: Pod Termination Flow

致命的时间差

在默认配置下,这两个流程往往存在竞态条件(Race Condition)

也就是说,应用流已经停止服务了,但节点上的转发规则还在。在这段“时间差”内进入 NodePort 的流量,会被转发到一个已经关闭的 Pod,导致请求失败。

三、深入内核:iptables 与 IPVS 的差异

kube-proxy 的工作模式(iptables vs IPVS)直接影响这个“时间差”的长短。

两种模式的流量路径对比

摘流性能对比

特性iptables 模式IPVS 模式摘流影响
规则更新机制全量刷新 (O(N))原子增量更新 (O(1))IPVS 更快
大规模性能规则多时更新慢,CPU 飙升几乎无延迟IPVS 更稳
连接追踪依赖 conntrack内置连接状态表IPVS 处理更精细

iptables 模式下,如果集群 Service 数量较多,kube-proxy 刷新一条规则可能需要消耗数秒甚至更久,这会极大地拉长“流量黑洞”的窗口期。而 IPVS 模式 得益于哈希表结构和增量更新,能更快地完成流量摘除。

四、瓶颈揭示:延迟从何而来?

除了 kube-proxy 的处理模式,NodePort 场景下还有其他延迟来源:

  1. 事件传播延迟:Pod 状态变化 -> API Server -> EndpointSlice Controller -> API Server -> kube-proxy。这个链路在繁忙的集群中可能有显著延迟。
  2. 跨节点转发延迟:NodePort 的流量可能被转发到另一台机器上的 Pod。这意味着涉及两台机器上的 kube-proxy 规则同步。如果入口节点的规则没更新,它仍会将流量发往目标节点;如果目标节点的 Pod 已死,流量就会被丢弃。

五、最佳实践:如何实现零停机摘流?

既然网络规则的更新总是有延迟的,解决思路就是:让 Pod 等待网络规则更新完毕后再真正停止。

1. 核心策略:PreStop Hook 缓冲

这是最有效的方法。在 Pod 收到 SIGTERM 之前,先执行一个钩子脚本。

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

原理

  • 当 Pod 进入 Terminating 状态时,Kubernetes 同时启动“从 Endpoint 移除 Pod”和“执行 PreStop”两个动作。
  • sleep 10 强行让 Pod 保持 Running 状态 10 秒。
  • 在这 10 秒内,kube-proxy 有充足的时间收到通知并更新 iptables/IPVS 规则,将该 Pod 从负载均衡池中摘除。
  • 10 秒后,流量不再分发给该 Pod,此时应用再真正停止,就不会有新流量受损。

2. 应用层:优雅关闭 (Graceful Shutdown)

PreStop 解决了“新流量不进”的问题,应用层代码则需要解决“旧流量处理完”的问题。

go
// Go 示例:收到 SIGTERM 后,停止接受新请求,但处理完存量请求
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGTERM)

go func() {
    <-sigChan
    // 1. 此时 PreStop 已经执行完毕(流量已切断)
    // 2. 关闭监听 socket,不再 accept 新连接
    server.SetKeepAlivesEnabled(false)

    // 3. 等待现有连接处理完成(设置超时)
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    server.Shutdown(ctx)
}()

3. 基础设施优化

  • 启用 IPVS 模式:对于规模较大的集群,切换到 IPVS 模式能显著降低规则同步的延迟。
    yaml
    # kube-proxy ConfigMap
    mode: "ipvs"
  • 调整同步周期 (慎用):可以适当减小 iptables.syncPeriod(默认 30s)或 minSyncPeriod,但这会增加 CPU 负载。

六、观测与排查:验证摘流效果

在实施优化后,可以通过以下工具验证流量摘除是否符合预期:

  1. 监控 iptables 规则变化: 在 Pod 删除期间,观察节点上规则被移除的时间点。

    bash
    watch -n 0.5 "iptables-save | grep <service-name>"
  2. 查看连接状态: 确认是否还有连接指向即将删除的 Pod IP。

    bash
    conntrack -L | grep <pod-ip>
  3. kube-proxy 日志: 检查 kube-proxy 收到 Endpoint 变更事件的时间戳,与 Pod 删除时间戳对比。

    bash
    kubectl logs -n kube-system -l k8s-app=kube-proxy --tail=100 -f

七、总结

NodePort 服务下的流量摘除问题,本质上是网络配置收敛速度应用退出速度之间的赛跑。

  • kube-proxy 负责维护转发规则,但它的更新是被动且有延迟的。
  • IPVS 比 iptables 更快,但无法消除分布式系统的传播延迟。
  • PreStop Hook (sleep) 是最简单且成本最低的解决方案,它为网络规则的收敛争取了宝贵的时间。

通过组合使用 PreStop Hook(阻断新流量进入)和 应用层 Graceful Shutdown(处理存量流量),我们可以确保 NodePort 服务在任何变更场景下都能保持零停机。