记一次Docker/Kubernetes上无法解释的连接超时原因探寻之旅

6 阅读6分钟

大家好,我是大闸蟹🦀,今天来分享一个真实踩过的坑:在Kubernetes集群里,Pod偶尔出现连接超时(connection timeout),日志里啥异常都没有,看起来像网络幽灵。排查过程曲折,涉及多个“经典神坑”,最后发现是**Linux内核conntrack + iptables SNAT的赛跑条件(race condition)**导致的SYN包被静默丢弃。

这个坑在2018-2019年特别常见(Flannel + Docker时代),但到现在还有人踩,尤其在高并发、出站连接多的场景下。

问题现象(越描述越魔性)

  • Pod A → Pod B(ClusterIP)或 Pod → 外网服务,大部分请求正常,但偶尔卡住几十秒甚至超时。
  • 客户端日志:dial tcp xxx: connect: i/o timeoutcontext deadline exceeded
  • 服务端无任何错误日志(甚至没收到SYN)。
  • 现象完全随机,重启Pod/节点/iptables有时缓解,但不彻底。
  • 高峰期更明显,QPS一上来就雪上加霜。

排查过程(一步步挖坑)

  1. 先排除显而易见的问题

    • 检查Pod健康探针、资源限制 → 正常
    • 检查Service/Endpoints → 正常
    • tcpdump抓包:发现SYN包发出去了,但没收到SYN-ACK(或SYN-ACK没回来)
    • 节点上conntrack -L看连接跟踪表 → 没满,但有大量ESTABLISHED/TIME_WAIT
    • 换CNI(Flannel → Calico)试试 → 问题依旧(排除CNI插件bug)
  2. 深入内核网络栈

    • 发现出站连接用的是iptables MASQUERADE(Docker默认SNAT规则)
    • 高并发下,多个Pod同时连同一个外部IP:Port时,内核在分配源端口时会竞争
    • 关键发现:Linux内核有一个已知bug(race condition)——在SNAT时,如果两个连接同时抢到同一个源端口,内核会丢弃其中一个SYN包,但不报错!
      → 客户端等不到ACK → 超时重传 → 最终超时
  3. 为什么这么难发现?

    • 丢包是静默的(内核不log)
    • 概率低(1/10000级别),低并发时几乎不复现
    • 高并发时,丢包率指数级上升
  4. 最终根因确认

    • 复现脚本:写一个Pod狂发curl到同一个外部域名
    • 观察conntrack -S:看到insert_failed计数暴涨
    • 看内核源码(net/netfilter/nf_nat_core.c):确实存在端口分配的race

解决方案(按推荐度排序)

  1. 推荐方案:切换到IPVS模式(kube-proxy)(Kubernetes 1.8+)

    • kube-proxy --proxy-mode=ipvs
    • IPVS不依赖iptables SNAT,不受conntrack race影响
    • 性能更好,高并发稳如老狗
  2. 临时缓解:增大conntrack表 + 调低超时

    sysctl -w net.netfilter.nf_conntrack_max=1048576
    sysctl -w net.netfilter.nf_conntrack_tcp_timeout_established=300
    sysctl -w net.netfilter.nf_conntrack_tcp_timeout_syn_sent=30
    
    • 但治标不治本,高并发还是会爆
  3. 彻底避免SNAT race:用Calico BGP模式(不依赖masquerade)

    • Calico host-gateway或BGP模式下,出站不做SNAT,绕过race
  4. 其他神坑补充(万一不是这个呢?)

    • MTU不匹配:overlay网络MTU 1450,物理1500 → 大包丢弃 → 超时
      → 统一调MTU为1400-1450
    • conntrack表满nf_conntrack: table full, dropping packet → 直接丢包
    • DNS解析超时:CoreDNS卡住 → 用nslookup测试
    • idle连接被防火墙杀:调TCP keepalive

conntrack -L 怎么判断连接跟踪表满没满?

conntrack -L(或 conntrack -L --family ipv4)是列出当前所有连接跟踪条目(entries)的命令,但它本身无法直接告诉你表是否“满”,因为它只显示已存在的条目,不显示容量上限。

