nf_conntrack table full 刷屏别急着调大:先算清连接追踪表被谁填满
下午三点多,网关节点上的几个服务同时出现超时。监控上的现象很零碎:部分新请求连接超时,已经建好的长连接基本正常,重试后有一部分能成功。节点 CPU 和内存都不高,网卡流量也没到上限。
有人登录节点执行了 dmesg,屏幕上反复出现一行日志:
nf_conntrack: nf_conntrack: table full, dropping packet群里很快有人贴出“标准答案”:把 nf_conntrack_max 调大一倍,问题就没了。这一招确实经常管用,几分钟内告警就能消下去。
我的看法是,这一步可以作为止损,但做之前要先回答两个问题:这台机器还有没有内存装下更大的表?是谁在短时间内造出了这么多连接?第二个问题不回答,表调多大,过几天都可能再次打满。
下面按一次脱敏后的典型排查过程,把判断依据、只读采证、扩容的灰度和回滚、根因修复,以及文末的检查清单讲清楚。
连接追踪表满了,为什么丢的偏偏是新连接?
nf_conntrack 是 Linux netfilter 的连接追踪模块。只要节点上用到 NAT、有状态防火墙规则,或者 Kubernetes 的 kube-proxy(iptables 模式和 IPVS 模式都依赖它),经过的每条连接都会在表里占一个条目,记录五元组、状态和剩余超时时间。
条目数达到 nf_conntrack_max 后,内核会先尝试提前回收一部分还没进入确认状态的条目(early drop)。回收不出空位时,新连接的首包就会被丢掉,同时打印上面那行日志。已建立的连接已经有条目,不受影响。这正好解释了现场“老连接正常、新连接超时、重试有时能成功”的样子。
还有一点容易被忽略:条目不会在连接关闭时立刻消失。一条 TCP 连接正常关闭后,对应的条目会进入 TIME_WAIT 状态,并按 nf_conntrack_tcp_timeout_time_wait 的时长保留,这个值通常是 120 秒。所以表的大小大致等于“新建连接速率 × 条目平均存活时间”。每秒新建 2000 条短连接,光是 TIME_WAIT 条目就会稳定在 24 万条左右。
这篇的核心观点就一句:conntrack 表满是结果,“建连太快”或者“条目死得太慢”才是原因。
动手之前,先确认环境和当前水位
这一节的操作全部是只读的,在宿主机的网络命名空间里执行,需要 root 权限。
uname -r
lsmod | grep nf_conntrack
sysctl net.netfilter.nf_conntrack_count net.netfilter.nf_conntrack_max net.netfilter.nf_conntrack_bucketsnf_conntrack_count 是当前条目数,nf_conntrack_max 是上限,nf_conntrack_buckets 是哈希桶数量。count 贴着 max 的时候,丢包基本就能对上了。如果 lsmod 里没有 nf_conntrack,这条日志也就不可能来自本机,要回头确认自己登录的是不是出问题的那台机器。
内核日志带上时间戳,方便和监控曲线对齐:
dmesg -T | grep -i "nf_conntrack" | tail -n 20这条日志有速率限制,刷屏程度并不等于丢包数量,丢包量要看计数器。计数器需要用到 conntrack-tools 里的 conntrack 命令,先用 which conntrack 确认有没有安装。
conntrack -S输出按 CPU 分行,每行有一组计数字段。下面是示例,字段顺序随版本可能不同:
cpu=0 found=12 invalid=310 insert=0 insert_failed=0 drop=1532 early_drop=40 error=0 search_restart=8
cpu=1 found=9 invalid=287 insert=0 insert_failed=0 drop=1498 early_drop=37 error=0 search_restart=5重点看三个字段:drop 是因为表满而丢掉的次数,early_drop 是为了腾位置提前回收的次数,insert_failed 是插入失败的次数。这些都是累计值,单看一次没有意义。隔 60 秒执行两次,用差值判断丢包是不是仍在发生。
判断分支很清楚:drop 持续增长,而且 count 贴着 max,就是表满;count 离 max 还很远,但 insert_failed 在涨,更可能是并发插入冲突,常见于 UDP 场景,比如 DNS,和这篇讨论的表满不是一回事,要换个方向查;两者都不涨,丢包原因就在别处,回到网卡、iptables 规则和应用自身去查。
如果是 Kubernetes 节点,还要确认 kube-proxy 的模式和它对 conntrack 的配置,因为 kube-proxy 启动时会主动设置这些内核参数:
curl -s http://127.0.0.1:10249/proxyMode
kubectl get cm kube-proxy -n kube-system -o yaml | grep -A 6 conntrackmaxPerCore 和 min 决定了 kube-proxy 计算出的 nf_conntrack_max,tcpEstablishedTimeout 和 tcpCloseWaitTimeout 会改对应的超时时间。如果集群是托管版本,或者 ConfigMap 名称不同,就以平台文档为准。
表里装的到底是什么?
知道表满了还不够,要看清是哪类条目占了位置。
先看状态分布。表里有几十万条时,导出会消耗一些 CPU,耗时也可能达到几秒到几十秒,所以要加上超时,把结果写到文件再离线分析。文件里包含真实的 IP 和端口,分析完记得删掉,不要随手贴到群里。
timeout 60 conntrack -L -p tcp > /tmp/ct_tcp.txt 2>/dev/null
wc -l /tmp/ct_tcp.txt
awk '{print $4}' /tmp/ct_tcp.txt | sort | uniq -c | sort -rnTCP 条目的第 4 列是状态。这里有三种常见情况:
- TIME_WAIT 占大头:说明短连接很多,每次请求都新建、关闭连接。
- ESTABLISHED 占大头:说明长连接数量本来就多,或者连接泄漏后一直没有关闭,再加上 ESTABLISHED 的超时时间很长(内核默认 5 天,kube-proxy 默认改成 24 小时),这些条目会占位很久。
- SYN_SENT 很多:说明有大量建连没有得到回应,要检查下游是否不可达,或者是否正在被扫描。
再按目标地址聚合,找出造成问题的连接。每行第一个 dst= 是原始方向的目标:
grep TIME_WAIT /tmp/ct_tcp.txt \
| awk '{for(i=1;i<=NF;i++) if($i ~ /^dst=/){print $i; break}}' \
| sort | uniq -c | sort -rn | head把 dst= 换成 src=,就能看到是哪些客户端或 Pod 在发起这些连接。在 Kubernetes 节点上,src 往往是 Pod IP,可以用 kubectl get pod -A -o wide | grep <ip> 对应到具体的工作负载。
最后估算内存,这是决定能不能扩容的关键数据:
grep -E "^nf_conntrack" /proc/slabinfo
free -mslabinfo 的 objsize 列是单个条目的字节数,不同内核和不同开启的扩展下数值不同,要以你机器上的实际输出为准。上限乘以 objsize,就是表满时条目占用内存的大致数量。
下面用一个脱敏后的典型场景说明
以下时间、IP 和数值都是示例或脱敏数据。
某集群中的一台节点,16 核 64GB,nf_conntrack_max 为 262144。周四 15:10 开始,节点上的三个服务出现新连接超时,调用方错误率从接近 0 升到约 3%。影响范围只在这台节点和另一台调度情况相似的节点上,其他节点正常。前一天有一次发版:订单服务升级了 HTTP 客户端依赖。
排查时依次排除了几个方向。节点 CPU 不到 40%,内存余量充足,网卡吞吐不到带宽的 30%,排除了资源打满。iptables 规则数量和前一周一致,最近没有网络策略变更。下游服务的监控显示收到的请求量正常,响应耗时也平稳,说明不是下游变慢。
关键证据来自 conntrack:nf_conntrack_count 在 26.1 万到 26.2 万之间波动,基本贴着上限;间隔 60 秒的两次 conntrack -S 显示,drop 合计增加了约 1.8 万;状态分布中 TIME_WAIT 约 23 万条,占九成;按 dst 聚合后,排在第一的是库存服务的 ClusterIP 和端口,占 TIME_WAIT 条目的八成以上;按 src 对应回去,几乎都来自订单服务的 Pod。
这些数据可以对上:订单服务高峰期调用库存服务约 1900 次每秒,乘以 120 秒的 TIME_WAIT 保留时间,约 22.8 万条,和观测值基本一致。最后一个环节在代码里找到:新版 HTTP 客户端的默认配置让每次请求都新建连接、用完关闭,旧版本则默认复用连接。
临时止损分两步。先确认内存够用:示例中 objsize 约 320 字节,上限翻倍到 524288,满表时占用约 160MB,相对 64GB 的内存可以接受。再在一台受影响的节点上调大上限,观察 15 分钟,drop 停止增长,错误率回落,然后推广到另一台。
根因修复是在订单服务里显式开启连接复用并设置连接池上限,按灰度发布。全部发布完成后,节点上的 TIME_WAIT 条目从 23 万降到 1 万以下,count 稳定在 4 万左右。一周后观察平稳,再通过 kube-proxy 配置把上限统一收敛到经过评估的值,不再保留手工调大的状态。
调大参数的正确姿势:灰度、观察、能退回
修改 nf_conntrack_max 属于有状态变更。它会立即生效,影响整台节点的所有流量。我会按下面的顺序操作。
先记录当前值,作为回滚依据:
sysctl net.netfilter.nf_conntrack_max net.netfilter.nf_conntrack_buckets > /root/conntrack-before-$(date +%Y%m%d%H%M).txt
cat /sys/module/nf_conntrack/parameters/hashsize在单台节点上临时调大,一次不超过翻倍:
sysctl -w net.netfilter.nf_conntrack_max=524288上限和哈希桶要配合着调。上限翻倍、桶数不变,每个桶的链表会变长,查表开销也会增加。桶数可以通过模块参数调整:
echo 131072 > /sys/module/nf_conntrack/parameters/hashsize调整桶数时内核会重建哈希表,可能产生短暂的 CPU 开销,最好避开流量最高的时段。部分较新的内核允许直接通过 sysctl 写 nf_conntrack_buckets,用之前先在测试机上确认你的内核是否支持。
观察窗口至少 15 分钟,最好覆盖一个流量高峰。盯住四项:drop 的增量、count 和新上限的距离、节点内存、业务错误率。如果内存明显上涨超出预期,或者错误率没有回落,就停止推广,这说明表满可能不是唯一的原因。
回滚就是把记录下来的原值写回去。这里有一个细节:把上限调低到小于当前 count,内核不会删除已有条目,但新连接会一直被丢弃,直到条目自然过期、count 降下来。所以回滚前先看一眼 count,确认它已经低于原值,否则回滚本身就会造成一次故障。
确认有效后再做持久化,否则重启就会恢复原值。普通 Linux 主机可以写入 /etc/sysctl.d/,桶数写入 /etc/modprobe.d/:
# /etc/sysctl.d/90-conntrack.conf
net.netfilter.nf_conntrack_max = 524288
# /etc/modprobe.d/nf_conntrack.conf
options nf_conntrack hashsize=131072sysctl 文件生效时,nf_conntrack 模块必须已经加载,否则这一项会报错或被跳过。改完以后重启一台节点,确认值确实保持。Kubernetes 节点要优先改 kube-proxy 的 conntrack.maxPerCore 和 min。kube-proxy 启动时会按自己的配置设置这些参数,只在节点上手工改,下次 kube-proxy 重启后可能被覆盖。不同版本对“已有值比计算值更大”的处理不完全一样,所以改完后要重启一次 kube-proxy,验证值是否保持。
几个常见误区
“直接调到几百万,一劳永逸。”上限对应的是真实内存,表越大,满表时占用越多,查表的开销也越大。更重要的是,它掩盖了连接异常增长的信号,下次表满时问题会更严重,也更难查。
“把 TIME_WAIT 超时改短到几秒。”缩短 nf_conntrack_tcp_timeout_time_wait 能更快释放条目,但这个超时本来是为了正确处理连接关闭后延迟到达的数据包。改得过短,在 NAT 场景下可能出现端口复用后的状态错乱。可以作为评估过的手段,但不要当成默认操作,改之前在测试环境用相同的流量模型验证。
“用 NOTRACK 让流量绕过 conntrack。”在 raw 表里对特定流量禁用连接追踪确实能减少条目,但依赖连接状态的规则(比如放行 ESTABLISHED、RELATED 的防火墙规则)和 NAT 都会对这部分流量失效。在 Kubernetes 节点上,这可能直接导致 Service 转发失败。不熟悉节点上全部规则时,不要这么做。
“重启节点就好了。”重启会清空表,但证据也一起没了,而且建连模式不变,表很快会再次填满。如果一定要用重启或摘流来止损,先把 conntrack -S、状态分布和聚合结果保存下来。
“容器里看到的就是全部。”从 Pod 里看到的 count 只是它所在网络命名空间的情况,上限一般在宿主机上设置。排查和调整都要回到节点上做。
把它放进日常监控
表满几乎都有一个水位持续上涨的过程,提前告警比事后扩容更省事。node_exporter 默认会暴露 node_nf_conntrack_entries 和 node_nf_conntrack_entries_limit,可以直接计算水位:
groups:
- name: conntrack
rules:
- alert: ConntrackTableUsageHigh
expr: node_nf_conntrack_entries / node_nf_conntrack_entries_limit > 0.75
for: 10m
labels:
severity: warning
annotations:
summary: "{{ $labels.instance }} conntrack 表使用率持续超过 75%"75% 只是一个起点,要根据节点的流量波动调整。较新版本的 node_exporter 还能采集 drop、early_drop 等统计,具体指标名以你所用版本的文档为准,有的话就加一条“drop 增量大于 0”的告警。
除了告警,还可以在服务上线评审里加一项:调用下游时是否开启连接复用,连接池上限是多少。这一项比任何内核参数都更能防止复发。
收藏清单:conntrack 表满处理
- 务必先在宿主机上确认 count、max 和 drop 增量,再下结论。只看 dmesg 不能判断当前是否仍在丢包。
- 千万别在不看 slabinfo 和空闲内存的情况下直接调大上限。条目占用的是内核内存,表满时可能挤压业务进程。
- 一次调整不超过翻倍,先在单台节点上验证。多台同时变更,出问题时无法区分原因,也来不及回退。
- 调大上限时同步评估哈希桶数量。只加上限、不加桶数,查表开销会上升。
- 回滚前务必确认 count 已低于原值。上限低于当前 count,新连接会被持续丢弃。
- 不要在 Kubernetes 节点上只手工改 sysctl。kube-proxy 重启后可能覆盖,导致问题在半夜复现。
- 禁止在不了解全部规则的情况下使用 NOTRACK。有状态规则和 Service NAT 会对这部分流量失效。
- 导出 conntrack 列表时务必加超时并写入文件,用完删除。大表导出有 CPU 开销,文件里有真实的地址信息。
- 不要用重启代替排查。证据会一起丢失,建连模式不变,表很快会再次填满。
- 务必找到建连最多的 src 和 dst。不改造这些连接,调大上限只是推迟下一次故障。
conntrack 表满看起来是内核参数问题,实际上大多是连接模型问题。先用 count、drop 和状态分布证明表满确实存在,再用“建连速率 × 存活时间”找到填满它的连接,扩容只用来争取修复的时间。
现在就可以做一件事:在你的 Prometheus 里跑一下 node_nf_conntrack_entries / node_nf_conntrack_entries_limit,把使用率超过 50% 的节点挑出来,提前看看它们的表里装的是什么。
如果这篇对你有帮助,欢迎关注、点赞、点个在看,也转发给经常处理节点网络问题的同事。
公众号:耕云躬行录
个人博客:躬行笔记