首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >nf_conntrack table full 排查:连接跟踪表的占用分析、扩容与 notrack 绕过

nf_conntrack table full 排查:连接跟踪表的占用分析、扩容与 notrack 绕过

原创
作者头像
一只特立独行的兔先生
发布于 2026-11-09 10:10:49
发布于 2026-11-09 10:10:49
20
举报

云服务器上的 Web 服务或网关,压力上来后出现这种现象:新连接偶发超时、curl 卡在握手,但已经建立的连接基本正常;应用日志里没有对应请求,安全组和端口监听也都没问题。这时去看内核日志,经常能看到 nf_conntrack: table full, dropping packet。本文按「确认 → 看清表里是什么 → 扩容 / 缩短超时 / 绕过跟踪 → 验证」的顺序整理一套排查做法,命令针对常见的 CentOS / Ubuntu / Debian 发行版内核,示例中的 IP、数值均为演示用。

1. 先确认是不是 conntrack 在丢包

conntrack(连接跟踪)是 netfilter 的一部分。只要内核加载了 nf_conntrack 模块,经过本机的每条连接(包括 NAT、有状态防火墙规则、Docker / kube-proxy 的转发规则带来的跟踪)都会在表里占一个条目。表满了,新连接的第一个包(比如 SYN)会被直接丢弃,已有条目对应的连接不受影响。这解释了「新连接超时、老连接正常」的典型特征。

先看内核日志:

代码语言:bash
复制
dmesg -T | grep -i conntrack | tail
# 或者
journalctl -k --since '1 hour ago' | grep 'nf_conntrack: table full'

看到 nf_conntrack: table full, dropping packet 基本可以定性。但要注意两点:

  • 这条日志是限速打印的,日志里只有几行不代表只丢了几个包,真实丢包量要看后文的计数器。
  • 日志里没有这条,也不能完全排除,日志可能已被轮转,或者 dmesg 环形缓冲被刷掉了。

容器服务 TKE 的节点检查也把这条日志作为一个独立的异常关键字(NFOverFlow,含义为 conntrack 表满),匹配的正是这行内核日志,见文末参考文档。

和 accept 队列溢出区分开

「新连接握手失败」还有一个常见原因是服务端 accept 队列溢出,现象很像,但处理方式完全不同。可以用计数器和抓包区分:

代码语言:bash
复制
# 全连接队列溢出相关计数,数值在持续增长说明是队列问题
nstat -az TcpExtListenOverflows TcpExtListenDrops

# 终端 1:看客户端 SYN 是否到达网卡
tcpdump -nn -i eth0 'tcp dst port 80 and tcp[tcpflags] == tcp-syn'
# 终端 2:看服务端有没有回 SYN+ACK
tcpdump -nn -i eth0 'tcp src port 80 and tcp[tcpflags] == (tcp-syn|tcp-ack)'

如果 SYN 能在终端 1 看到、终端 2 却没有对应的 SYN+ACK,而 ListenOverflows 没有增长,说明包在到达 socket 之前就被丢了,conntrack 满就是其中一个典型原因。eth0 请换成实际网卡名。

2. 量化:占用率和丢包计数

先看当前条目数和上限:

代码语言:bash
复制
cd /proc/sys/net/netfilter
echo count=$(cat nf_conntrack_count) max=$(cat nf_conntrack_max) buckets=$(cat nf_conntrack_buckets)

nf_conntrack_buckets 是哈希表桶数,较新的内核才有这个只读项;读不到不影响后面的步骤。nf_conntrack_max 的默认值由内核按内存大小推算,不同内核版本、不同内存规格的机器不一样,以你机器上读到的值为准,不要照搬别人的数字。

count 逼近 max 就是表接近满。但 count 只是一个瞬时值,判断有没有丢过包要看统计计数器,需要安装 conntrack-tools(包名通常就是 conntrack 或 conntrack-tools):

代码语言:bash
复制
conntrack -S

输出每个 CPU 一行,字段随 conntrack-tools 版本略有差异,大致形如(示例):

