Istio ztunnel 滚动升级时如何利用 SO_REUSEPORT 与 drain 机制减少流量中断
【免费下载链接】istioConnect, secure, control, and observe services.项目地址: https://gitcode.com/GitHub_Trending/is/istio
在 ambient 模式下,ztunnel 以 DaemonSet 方式运行,每个节点一个,接管该节点上所有 mesh Pod 的 L4 流量。当需要升级 ztunnel(更换镜像版本或修改 DaemonSet spec)时,每个节点都会发生一次旧 Pod 退场、新 Pod 接管的滚动重启,而流量中断风险就集中在这一瞬间。Istio 用SO_REUSEPORT加 drain(连接排空)两个机制保证整个过程中"新连接随时都能成功"。这篇文章基于仓库中的 ztunnel 生命周期文档 和 ztunnel Helm chart 说明这套机制如何工作、升级前应检查哪些配置项、以及如何判断升级是否达到预期效果。
为什么 ztunnel 升级时流量无法"无损接管"
在讨论配置之前,先明确 ztunnel 升级时的能力边界。生命周期文档指出 ztunnel 只工作在 Layer 4,这带来两个限制:
- TCP 是有状态的,旧 ztunnel 的连接状态无法直接传给新进程(如果在 L3 层工作,新进程直接处理报文即可);
- ztunnel 不工作在 L7,因此没有任何方式通知应用"你应该重连到新 ztunnel"。
所以官方给出的现实目标是两条:
- 任意时刻,任何新连接都能成功——不存在新连接被丢弃的窗口;
- 给旧 ztunnel 一段drain period,让它继续处理已建立的连接。
文档对此的判定是:如果 drain period 长于任何一条连接的生命周期,应用完全无感;否则 drain period 结束时,剩余连接会被强制拆掉,影响程度取决于应用(可能终止 in-flight 事务)。
SO_REUSEPORT 如何让新旧 ztunnel 同时监听同一端口
新旧 ztunnel 之间"交接"的关键是SO_REUSEPORT:ztunnel默认在所有 listener 上设置该 socket 选项(文档 Ztunnel Shutdown Implementation 一节)。它允许同一有效 UID 的多个进程绑定同一端口(详见man 7 socket),之后 Linux 会把新连接分给其中任一进程——先accept()的进程赢得连接,效果上等价于随机。
两个进程要满足"同一有效 UID"这一前提,而 ztunnel DaemonSet 模板 中容器以runAsUser: 0运行,因此同节点的旧、新 ztunnel 天然满足该条件,无需额外配置。
基于这个前提,一次 DaemonSet 滚动重启(即文档所说"upgrading to a new version, or changing some part of the DaemonSet spec")的完整时序是:
ztunnel-new启动,连接 CNI;- CNI 把节点上所有 Pod 的当前状态发给新 ztunnel;新 ztunnel 在每个 Pod 网络命名空间内建立 listener,并将自身标记为 "ready";
- 此时新旧两个 ztunnel 都在监听,新连接会被分给任意一个;
- 随后 Kubernetes 开始终止
ztunnel-old,先发送SIGTERM;ztunnel-old捕获该信号并开始 draining; - drain 一开始,
ztunnel-old立即关闭自己的 listener——此后只有ztunnel-new在监听。关键不变量:任何时刻都至少有一个 ztunnel 在监听; ztunnel-old不再接受新连接,但继续处理存量连接;drain period秒后,ztunnel-old强制终止仍未结束的存量连接。
升级前应检查的两个配置项
ztunnel chart 的 values.yaml 中有两个字段直接决定上述时序的行为,升级前建议逐项核对。
1. updateStrategy:保证"新 Pod 就绪后才动旧 Pod"
chart 默认值:
# manifests/charts/ztunnel/values.yaml updateStrategy: type: RollingUpdate rollingUpdate: maxSurge: 1 maxUnavailable: 0maxUnavailable: 0意味着旧 ztunnel Pod 只在新 Pod 就绪之后才会被删除,这与上面时序中"先让新 ztunnel 建好全部 listener,再向旧进程发 SIGTERM"的交接顺序配合,避免出现节点上没有任何 ztunnel 的间隙。如果你的集群里被改过这个值,升级前先确认它仍满足该语义。
2. terminationGracePeriodSeconds:一个值,两个含义
chart 默认值为 30(与 Kubernetes 默认一致),注释明确它同时定义了两件事:
# manifests/charts/ztunnel/values.yaml terminationGracePeriodSeconds: 30- kube 等待 ztunnel Pod 优雅退出的时间,到点仍未退出就强制终止(发
SIGQUIT); - ztunnel 用来 drain 自身连接的时间,即该值减 1 秒。
DaemonSet 模板 会把该值通过环境变量TERMINATION_GRACE_PERIOD_SECONDS传给 ztunnel 容器,同时写入 Pod 级的terminationGracePeriodSeconds字段,二者保持同源,不需要分别设置。
如果你的应用存在长连接(例如保持数分钟的 HBONE 隧道),drain 时间不够时这些连接会在 drain 结束点被强制拆掉。文档给出的调整原则是:让 drain period足够长以满足应用需求,但又足够短以在 Kubernetes 强杀之前完全退出(生命周期文档原文)。调整时按 chart 顶部注释的说明直接设置字段本身,不要走_internal_defaults_do_not_set前缀——例如用--set terminationGracePeriodSeconds=<你的值>而不是--set _internal_defaults_do_not_set.terminationGracePeriodSeconds=<你的值>。具体取多大,文档没有给出统一数值,由你按节点上最长连接的生命周期决定,只需保证 drain(值减 1 秒)覆盖它。
如何判断滚动升级是否按预期完成
文档没有给出一条专用的健康检查命令,但提供了可用于判断的状态点:
- 新 Pod 就绪:DaemonSet 模板 为 ztunnel 配置了 readinessProbe(
httpGet,端口15021,路径/healthz/ready)。新 ztunnel 建好节点上所有 Pod 的 listener 后将自己标记为 "ready",滚动更新才会继续推进到终止旧 Pod。 - 不变量成立:整个过程中节点上始终至少有一个 ztunnel 在监听,因此新连接不会出现失败窗口。若升级期间观察到新连接被拒,说明时序被破坏(例如
maxUnavailable被改成正数、或新 Pod 未能通过就绪检查),应回到上面的时序逐步核对。 - 旧 Pod 的退场方式:旧 ztunnel 在 drain period 内结束存量连接后自行退出。如果它在 kube 的 grace period 内没退出完,Kubernetes 最终会发
SIGQUIT将其连同所有连接一起突然终止。这种强杀与 ztunnel 自身 drain 的区别在于:无法发送 TLSclose_notify、HTTP/2GOAWAY等优雅通知(文档 原文)。如果你的日志里看到连接以非优雅方式中断,优先检查 drain 时间是否被强杀路径抢先。
边界与限制
- drain 机制只能保护存活期不超过 drain period 的连接;更长的连接一定会在 drain 结束点被拆掉,这是 L4 代理无法转移 TCP 状态的固有限制,不是配置错误。
- ztunnel 无法通过任何方式促使应用重连到新 ztunnel,存量连接的行为完全由应用自身对断连的处理决定。
- 以上时序描述的是 ztunnel 自身升级/重启;Pod 正常退出是另一场景——应用进程已退出时不需要 drain,ztunnel 直接拆除即可(见 生命周期文档 Pod Shutdown 一节),不要把两套逻辑混用。
ztunnel 的整体架构与 HBONE 协议背景可参考 ztunnel 架构文档,CNI 侧规则见 CNI README。
【免费下载链接】istioConnect, secure, control, and observe services.项目地址: https://gitcode.com/GitHub_Trending/is/istio
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考