侧边栏壁纸
  • 累计撰写 149 篇文章
  • 累计收到 2 条评论

服务器突然大面积连接超时但 ping 正常?排查 Linux nf_conntrack 满载丢包与内核调优我踩过的几个坑

2026-10-2 / 0 评论 / 11 阅读
AI 摘要由 AI 生成

文章讲述了作者在排查服务器大面积连接超时问题时,如何发现并解决Linux系统nf_conntrack满载丢包的问题。作者发现尽管ping正常,但HTTP/HTTPS请求却频繁超时,通过深入分析,发现是内核层的nf_conntrack表满导致丢包。文章还探讨了服务器开启nf_conntrack的原因,并指出直接调大max值而忘记调整hashsize导致CPU负载过高的教训。

上周做业务压测,外部拨测监控突然开始狂发告警。

大量 API 接口卡顿 10 秒后返回 504 或者直接报连接超时。我当时的第一反应是先上堡垒机 ping 目标机器,结果 ping 延迟只有 0.8 毫秒,连续发了上百个 ICMP 包,丢包率是干净利落的 0%。

这种现象非常具有迷惑性。网络链路看着通畅无比,SSH 也能秒连,但只要一跑 HTTP 或者 HTTPS 压测,客户端的 TCP 握手就会大面积超时。

以为是 Nginx 满连接,结果日志里根本没记录

最开始我以为是反向代理的问题。

当时怀疑 Nginx 的 worker_connections 耗尽了,或者后端 upstream 的长连接池被击穿。我连上机器抓 nginx/error.log,发现根本没有 7b worker_connections are not enough 之类的报错。更诡异的是,access 日志里甚至连客户端的请求记录都没有。

也就是说,客户端发出的 TCP SYN 包,根本就没走到用户态的 Nginx 监听套接字。

紧接着查系统负载,CPU 占用不到 20%,内存还剩十几个 G,网络带宽也只跑了不到三分之一。

再用 ss -s 查看套接字汇总,TIME_WAIT 状态虽然有几千个,但远没达到本地端口耗尽的程度。

顺着 dmesg 抓出真凶:丢包在内核层就已经发生

既然应用层拿不到数据,说明包在操作系统内核网络栈被掐断了。

我敲了下面这条命令翻看内核环形缓冲区:

dmesg -T | grep -i conntrack

终端屏幕瞬间刷出一整屏红字警告:

[Fri Oct  2 14:22:18 2026] nf_conntrack: table full, dropping packet
[Fri Oct  2 14:22:19 2026] nf_conntrack: table full, dropping packet
[Fri Oct  2 14:22:19 2026] nf_conntrack: table full, dropping packet

看到这几行的时候,原因就很明朗了。

Netfilter 的连接跟踪表满了。Linux 内核在处理网络包时,直接在 Netfilter 钩子层把进来的包给丢掉了。既然包在内核层就被丢了,Nginx 当然收不到三次握手的 SYN,应用层日志自然一片空白;而 ping 命令虽然也受连接跟踪影响,但 ICMP 连接超时的判定和处理时机与频繁建立的 TCP 短连接完全不同,因此在表满丢包的间隙依然能偶尔测出正常的延迟。

为什么服务器会莫名其妙开启 nf_conntrack

很多朋友以为只有配了复杂的 iptables 防火墙或者做 NAT 路由器时,系统才会用到 nf_conntrack。

我这台机器其实只跑了几个容器。但 Docker 启动的时候为了处理容器间的网络通信与端口映射,会在 iptables 里配置 MASQUERADE 规则。

只要内核加载了 iptables 的 NAT 扩展,nf_conntrack 模块就会被自动依赖加载。

只要这个模块在运行,经过这台机器网卡的每一个数据流,都会在内核的一张哈希表里占一个条目。

查看当前机器正在跟踪的连接数和系统上限:

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

当时查出来的结果:count 已经飙到了 65536,而 max 也是 65536。池子满了,新进来的包就只能排队被扔进垃圾桶。

坑一:直接调大 max 却忘了改 hashsize,软中断把 CPU 跑满

找到了原因,最容易想到的办法就是直接扩容。

我当时直接用 sysctl 把连接数上限提到了 100 万:

sysctl -w net.netfilter.nf_conntrack_max=1048576

改完之后,dmesg 里的 table full 确实立刻消失了,接口请求也恢复了响应。

但高兴了没到十分钟,系统的 CPU 使用率突然从 20% 冲到了 80%,其中 si(软中断)占用奇高。

原因在于 nf_conntrack 的底层数据结构是哈希表配合链表存储的。

哈希表的桶数量叫 hashsize,而 nf_conntrack_max 是表中允许存入的最大条目数。

系统默认的 hashsize 往往只有 16384 或者 65536。如果只把 nf_conntrack_max 扩大到 100 万,但桶数量不变,每个桶后面的单向链表平均长度就会拉长到数十甚至上百个节点。

每次有网络包经过,内核在遍历哈希桶寻找连接记录时,就得挨个比对链表节点。高并发网络包一进来,CPU 软中断直接被这种线性查找给耗尽了。

通常合理的比例是:nf_conntrack_max = hashsize * 4 或者是 hashsize * 8。

另一个坑是:hashsize 根本不是通过 sysctl 修改的。

