nf_conntrack 连接跟踪表耗尽排查实战:table full 丢包、表构成聚合与超时调优全流程

为什么 Nginx 会突然拒绝所有新连接:从一个 nf_conntrack 报错说起

很多站长都遇到过这种诡异场景:服务器 CPU 不高、内存还剩一大半、Nginx 进程也活得好好的,但网站突然全部超时。SSH 能连进去(说明网络没断),可任何新的 HTTP 请求都进不来。翻看 dmesg 就会发现一条很有辨识度的日志:nf_conntrack: table full, dropping packet。这不是 CPU 打满,也不是带宽跑满,而是 Linux 内核的连接跟踪表(connection tracking table)被填满了,内核开始静默丢包。

这篇文章基于一次真实的排查复盘,把 nf_conntrack 是什么、为什么会满、怎么定量排查、怎么调参、以及为什么"无脑调大"往往是错的,一次讲清楚。

conntrack 到底是什么:内核的"连接记忆"

Linux 内核里的 netfilter 框架为了实现有状态防火墙(stateful firewall)、NAT(网络地址转换)和连接跟踪,维护了一张表来记录每一条被它"看到"的网络连接。这张表就是 conntrack table,每条记录(entry)代表一条连接,包含源 IP、目的 IP、源端口、目的端口、协议、连接状态(NEW / ESTABLISHED / RELATED / TIME_WAIT 等)以及超时计时器。

关键在于:conntrack 表是有限的。它有最大容量(默认可查、可调),并且每条记录都有存活时间。当表满且新连接又进来时,内核在无法为其创建记录的情况下,直接丢弃该数据包——这就是"silently dropping"的可怕之处:没有 TCP RST,没有 ICMP 错误,客户端只是傻等直到超时。

对于个人站长来说,conntrack 尤其重要,因为几乎所有的实际部署都同时启用了 NAT 和有状态规则:

  • 用了 iptables / nftables 的 NAT 转发(比如内网穿透、虚拟机上网、Docker 的默认网络)
  • 用了 iptables -m state 或 -m conntrack 做有状态过滤
  • 跑着 Docker——Docker 默认会插入大量 NAT 规则,容器通信全部走 conntrack
  • 服务端高并发短连接,比如大量 API 调用、爬虫请求

哪怕你没写过一条 iptables 规则,只要装了 Docker,conntrack 表就已经在默默工作了。

定量排查:先确认是不是它满了

遇到"网站全超时但机器没死"这类症状,第一步永远是先确认 conntrack 是否真的满了,而不是急着调 Nginx 参数。以下是标准排查命令,全部在服务器上原样执行即可。

1. 看当前记录数和上限

cat /proc/sys/net/netfilter/nf_conntrack_count
cat /proc/sys/net/netfilter/nf_conntrack_max

如果 count 已经贴着 max,比如 65534 / 65536,那基本可以确诊。注意:这个计数是实时的,只有出问题的那一刻看才有意义。很多站长事后去查发现数字很低,就排除了这个原因,其实是误判——必须结合监控或历史日志看峰值。

2. 看内核有没有报"表满"

dmesg -T | grep -i conntrack
journalctl -k | grep -i conntrack

只要出现过 table full, dropping packet,就说明确实溢出过。-T 会把内核时间戳转成可读时间,方便和故障时间点对齐。

3. 看表的构成:到底是谁占满了

conntrack -L | head
conntrack -C
conntrack -S

conntrack -C 输出当前总数,conntrack -S 输出每 CPU 的统计(包括 insert_failed、drop、early_drop)。如果 insert_failed 在涨,说明有连接因为表满被拒。conntrack -L 能列出具体条目,用它来判断是不是某个 IP 在刷(比如同一个小网段几万条 TIME_WAIT)。如果 conntrack 命令不存在,装一下:Debian/Ubuntu 下是 conntrack 包,CentOS 下是 conntrack-tools。

4. 按状态和来源聚合

