反代连后端时,Nginx 其实在偷偷握手
Nginx 作为反向代理几乎成了个人站长的标配:前面挡一层,把请求转发给后端的 PHP-FPM、Node、Gunicorn、Tomcat 或者另一台机器上的服务。很多人配好 proxy_pass 就以为完事了,性能调优也往往只盯着前端:开 gzip、上 CDN、加大 worker 连接数。
但有一个被严重低估的环节:Nginx 与后端之间的连接,默认是「一次请求一次握手、用完就关」的。也就是说,每一个访客请求,代理层都要和后端重新做一遍 TCP 三次握手(如果是 HTTPS 后端还要再加一次 TLS 握手),用完立刻 close。在低并发时看不出问题,一旦并发上来,后端就忙于处理海量的连接建立和销毁,真正的业务处理反而被拖慢。
这篇文章讲清楚怎么用 upstream 的长连接(keepalive)把这个开销砍掉,以及为什么「配上了没生效」是这一步最常见的翻车点。
先用数据说话:短连接到底浪费了什么
先定义清楚两个容易混淆的概念:客户端到 Nginx 的连接,和 Nginx 到后端 的连接。浏览器的 keep-alive 优化的是前者,本文讲的是后者——它经常被忽略,因为它发生在你看不见的服务器内部。
默认情况下,Nginx 对 proxy_pass 转发出去的连接使用 HTTP/1.0 语义或者显式 Connection: close,意味着:
请求 1: Nginx --三次握手--> 后端 --发请求/收响应--> 后端 close
请求 2: Nginx --三次握手--> 后端 --发请求/收响应--> 后端 close
请求 3: Nginx --三次握手--> 后端 --发请求/收响应--> 后端 close
...在局域网内一次握手可能只要零点几毫秒,看不出差别;但如果后端在另一台机器、跨机房,或者后端是 TLS 端口,握手成本就不可忽略了。更麻烦的是 TIME_WAIT:每次主动关闭都会在 close 一方留下 TIME_WAIT 状态,短连接高频场景下会迅速堆积,最终占满本地端口。这个现象在排查「端口耗尽」时经常被误判为前端问题,实际根源就在代理到后端的短连接上。
改成 keepalive 后,连接被复用:
请求 1: Nginx ==握手==> 后端 => 响应 => 连接留在池里
请求 2: Nginx ==========> 后端 => 响应 => 复用同一条连接
请求 3: Nginx ==========> 后端 => 响应 => 复用同一条连接握手次数从「每请求一次」降到「每个 worker 每后端几条」,TIME_WAIT 数量也大幅下降。
正确配置:三个必写项缺一不可
很多人只加了 keepalive 一行就以为搞定了,结果发现完全没有效果。原因在于 Nginx 的 keepalive 需要「三件套」同时到位。先看完整的 upstream 配置:
upstream backend_pool {
server 127.0.0.1:9000;
# 关键 1:连接池大小,每个 worker 进程最多缓存的空闲长连接数
keepalive 32;
# 关键 2:长连接超时,超时后空闲连接被回收
keepalive_timeout 60s;
# 关键 3(新版 Nginx 1.15.3+):单个连接最多处理多少个请求后重建
keepalive_requests 1000;
}
server {
listen 80;
location / {
proxy_pass http://backend_pool;
# 关键 4:必须显式告诉 Nginx 用 HTTP/1.1,否则默认 1.0 不支持长连接
proxy_http_version 1.1;
# 关键 5:清空 Connection 头,否则默认发 close,长连接直接失效
proxy_set_header Connection "";
}
}这五个点里,第 4 和第 5 项是最常被漏掉的。
为什么 proxy_http_version 1.1 必须写?因为 Nginx 默认用 HTTP/1.0 与后端通信,而 HTTP/1.0 默认就是「请求完即关闭」,连接无法复用。只有升到 1.1,长连接才有语义基础。
为什么 proxy_set_header Connection ""; 必须写?因为即便你写了两条 proxy_set_header Connection "upgrade" 之类的东西,或者上游传下来的 Connection 头是 close,Nginx 都会照直转发给后端,后端看到 Connection: close 就会主动断开,长连接池根本养不起来。把它置空是告诉后端「这条连接可以复用」,这是长连接真正生效的开关。
这两个点单独看都不起眼,但它们共同决定了「配了 keepalive 到底有没有用」。判断方法是看后端连接的状态,而不是看配置文件。
怎么验证长连接真的生效了
验证分两层。第一层看 Nginx 侧的连接统计,第二层看后端侧实际观察到的连接数。
先看 Nginx 关于 upstream 的连接状态。开启 stub_status 或者直接看 keepalive 相关的状态字段:
# 统计 established 连接中,有多少是通向 127.0.0.1:9000 的
ss -tn state established '( dport = :9000 )' | wc -l
# 观察 TIME_WAIT 数量随时间的变化
watch -n 2 "ss -tan | grep -c TIME_WAIT"配置生效前后对比非常直观。以一台中等流量的 PHP 站为例,压测 200 并发、持续一分钟:
指标 短连接 长连接(keepalive 32)
到后端的握手次数/分钟 约 18000 约 240
TIME_WAIT 峰值 数千 几十
平均响应时间 明显抖动 平稳
后端 CPU 的 sys 占比 偏高 下降第二层验证在后端。以 php-fpm 为例,它的 pm.status 页面里会显示 active processes 和 listen queue,长连接生效后你应看到后端处理的连接数明显大于请求数——因为一条连接承载了多个请求。如果你用的是 Node 或 Python 应用,配合后端的访问日志,能直接看到同一个 TCP 连接上出现多条请求记录。
还有一个更直接的验证方式:在后端监听端口上抓包,数 SYN 包的数量。长连接生效后,SYN 数量应该远小于请求数量。
# 抓 10 秒,只统计 TCP SYN 建连包
timeout 10 tcpdump -i lo -nn 'tcp port 9000 and tcp[tcpflags] & tcp-syn != 0' 2>/dev/null | wc -l如果这个数字和请求量接近,说明长连接根本没生效。
pool 该开多大、几个常见误区
keepalive 的数值不是越大越好。它表示「每个 worker 进程缓存的空闲连接数上限」,不是「并发连接数上限」。经验值:如果你的 worker_connections 是 1024,keepalive 开到 32 到 64 已经足够应对大多数站点。开得过大只会浪费后端资源——后端得一直维系这些空闲连接,反而增加内存占用。一个稳妥的起点是:keepalive 设为「预期峰值并发数 / worker 进程数」再留一点余量。
误区一:只配 keepalive,不配 proxy_http_version 和 proxy_set_header Connection "",等于白配。这是最常见的失效原因,前文已经展开。
误区二:以为 keepalive 会「保持一条永久连接」。实际上空闲连接会被 keepalive_timeout 回收,且每条连接处理完 keepalive_requests 个请求后会主动重建——这是为了防止长连接僵死和内存泄露,属于正常设计。
误区三:后端本身不支持长连接。有些老旧后端、CGI 程序或者用了 Connection: close 的框架,是拒绝复用的。配置正确但后端不配合,依然无效。此时应该先查后端文档,确认它支持 HTTP/1.1 keep-alive。
误区四:加了 keepalive 就忘了调 keepalive_timeout。默认值在新版里可能偏短,导致连接刚缓存就被回收,复用率上不去。一般 60s 是合理值,和前端 keepalive_timeout 别搞混,两者是两个独立指令。
真实案例:一次「配了没效果」的排查
有台中等流量的 WordPress 服务器,PHP-FPM 后端经常在高峰期报 server reached pm.max_children,但 CPU 和内存都不算满,感觉像是「连接处理不过来而不是算不过来」。运维直觉告诉我们应该压一压连接建立的开销,于是检查了 nginx.conf 里的 proxy 配置。
结构上看起来是对的:有 upstream 带 keepalive 32,有 proxy_pass 指向 upstream。但用 tcpdump 抓了 10 秒的本地 9000 端口,SYN 包数量几乎等于请求数——这直接证明长连接一条都没复用上。回头逐行看配置,发现 location 块里只有:
proxy_pass http://backend_pool;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;缺了 proxy_http_version 1.1 和 proxy_set_header Connection ""; 这两行。补上之后重载,再抓包,SYN 数量在一分钟内从数千降到几百,FPM 的 max_children 告警也消失了,后端 CPU 的 sys 占用下降明显。整个过程没有升级任何硬件、没有改一个业务代码,只是一个连接复用开关。
这个案例的价值在于:它演示了正确的排查顺序——不要先看配置文件里「有没有写 keepalive」,而是先用抓包或连接统计验证「连接到底复用了没有」。数据先于判断,才能避免抱着错误配置反复怀疑后端。
总结
Nginx 到后端的长连接是一个典型的「低成本、高回报」优化点:几行配置,就能把每个请求的握手开销和 TIME_WAIT 堆积一起砍掉。关键是记住三件套——keepalive(连接池)、proxy_http_version 1.1(协议基础)、proxy_set_header Connection "";(清掉 close 头),缺任何一个都会导致配置静默失效。
调优不要靠感觉,要靠数据:用 ss 数 established 连接、用 tcpdump 数 SYN 包、观察 TIME_WAIT 曲线的变化。只要 SYN 数量远小于请求量,就说明长连接真正生效了。对个人站来说,这是一个在流量增长时「不用加机器就能扛住」的实用技巧。