它是 nf_conntrack 模块的底层参数,得通过 sysfs 修改:

echo 262144 > /sys/module/nf_conntrack/parameters/hashsize

改完之后再查看,每个桶只挂 4 个左右的节点,软中断占用马上就降回了正常水平。

坑二:没算过内存开销就乱设参数

扩大连接跟踪表不是没有代价的。

每个 conntrack 条目在内核内存中大概占用 320 字节(具体视内核版本而定,通常在 288 到 352 字节之间)。

你可以通过 slabinfo 查看具体的内核对象大小:

cat /proc/slabinfo | grep nf_conntrack

如果随手设一个特别夸张的数字,比如 400 万条:

4,000,000 * 320 字节 ≈ 1.28 GB

这 1.28 GB 的物理内存会直接被内核 slab 分配器锁定,既不会被 swap 到磁盘,也不会参与系统的页面缓存释放。如果是在 4G 或者 8G 内存的小规格云服务器上盲目调大,很容易挤压业务容器的可用内存,甚至诱发系统的 OOM 机制杀进程。

在调高上限前,最好拿服务器的物理内存总量算一下,预留给内核网络的开销不要超过总内存的 10%。

坑三:TCP 默认连接超时居然长达 5 天

光扩容只能治标。为什么会有那么多连接堆在表里不释放?

用下面的命令看了一下当前连接状态的统计:

cat /proc/net/nf_conntrack | awk '{print $3}' | sort | uniq -c | sort -nr

结果发现里面积压了大量的 ESTABLISHED 状态条目。

去翻内核参数,Linux 默认的 net.netfilter.nf_conntrack_tcp_timeout_established 居然是 432000 秒。

也就是整整 5 天。

很多客户端早就不通信了,或者由于网络抖动悄无声息地断开了连接,只要双方没有正常发送 FIN 或 RST 包,这个连接条目就会在内核的连接跟踪表里傻傻占着坑位停留整整 5 天。

对于对外提供 Web 服务的机器,很多请求都是短时间内完成的,维持 5 天的超时完全没有必要。

我把这个超时时间压缩到了 1200 秒(20 分钟),顺便把 TIME_WAIT 和 CLOSE_WAIT 的连接跟踪超时也做了适当收紧:

# /etc/sysctl.d/99-conntrack.conf
net.netfilter.nf_conntrack_max = 262144
net.netfilter.nf_conntrack_tcp_timeout_established = 1200
net.netfilter.nf_conntrack_tcp_timeout_time_wait = 30
net.netfilter.nf_conntrack_tcp_timeout_close_wait = 30
net.netfilter.nf_conntrack_tcp_timeout_fin_wait = 30

配置生效之后:

sysctl -p /etc/sysctl.d/99-conntrack.conf

系统里的空闲僵尸连接能够快速被淘汰回收,表里的活跃记录数直接从几万个滑落到几千个。

坑四:纯内网高频服务其实可以绕过连接跟踪

如果这台机器承载的是纯内网反向代理,或者本地端口间的高并发转发,根本不需要 NAT,那这些流量完全可以不进 conntrack 表。

iptables 提供了一个 raw 表,在整个 Netfilter 流程的最前端(PREROUTING 和 OUTPUT 链)执行。在 raw 表里对特定端口打上 NOTRACK 标记,内核就会直接放行,不给它分配任何跟踪条目:

# 本地 80 和 443 端口不走连接跟踪
iptables -t raw -A PREROUTING -p tcp -m multiport --dports 80,443 -j NOTRACK
iptables -t raw -A OUTPUT -p tcp -m multiport --sports 80,443 -j NOTRACK

打上标记后,高频进出的 Web 流量就不会在连接跟踪表里挤占资源了。

需要说明的是,如果你的容器网络重度依赖 iptables 的 SNAT/DNAT 或者有状态防火墙,打 NOTRACK 会让涉及 NAT 的端口映射失效,所以在生产环境配置前一定要核对清楚流量路径。

坑五:机器重启后 sysctl 配置丢失

配好了 sysctl 和 hashsize 之后,我测试重启服务器验证持久化,结果机器起来后一查:

nf_conntrack_max 又恢复成了默认值。

排查之后发现,系统开机运行 systemd-sysctl.service 去加载 /etc/sysctl.conf 的时候,nf_conntrack 内核模块其实还没有被加载进内存。

等到系统启动完、Docker 服务启动时,Docker 才拉起 nf_conntrack 模块。模块一初始化,又把自己的默认参数给载入了,覆盖了开机时的配置。

要彻底解决开机持久化的问题,需要做两件事:

第一,强制系统在开机阶段就预加载模块:

echo "nf_conntrack" > /etc/modules-load.d/nf_conntrack.conf

第二,把 hashsize 的持久化配置写到 modprobe 规则里:

echo "options nf_conntrack hashsize=65536" > /etc/modprobe.d/nf_conntrack.conf

配置好这两处,再配合 /etc/sysctl.d/ 下的参数,服务器无论怎么重启,连接表大小和超时设置都能稳妥生效。

日常排错时,如果系统出现接口超时而硬件资源明明很空闲的状况,第一步先敲 dmesg -T 看有没有这行丢包记录,往往能省下大半天的无头排查时间。

评论一下?

OωO
取消