文章讲述了作者在排查服务器大面积连接超时问题时,如何发现并解决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 看有没有这行丢包记录,往往能省下大半天的无头排查时间。
文章标题:服务器突然大面积连接超时但 ping 正常?排查 Linux nf_conntrack 满载丢包与内核调优我踩过的几个坑
文章链接:https://llbbs.cn/jishujaocheng/206.html
本站文章均为原创,未经授权请勿用于任何商业用途
评论一下?