云服务器上的 Web 服务或网关,压力上来后出现这种现象:新连接偶发超时、curl 卡在握手,但已经建立的连接基本正常;应用日志里没有对应请求,安全组和端口监听也都没问题。这时去看内核日志,经常能看到 nf_conntrack: table full, dropping packet。本文按「确认 → 看清表里是什么 → 扩容 / 缩短超时 / 绕过跟踪 → 验证」的顺序整理一套排查做法,命令针对常见的 CentOS / Ubuntu / Debian 发行版内核,示例中的 IP、数值均为演示用。
conntrack(连接跟踪)是 netfilter 的一部分。只要内核加载了 nf_conntrack 模块,经过本机的每条连接(包括 NAT、有状态防火墙规则、Docker / kube-proxy 的转发规则带来的跟踪)都会在表里占一个条目。表满了,新连接的第一个包(比如 SYN)会被直接丢弃,已有条目对应的连接不受影响。这解释了「新连接超时、老连接正常」的典型特征。
先看内核日志:
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 队列溢出,现象很像,但处理方式完全不同。可以用计数器和抓包区分:
# 全连接队列溢出相关计数,数值在持续增长说明是队列问题
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 请换成实际网卡名。
先看当前条目数和上限:
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):
conntrack -S输出每个 CPU 一行,字段随 conntrack-tools 版本略有差异,大致形如(示例):
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几个字段的读法:
字段 | 含义 | 怎么用 |
|---|---|---|
| 因为跟踪失败(包括表满)被丢弃的包数 | 间隔几秒执行两次,持续增长就是正在丢包 |
| 表满时为腾位置而淘汰的条目数 | 内核只会淘汰尚未被确认(没有 ASSURED 标记)的条目;它增长说明已经顶到上限边缘 |
| 插入表失败 | 并发建立相同连接时的竞争也会增加它,单独看它不足以下结论 |
| 无法归类的包 | 和表满无直接关系,量很大时另查 |
这些计数器也能在 /proc/net/stat/nf_conntrack 里看到,那里是十六进制的原始值,每个 CPU 一行。排查时重点只看 drop 的增量,累计值没有意义,因为它从模块加载开始就一直累加。
直接扩容之前先看表里装的是什么,因为不同成因的处理方向不同。
# 按协议统计
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一条典型记录长这样(示例):
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。
按统计结果大致分四种情况:
看到的现象 | 常见成因 | 优先考虑的处理 |
|---|---|---|
大量 | 真实并发连接数就超过了上限(长连接、网关、NAT 出口) | 扩容;必要时缩短 established 超时 |
大量 | 短连接并发高,条目在 conntrack 里还要多留一段时间 | 扩容;评估是否缩短 time_wait 超时;短连接改连接池 |
大量 | 半开连接堆积,可能是 SYN 洪泛或者上游异常 | 先判断是不是攻击,扩容只是缓冲,不是解决方案 |
大量 UDP 记录,目的端口 53 | DNS 查询量大,每个请求一条短命条目 | 在容器场景里考虑 NodeLocal DNSCache、减少查询次数;见下文超时调整 |
如果某一个源地址占了大头,要先弄清楚它是谁:是你自己的上游代理、压测机,还是不明来源,再决定怎么处理,不要上来就改内核参数。
扩容前先估算内存。每个条目占用的内存可以从 slab 信息里读到(需要 root):
grep nf_conntrack /proc/slabinfo | head -n 3第 4 列 objsize 就是单条目大小,常见在 300 字节左右,具体以你的内核为准。用它乘以目标上限就是最坏情况下的内核内存占用。比如单条 320 字节、上限 1048576,约为 320 MiB(320 × 1048576 ≈ 335544320 字节),这是不可换出的内核内存,2 GiB 的小规格机器上这样设就不合适。哈希桶本身还要占用 buckets × 指针大小 的内存,通常相对条目本身可以忽略。
临时生效:
sysctl -w net.netfilter.nf_conntrack_max=524288如果还想同步调大哈希桶数(桶数太少会让链变长、查找变慢),较新内核上 nf_conntrack_buckets 是只读的,需要改模块参数:
echo 131072 > /sys/module/nf_conntrack/parameters/hashsize桶数与上限的比例按内核默认的比值(上限约为桶数的 4 到 8 倍,随版本而异)来取即可,不用刻意追求更小。
把参数写进 /etc/sysctl.d/ 之后,重启却发现没生效,最常见的原因是开机时 sysctl 执行得比 nf_conntrack 模块加载早,此时 net.netfilter.nf_conntrack_max 这个键还不存在,设置失败。让模块在开机早期加载即可:
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 集群里应以集群和组件的配置方式为准,不要只在节点上手工改完就算结束。
扩容只是把天花板抬高,如果条目是因为存活太久堆积起来的,应该回头看超时。先把当前值读出来:
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 或有状态规则的机器上会被当成无效包丢掉,表现为长连接被莫名其妙断开。
# 示例:保活间隔为默认 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,更根本的做法是让客户端和服务端之间走连接池、复用长连接,而不是继续压缩超时。
如果这台机器是单纯的 Web / API 服务器,不做 NAT,也没有依赖连接状态的防火墙规则,那么 80 / 443 的流量根本不需要进入 conntrack。可以在 raw 表里把它们标记为不跟踪,从源头减少条目:
# 入方向:客户端访问本机 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用之前一定要核对下面几点,否则会把线上搞坏:
MASQUERADE 都依赖 conntrack,这类机器不能这么做,至少不能对相关端口这么做。UNTRACKED,原来写的 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT 对它们不起作用。如果 INPUT 链默认策略是 DROP,需要为 80 / 443 单独加显式放行规则。iptables-save、firewalld、ufw 等)持久化。如果机器用的是 nftables,思路一样(在优先级早于 conntrack 的 raw 类 hook 里使用 notrack 语句),具体语法以所用版本的手册为准。
改完之后,要验证两件事:不再丢包,且条目数低于上限。
# 间隔几秒执行两次,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。iptables -F 想清表:清规则不会清掉已有条目,要清表需用 conntrack -F,而它会让所有现存连接失去状态,在生产机上要慎用,确需操作应在业务低峰执行。conntrack -S 里 drop 的增量。dmesg -T | grep -i conntrack 确认日志,nstat 和 tcpdump 排除 accept 队列问题。nf_conntrack_count 与 nf_conntrack_max,用 conntrack -S 观察 drop 的增量。conntrack -L 按协议、状态、源地址统计,判断是并发真实增长、TIME_WAIT 堆积、半开连接还是 DNS UDP。objsize 估算内存后扩容 nf_conntrack_max,并保证 nf_conntrack 模块早于 sysctl 加载,Kubernetes 节点还要留意 kube-proxy 覆盖。nf_conntrack_tcp_timeout_established,但必须大于保活间隔。-j CT --notrack,并补齐显式放行规则。drop 增量与占用率,并把占用率加进监控告警。原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。