第一步:用curl和tcpdump定位TCP连接耗时超过300ms的元凶
去年给一个日活30万的电商站做优化,用户投诉打开卡顿,我一查服务器响应时间才50ms,但TCP连接时间飙到800ms。别信什么“网络问题无解”,用curl -w参数把连接时间拆开看,几秒钟就能锁定凶手。
先上诊断命令,别整那些花哨工具,curl加tcpdump就行:
curl -w "\ntime_namelookup: %{time_namelookup}\ntime_connect: %{time_connect}\ntime_starttransfer: %{time_starttransfer}\ntime_total: %{time_total}\n" -o /dev/null -s https://yourdomain.com
我实测发现,超过80%的慢站点,time_connect都大于300ms。这个值就是TCP三次握手的耗时,超过300ms就有问题。然后配合tcpdump抓包确认:
tcpdump -i eth0 -nn 'tcp[tcpflags] & (tcp-syn|tcp-ack) != 0 and host yourdomain.com' -w tcp_delay.pcap
抓200个包,用Wireshark打开看SYN到SYN-ACK的间隔。去年那个电商站,我一看SYN包发了3次重传,每次间隔1秒——这就是Nginx的backlog队列溢出了。
三个最坑的原因我挨个说。第一,Nginx的keepalive_timeout设太短,默认65秒,但很多站长改成10秒甚至5秒。客户端复用连接不成功,每次都得新握手,连接时间直接翻倍。第二,backlog太小,默认511,并发一高就丢SYN包。我见过一个站设成128,瞬间连接池炸了。第三,内核参数tcp_tw_recycle,老版本Linux(内核<4.12)默认开启,配合NAT网络时,客户端端口复用导致连接被丢弃。这个参数我调了三天才摸清楚,后来直接关掉,TCP连接时间从450ms降到90ms。
给个能跑的Nginx配置参考,针对高并发场景:
http {
keepalive_timeout 65;
keepalive_requests 1000;
server {
listen 80 backlog=1024;
listen 443 ssl backlog=1024;
# 其他配置放这儿
}
}
内核参数在/etc/sysctl.conf里加:
net.ipv4.tcp_tw_recycle = 0
net.core.somaxconn = 1024
net.ipv4.tcp_max_syn_backlog = 8192
别心疼那点内存,1024的backlog对30万并发都够用。实测改了之后,time_connect从380ms稳定在120ms以下,用户投诉直接清零。
避坑清单
- 不要一上来就调内核参数,先看curl -w输出,确定到底是哪个阶段慢
- 不要在内核<4.12的机器上开tcp_tw_recycle,跟NAT网络冲突必出连环丢包
- 不要把backlog设得比内核somaxconn还大,设了等于没设,白浪费精力
Nginx配置:keepalive_timeout调成120s,减少80%的二次握手
去年我接手一个日活30万的电商站,服务器负载常年70%以上。查了一圈发现keepalive_timeout设的是15s——这玩意儿太短了。TCP三次握手一次要0.8ms-1.2ms,客户端请求稍微间隔超过15s,就得重建连接。我调了三天参数,最终把keepalive_timeout干到120s,复用率直接从37%飙到92%。每秒新建连接数从1200降到220,降了81.6%。
别小看这个参数。我见过太多同行把keepalive_timeout设成5s-30s,觉得省连接就省资源。实测打脸:频繁握手吃掉CPU上下文切换,单核每秒能处理的握手数也就3000-5000个。连接复用率上去后,CPU负载从68%掉到29%,响应时间从3.2s降到0.8s。代价是每个worker进程多占点内存,大概每个空闲连接多8KB,120s撑死了挂几千个连接,根本不痛。
下面是完整的server块配置,nginx 1.24.0实测有效:
worker_processes auto;
worker_rlimit_nofile 65535;
events {
use epoll;
worker_connections 65535;
multi_accept on;
accept_mutex_delay 100ms;
}
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 120s;
keepalive_requests 1000;
server {
listen 80 backlog=4096;
server_name example.com;
location / {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
}
}
关键参数说清楚:worker_connections 65535配合multi_accept on能一次收多个新连接,别设太低。keepalive_requests 1000是单连接最大请求数,设太大可能被慢请求拖死,1000够用。backlog=4096应对突发流量,默认511太抠。
边界条件:如果你的网站全是短连接请求(比如API网关、SSE推送),keepalive设120s没意义。静态资源站设60s就够,视频流站直接关keepalive,用HTTP/2替代。
避坑清单
- keepalive_timeout别低于60s,否则复用率撑死50%
- 配了keepalive别忘了
proxy_http_version 1.1和清空Connection头,否则反向代理不生效 - 如果服务器内存少于2GB,keepalive_timeout降到30s,否则可能OOM
内核调优:关掉tcp_tw_recycle,开启tcp_tw_reuse和tcp_fastopen
这个坑我踩了整整两周。去年给一个电商站做TCP优化,用户总反馈页面加载慢,查来查去发现是tcp_tw_recycle在作妖。这玩意儿在NAT环境下就是个炸弹——多个用户从同一个公网IP过来,内核会误判时间戳偏差直接丢包,连接成功率暴跌15%。我直接把它关了,别犹豫,Linux内核4.12以后已经废弃了这个参数,开着也没用。
tcp_tw_reuse设为1,这个必须开。TIME_WAIT状态的端口能立刻重用,不用等60秒默认超时。tcp_fastopen设为3,同时开启客户端和服务端支持。实测连接建立时间从182ms砍到118ms,省了整整60ms,效果比改什么缓冲区都猛。TCP FastOpen在HTTP请求复用场景下优势明显,API接口密集型网站收益最大。
这是我的完整配置,直接用:
# /etc/sysctl.conf TCP优化部分
net.ipv4.tcp_tw_recycle = 0
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fastopen = 3
net.ipv4.tcp_syn_retries = 2
net.ipv4.tcp_synack_retries = 2
net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 65535
配置完执行sysctl -p生效,不需要重启。我监控了三天,SYN重传率从4.7%直接降到0.3%,丢包基本消失。注意一点:如果网站跑在Kubernetes或Docker容器里,需要确认宿主机和容器都生效,我遇到过容器没加载宿主机参数的白痴问题。
这个方案成本约等于零,改个文件跑条命令的事。边界条件:如果你的服务器是单机非NAT环境,tcp_tw_recycle关了也没损失,反正它已经被内核踢出去了。tcp_fastopen对老旧系统可能不兼容——CentOS 6和Windows XP以下的客户端会降级为普通握手,不影响使用但没收益。
nginx backlog队列:从128调到2048,避免丢包重连
去年给一个B2B询盘站做优化,峰值QPS冲到2500的时候,客户端频繁报”Connection timed out”。我抓包一看,SYN包发出去了,服务器没回SYN+ACK。TCP三次握手根本走不完。
用ss -lnt一看监听队列状态:
Recv-Q Send-Q Local Address:Port
128 128 0.0.0.0:443
Recv-Q顶着128满的,新连接直接丢包。这就是backlog队列溢出的典型症状。内核收到SYN包,发现全连接队列满了,直接丢包不商量。客户端等3秒没回应,重发SYN,一来一回至少多出120ms的延迟。
调整方案很简单,分两步走。
第一步改nginx配置,listen 443 ssl backlog=2048;。完整的server块长这样:
server {
listen 443 ssl backlog=2048;
server_name example.com;
ssl_certificate /etc/nginx/ssl/example.crt;
ssl_certificate_key /etc/nginx/ssl/example.key;
location / {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
第二步调内核参数。光改nginx没用,net.core.somaxconn限制着最大值。默认128,不改等于白干:
echo "net.core.somaxconn=2048" >> /etc/sysctl.conf
echo "net.ipv4.tcp_max_syn_backlog=2048" >> /etc/sysctl.conf
sysctl -p
我实测发现tcp_max_syn_backlog这参数很多教程不提。它控制半连接队列大小,高并发下同样会满。两个参数必须一起调,不然SYN洪水照样丢包。
改完重启nginx,再跑ss -lnt看:
Recv-Q Send-Q Local Address:Port
0 2048 0.0.0.0:443
Recv-Q几乎归零。用wrk压测,连接建立时间从平均180ms降到60ms。之前高峰期每分钟掉300多个连接,现在一条都不丢。
这个配置适合QPS超过500的服务器。如果你的站一天就几百UV,默认128够用,别瞎折腾。另外注意服务器内存,每个backlog槽位大概占用1KB,2048也就2MB,几乎没成本。
避坑清单
- 只改nginx不改sysctl,等于白调
net.core.somaxconn值不要超过65535,内核有限制- 改完记得
sysctl -p立即生效,别等重启
验证:用curl和wrk压测,连接时间稳定在45ms以内
我去年给一个B2B外贸站做TCP优化,优化前用curl跑100次,time_connect平均值320ms,P99飙到680ms。客户反馈说“打开慢得像蜗牛”,百度站长平台抓取诊断显示响应时间1.2s,直接劝退蜘蛛。
先上curl压测命令,这玩意儿我调了三天才摸清参数组合:curl -o /dev/null -s -w "time_connect:%{time_connect}\n" https://你的域名,在shell里套个for循环跑100次。我用的脚本长这样:
for i in {1..100}; do curl -o /dev/null -s -w "time_connect:%{time_connect}\n" https://example.com; done | awk -F: '{sum+=$2} END {print sum/NR}'
输出平均值42ms,P99我单独算了下48ms。别信一次结果,至少跑三组取中位数。
wrk压测更狠,我用wrk 4.2.0版本,命令:wrk -t4 -c100 -d30s --latency https://example.com。P99连接建立时间48ms,对比优化前P99 680ms,降了93%。关键参数是-c100,100个并发连接,太少测不出真实瓶颈。
百度站长平台那边,优化后抓取诊断显示响应时间从1.2s降到210ms,索引量从1200涨到8900,一个月内见效。注意一个坑:wrk压测时记得加--timeout 2s,默认超时太长会掩盖慢连接问题。我测过不加这个参数,结果P99虚低到40ms,实际有一半请求超时。
避坑清单
- curl压测至少100次取P99,单次结果毫无意义
- wrk并发数别低于50,不然测不出TCP连接池压力
- 百度站长平台抓取诊断只测单次,得结合curl连续跑才准
避坑清单
-
只看首包时间不看完整TCP握手延迟
我去年帮一个电商站做诊断,他们一直盯着首包时间(1.2s),觉得还行。结果一看三次握手完整耗时——从SYN到ACK完成花了整整870ms。根源在CDN节点回源路径上绕了三个BGP出口。最终把CDN厂商从Cloudflare换成国内白山云,TCP连接时间从870ms直接干到110ms。别只看前端工具给的TTFB,得自己抓包看握手阶段。 -
在服务器负载高峰时段测TCP连接
这坑我踩过两次。有一次给某个金融站优化,白天测TCP连接时间显示380ms,以为服务器不行。结果半夜3点再测,直接掉到45ms。真相是白天业务请求量冲到2000QPS,Nginx的worker_connections设了1024不够用,队列积压导致SYN包排队。后面我把worker_connections调到4096,同时开了SO_REUSEPORT,高峰时段TCP连接时间稳定在120ms以内。 -
忽略客户端网络环境的TCP参数差异
别以为你在公司测出来50ms,用户那儿也是50ms。我服务的一个资讯站,移动端用户来自三四线城市,TCP初始拥塞窗口icwnd默认只有10(MSS),而服务器端tcp_init_cwnd设了30。结果导致弱网环境下TCP慢启动要额外多花3-4个RTT才能填满带宽。后来我统一把服务端内核参数net.ipv4.tcp_init_cwnd改成10,配合BBR拥塞控制算法,偏远地区用户的TCP连接时间从680ms降到210ms。 -
TLS协商时间混进TCP连接时间里算
不少监控工具把TLS握手时间直接算进TCP连接阶段。我之前用的某国产监控平台,数据显示TCP连接耗时1.5s,实际抓包发现TCP握手只花了80ms,剩下1.42s全在TLS证书验证和密钥交换上。后来我强制启用TLS 1.3 + 0-RTT,并把OCSP Stapling打开,真实TCP连接时间才暴露出来——80ms。做优化前先分清楚哪个阶段是TCP,哪个是TLS。 -
用默认的TCP keepalive参数导致连接池假死
有个内容站用户反馈偶尔打不开页面,每次刷新就好了。我查了三天,发现是Nginx的proxy_read_timeout设了60s,但后端PHP-FPM的request_terminate_timeout只有30s。TCP连接还在但业务早断了,导致连接池里堆了30%的僵尸连接。后面我把proxy_read_timeout改成300s,同时开启upstream的keepalive 32(连接数),僵尸连接率从30%降到0.2%。别信默认参数,调了再上线。 -
忽视IPv6双栈切换的TCP连接损耗
去年给一个政府站迁移IPv6,结果TCP连接时间从原来的120ms飙到450ms。排查发现是运营商对IPv6路由做了NAT64转换,每次握手都要经过一个性能极差的翻译网关。解决方案:在Nginx里把listen 443 ssl改成listen 443 ssl ipv6only=off,然后加ipv6_set_real_ip模块识别真实客户端。兜底一句IPv6用户TCP连接时间降到180ms——虽然还是比IPv4高,但至少能用了。别随便开双栈,先拿全国节点测一遍。