代码语言:plaintext
复制
cpu=0   found=0 invalid=12 insert=0 insert_failed=0 drop=37 early_drop=5 error=0 search_restart=1520
cpu=1   found=0 invalid=9  insert=0 insert_failed=0 drop=41 early_drop=3 error=0 search_restart=1413

几个字段的读法:

字段

含义

怎么用

drop

因为跟踪失败(包括表满)被丢弃的包数

间隔几秒执行两次,持续增长就是正在丢包

early_drop

表满时为腾位置而淘汰的条目数

内核只会淘汰尚未被确认(没有 ASSURED 标记)的条目;它增长说明已经顶到上限边缘

insert_failed

插入表失败

并发建立相同连接时的竞争也会增加它,单独看它不足以下结论

invalid

无法归类的包

和表满无直接关系,量很大时另查

这些计数器也能在 /proc/net/stat/nf_conntrack 里看到,那里是十六进制的原始值,每个 CPU 一行。排查时重点只看 drop 的增量,累计值没有意义,因为它从模块加载开始就一直累加。

3. 看清表里到底是什么

直接扩容之前先看表里装的是什么,因为不同成因的处理方向不同。

代码语言:bash
复制
# 按协议统计
conntrack -L 2>/dev/null | awk '{print $1}' | sort | uniq -c

# TCP 按状态统计(TCP 行的第 4 列是状态)
conntrack -L -p tcp 2>/dev/null | awk '{print $4}' | sort | uniq -c | sort -rn

# 按源地址统计 Top 10(取每条记录里第一个 src=,即原始方向的源地址)
conntrack -L 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i ~ /^src=/){print $i; break}}' | sort | uniq -c | sort -rn | head

一条典型记录长这样(示例):

代码语言:plaintext
复制
tcp      6 431987 ESTABLISHED src=10.0.0.12 dst=10.0.0.5 sport=51234 dport=80 src=10.0.0.5 dst=10.0.0.12 sport=80 dport=51234 [ASSURED] mark=0 use=1

第三列 431987 是该条目剩余的存活秒数。UDP 记录没有状态列,所以第三条命令里统计状态只针对 -p tcp。

按统计结果大致分四种情况:

看到的现象

常见成因

优先考虑的处理

大量 ESTABLISHED,源地址分布正常

真实并发连接数就超过了上限(长连接、网关、NAT 出口)

扩容;必要时缩短 established 超时

大量 TIME_WAIT

短连接并发高,条目在 conntrack 里还要多留一段时间

扩容;评估是否缩短 time_wait 超时;短连接改连接池

大量 SYN_RECV 或 SYN_SENT,源地址分散

半开连接堆积,可能是 SYN 洪泛或者上游异常

先判断是不是攻击,扩容只是缓冲,不是解决方案

大量 UDP 记录,目的端口 53

DNS 查询量大,每个请求一条短命条目

在容器场景里考虑 NodeLocal DNSCache、减少查询次数;见下文超时调整

如果某一个源地址占了大头,要先弄清楚它是谁:是你自己的上游代理、压测机,还是不明来源,再决定怎么处理,不要上来就改内核参数。

4. 处理一:扩容 conntrack_max

扩容前先估算内存。每个条目占用的内存可以从 slab 信息里读到(需要 root):

代码语言:bash
复制
grep nf_conntrack /proc/slabinfo | head -n 3

第 4 列 objsize 就是单条目大小,常见在 300 字节左右,具体以你的内核为准。用它乘以目标上限就是最坏情况下的内核内存占用。比如单条 320 字节、上限 1048576,约为 320 MiB(320 × 1048576 ≈ 335544320 字节),这是不可换出的内核内存,2 GiB 的小规格机器上这样设就不合适。哈希桶本身还要占用 buckets × 指针大小 的内存,通常相对条目本身可以忽略。

临时生效:

代码语言:bash
复制
sysctl -w net.netfilter.nf_conntrack_max=524288

如果还想同步调大哈希桶数(桶数太少会让链变长、查找变慢),较新内核上 nf_conntrack_buckets 是只读的,需要改模块参数:

代码语言:bash
复制
echo 131072 > /sys/module/nf_conntrack/parameters/hashsize

桶数与上限的比例按内核默认的比值(上限约为桶数的 4 到 8 倍,随版本而异)来取即可,不用刻意追求更小。

