资讯详情

Docker/Kubernetes集群连接超时:conntrack表满排查实录

📅 2026/10/10 3:21:32 | 华诺云谱 👁 阅读
Docker/Kubernetes集群连接超时:conntrack表满排查实录
某个工作日的下午某业务集群的服务可用率监控曲线突然开始抖动。告警里报的是 A 服务调用 B 服务时出现大量连接超时应用日志里刷出一屏又一屏curl 56 recv failure: 连接超时。当时第一反应是服务出问题了可登录节点一看Docker 容器和 Kubernetes Pod 全都活得好好的CPU、内存、磁盘水位正常重启次数为零。这个场景相信不少维护容器集群的人都遇到过服务明明活着但新连接就是建立不起来而且失败是间歇性的让人完全摸不着头脑。这篇内容主要面向正在维护 Docker/Kubernetes 混合集群、被“时好时坏的网络超时”折磨过的人。我会按当时实际排查的顺序把从应用层一路打到内核层的完整过程写出来包括几个容易误判的方向、最后锁定的根因以及顺带排掉的另一个经典网络坑。如果你手头正好有类似的偶发超时问题照着这个链路走一遍大概率能少熬几个通宵。1. 故障表象服务活着新连接却在排队超时1.1 指标都正常但失败率就是高接到告警后我做的第一件事不是去抓包而是先确认服务到底有没有挂。因为大多数时候“偶发连接超时”很容易让人联想到发布变更、配置错误或者资源耗尽真正服务死掉的场景反而不多。我拉了一堆监控面板结果很尴尬Pod 的 CPU 使用率只有 20% 左右内存完全够用没有 OOMKilled日志里也没有明显异常。B 服务的接口其实已经成功返回了大量请求只是有一部分请求连 TCP 连接都没有建立起来更谈不上应用层逻辑执行。再看失败的时间分布它不像“高峰期流量压垮服务”那样有规律而是随机的、尖锐的抖动每次持续几分钟然后自己恢复。我把现象整理成一张表用来提醒自己不要被表象带偏现象当时的观察初步判断服务可用率下降下降幅度 1%-5%反复抖动偏偶发不像应用崩溃Pod 状态全部 Running重启次数 0排除容器 OOM / 探针杀进程CPU / 内存使用率低且平稳排除资源瓶颈失败类型连接未建立请求直接超时指向网络路径而非应用代码旧连接感知已建立的连接不受影响指向新建连接的“入口”被卡住这张表做完我心里已经把目标从“B 服务是不是挂了”转移到了“新建连接到底是在哪一步被丢掉”。这一步看起来简单但特别重要如果一开始就钻进应用日志里找异常很可能一晚上都出不来。1.2 先搞清楚“连接超时”的本质很多人看到curl 56 recv failure: 连接超时会以为是对端太慢、响应迟迟不来。其实这个报错的本质是 TCP 三次握手没有完成客户端发出 SYN 数据包然后一直等 SYN-ACK等了好几个超时周期后内核放弃了这次尝试。三次握手可以类比成一个打电话的场景你拨号过去电话通了但对面一直不接系统重拨几次之后告诉你“对方无人接听”。这里的“有人不接”可能是电话坏了、信号被中断也可能是有人故意把电话线掐了。放在网络里就是 SYN 包被中间某个环节静默丢弃而不是被明确拒绝。如果是 RST 拒绝客户端通常很快能收到“连接被拒绝”唯独“连接超时”最难缠因为包丢了你根本不知道丢在了哪里。这个认知直接决定了后续的排查方向。我不该再死死盯着应用层而是应该沿着网络路径一层层往下找从 DNS、Service NAT、宿主机内核一直查到物理链路。后面每一步都是顺着“包在哪一层丢掉”这个问题展开的。2. 第一轮排查应用层、DNS、健康检查全部排除2.1 先翻遍应用服务的“家门口”在把锅甩给内核之前我习惯先把最简单的可能排除干净。应用层最容易引发“连接超时”的其实是 listen 队列溢出。一个服务进程虽然活着但如果它的 accept 队列满了新来的 TCP 握手会被内核延迟处理表现为握手超时或者直接失败而且完全不会体现在 CPU 内存指标上。我用ss -lnt看了一下 B 服务监听的端口确认 Recv-Q 和 Send-Q 都没有积压。Recv-Q 如果长期接近 backlog 上限说明应用线程池扛不住但当时应用侧一切正常accept 队列处于空置状态。客户端侧的超时设置我也复测过从几百毫秒到十秒都试过只要是失败的业务请求无论怎么放宽超时都会稳定地落在同一个“连接建立不了”的区间。接着查健康检查。Kubernetes 的探针是容器网络内部的访问它走的是 Pod 网卡到应用端口这条直连路径没经过 Service 的负载均衡逻辑。探针正常只能说明应用和 Pod 网络栈本身还活着并不能代表整条业务链路是通的。所以探针绿灯只能算最低限度的“活”不能让我放心。2.2 三种访问方式把嫌疑圈定在 NAT 层这一步是整个排查里最有信息量的一次实验。我在故障节点上的一个临时 Pod 里分别通过三种地址访问 B 服务观察哪种路径会超时。访问方式结果推断直连 Pod IP稳定秒开连续测 50 次无异常Pod 内应用和网络栈正常访问 Service ClusterIP间歇性超时失败概率约 3%问题出在 Service/NAT 后的转发层访问 Service 域名和 ClusterIP 表现一致DNS 解析没问题瓶颈在解析之后这个对照结果非常有价值。ClusterIP 本身是一个虚拟 IP报文必须经过内核的 DNAT 转换才能被转发到具体的 Pod而直连 Pod IP 时根本不涉及这一层转换。两者差距如此明显几乎等于在说问题不出在 B 服务的代码里而出在 Kubernetes Service 的 NAT 转发路径上。为了把问题钉死我在源节点和目标节点上都抓了包。目标节点的 Pod 网卡上根本看不到那个失败的 SYN 报文说明包压根没送到容器里但源节点的物理网卡上确实看到 SYN 发了出去。于是排查范围被进一步压缩到宿主机内核的网络栈尤其是 netfilter/iptables 这一层。到这里“Docker/Kubernetes 上无法解释”的神秘感已经消了一大半。3. 解开“无法解释”的第一层conntrack 表满导致静默丢包3.1 被日志限速淹没的关键一行真正给出答案的是一条很容易被人忽略的内核日志。我在故障节点的宿主机上执行了下面这条命令dmesg -T | grep -i nf_conntrack | tail -20结果看到一行我非常熟悉的报错nf_conntrack: table full, dropping packet这行日志低调得过分内核默认对这类消息做了限速一分钟可能只打印几条而且它不会出现在应用日志、容器日志里只存在于宿主机的 dmesg 缓冲中。很多问题处理到一半就被“服务指标正常、容器正常”给劝退了很少有人会专门打开 dmesg 翻内核网络栈的记账信息。这也是这类问题“无法解释”的根本原因之一不是没有答案而是答案被藏在大家都不看的日志层。看到这行日志我的第一反应是“破案了”但很快冷静下来内核只说表满了没说为什么满。在 Kubernetes 环境里conntrack 表满导致的丢包像一种慢性病一天两天看不出问题流量稍微集中就爆发而且只要旧连接不受影响监控面板上就永远是“一切正常”。3.2 conntrack 是什么以及它为什么和 Service 绑定得这么紧简单解释一下 conntrack这是 Linux 内核 netfilter 框架里的连接跟踪模块。它会为每个经过的网络连接维护一条记录包括五元组源 IP、源端口、目标 IP、目标端口、协议和连接状态。它的存在让 iptables 的 NAT 功能能够工作一个数据包经 DNAT 改了目标地址后回包还要能原路改回来靠的就是这张“登记表”。可以把它想象成快递公司的改址登记本。包裹进来时快递员在登记本上写“这个客户的新地址是 XX”回程凭证才能发对地方。问题是登记本有厚度上限一旦写满新来的包裹就没法登记快递员只能把包裹丢在门口。table full, dropping packet就是这个丢门口的动作。Kubernetes Service 的 ClusterIP 之所以能访问是因为 kube-proxy 在 iptables 里预设了 DNAT 规则把 ClusterIP 换成后端 Pod IP。而每一次这样的转换都要依靠 conntrack 记录否则响应报文无法正确还原源地址。换句话说Service 的流量规模越大conntrack 表的压力就越大。整个集群里所有经过 Service 的调用、健康检查、监控抓取甚至节点上的 DNS 查询都会被登记进来。默认上限通常是 262144 条取决于nf_conntrack_buckets听起来不少但节点上托管的 Pod 一多、短连接频率一高这 26 万条很快就会被用完。3.3 现场验证不是猜测是实打实的计数为了确认当前就是 conntrack 表满我做了三件事。第一查看当前表项数和上限cat /proc/sys/net/netfilter/nf_conntrack_count cat /proc/sys/net/netfilter/nf_conntrack_max故障节点上count已经顶到max的 99%有些节点短暂溢出数值直接贴着上限走。第二看失败计数conntrack -S结果显示insert_failed在持续增长。insert_failed代表试图新建连接跟踪条目但没有成功的次数这个字段一旦快速增长基本就是“表满丢新包”的实锤。第三再次验证访问路径在同一个时间窗口里直连 Pod IP 稳定、走 ClusterIP 间歇失败。直连可能因为同网段不需要 NAT 而避开了 conntrack 的瓶颈需要新建条目、走 NAT 转发的请求则被丢弃。这几个证据合在一起根因已经不用再猜了。4. 表满不是偶然从计数分析到短连接风暴4.1 谁把 26 万条记录吃掉了根因确认后接下来要做的是“清理现场”和“防止复发”。但如果只调大表不搞清楚是谁吃掉的过几天问题还会回来。我把conntrack -L的输出导到文件里用 awk 把 src 和 dst 字段拆出来做了个简单的频率统计conntrack -L | awk {for(i1;iNF;i){if($i ~ /^src/){print $i}}} | sort | uniq -c | sort -rn | head结果很有意思占比最高的不是业务大流量也不是数据库连接而是几个很容易被忽略的角色——监控采集器、Kubernetes 的探针请求、日志采集 Agent 的 HTTP 回调以及一大批没有开启 keep-alive 的服务间调用。这些流量的共同特征是“短小频繁”建立连接发几个字节断开过一会再来一次。每一次连接都会在 conntrack 里留下条目连接结束后进入 TIME_WAIT 状态默认还要再停留 120 秒才被回收。如果连接是长连接表项虽然一直存在但数量是稳定的偏偏大部分是高频短连接所以表项的新增速度远超回收速度很快就把容量吃满。而且服务规模越大的集群这种“短连接日常噪声”就越可观因为每个组件的连接量单独看都不起眼叠加上千个 Pod 之后就是一场风暴。4.2 用一道乘法题估算容量够不够这里给一个可以复用的估算逻辑帮助大家判断自己集群的 conntrack 容量是否即将告急。假设集群里有一个服务实例每秒对外发起 200 个短连接调用每个连接结束后要停留在 TIME_WAIT 状态 120 秒。那么这一个实例贡献的常驻表项就是200 连接/秒 × 120 秒 24000 条如果默认上限是 262144 条那么不到 11 个这样的实例就能把整张表塞满。这还没算 ESTABLISHED 状态的长连接、DNS 请求、探针和其他节点上的流量。而现实中一个中等规模的 Kubernetes 集群服务实例数量动辄上百每个实例每秒发起几十到几百个连接都非常正常。再加上产品侧的调用链路放大表满几乎是必然事件。连接来源估算条数贡献是否可控高频服务间短连接每实例数千到数万条可用长连接/连接池控制K8s 探针liveness/readiness每个 Pod 两条按探针频率计算适当调低频率可缓解监控采集 / 日志回调每节点数百到数千条可改用长连接或减小频率DNS 查询单条超时短但量大配置好本地缓存即可这个表格说明了一件事单纯把nf_conntrack_max调大只是止痛真正的根治办法是减少不需要的连接新建频率让表的新增和回收回归平衡。4.3 与表满伴生的“僵尸 conntrack 坑”排查过程中我还注意到一个和 conntrack 经常一起出现、同样会让连接“偶发超时”的坑Pod 重建后旧 conntrack 条目里的目标 IP 还指向已经被销毁的旧 Pod 地址。Kubernetes 里 Pod IP 是短命的每次滚动更新都可能变化但 Service 的 ClusterIP 不变。如果外部流量通过固定源端口持续重连同一个 Service新连接很可能因为五元组一致被 conntrack 命中旧条目进而被转发到已经不存在的 Pod IP 上。结果就是从负载均衡器进来的请求时不时超时或者连接被重置完全随机特别像玄学故障。这一类问题在外部流量经过 NAT 网关、源端口被网关固定复用时尤其突出。遇到这种情况等旧 conntrack 条目超时后会自动恢复也可以主动清理相关条目conntrack -D --orig-src 10.233.20.1 --orig-dst 10.233.20.2 -p tcp --orig-sport 8080 --orig-dport 443如果分不清是哪条可以直接清掉某个目标 IP 的全部条目但要谨慎这可能会打断正在进行的存量连接。5. 处理手段与调优参数不仅要扩容还要管住连接寿命5.1 先止血把表容量临时扩到足够大故障恢复永远是第一步。受影响节点上我先临时把表上限调大让业务先跑起来sysctl -w net.netfilter.nf_conntrack_max1048576这个动作几乎立刻生效不需要重启任何进程。把容量从 26 万拉到 100 万相当于给快递公司加了一倍的登记本。需要说明的是nf_conntrack_buckets这个哈希桶数量通常是模块加载时确定的很多新内核虽然支持动态扩容但在生产环境里临时折腾它性价比不高。如果你希望稳定支撑更大的表容量建议在启动参数nf_conntrack.hashsize或模块加载参数里配好比如echo 262144 /sys/module/nf_conntrack/parameters/hashsize之后把相关参数写进/etc/sysctl.d/99-conntrack.conf让它持久化net.netfilter.nf_conntrack_max 1048576 net.netfilter.nf_conntrack_tcp_timeout_established 1800 net.netfilter.nf_conntrack_tcp_timeout_time_wait 60注意一点在调大 max 的同时要留意哈希桶数量和内存开销。每个 conntrack 条目大约占用几百字节100 万条会吃掉几百 MB 内存对节点内存规划是有影响的。别只盯着“数量大就安心”。5.2 给连接寿命设置更合理的“保鲜期”扩容只是短期手段更关键的是让 conntrack 条目的生命周期匹配真实业务节奏。Linux 的默认超时对大型容器集群来说太宽松了尤其是 ESTABLISHED 状态默认长达 5 天。如果一些空闲连接长期没有流量表项就一直挂在那里完全浪费容量。参数默认值秒建议值秒理由nf_conntrack_tcp_timeout_established4320001800 ~ 360030 分钟到 1 小时内无包即判定结束足以覆盖正常业务nf_conntrack_tcp_timeout_time_wait12060缩短 TIME_WAIT 留存快速腾出位置但我必须提醒一句不要把 established 超时调得过分激进比如 300 秒。如果应用本身有长连接且 TCP keepalive 配置较疏conntrack 可能会在连接仍被使用者视为“活着”时就把它清掉导致后续报文无法完成 NAT 逆转换反而出现更诡异的“断流”。内部服务之间基本都有周期性的心跳或者探活1800 秒是比较安全的起点数据库等连接如果确实需要长时间空闲挂起可以单独保留更长的超时。5.3 应用侧改造让连接自己尽量活久一点这一步才是根治。因为不管把表扩多大只要业务保持高频短连接的行为未来终究还会撞上限。第一服务间 HTTP 调用必须开启 keep-alive。很多语言的 HTTP 客户端默认复用连接但也有不少版本默认每次请求新建连接这是最典型的 conntrack 杀手。第二引入连接池。数据库、Redis、MQ 这类资源型连接全部改成连接池管理池化后的复用率能直接把新建连接频率降一个量级。第三Kubernetes 探针频率可以适当调低比如 liveness 从 10 秒改成 30 秒在大多数场景下不影响故障发现速度却能为 conntrack 表减负。第四如果确实无法改造短连接那就只能靠监控提前发现水位把容量松紧掌握在手中。改造完成后我盯着监控面板验证了整整一个发布周期nf_conntrack_count的均值从 26 万附近降到了 9 万左右insert_failed归零dmesg 里也不再出现 dropping packet 的新日志业务失败率曲线完全放平。6. 顺手排掉的另一个经典坑MTU 与 overlay 封装6.1 “大包必挂、小包正常”的信号更接近 MTUconntrack 的问题解决后复盘时我们又发现了一个容易被忽略的姊妹坑客户端只要发起稍大一点的请求体就稳定超时而普通的小请求完全正常。这类故障的典型特征是“由包大小触发”而不是“由连接并发触发”和刚才的 conntrack 表现明显不同。原因在 overlay 网络。Docker/Kubernetes 集群的 Pod 网络很少直接使用宿主机网卡而是通过 VXLAN、IPIP 这类隧道把数据包封装起来再发送。隧道封装会给原始数据包增加几十字节的外层头部比如 VXLAN 大约增加 50 字节。如果容器网卡和宿主机网卡都被设成了标准 1500 MTU一个 1500 字节的 Pod 数据包封装后变成 1550 字节超过了底层链路能承载的上限结果就是大包被丢弃小包安然无恙。TCP 对大包的表现尤其让人迷惑握手是几十字节的小包能正常完成一旦传输阶段出现超过路径 MTU 的数据段就开始重传、超时。应用侧看到的依然是“连接超时”但原因完全在另一个层面。这种故障如果运气不好会和你刚解决的 conntrack 问题同时存在让人误以为没有修好。6.2 一条 ping 命令快速定位 MTU 问题排查这类问题不用靠猜直接用禁止分片的方式发大包探测ping -M do -s 1472 目标IP这里的-M do表示禁止分片-s 1472是 ICMP payload 大小加上 8 字节 ICMP 头和 20 字节 IP 头正好等于标准以太网 MTU 1500。如果执行结果出现Frag needed and DF set或message too long说明路径上的 MTU 小于 1500如果这条命令能通说明两层之间的基础 MTU 没问题再用-s 1450、-s 1400逐级下探找到临界值。我还习惯配合测量 Pod 到宿主机网关的链路ping -M do -s 1450 宿主机网关IP如果 Pod 内 1450 能通而 1472 不通并且底层物理链路是 1500通常就需要把隧道网络配置的 MTU 降下来例如把 VXLAN 的对外传输 MTU 设为 1450 或更低让上层封装之后仍不超过物理链路。6.3 两种故障叠加时的判断顺序经历过这次排查我的经验是先看并发水位再看包大小。如果失败集中在请求高峰、且insert_failed在飙升优先怀疑 conntrack如果失败总是和“比较大的请求体”绑定小请求再怎么高峰也不会出问题就优先测 MTU。两者也可以同时量一边执行conntrack -S一边跑 1472 的探测包几分钟内就能把最常见的两个嫌疑排掉。这类网络问题还有一个共同点容器里看到的永远只是“对方没回应”。真正的答案往往在容器外面的宿主机内核里这也是我后来每次排查都先上宿主机开 dmesg、看 proc/sys 的原因。经历这次以后我把 conntrack 的水位、insert_failed 计数和 MTU 探测脚本都加进了宿主机初始化检查和日常巡检告警里。再遇到“服务活着但连接超时”的告警至少不会再从应用日志里大海捞针了。如果你想在自己的环境里提前预防最值得做的一件事就是在每台节点上把dmesg -T | grep nf_conntrack和cat /proc/sys/net/netfilter/nf_conntrack_count这两条命令加到监控脚本里。这次的“无法解释”说到底只是因为内核的无声抱怨一直没有被人听见罢了。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