上一篇 下一篇 分享链接 返回 返回顶部

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

发布人:Minchunlin 发布时间:2025-08-28 09:54 阅读量:656


那天是周五晚 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 与监控曲线带你找到那条最稳的线。

目录结构
全文