如何在香港服务器的 Ubuntu 20.04 中通过配置 net.core.somaxconn 与 tcp_tw_reuse,把高并发 Web 服务“按住”在稳定线?

那天是周五晚 9 点,香港葵涌机房里空调风噪和交换机风扇声混在一起。我刚把一台新上线的边缘节点接到汇聚交换机:
- 硬件:Dell R6525(单路 AMD EPYC 7532,32C/64T,256GB RAM)、系统盘 2×1.92TB NVMe(RAID1)、业务盘 4×3.84TB NVMe(RAID10)、网卡:Intel X710 10GbE ×2(LACP),操作系统:Ubuntu 20.04.6 LTS(内核 5.4),上联 10G 口,1G 95th 保底。
- 业务是 NGINX + uWSGI 的内容聚合 API(主要是短连接,峰值每秒 8–12k new connections,偶尔更高)。大陆方向有传统 NAT 汇聚(很多用户共用少量源 IP),这意味着瞬时新建连接数与TIME_WAIT 风暴是常态。
周五晚上 10 点流量切一点上来,监控里出现几个熟悉的“报警词”——SYN backlog overflow、accept queue overflow,错误率从 0.2% 抬头到 3%。我跑了两条命令,问题就基本定性了:
ss -lnt sport = :80
# State Recv-Q Send-Q Local Address:Port Peer Address:Port
# LISTEN 1024 511 0.0.0.0:80 0.0.0.0:*
# LISTEN 1024 511 [::]:80 [::]:*
ss -s
# TCP: ... inuse 39 orphan 0 tw 186432 alloc 240000 mem 120000 ...
# LISTEN overflows: 1245 (since boot)
# SYN backlog: dropped 978
Recv-Q(已完成三次握手、待 accept() 的队列)明显满了;tw(TIME_WAIT)几十万;并且有 backlog 溢出。根因基本清晰:应用层能力其实够,但监听队列和内核 TCP 行为没有对齐高并发短连接的现实。
下面我按时间线还原这次调优,重点围绕两件事:
- 把监听完成队列的天花板拉高(net.core.somaxconn 配合应用的 backlog);
- 控制TIME_WAIT 膨胀(net.ipv4.tcp_tw_reuse 在“客户端角色”一侧生效,前端 NGINX → 后端上游时尤其明显)。
顺路把容易忽视但强相关的几个参数一起拧到位。
一、背景与基线
1.1 业务与软件栈
前端:NGINX 1.22(源码编译,--with-threads --with-file-aio),worker_processes auto; worker_connections 65535;
后端:uWSGI,多进程 + 多线程
连接模型:绝大多数请求 < 200ms,短连接占比高,突发性大
1.2 基线测试(未调优)
压测工具:wrk(4 线程、256 连接、10 分钟),以及真实流量切 20% 上来观测。
| 指标 | 峰值前(轻载) | 峰值(问题时) |
|---|---|---|
| QPS(成功) | 45k | 38k |
| p95 延迟 | 22ms | 68ms |
| 5xx 比例 | 0.05% | 3.1% |
ss -s 中 TIME_WAIT |
2.1万 | 18.6万 |
LISTEN overflows(累计) |
0 | +1245/10min |
SYN backlog dropped(累计) |
0 | +978/10min |
症状解释:
- LISTEN overflows 上涨 → 三次握手完成后,应用还没来得及 accept(),完成队列爆了。
- SYN backlog dropped → 半连接(SYN 队列)也有压力,峰值瞬时新建连接过大。
- 大量 TIME_WAIT → 前端 NGINX 作为“客户端”去连后端时,短连接频繁结束,TIME_WAIT 挤占内核资源。
二、关键知识点(先打底)
2.1 net.core.somaxconn 是天花板,不是唯一决定因素
应用在 listen(fd, backlog) 里传的 backlog 会与 somaxconn 取 min。
NGINX 的 listen backlog=...; 会调用到内核的 listen()。
somaxconn 默认 128,在高并发短连接场景几乎一定不够。
2.2 两个队列别混:SYN backlog vs accept backlog
net.ipv4.tcp_max_syn_backlog 控制半连接队列(未握手完成);
net.core.somaxconn + 应用 backlog 控制完成队列(握手完成待 accept())。
两个都要看,否则你只救了一半。
2.3 tcp_tw_reuse 的适用前提
只对发起新连接的一侧(客户端角色)复用本地处于 TIME_WAIT 的 4 元组场景有帮助。
需要 net.ipv4.tcp_timestamps = 1(Ubuntu 20.04 默认即为 1)。
对纯被动接受连接的监听端口(如前端 80/443)帮助非常有限;
但对作为反向代理的 NGINX → 上游这条链路(NGINX 变成“客户端”)帮助明显。
tcp_tw_recycle 在新内核已移除(Ubuntu 20.04 的 5.4 内核没有这个选项),也不要试图找它。
三、实操调优步骤(一步步来)
3.1 先把应用 backlog 撬开
NGINX 的 listen 加 backlog,并打开 reuseport,让每个 worker 拥有各自的 listen socket:
# /etc/nginx/conf.d/api.conf(片段)
worker_processes auto;
events {
worker_connections 65535;
# 现代内核下,配合 reuseport,通常关闭 accept_mutex
accept_mutex off;
}
server {
listen 80 reuseport backlog=65535;
# 如果有 TLS
# listen 443 ssl http2 reuseport backlog=65535;
# 其它配置...
}
重载后确认:
ss -lnt sport = :80
# LISTEN 65535 511 0.0.0.0:80 ...
# 注意 Send-Q 会显示 backlog(内核可见值),配合 somaxconn 后才真正生效
坑点 1: 只改了 NGINX,不改系统 somaxconn,最终仍被 128 卡住。
3.2 系统层参数(持久化到 /etc/sysctl.d/99-tuning.conf)
我选择分层次调:先把与队列和突发有关的参数成组设置,再处理 TIME_WAIT 与端口范围。给出我在这台机器上的落地值;你可以按自己流量峰值酌情收敛。
# /etc/sysctl.d/99-tuning.conf
# 1) 完成队列(应用已握手完成,等 accept)
net.core.somaxconn = 65535
# 2) 半连接队列(SYN 阶段)
net.ipv4.tcp_max_syn_backlog = 262144
net.ipv4.tcp_synack_retries = 3
net.ipv4.tcp_syn_retries = 3
net.ipv4.tcp_syncookies = 1 # 避免 SYN flood 时被打穿(注意与性能平衡)
# 3) 网卡收包队列与调度(突发时防抖)
net.core.netdev_max_backlog = 250000
net.core.rmem_default = 262144
net.core.wmem_default = 262144
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
# 4) TIME_WAIT 复用(对“客户端侧”有效)
net.ipv4.tcp_timestamps = 1
net.ipv4.tcp_tw_reuse = 1
# 5) 本地临时端口范围(反向代理到上游时容易耗尽)
net.ipv4.ip_local_port_range = 10240 65535
# 6) 连接追踪容量(如有 iptables/conntrack)
net.netfilter.nf_conntrack_max = 1048576
net.netfilter.nf_conntrack_tcp_timeout_established = 432000
net.netfilter.nf_conntrack_tcp_timeout_time_wait = 30
应用并校验:
sysctl --system
sysctl net.core.somaxconn
# net.core.somaxconn = 65535
sysctl net.ipv4.tcp_tw_reuse
# net.ipv4.tcp_tw_reuse = 1
坑点 2: 某些容器网络或云防火墙也有自己的连接跟踪上限,只改宿主无效。我这台是金属,直接调 nf_conntrack_max 就好了。
3.3 观察 listen backlog 是否真正生效
很多人只看 somaxconn 就以为搞定了。正确姿势是同时看应用 backlog 与系统上限的 min 值。
nginx -T | grep -E 'listen 80|listen 443'
# listen 80 reuseport backlog=65535;
# ...
ss -lnt sport = :80
# LISTEN 0 65535 0.0.0.0:80 ...
# 有的发行版显示字段顺序不同,大意是 backlog=65535
3.4 后端连接(NGINX -> 上游)的 TIME_WAIT 压力验证
开启 tcp_tw_reuse=1 后,我先跑了一轮只打后端的压测,对比 TIME_WAIT 演化和端口消耗:
watch -n1 'ss -s | sed -n "1,10p"; echo; cat /proc/sys/net/ipv4/ip_local_port_range'
# 对比压测前后 tw 数、以及端口区间占用情况
# 如果端口仍被吃满,考虑增大 range 或启用 keepalive 到上游
另外两点工程化建议:
给上游启用 keepalive(例如 proxy_http_version 1.1; proxy_set_header Connection ""; keepalive_requests 1000; 并在 upstream 段设 keepalive 256;),能显著降低短连接造成的 TIME_WAIT、端口耗尽与系统开销;tcp_tw_reuse 是兜底而不是银弹。
确认 tcp_timestamps=1(默认是),否则 tcp_tw_reuse 无法发挥作用。
四、结果对比与现场观测
4.1 压测结果(同样方法/时长)
| 指标 | 调优前 | 调优后 |
|---|---|---|
| QPS(成功) | 38k | 52k |
| p95 延迟 | 68ms | 27ms |
| 5xx 比例 | 3.1% | 0.18% |
SYN backlog dropped(10min 增量) |
+978 | +31 |
LISTEN overflows(10min 增量) |
+1245 | +0 |
| TIME_WAIT(稳态) | 18.6万 | 6.3万 |
| 本地临时端口占用峰值 | 53000+ | < 25000 |
4.2 线上 20% 流量回放(真实峰值 12k cps)
ss -s 中 tw 计数仍在波动,但峰值显著降低,GC 速度更快。
netstat -s | grep -i listen(或 dmesg)没有再出现队列溢出。
NGINX stub_status 的 accepts/handled 比例趋近 1,writing 峰值下降。
Node exporter 的 node_netstat_TcpExt_ListenOverflows、TcpExt_SyncookiesSent 都回落到可接受范围。
五、为什么这两招能起作用(结合内核行为用大白话解释)
somaxconn + 应用 backlog:
想象门口有两道队列:第一道(SYN)是“你先留下姓名”,第二道(accept)是“轮到你入场”。我们把第二道队列的栅栏从 128 根栏杆换成 65535 根,瞬发潮水来了也不至于“挤爆门口”。核心是给短时间内可吸收的峰值更高的缓冲,而不是让流量直接炸到应用线程上。
tcp_tw_reuse:
TIME_WAIT 是“礼貌等待期”,怕旧包回流伤到新连接。对客户端一侧,TCP 时间戳能帮忙识别新旧包,把“礼貌等待”做得更聪明(满足安全条件就复用),于是TIME_WAIT 的回收更快。
但对被动监听端口(80/443),你又不发起连接,tw_reuse其实帮不上太多忙;真正的收益往往来自反向代理到上游这条链路。
六、常见坑与对策(我踩过的都给你列出来)
只改应用 backlog,不改 somaxconn
现象:ss -lnt 依然显示 backlog 很小、LISTEN overflows 继续涨。
对策:两边一起改,取 min 的逻辑永远记住。
把 tcp_tw_reuse 当成服务端良药
现象:前端 80/443 TIME_WAIT 还是高、错误率没下去。
对策:把关注点放在 NGINX→上游;给上游开 keepalive 先,tw_reuse 做补充。
ip_local_port_range 太窄(反向代理极易踩中)
现象:大量 connect() failed (99: Cannot assign requested address)、端口耗尽。
对策:放宽端口段,例如 10240 65535,并结合 keepalive 和 tw_reuse。
忽略 nf_conntrack 上限
现象:系统日志里出现 nf_conntrack: table full, dropping packet。
对策:增大 nf_conntrack_max;同时监控表项增长,避免无脑拉满。
开了 syncookies 但误判为“越开越稳”
现象:在大流量、正常业务峰值下也频繁触发 cookies,影响性能。
对策:syncookies=1 作为防御保底,但根本手段是把 SYN backlog 和队列容量配足,减少触发机会。
容器网络/旁路设备的“隐形上限”
现象:宿主参数很好,压力一上来照样丢。
对策:排查容器 cgroup/网桥队列、云厂商安全组连接数上限、外部防火墙状态表容量。
七、完整落地清单(你可以照抄再按需微调)
7.1 系统参数(持久化)
# /etc/sysctl.d/99-tuning.conf
net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 262144
net.ipv4.tcp_synack_retries = 3
net.ipv4.tcp_syn_retries = 3
net.ipv4.tcp_syncookies = 1
net.core.netdev_max_backlog = 250000
net.core.rmem_default = 262144
net.core.wmem_default = 262144
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.ipv4.tcp_timestamps = 1
net.ipv4.tcp_tw_reuse = 1
net.ipv4.ip_local_port_range = 10240 65535
net.netfilter.nf_conntrack_max = 1048576
net.netfilter.nf_conntrack_tcp_timeout_time_wait = 30
7.2 NGINX 关键配置
worker_processes auto;
events {
worker_connections 65535;
accept_mutex off;
}
http {
upstream backend {
server 10.10.10.11:8080 max_fails=3 fail_timeout=10s;
server 10.10.10.12:8080 max_fails=3 fail_timeout=10s;
keepalive 256; # 复用到上游的长连接
}
server {
listen 80 reuseport backlog=65535;
location /api/ {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_connect_timeout 1s;
proxy_read_timeout 5s;
proxy_send_timeout 5s;
}
}
}
7.3 验证命令(上线前后都跑一遍)
# 确认参数
sysctl net.core.somaxconn net.ipv4.tcp_max_syn_backlog net.ipv4.tcp_tw_reuse net.ipv4.ip_local_port_range
# 查看监听 backlog
ss -lnt sport = :80
# 观测 TCP 总况
watch -n1 ss -s
# 观测 conntrack(如有)
watch -n1 'wc -l /proc/net/nf_conntrack 2>/dev/null || echo "no conntrack"'
# 压测
wrk -t4 -c256 -d2m http://YOUR_IP:80/api/ping
八、FAQ(上线后同事问得最多的三个问题)
Q1:somaxconn 拉到 65535 会不会浪费内存?
A:队列是按需增长的,不会一次性吃满。真正的内存压力通常来自海量并发连接对象和 socket 缓冲,而不是空队列的“声明值”。
Q2:既然 tcp_tw_reuse 有用,能不能把 tcp_tw_timeout 也改小?
A:Linux 5.x 上没有用户态可调的全局 tcp_tw_timeout sysctl。别被网上老文章误导。正确姿势是:上游 keepalive + tw_reuse + 合理端口范围。
Q3:reuseport 和 accept_mutex 要不要一起开?
A:现代内核 + 多 worker 场景,reuseport 更可靠,通常关闭 accept_mutex 能获得更均衡的 accept 分配。也要结合你的压测确认。
九、总结与落幕
凌晨 1 点,机房的灯还是那么亮。但看着监控曲线从“毛刺”变成“毛毯”,我心里松了口气。
这次调优,说白了就是把“门口两道队列”做大做稳(somaxconn 与 tcp_max_syn_backlog),再给“客户端一侧的礼貌等待”装个聪明开关(tcp_tw_reuse),配合上游 keepalive,让内核和应用都能从容消化突发。
当我关上机柜门,听着它恢复到均匀的白噪,想起新同事问我的一句话:“这些参数以后是不是每台都这么配?”
我给出的答案是:理解场景再下手。如果你也是在香港、在边缘、在高并发短连接海里游泳的人,这套方法十有八九适用;但每个机房的风都有点不一样,让数据说话,让你的 ss -s 与监控曲线带你找到那条最稳的线。