conntrack -L 2>/dev/null | awk '{print $4}' | sort | uniq -c | sort -rn | head
conntrack -L 2>/dev/null | awk '{print $4}' | cut -d= -f2 | sort | uniq -c | sort -rn | head

第一个命令按连接状态聚合,常见的 TIME_WAIT、ESTABLISHED 分布能直接告诉你是不是短连接过多;第二个命令按源 IP 聚合,一眼看出是不是某个 IP 在刷连接。这一步是排查的分水岭——决定你是要"扩容表"还是"限流源"。

调参:nf_conntrack_max 到底该设多大

确认表满后,很多人第一反应是把它调大。方向没错,但有几个坑必须知道。

先看内存消耗:每条 conntrack 记录在内核里大约占 300 字节左右(不同内核版本有差异)。65536 条大约占 20MB,262144 条约 80MB,再加哈希表本身的额外开销。对个人 VPS 来说,把 max 设到 131072 或 262144 通常是安全的,但不建议无脑设成百万级——那会挤占内核内存,反而引发别的 OOM。

调整方式(临时生效):

sysctl -w net.netfilter.nf_conntrack_max=131072

永久生效写入 /etc/sysctl.d/99-conntrack.conf:

net.netfilter.nf_conntrack_max = 131072
net.netfilter.nf_conntrack_tcp_timeout_established = 3600
net.netfilter.nf_conntrack_tcp_timeout_time_wait = 30
net.netfilter.nf_conntrack_tcp_timeout_close_wait = 30
net.netfilter.nf_conntrack_generic_timeout = 120

然后 sysctl --system 加载。注意 nf_conntrack_max 不能超过 nf_conntrack_buckets 的 4 倍(buckets 是内部哈希桶数),如果 max 设得很大但 buckets 太小,哈希链会变长,查找变慢。buckets 在多数内核里可由内核参数或 modprobe 参数 nf_conntrack hashsize= 指定。

这里有个大坑:只调 max 不调超时,治标不治本

如果表是被大量 TIME_WAIT 或长时间 ESTABLISHED 撑满的,单纯调大 max 只是把崩溃点往后推。更有效的做法是缩短超时。默认 tcp_timeout_established 是 432000 秒(5 天),对于有 NAT 的场景这个值大得离谱,很容易堆积僵尸条目。把它改成 3600(1 小时)通常没有副作用,因为真正活跃的连接会持续刷新计时器,只有空闲连接才会被回收。同理 tcp_timeout_time_wait 从默认 120 秒降到 30 秒,能显著降低短连接场景下的表占用。

根因治理:调参之外该做的事

把表调大只是止血。真正的治本要从连接源头下手:

  • 上代理缓存,减少回源。用 Nginx 的 proxy_cache 或 FastCGI 缓存拦住重复请求,从源头上减少被内核跟踪的连接数量。
  • 消灭短连接泄漏。如果某个程序疯狂开短连接且不复用,conntrack 会迅速堆满 TIME_WAIT。开启 HTTP keepalive、用连接池(如 PgBouncer 管数据库连接)能治本。
  • 限流恶意来源。如果是单个 IP 或小网段在刷,用 ipset + iptables 或 Nginx 的 limit_req/limit_conn 直接在 conntrack 之前把恶意流量挡掉。用 ipset 做十万级 IP 封禁几乎是零成本。
  • 给 Docker 场景单独收口。Docker 会插入大量规则,容器数量一多 conntrack 压力陡增,可以给 Docker 配 --userland-proxy=false 并用 iptables-save 梳理冗余规则。

真实复盘:一次"网站全挂"的完整处置过程

背景:一台 2 核 4G 的 VPS,上面跑着 Nginx + PHP-FPM + Docker 化的一个统计服务。某天下午,监控告警"网站 5xx 比例 100%"。SSH 上去发现机器负载只有 0.4,内存还剩 2G,Nginx 进程正常。curl localhost 也超时。

