一、背景与现象
在云栈社区的技术排障讨论里,这类“偶发网络抖动”经常被低估。我们最近也碰到一个很典型的现象:从业务 Pod 访问某个 ClusterIP 类型的 Service,十次里总有一次会卡住然后超时,或者偶发 Connection reset by peer。更奇怪的是,Ingress/SLB 入口正常,Pod IP 直连也正常,唯独“过一道 Service”就抽风。
因为偶发,监控图上的表现只是一个小毛刺,很容易被当成网络抖动忽略。但业务方反馈“下单偶尔失败”,我们决定把它彻底查清楚。
二、排查过程
第一步,确认 kube-proxy 跑在哪种模式:
# 看 kube-proxy 配置
kubectl -n kube-system get configmap kube-proxy -o yaml | grep mode
# mode: "ipvs"
# 到节点上直接看 IPVS 规则
ipvsadm -Ln
# 能看到 Virtual Service 和对应的 Real Server(Pod IP)
第二步,对比 endpoint 是否和 IPVS 规则一致:
# Service 对应的 endpoints
kubectl get endpoints <svc> -o wide
# IPVS 里实际的 RS 列表
ipvsadm -Ln | grep -A10 <cluster-ip>
滚动发布期间,我们发现:endpoints 已经摘掉旧 Pod,但 ipvsadm -Ln 里旧 RS 还在,且权重变成 0;而另一瞬间,新 RS 还没被加进来。这中间有几秒窗口,请求会被打到不存在的 RS 上。
第三步,看 kube-proxy 的同步节奏:
# kube-proxy 日志里关注 endpoint 到 ipvs 规则的刷新
journalctl -u kube-proxy -n 200 | grep -i "sync\|endpoint"
# 默认 --ipvs-sync-period=30s,意味着最长 30s 才全量刷新一次
第四步,进 IPVS 连接表看实际打到哪了:
# 查看当前 IPVS 连接,确认有没有打到已销毁的 RS
ipvsadm -Ln -c | grep <cluster-ip>
# 结合 conntrack 看新建连接是否被静默丢弃(见同系列 NTP 那篇的 conntrack 思路)
三、根因分析
kube-proxy 在 IPVS 模式下,是把 Service 翻译成 IPVS 的 Virtual Service,后端 Pod 作为 Real Server。它监听 Endpoints/EndpointSlice 变化,再 refresh IPVS 规则。关键在于刷新不是瞬时、也不是每次事件都全量:
-
同步周期造成的时间窗。默认 --ipvs-sync-period=30s、--ipvs-min-sync-period=10s。Pod 滚动销毁/新建时,endpoints 变了,但 IPVS 规则要等下一个同步周期才更新。这个窗口里,旧 RS 还在、新 RS 没来,或者反之,请求就会落到“悬空”的 RS 上,表现为偶发超时/reset。
-
conntrack 兜底缺失。IPVS 做 SNAT/masquerade 依赖 netfilter conntrack。如果节点 conntrack 表被打满(短连接、NAT 出口常见),新建连接会被静默丢弃,叠加在上面的“偶发超时”上,雪上加霜。
-
滚动更新没先摘流量。如果 Pod 的 readinessProbe 没配好,旧 Pod 还在 endpoints 里就被杀掉,或者新 Pod 没 ready 就被加进来,放大了 IPVS 规则与实际后端的错位。
本质是:Service 转发的“控制面”(endpoint 同步)和“数据面”(实际 Pod)在短时间内出现了视图不一致。
四、解决方案
调小 kube-proxy 的 IPVS 同步周期,让规则跟 endpoints 变化贴得更紧:
# kubectl -n kube-system edit configmap kube-proxy
apiVersion: kubeproxy.config.k8s.io/v1alpha1
kind: KubeProxyConfiguration
mode: "ipvs"
ipvs:
# 最小同步间隔和全量同步间隔都调小,缩短视图不一致的窗口
minSyncPeriod: 5s
syncPeriod: 15s
# 严谨场景可开启,RS 消失时立即把相关连接置为异常,避免打到死 RS
expireNonActiveConn: true
滚动更新侧,让 endpoint 先摘流量再杀进程(呼应之前《滚动更新 502 抖动》那篇):
spec:
template:
spec:
containers:
- name: app
readinessProbe:
httpGet: { path: /healthz, port: 8080 }
initialDelaySeconds: 5
periodSeconds: 5
lifecycle:
preStop:
exec:
command: ["/bin/sh","-c","sleep 15"] # 给 endpoint 摘流量留缓冲
strategy:
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
如果节点 conntrack 压力大(IPVS 大规模集群常见),顺手调一下:
# 调大 conntrack 表,缩短 TIME_WAIT 占用
sysctl -w net.netfilter.nf_conntrack_max=262144
sysctl -w net.netfilter.nf_conntrack_tcp_timeout_time_wait=30
验证:ipvsadm -Ln -c 里看到连接都打到 ready 的 RS;滚动发布期间抓包确认没有请求落到已销毁 Pod;超时毛刺消失。
五、复盘与预防
-
监控:暴露 kube-proxy 的 rest_client_requests_total(看 endpoint 同步频率)、节点 node_time_seconds 漂移;用 ipvsadm -Ln -c 巡检有无打到 FIN_WAIT/SYN_SENT 卡死的连接。
-
告警:endpoints 的 ready 地址数与 Pod ready 数不一致,持续 30s 就告警;Service 成功率低于 99.9% 直接告警而非等毛刺。
-
规范:所有被 Service 暴露的服务必须配 readinessProbe + preStop;IPVS 集群把 minSyncPeriod 写进 kube-proxy 标准配置。
-
压测:发布窗口做“滚动更新 + 持续压测”,提前暴露 endpoint 与 IPVS 规则错位。
-
修复验证:连续两次滚动发布零超时毛刺、IPVS 连接表无悬空 RS,才算真正闭环。
六、小结
IPVS 模式性能比 iptables 好,但代价是“规则同步有周期”。只要把同步周期调小、滚动更新先摘流量、conntrack 别打满,偶发超时基本能消除。
后续我们还会继续展开一个 Ingress 高频坑:路径 rewrite 没写对,后端收到错误 path 直接 404。