ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Nginx高并发调优实战:从fd、TIME_WAIT到keepalive的完整指南

Nginx高并发调优实战:从fd、TIME_WAIT到keepalive的完整指南 如果你在负责一个秒杀系统或者IM推送服务流量高峰到来时第一个报警的往往不是业务应用而是最前面那层HTTP代理。这不是玄学而是代理层在高并发下要同时面对客户端和上游两侧的压力任何一个连接细节没有处理好都会把故障提前引爆。我这几年维护的Nginx反向代理网关从日常几百QPS到压测几万QPS都跑过核心感受是高并发从来不是单靠加机器解决的文件描述符、TIME_WAIT、连接复不复用、超时和限流这些才是决定代理层能不能扛住的关键。这篇文章就把我完整踩过一遍的路写出来——从挑战拆解、架构选型到Nginx关键参数、压测和排查实录。适合正在搭网关的人、被线上502/504折磨过的人以及打算做高并发压测但还没踩过坑的人。我没有用“先讲原理再讲操作”的学院派写法而是按真实工作流的顺序来讲先搞清楚代理层在高并发下到底为什么容易先挂再决定选什么和怎么调最后用压测结果验证。这样你看完可以直接对着自己的环境做一遍。1. 高并发场景下HTTP 代理到底在扛什么1.1 代理是双向连接的“中转柜台”可以把代理层想象成一个中转柜台用户从窗口A递单子代理转身上游窗口B办业务办完再把结果送回窗口A。对一个HTTP反向代理来说每个并发请求会同时占用一条客户端连接和一条到上游的连接所以并发连接数约等于当前正在处理的请求数乘2另外还要加上监听socket、日志文件、监控采集连接这些常驻开销。这里有一个很实用的估算公式并发连接数 ≈ QPS × 平均响应时间连接占用 ≈ 并发连接数 × 2 常驻连接举个例子QPS 5000平均响应时间200ms任意时刻在途请求约1000个代理需要同时维护约2000个socket连接。如果平均响应时间涨到800ms同样QPS下并发连接变成8000个连接占用接近16000。这就是为什么后端一旦变慢代理层比业务层更快出问题——不是代理不行是连接量随着耗时线性放大。高并发HTTP代理的瓶颈常常不在CPU。Nginx这类事件驱动模型CPU只在有事件时被唤醒大部分时间是在等网络事件真正开销分散在内核协议栈、socket缓冲区、系统调用和上下文切换。我经常遇到的情况是代理CPU只用到20%但accept()开始报Too many open files新连接进不来。这时候加CPU没用先得解决连接层面的问题。1.2 高并发下第一个杀手文件描述符文件描述符是Linux对打开文件的编号一个socket连接至少要占用一个fd。多数Linux默认的ulimit -n是1024你稍微压测一下就会撞墙所以生产环境通常要调高到65535以上极端场景会到百万级别。但调系统参数只是第一步Nginx自身还有一个独立限制worker_rlimit_nofile。这个值控制单个worker进程能打开的fd数量它不能超过操作系统对进程的硬限制。我实际踩过的坑是这样的shell里执行ulimit -n发现是1048576但Nginx是通过systemd启动的systemd默认LimitNOFILE是1024导致Nginx进程最多只能开1024个fd。结果压测到几百并发就开始大量accept失败日志刷屏CPU却只有个位数。排查了半天才想到用cat /proc/{pid}/limits去看进程真实限制。所以调参之前先确认生效范围别只改了一半。如果是高并发IM长连接场景fd问题会更突出。每个在线用户的长连接会持续占一个客户端侧fd用户在线10万代理层仅维护客户端连接就需要10万个fd还有上游心跳连接。这种场景下“并发连接数”和“QPS”是两个概念QPS可能不高但连接数巨大fd预算和内存占用要先按连接数算。1.3 TIME_WAIT 堆积与本地端口耗尽TCP四次挥手后主动关闭方会进入TIME_WAIT状态默认持续2MSL差不多60秒。反向代理作为出站方如果每次请求都新建一条到上游的连接请求结束由代理先发FIN那就会产生大量TIME_WAIT。压测几分钟TIME_WAIT堆积到几万个很常见。TIME_WAIT多会带来两个直接影响。第一本地端口范围有限默认ip_local_port_range通常是32768到60999约三万个端口。如果每分钟新建连接超过这个数四元组就无法复用新连接会直接报Cannot assign requested address。第二大量TIME_WAIT本身也占用内核内存虽然单个很小但几十万个累积起来不可忽视。查看命令很简单ss -s ss -ant state time-wait | wc -l解决办法有两个层级。第一层是把上游连接复用起来让请求继续使用已有连接从源头减少TIME_WAIT产生这是首选。第二层是允许内核安全复用TIME_WAIT连接对应参数net.ipv4.tcp_tw_reuse1。tcp_tw_reuse只对出站连接有效要求新连接的时间戳严格大于旧连接在Nginx访问上游这种场景下一般没问题。不推荐开tcp_tw_recycle这个参数在NAT环境下很容易误杀正常连接坑太大。1.4 慢客户端、队头阻塞与超时代理层的响应是一条“客户端—代理—上游”的双通道。上游返回得很快但如果客户端下载慢代理就得慢慢把缓冲数据发给客户端这段时间连接一直被占用并发处理能力被白白消耗。这也是为什么代理层要设计缓冲区和超时参数。proxy_buffering开启时Nginx会先把上游的完整响应收进缓冲区再统一发给客户端这样能尽快释放上游连接关闭时则边收边发内存占用小但连接占用时间长。对于普通API建议开启缓冲对于SSE、流式响应、视频流这类需要边产生边消费的场景关闭缓冲更合理但要做好连接被长时间占用的心理准备。超时参数也要区分场景。proxy_read_timeout定义的是两次读操作之间的间隔时间不是总耗时。长轮询或者IM推送的空闲连接间隔可能超过30秒参数设太短就会被误杀普通API请求反而怕设太长因为慢客户端会一直占着连接。我习惯普通接口设30秒长连接接口单独设一个更长的超时或单独路由。2. 架构设计与选型先想清楚再动手2.1 自研、Nginx、HAProxy 还是 Envoy选型永远比调参重要。我在不同项目里用过Nginx、HAProxy和Envoy简单总结一下取舍。对比维度NginxHAProxyEnvoy自研HTTP路由/改写能力强弱强可定制高并发连接能力很高极高高取决于实现动态配置一般需reload一般需reload强支持xDS动态下发自控可观测性一般需模块或日志一般有指标接口强内置丰富指标自研成本高学习与运维成本低低高很高典型场景L7反向代理、API网关四层入口、TCP负载均衡服务网格、大规模L7特殊协议定制我的选择是Nginx。原因很实际团队熟悉、文档丰富、nginx高并发相关案例随处可见遇到问题能快速找到参考。如果入口流量超过单机能力前面再放四层负载均衡比如LVS或者云厂商的SLB把连接分摊到多台Nginx上。HAProxy在四层的性能和连接管理上确实强悍但论L7的rewrite、限流、灰度分流我还是觉得Nginx更顺手。Envoy适合已经有服务网格基础的团队如果只是做一个反向代理网关引入Envoy带来的配置复杂度有些得不偿失。2.2 分层设计网关要能水平扩展高并发代理架构最后都会收敛成一个经典三层结构客户端先到四层负载均衡再到Nginx网关层最后到业务服务。四层LB只做连接分发不解析HTTP性能极高Nginx网关层负责HTTP路由、限流、改写、灰度业务服务专注于业务逻辑。网关层必须无状态这样每台Nginx才能独立水平扩容。注意Nginx upstream如果配置的是域名默认只在启动时解析一次后面域名解析变了不会自动更新。如果你在容器环境或者服务地址会动态变化的场景需要借助resolver指令配合变量使用。另一个点是健康检查Nginx开源版只有被动健康检查也就是max_fails和fail_timeout请求失败达到阈值后把节点标记为不可用。对于高并发场景我更习惯再配一个外部探活脚本定期探测上游健康状态避免故障节点被流量打爆。upstream配置里也可以控制权重和故障转移upstream backend { server 192.168.1.10:8080 weight3 max_fails3 fail_timeout10s; server 192.168.1.11:8080 weight2 max_fails3 fail_timeout10s; server 192.168.1.12:8080 backup; keepalive 64; }backup节点平时不参与流量只有主节点全部不可用时才启用适合用来兜底。不过要注意一旦触发fail_timeout节点会在10秒内不带流量如果上游只是偶发抖动反而会被误伤所以max_fails和fail_timeout要结合上游实际情况设置。3. 关键参数与落地配置以 Nginx 为例3.1 先放开系统层资源调Nginx之前先把内核和进程资源放开否则后面都是白忙。以下参数是我在高并发场景下的基准配置# 临时生效 sysctl -w net.core.somaxconn65535 sysctl -w net.ipv4.tcp_max_syn_backlog65535 sysctl -w net.ipv4.ip_local_port_range1024 65535 sysctl -w net.ipv4.tcp_tw_reuse1 sysctl -w net.ipv4.tcp_fin_timeout15 sysctl -w net.ipv4.tcp_keepalive_time300 # 写入持久化配置 cat /etc/sysctl.conf EOF net.core.somaxconn 65535 net.ipv4.tcp_max_syn_backlog 65535 net.ipv4.ip_local_port_range 1024 65535 net.ipv4.tcp_tw_reuse 1 net.ipv4.tcp_fin_timeout 15 net.ipv4.tcp_keepalive_time 300 EOF sysctl -p这些参数分别解决什么问题net.core.somaxconn和net.ipv4.tcp_max_syn_backlog控制的是TCP监听队列长度高并发瞬间涌入的连接如果超过队列长度客户端会握手超时或者被重置表现就是连接不稳定。net.ipv4.ip_local_port_range扩大本地出站端口范围配合tcp_tw_reuse缓解端口耗尽。tcp_fin_timeout调短可以减少FIN_WAIT状态的存量连接但注意它不会直接消灭TIME_WAITTIME_WAIT有自己固定的2MSL生命周期。tcp_keepalive_time把内核探活从默认的2小时缩短到5分钟能及早清理死连接但应用层有心跳机制的话这个参数就不那么关键。进程限制这一步我用的是systemd管理Nginx需要在service文件里显式配置[Service] LimitNOFILE200000 LimitNPROC65535改完记得daemon-reload并重启Nginx再用cat /proc/{nginx_pid}/limits确认是否生效。只看shell的ulimit不够必须看进程真实限制。3.2 Nginx 全局与事件模块配置来看一份我生产环境在用的核心配置骨架加了详细注释user nginx; worker_processes auto; worker_rlimit_nofile 200000; events { worker_connections 65535; use epoll; accept_mutex off; multi_accept on; } http { include /etc/nginx/mime.types; default_type application/octet-stream; access_log off; sendfile on; tcp_nopush on; tcp_nodelay on; keepalive_timeout 65; }逐个说为什么。worker_processes auto会按CPU核心数启动worker进程对纯I/O密集的代理层来说auto通常够用没必要盲目加大。worker_rlimit_nofile必须和系统LimitNOFILE匹配它限制单个worker能打开的fd上限。worker_connections是单个worker能同时处理的连接数上限注意它不是QPS上限而是并发连接上限。容量估算公式是单机最大连接能力约等于worker_processes乘以worker_connections。8个worker乘以65535理论值是52万并发连接但每个连接至少占两个fd所以fd预算至少要104万还要加上socket缓冲区内存。真实场景远达不到理论值别照着最高配去设。use epoll在Linux上是默认也是最优选择显式写上更清晰。accept_mutex off是我实测后的选择高版本Nginx事件模型已经解决了大部分惊群问题关掉mutex可以让多个worker更积极地accept连接减少锁竞争。multi_accept on让worker一次事件循环尽量多接受连接配合高并发突发场景效果更好。tcp_nodelay和tcp_nopush同时开前者禁用Nagle算法降低小包延迟后者优化大包发送两者对代理层延迟和吞吐都有正向作用。3.3 upstream keepalive 与 HTTP/1.1直接给配置location /api/ { proxy_http_version 1.1; proxy_set_header Connection ; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_pass http://backend; proxy_connect_timeout 5s; proxy_send_timeout 10s; proxy_read_timeout 30s; proxy_buffer_size 8k; proxy_buffers 8 8k; proxy_buffering on; }这里最关键的其实是三行proxy_http_version 1.1、proxy_set_header Connection 、以及upstream里的keepalive 64。不要小看这三行我第一次压测时漏掉了Connection 结果Nginx对上游使用HTTP/1.0协议每个请求都新建TCP连接TIME_WAIT在三分钟内堆到四万多个直接端口耗尽。加上这三行之后TIME_WAIT从四万降到不到一千QPS还涨了差不多一倍。背后原理是HTTP/1.0默认Connection: close请求结束就断开HTTP/1.1默认Connection: keep-alive。Nginx默认用HTTP/1.0和上游通信所以必须显式指定proxy_http_version 1.1。但光指定1.1还不够Nginx还会自动加上Connection: close请求头必须用空字符串覆盖掉告知上游这条连接可以复用。upstream keepalive 64表示每个worker进程与上游保持最多64条空闲的keepalive连接不是并发请求上限而是空闲连接池上限按需复用。3.4 超时、缓冲与限流再往下是超时和缓冲的调优。proxy_connect_timeout控制Nginx与上游建立TCP连接的超时时间5秒足够没必要太长否则上游不可用时请求会长时间挂起。proxy_send_timeout是Nginx向上游发送请求数据的间隔超时proxy_read_timeout是等待上游响应数据的间隔超时。这两个都是“间隔超时”不是总耗时。对于普通API我会把read timeou设30秒如果上游有耗时超过30秒的接口要单独路由放大对于WebSocket和SSE这类长连接通常关闭缓冲并设置更长的空闲超时location /stream/ { proxy_http_version 1.1; proxy_set_header Connection ; proxy_set_header Host $host; proxy_pass http://backend; proxy_buffering off; proxy_read_timeout 3600s; }proxy_buffering on时Nginx会先把上游响应收到缓冲区再发给客户端这样做能快速释放上游连接对高并发更有好。代价是占用内存并且客户端会感觉首字节来得更慢。proxy_buffer_size 8k是单个响应头缓冲区通常够用proxy_buffers 8 8k是响应体缓冲池需要根据响应大小适当放大。高并发下更重要的反而是限流。代理层如果不设限流量打到后端就是雪崩。Nginx自带漏桶限流limit_req_zone $binary_remote_addr zoneapi_limit:10m rate100r/s; location /api/ { limit_req zoneapi_limit burst200 nodelay; }rate100r/s表示每个客户端IP每秒允许100个请求burst200表示允许临时积压200个请求nodelay表示积压的请求不延迟排队直接放行超过burst的请求返回503。这个配置用来防止单个客户端刷爆服务也用来保护后端整体入口。对这种“全局总入口”的限流更常见的是按IP限流和一个更粗粒度的全局限额可以拆成多个zone分别配置。nginx高并发调优里限流和扩容往往同样重要甚至更重要。4. 压测、踩坑与问题排查实录4.1 先建立压测基线我压测首选wrk轻量、支持Lua脚本、结果清晰。基本命令wrk -t8 -c500 -d60s --latency http://localhost/api/test-t8表示8个线程-c500表示500个并发连接-d60s表示持续60秒--latency会输出延迟分布。压测时注意压测机本身也要放开资源。压测机默认fd和端口范围不够时会先报“Cannot assign requested address”这种情况不是代理层的问题而是压测机先崩了。压测前先在压测机上执行ulimit -n 200000 sysctl -w net.ipv4.ip_local_port_range1024 65535如果单台压测机仍然打不满目标QPS就部署多台压测机同时打。压测结果不能只看Requests/sec还要看P99延迟和错误数量。一次完整的压测应该包括QPS、P50/P99延迟、错误率、代理层CPU/内存、上游CPU/内存以及代理层当时的ESTABLISHED和TIME_WAIT数量。我习惯先跑一个低并发基线比如-c200再逐步增加到-c500、-c1000、-c2000观察拐点出现在哪里。很多情况下QPS不再上涨但延迟飙升说明连接数已经到瓶颈再调高并发没有意义。我实测遇到过一次这样的情况c2000时QPS死活上不去代理CPU很低查日志发现worker_connections默认只有1024连接全被拒绝。把worker_connections调到65535后QPS直接翻倍。这说明压测结果一定要结合连接状态去分析而不是只看一个QPS数字。4.2 线上常见故障速查这里整理一个高频问题速查表都是我在Nginx日志和系统状态里实际碰到过的故障现象可能原因排查工具/命令Too many open files系统fd限制或worker_rlimit_nofile未生效ulimit -n、cat /proc/pid/limitsaccept() failed (24)fd打满或worker_connections过小ss -lnt、错误日志connect() failed (111)上游未启动、过载、防火墙拦截curl上游探活、ss -lntconnect() failed (110)上游网络不通或握手超时telnet、tcpdump抓包upstream timed out (110)proxy_read_timeout过短或上游处理慢上游访问日志耗时no live upstreams while connecting上游全部被fail_timeout标记不可用上游健康状态、remove故障节点大量TIME_WAIT连接没有复用ss -ant state time-waitlisten queue overflowbacklog偏小ss -lnt看Recv-Qnetstat -sNginx错误日志位于/var/log/nginx/error.log出现问题时第一步永远是tail -n 100 error.log它会非常直接地告诉你连接失败的类型和原因。比盲调参数高效得多。4.3 一个典型的排查过程讲一个我印象很深的真实案例。某次压测30分钟后Nginx开始大量返回503和502CPU不高上游服务也没崩。第一反应查错误日志刷出来的是connect() failed (111: Connection refused)。继续看上游服务器进程连接数已经打满65535供不应求。再往深看ss -s显示TIME_WAIT多到吓人。原因清楚了Nginx没有开启upstream keepalive每个请求都向后端新建TCP连接后端每秒要accept大量新连接最终连接数上限被打爆。修复方式就是前面那套组合拳proxy_http_version 1.1、proxy_set_header Connection 、upstream keepalive 64。调整后的效果非常直观TIME_WAIT从四万降到不足一千后端连接数压力骤减QPS提升约60%。这个案例也验证了一个道理高并发下代理层的核心优化很多时候不是加机器而是减少不必要的重复连接。4.4 我踩过的几个坑整理几个让我熬夜过的教训。第一个坑只改了shell的ulimit但Nginx是通过systemd启动的进程限制没变。正确做法是改service文件里的LimitNOFILE再用cat /proc/{pid}/limits验证。这个步骤不能省。第二个坑本地回环压测通过上线后故障。回环网络的RTT几乎为零TCP握手的开销被掩盖了真实网络下RTT几十毫秒每请求新建连接的代价被放大。这就是为什么连接复用、keepalive、长连接在生产环境尤其重要。第三个坑把worker_connections误当成“QPS上限”拼命调大结果内存占用飙升。连接数上限和QPS是两个维度每个连接都要分配缓冲区内存连接数设得过高内存会先撑不住。要按实际并发模型估算而不是盲目追求大数值。第四个坑IM长连接场景里proxy_read_timeout设太短。客户端空闲20秒就被断开前端自动重连发起风暴反过来把服务打挂了。长连接路由必须单独设置空闲超时同时要确认业务心跳能正常探测到而不是依赖TCP自动超时去清理。最后分享一个我的习惯到现在我依然坚持一件事每次调整代理层参数先记录当前值无论是sysctl还是nginx -T输出再压一轮保留baseline改完参数再压一轮做对比。不要凭感觉调参高并发下的问题往往藏在细节里数据对比是唯一靠谱的判断依据。代理层调优没有银弹核心就是把连接生命周期、fd预算、超时策略和限流这几件事想明白、盯住。希望这篇记录能帮你少熬几个排查通宵。
返回列表