排查动作按顺序如下:

  1. ss -s 看到 timewait 数量爆表,达到六万多;
  2. cat /proc/sys/net/netfilter/nf_conntrack_count 返回 65535,nf_conntrack_max 是 65536——确认表满;
  3. dmesg -T | grep conntrack 刷屏的 table full, dropping packet;
  4. conntrack -L | awk '{print $4}' | sort | uniq -c | sort -rn 看到 TIME_WAIT 占了 90% 以上,源 IP 高度集中在某个网段;
  5. 进一步查 Nginx 日志,发现那个网段在对一个接口做高频轮询,每次都是新连接、不 keepalive。

处置分三步:先止血——临时 sysctl -w net.netfilter.nf_conntrack_max=262144,网站立刻恢复;再降表压——把 tcp_timeout_time_wait 降到 30 秒、tcp_timeout_established 降到 3600 秒,观察半小时,表占用稳定在 3 万以内;最后治根——给那个接口加了 Nginx 层的 limit_req 限流,并把异常网段通过 ipset 拉黑,同时推动该接口的调用方改成连接复用。

前后数据对比很说明问题:处置前 count 峰值 65535 且常年满格,处置后峰值不到 3 万,insert_failed 归零,网站再没出现过全站超时。这次事故最大的教训是:conntrack 表的容量是一个容易被忽视的系统级资源,它和文件描述符、内存一样,需要纳入日常监控。把 nf_conntrack_count / nf_conntrack_max 的比值做一条监控曲线,超过 80% 就告警,能提前发现这个问题。

把 conntrack 纳入日常监控

最省事的做法是用一条 cron 脚本每分钟采集一次并写入日志,或者接入任何一套监控系统。核心指标就三个:nf_conntrack_count(当前值)、nf_conntrack_max(上限),以及 conntrack -S 里的 insert_failed。用下面的脚本可以快速得到一个快照:

#!/bin/bash
cnt=$(cat /proc/sys/net/netfilter/nf_conntrack_count)
max=$(cat /proc/sys/net/netfilter/nf_conntrack_max)
pct=$(awk "BEGIN{printf \"%.1f\", $cnt*100/$max}")
echo "conntrack: $cnt/$max (${pct}%)"
if [ "${pct%.*}" -ge 80 ]; then
  echo "WARN: conntrack usage above 80%" >&2
fi

把它挂到 cron 或 systemd timer 上,配合告警(比如写进 syslog 后用 logwatch/任何日志告警),就能在表满之前得到预警。

常见问题

Q:为什么调大 max 之后还是会满?因为表满只是结果,源头在连接数。调大上限只是把崩溃点推后,如果短连接泄漏或攻击流量持续存在,早晚还会满。要配合超时调优和源限流。

Q:关掉 conntrack 行不行?可以,但要看你是否用到了 NAT 或有状态规则。如果纯做无状态转发(只用 DNAT/SNAT 之外的 stateless 规则,或者用 nftables 的 notrack),可以绕过 conntrack,性能更好。但绝大多数个人站部署都依赖它,不建议贸然关闭。

Q:buckets 调多大合适?经验法则是 buckets 约等于 max 的 1/4。多数发行版内核会根据内存自动设置一个合理值,除非你手动把 max 调得非常大,一般不用动。

总结

conntrack 表满导致网站全挂,是 Linux 服务器运维里非常典型、也非常隐蔽的故障。它隐蔽在于症状(全站超时)和根因(内核连接表满)之间隔着好几层抽象,很多人第一反应是去查 Nginx 进程、看 CPU、看内存,全都正常就懵了。掌握这套排查链路——nf_conntrack_count 对比 max、dmesg 找 dropped、conntrack -L 聚合定位源头——能把定位时间从几小时压缩到几分钟。调参时记住两点:调大 max 是止血,缩短超时和源限流才是治本;把 conntrack 使用率做成监控曲线,才能提前预警。这三条经验,比记住任何一个具体的 sysctl 数值都重要。

Last modification:October 13th, 2026 at 12:24 pm

Leave a Comment