持久化时的一个坑

把参数写进 /etc/sysctl.d/ 之后,重启却发现没生效,最常见的原因是开机时 sysctl 执行得比 nf_conntrack 模块加载早,此时 net.netfilter.nf_conntrack_max 这个键还不存在,设置失败。让模块在开机早期加载即可:

代码语言:bash
复制
echo nf_conntrack > /etc/modules-load.d/nf_conntrack.conf

cat > /etc/sysctl.d/90-conntrack.conf <<'EOF'
net.netfilter.nf_conntrack_max = 524288
EOF

sysctl --system
sysctl net.netfilter.nf_conntrack_max

如果要持久化哈希桶数,用 modprobe 选项:在 /etc/modprobe.d/nf_conntrack.conf 里写 options nf_conntrack hashsize=131072。

在 Kubernetes 节点上还要注意,kube-proxy 启动时会根据自己的参数(--conntrack-max-per-core、--conntrack-min)去设置 nf_conntrack_max,你手工改的值可能在 kube-proxy 重启后被覆盖。TKE 集群里应以集群和组件的配置方式为准,不要只在节点上手工改完就算结束。

5. 处理二:缩短不合理的超时

扩容只是把天花板抬高,如果条目是因为存活太久堆积起来的,应该回头看超时。先把当前值读出来:

代码语言:bash
复制
sysctl net.netfilter.nf_conntrack_tcp_timeout_established \
       net.netfilter.nf_conntrack_tcp_timeout_time_wait \
       net.netfilter.nf_conntrack_udp_timeout \
       net.netfilter.nf_conntrack_udp_timeout_stream \
       net.ipv4.tcp_keepalive_time

其中最值得关注的是 nf_conntrack_tcp_timeout_established,内核默认是 432000 秒,也就是 5 天。含义是:一条已建立连接如果 5 天内没有任何数据包,条目才会被清理。如果你的服务有大量客户端异常断开(断电、网络闪断、NAT 超时)后没有发 FIN / RST 的情况,这些「僵尸条目」会在表里占 5 天。

把它调小有一个硬约束:新值必须大于应用或系统层的保活(keepalive)间隔。Linux 的 net.ipv4.tcp_keepalive_time 默认是 7200 秒,如果你把 established 超时设成 3600 秒,而连接空闲 3600 秒后条目已被清理,随后到来的保活探测包或数据包就找不到条目,在带 NAT 或有状态规则的机器上会被当成无效包丢掉,表现为长连接被莫名其妙断开。

代码语言:bash
复制
# 示例:保活间隔为默认 7200 秒时,可调到 1 天
sysctl -w net.netfilter.nf_conntrack_tcp_timeout_established=86400

具体取值请结合自己的业务空闲时长、保活配置来定,同样要写进 /etc/sysctl.d/ 才能持久化。

nf_conntrack_tcp_timeout_time_wait(默认 120 秒)等项也能调,但收益有限:TIME_WAIT 条目本来就是短命的,把它调得过小,会让迟到的重复报文被当成新连接的包处理。如果表里大头是 TIME_WAIT,更根本的做法是让客户端和服务端之间走连接池、复用长连接,而不是继续压缩超时。

6. 处理三:对不需要跟踪的流量使用 notrack

如果这台机器是单纯的 Web / API 服务器,不做 NAT,也没有依赖连接状态的防火墙规则,那么 80 / 443 的流量根本不需要进入 conntrack。可以在 raw 表里把它们标记为不跟踪,从源头减少条目:

代码语言:bash
复制
# 入方向:客户端访问本机 80/443
iptables -t raw -A PREROUTING -p tcp -m multiport --dports 80,443 -j CT --notrack
# 出方向:本机回给客户端的包
iptables -t raw -A OUTPUT -p tcp -m multiport --sports 80,443 -j CT --notrack

# 确认规则命中(观察 pkts 计数)
iptables -t raw -L -n -v