要判断表是否(或接近满),需要对比两个关键 sysctl 值:

  1. 当前条目数(已用)

    cat /proc/sys/net/netfilter/nf_conntrack_count
    

    或用 conntrack -C(更简洁,直接输出数字):

    conntrack -C
    
  2. 最大容量(上限)

    cat /proc/sys/net/netfilter/nf_conntrack_max
    

    或:

    sysctl net.netfilter.nf_conntrack_max
    

判断标准

  • 如果 nf_conntrack_count 接近或等于 nf_conntrack_max(比如 95%以上),表就基本满了。
  • 真正满时,内核会直接在日志(dmesg 或 /var/log/messages)打出:
    nf_conntrack: table full, dropping packet
    
    这是最明确的信号,说明新连接无法建立(SYN包被静默丢弃),导致超时/连接失败。

推荐监控方式

  • 写个监控脚本报警:nf_conntrack_count / nf_conntrack_max > 0.9
  • 默认值通常是 65536 或 262144(取决于内存),高并发集群建议调大到 1M+(视节点内存而定):
    sysctl -w net.netfilter.nf_conntrack_max=1048576
    

额外提示

  • 高并发场景下,表满前往往先出现 conntrack: insert_failed(用 conntrack -S 查看统计),这是 race condition 的前兆。
  • 调低超时也能缓解:net.netfilter.nf_conntrack_tcp_timeout_established=300(默认 432000 秒,太长会占表)。

IPVS 不依赖 iptables SNAT?这俩模式的区别?

先澄清:IPVS 模式下仍然会用到 SNAT,但实现方式和依赖完全不同,这才是关键区别。

维度iptables 模式(默认)IPVS 模式(推荐高并发)
负载均衡实现纯 iptables 规则(大量链式规则)内核 IPVS(hash 表 + 负载均衡算法,如 rr/wrr/lc)
复杂度O(n)(n = 服务数 × 后端数),规则多时线性扫描慢O(1)(hash 查找,常量时间)
SNAT / Masquerade依赖 iptables POSTROUTING 链的 MASQUERADE 规则做 SNATkube-proxy 用 IPVS 的 NAT 模式做 SNAT,不走 iptables 链
conntrack 依赖重度依赖 netfilter conntrack(每个连接都进 conntrack 表)不依赖 netfilter conntrack(IPVS 自己维护简单连接跟踪)
conntrack race condition容易中招(多个 Pod 同时出站 SNAT 抢端口 → SYN 丢包)基本免疫(因为不走 iptables SNAT race)
性能表现服务数 > 5000 时规则爆炸,延迟暴增,CPU 高稳定,适合万级服务、万级 QPS
内存/CPU 开销高(规则多 + conntrack 表大)低(hash 表 + 轻量 conntrack)
其他缺点规则更新慢(kube-proxy sync 时卡)部分高级 iptables 功能(如复杂 filter)不支持;NodePort 仍需少量 iptables 辅助
推荐场景小集群、简单需求大规模生产(默认推荐从 k8s 1.11+ 开启)

为什么说“IPVS 不依赖 iptables SNAT”?

  • iptables 模式:SNAT 完全靠 iptables 规则 + conntrack(netfilter 模块),容易触发 race(nf_nat_core.c 的端口分配竞争)。
  • IPVS 模式:kube-proxy 用 ipvsadm 或 netlink 直接编程 IPVS 虚拟服务器,SNAT 由 IPVS 内核模块自己处理,不走 iptables 的 MASQUERADE 规则 → 绕过了 conntrack 的 race condition
  • 虽然 IPVS 也需要 conntrack(尤其是反向流量),但它用自己的轻量 conntrack(不进 netfilter 的 nf_conntrack 表),避免了经典的 SNAT race bug。

一句话总结

  • iptables:老实但慢,容易表满 + race 丢包
  • IPVS:快 + 稳,专治大规模 + 高并发下的连接超时幽灵

如果你集群里还有这种“莫名超时”,强烈建议切到 IPVS 模式(kube-proxy --proxy-mode=ipvs),基本一劳永逸。

总结

这个坑本质是Linux内核 + iptables SNAT的古老bug,Kubernetes + Docker/Flannel场景下被无限放大。
现在主流方案是IPVS + Calico,基本杜绝这类幽灵超时。

如果你也遇到类似“莫名其妙超时”,欢迎贴日志/抓包,我帮你继续挖🦀🍌!
有没有人踩过更奇葩的网络坑?来交流~