Linux内核参数调优:从somaxconn到TIME_WAIT

做站长这些年,我最怕的不是服务器崩,而是它「悄悄变慢」——CPU 不高、内存还有余、磁盘也不忙,可 PHP 就是卡,MySQL 连接就是慢,SSH 登录要等两秒。折腾半夜翻遍 Nginx 和 PHP 配置,最后发现根子上是内核参数没调:文件句柄上限卡在 1024、SYN 队列只有 128、端口范围只开两万多个。这篇把 sysctl 调优按「先看现状、再改参数、最后验证」的顺序讲清楚,重点是**哪些参数真的会影响网站、哪些是网上抄来但没用的**。

一、先搞清楚 sysctl 的三个生效层级

很多人改了 /etc/sysctl.conf 之后发现参数没变,问题出在没有理解生效层级。

sysctl -a | wc -l          # 看内核一共暴露多少参数,一般 1000+ 个
sysctl net.ipv4.tcp_max_syn_backlog
sysctl -n fs.file-max

# 不要只看当前值,要看它从哪来
sysctl -a --pattern 'net.ipv4.tcp' | head -40

生效层级大致是:内核默认值 → 内核模块初始化时设置 → /etc/sysctl.conf 与 /etc/sysctl.d/*.conf → 运行时 sysctl -w。在 Debian 12 上,systemd-sysctl.service 会按文件名顺序加载 /etc/sysctl.d/ 下的配置,/usr/lib/sysctl.d/ 里的默认项会先加载。如果你在 /etc/sysctl.conf 里写了一条,同时 /etc/sysctl.d/99-sysctl.conf 是个软链接又加载了一遍,看起来没问题;但如果你新建了 /etc/sysctl.d/10-network.conf,它排在 99-sysctl.conf 之前,会被后面的覆盖。

所以我现在的做法是:**只用一个文件** /etc/sysctl.d/99-zz-tune.conf,不再碰 /etc/sysctl.conf。所有改动集中在一处,注释写清楚改的原因和日期,运维半年后回来看得懂。

# /etc/sysctl.d/99-zz-tune.conf
# 2026-10 按 4C8G / 日均 20 万 PV 站点调整,修改人:zz

# ---- 连接与文件句柄 ----
fs.file-max = 200000
fs.nr_open = 200000

# ---- TCP 队列 ----
net.core.somaxconn = 4096
net.ipv4.tcp_max_syn_backlog = 8192
net.core.netdev_max_backlog = 4096

# ---- 端口与 TIME_WAIT ----
net.ipv4.ip_local_port_range = 10240 65000
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 30
net.ipv4.tcp_max_tw_buckets = 262144

改完执行 sysctl --system,它会把所有目录的配置重新按顺序加载一遍,比 sysctl -p 更可靠。sysctl -p 只加载你当前指定的那个文件,容易漏。

二、somaxconn 与 backlog:listen 队列到底谁说了算

「Nginx 明明开了 65535 个 worker 连接,为什么高峰期还是丢请求」——答案往往在 listen 队列。listen(2) 的 backlog 由三者取最小值决定:

# 1. Nginx 配置里的 listen backlog
grep -n 'listen' /etc/nginx/nginx.conf

# 2. 内核上限
sysctl net.core.somaxconn

# 3. 应用 accept 速度(由 worker 数量和 event 模型决定)

Debian 默认 somaxconn 是 4096(内核 5.4 之后从 128 提到 4096),看起来够用。但我遇到过一台 CentOS 7 的老机器,默认还是 128,Nginx 配置里写了 listen 80 backlog=8192,实际生效只有 128。压测时用 ss -lnt 看 Send-Q 列(对 LISTEN 状态,Send-Q 表示当前 backlog 上限):

ss -lnt 'sport = :80'
# State  Recv-Q Send-Q Local Address:Port
# LISTEN 0      128    0.0.0.0:80

# Recv-Q 是「当前排队未被 accept 的连接数」
# 如果 Recv-Q 经常接近 Send-Q,且 SYN 有重传,就是队列满了

判断队列溢出不看队列长度,看两个计数器:

nstat -az TcpExtListenOverflows TcpExtListenDrops TcpExtSyncookiesSent
netstat -s | grep -E 'listen|SYN'

# ListenOverflows / ListenDrops 增长 → accept 队列满(应用处理不过来)
# SyncookiesSent 增长 → SYN 队列满(很可能被 SYN Flood 或爬虫刷)

这两个数要区分清楚:ListenOverflows 涨说明 Nginx 有能力接但 accept 不过来(worker 太少或 upstream 阻塞拖住了 event loop);SyncookiesSent 涨说明半连接队列被灌爆(外部攻击或 CDN 回源大量握手)。搞混了会调错方向。

三、TIME_WAIT:什么时候该管,什么时候不该管

先说结论:**短连接密集型服务(反向代理、爬虫抓取、API 调用)TIME_WAIT 上千是正常的,不要一看到就上 tcp_tw_reuse 各种猛药。**

ss -s
# TCP: 2103 (estab 743, closed 1204, orphaned 0, timewait 1180)
ss -tan state time-wait | wc -l

TIME_WAIT 堆积到真正出问题的场景只有一个:**主动发起连接的一方把本地端口耗尽**,报错 Cannot assign requested address。比如你的服务器作为客户端频繁请求外部 API、或者 Nginx 作为反代到上游,每个请求新建一条连接。

判定方法:看 ip_local_port_range 的宽度与并发连接数的比值。

sysctl net.ipv4.ip_local_port_range
# net.ipv4.ip_local_port_range = 32768 60999   → 28231 个端口

# 对照:当前处于 TIME_WAIT 的连接
ss -tan state time-wait | wc -l

# 如果 TIME_WAIT 数接近端口范围宽度,就危险了

有效的做法按优先级排:

  1. 让上游连接复用,这才是根治。keepalive 32; 加在 upstream 块里,配合 proxy_set_header Connection "";,能让 Nginx 到 FPM/后端 HTTP 的连接不再每次新建。很多站点改完这一项,TIME_WAIT 直接掉一个数量级。
  2. 扩大端口范围:net.ipv4.ip_local_port_range = 10240 65000,多出两万多个端口。
  3. 开 tcp_tw_reuse=1:只对「主动发起连接」的客户端方向复用 TIME_WAIT 的 socket,且要求时间戳递增,相对安全。
  4. 绝对不要开 tcp_tw_recycle:它在 Linux 4.12 已被移除,老内核上开着会让 NAT 后面的客户端大量连接失败。这条在 4.x 以上内核里根本不存在,网上还在教,纯属坑人。

另外 tcp_max_tw_buckets 调大不是「优化」,是「允许更多 TIME_WAIT 存在」。默认 262144 一般够了,调太大反而占内存。

四、文件句柄:fs.file-max 调了也可能没用

fs.file-max 是系统级上限,真正的限制在进程级。有 systemd 的系统上,服务进程的上限由 unit 文件决定,不读 /etc/security/limits.conf 这段经常被忽略,导致「调用 ulimit -n 看到 65535,但服务里还是 1024」。

# 看某个进程的真实上限
cat /proc/$(pidof nginx | awk '{print $1}')/limits | grep -i 'open files'

# systemd 服务要写 drop-in
mkdir -p /etc/systemd/system/nginx.service.d
cat > /etc/systemd/system/nginx.service.d/limits.conf <<'LIM'
[Service]
LimitNOFILE=65535
LIM
systemctl daemon-reload
systemctl restart nginx

Nginx 自己还有 worker_rlimit_nofile,它和 systemd 的 LimitNOFILE 是「谁小听谁」。只设 Nginx 的不设 systemd 的,会被 systemd 的 1024 卡住;两个都设成 65535 才稳。

五、内核参数调优里最常见的五个无效动作

这些年看别人(包括曾经的自己)抄来的配置,无效甚至有害的不少:

  • 关掉 IPv6 全套:「提高安全性」是伪命题,关了 IPv6 但应用还在解析 AAAA 记录反而变慢。真不想用就在监听层面去掉 [::]:80,别关内核模块。
  • 加大 net.ipv4.tcp_rmem 到几 MB:对高带宽延迟积链路有意义,对国内机房 20ms 以内的小站没意义,只会多占内存。
  • vm.swappiness = 0:不是「不用 swap」,而是「尽量不用」。设 0 在内存吃紧时可能触发 OOM Killer 而不是换出,反而更危险。小内存 VPS 保持 10~30。
  • net.core.rmem_max 与 BBR 混用:开 BBR 确实是提速手段(拥塞控制换算法),但把它和一堆 buffer 调大混在一起,出了问题分不清是谁的功劳或锅。要开 BBR 就单独一个文件,记好基线。
  • 照着「某某云一键优化脚本」全盘照抄:那类脚本通常是为高并发网关调的,放到一台 2C2G 的博客机上,队列调大只会让内存被吃掉,延迟更差。

六、调优前的基线:不量就改等于赌博

我自己的规矩是:改任何一个内核参数之前,先用五分钟把基线记下来,改完第二天同一时间段再量一次。用的就是三条命令:

# 1) 连接与队列
ss -s; nstat -az | grep -E 'ListenOverflows|ListenDrops|Syncookies|RetransSegs'

# 2) 内存与 slab
free -m; cat /proc/meminfo | grep -E 'Slab|SReclaimable'

# 3) 网络吞吐与重传
sar -n TCP,ETCP 1 5

重传率(TcpExtTCPRetransSegs / 总发送段数)超过 0.5% 就说明链路有问题,这时候先查网卡和 MTU,别急着调缓冲区。这个数字在我排查过的几次「网站莫名慢」里,比 CPU 和内存更能说明问题——有一次重传率 4%,最后发现是机房上联口有光模块故障。

基线的另一层意义是「能回滚」。我习惯把每次改动做成一个带日期的备份:cp /etc/sysctl.d/99-zz-tune.conf /root/baseline/99-zz-tune.conf.$(date +%Y%m%d)。出现异常时先 sysctl --system 回到上一版,确认问题跟着参数走,再逐条加回来——这比盯着屏幕猜「是不是刚才那条导致的」快得多。

还有一个容易被忽略的观察点:同一台机器上的值会随业务变化而偏离最优。半年前调好的 somaxconn,在被爬虫扫过一轮、或者接入了新的 API 调用之后可能就不够了。所以我给这三个数配了个简单的巡检脚本,每周跑一次,只看变化趋势不看绝对值:

#!/bin/bash
# /usr/local/bin/net-check.sh  每周跑一次,输出追加到日志
echo "=== $(date +%F_%T) ==="
ss -s | head -3
nstat -az 2>/dev/null | grep -E 'TcpExtListenOverflows|TcpExtListenDrops|TcpExtSyncookiesSent|TcpExtTCPRetransSegs'
awk '/MemFree|Slab/{printf "%s %s MB\n", $1, int($2/1024)}' /proc/meminfo

连续几周数字平稳,说明参数和业务量匹配;某一周 ListenOverflows 突然开始涨,就是该扩 worker 或者扩机器的信号。这种「趋势比瞬时值重要」的判断方式,是所有监控里最省钱有效的。

七、一份可以直接用的保守配置

下面这套是我在一台 4C8G、日均 20 万 PV、Nginx + PHP-FPM + MySQL 同机的站上跑了半年没出问题的版本。它的原则是「只解决已知瓶颈,不做投机性放大」:

fs.file-max = 200000
net.core.somaxconn = 4096
net.core.netdev_max_backlog = 4096
net.ipv4.tcp_max_syn_backlog = 8192
net.ipv4.ip_local_port_range = 10240 65000
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 30
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_slow_start_after_idle = 0
vm.swappiness = 10
vm.vfs_cache_pressure = 50

逐条说明三个容易误解的:tcp_syncookies = 1 不是「只在攻击时生效」就随便开,它确实会改变正常 SYN 的 cookie 计算,但收益远大于代价,云服务器必开;tcp_slow_start_after_idle = 0 让空闲后的连接不重置拥塞窗口,对 keepalive 的长连接场景(CDN 回源、API 长轮询)效果明显;vm.vfs_cache_pressure = 50 让内核更愿意保留 inode/dentry 缓存,对有大量小文件的内容站是正收益。

最后提醒一句:这套参数不是「真理」,是「在一台特定机器上的当前最优」。你的机器什么配置、跑什么业务、瓶颈在哪,得自己量。我见过照抄大厂网关参数配到单机博客上,结果 CPU 反而高了 15% 的案例——参数调优从来是量出来的,不是抄出来的。

Last modification:October 22nd, 2026 at 09:26 pm

Leave a Comment