用之前一定要核对下面几点,否则会把线上搞坏:

  • NAT 失效:被标记为 notrack 的连接不能做 NAT。Docker 端口映射、kube-proxy 的转发、MASQUERADE 都依赖 conntrack,这类机器不能这么做,至少不能对相关端口这么做。
  • 有状态规则不再匹配:这些包的连接状态是 UNTRACKED,原来写的 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT 对它们不起作用。如果 INPUT 链默认策略是 DROP,需要为 80 / 443 单独加显式放行规则。
  • 规则持久化:直接执行的 iptables 命令重启后会丢,需要按所用发行版的方式(iptables-save、firewalld、ufw 等)持久化。
  • 云上的安全组是在虚拟网络层生效的,不依赖也不会改变你主机内的 conntrack 规则,这里改的只是主机内核的行为。

如果机器用的是 nftables,思路一样(在优先级早于 conntrack 的 raw 类 hook 里使用 notrack 语句),具体语法以所用版本的手册为准。

7. 验证与长期监控

改完之后,要验证两件事:不再丢包,且条目数低于上限。

代码语言:bash
复制
# 间隔几秒执行两次,drop 不再增长即为通过
conntrack -S | grep -o 'drop=[0-9]*'
sleep 10
conntrack -S | grep -o 'drop=[0-9]*'

# 观察占用
cat /proc/sys/net/netfilter/nf_conntrack_count
cat /proc/sys/net/netfilter/nf_conntrack_max

更稳妥的做法是在业务高峰期再看一遍,因为表满往往只在峰值出现。

长期来看要把「占用率」加进监控,而不是等到丢包才发现:

  • 用 nf_conntrack_count / nf_conntrack_max 做告警,比如超过 80% 触发。使用 node_exporter 的话,对应指标是 node_nf_conntrack_entries 和 node_nf_conntrack_entries_limit。
  • 在 CVM 上也可以用自定义指标或者脚本把这个比值上报到云监控,告警阈值和通知方式以控制台的配置为准。
  • 容器服务的节点检查会把 conntrack 表满当成节点异常关键字提示出来,可以作为兜底。

8. 常见误区

  • 只扩容,不看表里是什么:如果是 SYN 洪泛或者上游异常重试导致的暴涨,扩容只是延后爆发时间,还多占了内核内存。
  • 照搬网上的巨大数值:一些文章直接把上限设成上千万,不看自己机器的内存,小规格机器上反而把内存吃紧。
  • 把 established 超时调得太小:低于保活间隔会造成长连接莫名断开,且这类问题症状不明显,很难回溯到这个参数。
  • 用 iptables -F 想清表:清规则不会清掉已有条目,要清表需用 conntrack -F,而它会让所有现存连接失去状态,在生产机上要慎用,确需操作应在业务低峰执行。
  • 只看日志:内核日志是限速的,判断丢包要看 conntrack -S 里 drop 的增量。

总结:排查清单

  1. dmesg -T | grep -i conntrack 确认日志,nstat 和 tcpdump 排除 accept 队列问题。
  2. 读 nf_conntrack_count 与 nf_conntrack_max,用 conntrack -S 观察 drop 的增量。
  3. 用 conntrack -L 按协议、状态、源地址统计,判断是并发真实增长、TIME_WAIT 堆积、半开连接还是 DNS UDP。
  4. 并发真实增长:按 slab objsize 估算内存后扩容 nf_conntrack_max,并保证 nf_conntrack 模块早于 sysctl 加载,Kubernetes 节点还要留意 kube-proxy 覆盖。
  5. 僵尸条目多:缩短 nf_conntrack_tcp_timeout_established,但必须大于保活间隔。
  6. 纯入站服务、无 NAT 无状态规则:对相关端口使用 -j CT --notrack,并补齐显式放行规则。
  7. 再次查看 drop 增量与占用率,并把占用率加进监控告警。

参考文档

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 1. 先确认是不是 conntrack 在丢包
    • 和 accept 队列溢出区分开
  • 2. 量化:占用率和丢包计数
  • 3. 看清表里到底是什么
  • 4. 处理一:扩容 conntrack_max
    • 持久化时的一个坑
  • 5. 处理二:缩短不合理的超时
  • 6. 处理三:对不需要跟踪的流量使用 notrack
  • 7. 验证与长期监控
  • 8. 常见误区
  • 总结:排查清单
  • 参考文档
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档