Nginx高性能原理与反向代理调优实战 “Nginx 高性能”这五个字技术圈里几乎天天有人提但真正把它讲透的其实不多。我做了几年线上业务的后端运维处理过大大小小各种Nginx事故也压测过单机扛几万并发连接的场景。Nginx之所以能在今天的高并发架构里站稳脚跟靠的不只是一句“事件驱动、异步非阻塞”的广告词而是它从进程模型、网络模型到配置层面的整套设计都围绕着“把系统资源用在刀刃上”这件事来做的。这篇文章我会从底层机制说起到具体调优落地包括反向代理场景里的连接池、SSL、CORS 这些最容易踩坑的点也会把我实际压测和排查生产问题时积累的经验直接给你。无论你是刚把Nginx装好的新手还是已经在用Nginx反向代理多套服务的同学读完后至少能对自己的配置“为什么快、为什么慢、为什么报错”有一个清晰的判断框架。1. 高性能的根Nginx 凭什么能吃下高并发1.1 事件驱动模型与 master-worker 架构的底层逻辑我们经常听到“Nginx高性能”但高性能具体是怎么来的得从进程模型说起。Nginx启动后会有一个master进程和多个worker进程。master进程主要负责读取配置、管理worker生命周期、平滑重载这些“管理型”工作真正处理网络请求的是一个个worker进程。每个worker进程内部跑的是一个基于事件循环的模型——在Linux上就是epoll在macOS上是kqueue。事件驱动的意思通俗点讲worker进程不会傻等一个请求处理完才去处理下一个而是把所有连接交给内核事件表内核告诉它“哪个连接上有数据来了”它就去处理那个连接。没有数据来的连接就继续挂起不占进程、不占线程。这种设计让一个worker进程可以同时维护成千上万个TCP连接而不需要像传统多线程模型那样每个连接都开一个线程去伺候。我举个例子你就明白了。Apache经典的prefork模式一个请求基本就对应一个进程进程之间内存隔离、切换开销大连接数一上来内存和CPU上下文切换先把你拖垮。Nginx的worker进程虽然也是多进程但它的worker是共享同一个事件循环的不直接一对一绑定连接所以内存占用低得多CPU也主要花在处理实际IO上而不是花在线程调度上。这就是Nginx在“高并发连接”这个维度上能吊打传统web服务器的根本原因。1.2 worker 进程数量到底怎么设置才对很多人一上来就问worker_processes设多少网上答案很统一等于CPU核心数。但实际生产中这个结论只说对了一半。如果你的业务主要是静态文件、简单代理这类CPU密集型不高的场景worker_processes设为CPU核心数即可多了反而增加进程切换的开销。如果是HTTPS终结、复杂正则location、或者你在用Lua脚本做业务逻辑这些场景吃CPU比较狠那你甚至可以把worker_processes稍微设高一点比如CPU核心数的1.5到2倍让进程有更多机会抢占CPU时间片。我自己的习惯是先看机器的具体用途。一台纯Nginx反代网关4核8G的机器我一般设worker_processes 4如果这台机器同时还跑了PHP-FPM或者别的业务进程我会降为2避免Nginx把CPU全占了影响业务。还有一个很多人忽略的点worker进程数的修改是需要reload生效的但如果你在配置里用了worker_cpu_affinity这类绑定设置要注意和实际核心编号对应不然绑错核反而会引入性能问题。这里顺便提一句生产环境最重要的一个配套参数是worker_rlimit_nofile。它决定单个worker进程能打开的最大文件描述符数量。Nginx高并发时每个TCP连接至少占一个文件描述符反向代理场景甚至一个请求要占用两个。如果你不把这个参数提到几万甚至十万worker_connections设得再大也是白搭因为文件描述符不够用了。Linux下默认的1024非常坑一定要在配置里显式调大比如worker_rlimit_nofile 65535;同时还要在系统层面用ulimit把进程的文件描述符限制也调上去两头缺一不可。1.3 连接数与并发数的真实计算方式worker_connections这个参数是每个worker进程能同时保持的连接数上限。很多人直接套公式最大并发连接 worker_processes × worker_connections比如4个worker、每个1024连接得出4096。这个公式我建议你只把它当作一个理论参考实际场景要复杂得多。如果你做的是纯静态文件服务客户端连上Nginx拿完数据就走短连接居多那这个公式大概还能估算。但如果你做的是反向代理Nginx要同时维持客户端连接和后端连接一个请求消耗两个连接那么你的真实并发处理量大约就是“worker_processes × worker_connections / 2”。如果客户端还开着keepalive大量连接实际上处于“占着连接但没在发请求”的闲置状态那能同时处理的活跃请求数就会远低于连接数。因为这里有个容易混淆的概念并发连接数和QPS不是一回事。一个HTTP/1.1客户端keepalive连接可以连续发送很多个请求并发连接1000不代表QPS只有1000。压测时你会发现真正决定QPS的往往是worker进程数量和后端响应速度worker_connections只是兜底的一个上限防止连接数打爆内存和文件描述符。所以我调优的顺序是先定worker_processes再定worker_rlimit_nofile最后才是worker_connections。建议线上至少开到10240以上我之前一台4核机器开的是40960配合系统somaxconn等内核参数才能稳得住。2. 三个必调的核心性能参数从配置层面榨出吞吐量2.1 keepalive、sendfile、tcp_nopush 的配合HTTP层面最能直接影响性能的就是keepalive。客户端和Nginx之间保持长连接省去了反复三次握手和TLS握手的开销。配置里有两个参数要看keepalive_timeout和keepalive_requests。timeout设太短长连接很快断开客户端频繁重连白白浪费握手时间设太长又会让Nginx维护大量空闲连接占资源。我的线上一般设keepalive_timeout 65s这是一个比较稳妥的默认值。keepalive_requests表示一条长连接最多能复用多少次默认是1000如果你发现某些客户端在一个连接上请求特别多可以适当调高比如10000。再来看静态文件这块的传家宝配置sendfile on。它的作用是让静态文件直接从内核缓冲区发到网卡数据不经过Nginx用户态拷贝这叫零拷贝机制。配上tcp_nopush onsendfile会把多个小数据包攒成一个稍大的包再发出去减少网络包数量tcp_nodelay on则是反过来对小报文立刻发送不延迟。这两个参数看着矛盾实际是协作关系。tcp_nopush适合大文件下载、整包响应的场景tcp_nodelay适合交互性强、小数据量频繁交换的场景比如API接口。所以通常配置是sendfile on; tcp_nopush on; tcp_nodelay on;整体上效果是最好的。2.2 关于 gzip 压缩的正确选择很多人觉得开gzip就是性能优化其实不一定。gzip压缩会消耗CPU如果机器CPU本来就紧或者带宽根本不是瓶颈gzip反而拖慢响应。正确的做法是只压缩值得压缩的内容类型并且对太小文件不做压缩。比如1KB以下的文件压缩后体积变化不大但压缩耗时和传输时间比起来根本不划算。我的标准配置一般是gzip on; gzip_comp_level 5; gzip_min_length 1k; gzip_types text/plain text/css application/json application/javascript application/xml image/svgxml; gzip_vary on;gzip_comp_level不建议高于6压缩率提升有限但CPU开销翻倍。gzip_min_length设1k或者2k比较合理小于这个值不压缩。gzip_vary on是为了让代理服务器和客户端正确识别响应是否有Vary头避免缓存错乱。还有一点要注意如果你已经在前端挂了CDN或者又套了一层网关这一层的gzip需要和后端协调好最好不要层层压缩白白浪费CPU。Nginx做反向代理时如果后端已经返回了Content-Encoding: gzip的响应Nginx默认不会再次压缩这种透传行为是正常的。2.3 文件句柄缓存与 SSL 会话缓存的隐形收益open_file_cache很多人不重视但它在静态文件服务场景下能省掉大量系统调用。比如一个图片服务器每次请求都要打开文件、读取元数据如果每次open和stat都走一遍内核高并发下系统调用开销非常高。有了open_file_cacheNginx会缓存文件句柄、大小、修改时间这些信息定期校验是否失效。open_file_cache max10000 inactive30s; open_file_cache_valid 60s; open_file_cache_min_uses 2;max表示最多缓存多少条文件记录inactive表示30秒内没有被访问的记录会被淘汰valid表示60秒后重新校验文件是否变化min_uses表示文件至少被访问2次才进缓存。这个配置对图片、JS、CSS这类访问频繁的静态文件特别有效。SSL会话缓存也是被低估的一个配置。如果你的Nginx做了HTTPS终结客户端每次握手都要走一遍完整的TLS协商这一步开销非常大。设置ssl_session_cache shared:SSL:10m;之后TLS会话票据和会话ID可以在共享内存里复用后续请求直接恢复会话绕过完整握手。一个大概的参考1MB共享内存大约能存4000个会话10m就是4万左右对绝大多数站点够用了。ssl_session_timeout可以设到1天平衡内存占用和复用效率。3. 反向代理场景的高性能实践3.1 upstream 连接池是代理吞吐量的命门Nginx作为反向代理时很多人忽略了一个关键细节默认情况下Nginx和后端服务器之间的连接是短连接每个请求都重新建连。如果你的后端接口响应只要几十毫秒但TCP握手加TIME_WAIT状态却要消耗大量时间那整体吞吐量就被拖垮了。解决办法是给upstream配置keepalive连接池。核心配置长这样upstream backend { server 127.0.0.1:8080; keepalive 32; } server { location /api/ { proxy_pass http://backend; proxy_http_version 1.1; proxy_set_header Connection ; } }这里有个至关重要的点必须显式设置proxy_http_version 1.1否则upstream默认使用HTTP/1.0协议而HTTP/1.0不支持keepalive连接池根本不会生效。proxy_set_header Connection 则是把请求头里的Connection字段清空让后端知道这是一个可复用的长连接。keepalive 32表示每个worker进程维护到后端的长连接数上限这里设的是空闲连接池的上限不是总连接数上限。我遇到过很多次upstream keepalive配了但压测时发现后端连接数还是特别高抓包一看全是短连接。十有八九就是忘了上面这两个配套参数。顺便说一句如果后端是HTTP/2服务优先级会更高但一般国内业务基于HTTP/1.1的场景还是主流。3.2 proxy_buffering 与超时参数的场景化选择Nginx代理默认开启proxy_buffering也就是Nginx会先把后端响应读完再统一发给客户端。这个机制是为了保护后端避免响应数据在Nginx和后端之间反复横跳也能让Nginx根据客户端速度决定发送节奏。但如果你的业务是流式接口比如AI聊天这种返回就是一段一段往外吐的就必须关掉缓冲否则客户端会一直等不到数据。location /stream/ { proxy_pass http://backend; proxy_buffering off; proxy_read_timeout 600s; }proxy_read_timeout也需要跟着业务走。默认60秒对大多数API够用但碰到需要长计算的长接口60秒一到Nginx就返回504。我自己在代理一些AI推理服务时就把proxy_read_timeout调到300到600秒同时把proxy_send_timeout和proxy_connect_timeout分别设为合理的值。connect_timeout其实不建议设太大一般3到5秒就够了因为它只负责建立连接后端连不上的话等再久也没用。这里有个隐含的性能逻辑proxy_buffering on时Nginx和后端之间的读盘、网络IO都会被缓冲平滑掉后端返回速度不稳定的接口也能保持客户端侧响应稳定。所以我的建议是默认开着只有流式接口和需要实时推送的场景才关掉。你问我怎么判断一个接口适不适合关缓冲最简单的标准这个接口的响应能不能一次性完整返回。能就别关。3.3 本地多站点代理与自定义域名的开发环境配置顺便把搜索热词里“本地虚拟机多端口nginx开发环境多站点自定义域名配置”这个场景也说一下确实很多人问。本质就是利用Nginx的多个server块让不同的域名或端口走不同的站点。server { listen 80; server_name api.dev.example.com; location / { proxy_pass http://127.0.0.1:3000; } } server { listen 80; server_name admin.dev.example.com; location / { proxy_pass http://127.0.0.1:8080; } }本地环境只要在hosts文件里把api.dev.example.com映射到127.0.0.1再用Nginx把不同域名转发到不同本地端口即可。这样开发时多个项目互不干扰不用改端口访问。如果你在虚拟机里跑Nginx宿主机要访问虚拟机里的站点把hosts文件里的IP改成虚拟机IP就行前提是虚拟机的防火墙放行了80端口。这套玩法本质上就是生产环境反向代理的微型版理解透了生产环境也就顺手了。还有一个小坑Nginx的listen指令可以同时指定IP和端口比如listen 127.0.0.1:8080和listen 8080效果不一样。如果只想让本机访问务必加上IP限制避免暴露到外部网络。这在开发环境配置多端口时尤其值得注意。4. 反向代理与 HTTPS 场景下的故障排查实录4.1 SSL 证书“换了不生效”和 ERR_CERT_COMMON_NAME_INVALID这个问题的出现频率在搜索词里高居不下我在实际排查中也屡屡碰到。先说“证书换了不生效”。大部分情况下不是Nginx不生效而是你改完配置之后没有正确重载。注意nginx -s reload是最常用的平滑重载方式它会让master进程重新读取配置并重新加载证书。如果reload之后证书还是旧的就要考虑是不是nginx.conf里的ssl_certificate路径指向了错误的文件或者服务器块配置错误。我遇到过一个很典型的案例用户有两个server块都监听443端口不同server_name。证书文件换好后reload了但访问A域名时看到的居然还是旧证书。原因就是浏览器缓存了证书或者HTTP/2连接被复用。这时用隐身窗口再访问一次或者用curl手动指定域名去验证证书指纹就能确认问题到底出在Nginx还是浏览器。至于net::ERR_CERT_COMMON_NAME_INVALID最常见的三种原因证书的域名和浏览器地址栏域名对不上比如证书只签了example.com你用IP地址访问必然报错。Nginx配置里server_name和证书域名不一致比如证书签的是www.example.com但server_name写的是api.example.com。证书链不完整中间证书没配。浏览器无法验证证书链也会报类似错误。这里我建议每个配置HTTPS的人养成一个习惯使用openssl s_client -connect 你的域名:443 -servername 你的域名去检查实际返回的证书信息确认证书域名、签发机构、有效期都符合预期。这比在浏览器里看错报错信息要直接得多。反向代理转发到后端HTTPS时还有一层坑Nginx作为客户端去连接后端HTTPS时默认会做证书校验。如果后端的证书是自签名或者域名不匹配Nginx会直接报错返回502。此时可以在location里用proxy_ssl_server_name on;保证SNI跟随后端域名或者在后端证书可信的情况下关闭校验但生产环境我更推荐正确配置证书链而不是关校验。4.2 invalid CORS request 是怎么来的“nginx invalid cors request”这组词最近热度很高。这个报错出现时大部分请求是OPTIONS预检请求。现象是你的前端在浏览器里调用后端API时报错但用curl直接测接口又是通的。问题核心在CORS配置没写对。其中一个常见原因是Nginx里配置了不止一处add_header Access-Control-Allow-Origin导致响应头重复。浏览器一旦读到重复的Access-Control-Allow-Origin头就会判定CORS失败。另一个原因是OPTIONS预检请求返回的状态码不对。规范要求预检请求应返回2xx最好是204但很多配置里直接把OPTIONS请求扔给了后端后端没处理就返回了405或403。建议配置写成这样location /api/ { if ($request_method OPTIONS) { add_header Access-Control-Allow-Origin * always; add_header Access-Control-Allow-Methods GET, POST, PUT, DELETE, OPTIONS; add_header Access-Control-Allow-Headers Authorization, Content-Type, X-Requested-With; add_header Access-Control-Max-Age 86400; return 204; } add_header Access-Control-Allow-Origin * always; proxy_pass http://backend; }注意always参数它表示即使响应状态码不是200也会加上这个头。还有一点add_header在Nginx的location里是有继承规则的内层location如果没有声明add_header会继承外层一旦内层自己声明了外层的所有add_header都不会再传递。这是很多人CORS头越配越乱的根本原因。4.3 location 匹配顺序和 proxy_pass 的路径拆解搜索词里“nginx中location工作流机制”和“nginx中location工作原理”反复出现说明大家对这个匹配顺序确实容易混淆。Nginx的location匹配规则从优先级高到低大致是精确匹配 强制前缀匹配^~ 正则匹配~和~* 普通前缀匹配。普通前缀匹配中哪个字符串最长就用哪个。注意^~是“如果前缀匹配到了就不再继续检查正则”这一点非常关键。很多人写location ^~ /api/结果其他正则location永远不生效半天找不到原因。这里我给一个典型例子location /status { return 200 exact\n; } location ^~ /static/ { alias /data/static/; } location ~ \.php$ { proxy_pass http://php_backend; } location /api/ { proxy_pass http://api_backend; }请求/status走精确匹配请求/static/js/app.js因为^~存在直接走静态目录不会再检查php正则请求/api/user走普通前缀匹配但如果在/api/下面还有location ~的正则规则会优先于普通前缀。正则匹配一旦命中就停止继续向下检查。如果你发现某些请求走的location和你预期不符就用nginx -t配合实际请求日志看一眼多半是匹配顺序踩坑了。proxy_pass的路径拼接是另一个大坑。记住一个简单规则location里配置了URI比如location /api/并且proxy_pass后面只是http://backend这种不带路径的写法请求URI会原样转发。但如果proxy_pass后面带了路径比如proxy_pass http://backend/或者proxy_pass http://backend/api/那原始URI里的前缀就会被替换掉。这个替换逻辑非常容易算错我建议每次改完都实际curl一个带路径的请求看看后端拿到的URI是什么。后端如果输出日志直接在日志里对比一下就清楚了。5. 监控与诊断如何证明“高性能”不是玄学5.1 用 stub_status 和 Zabbix 把 Nginx 状态捞出来没有监控的调优都是盲调。Nginx自带了一个轻量状态页叫stub_status可以通过配置暴露出来location /nginx_status { stub_status on; access_log off; allow 127.0.0.1; deny all; }这个页面输出非常朴素会给出几个数字active connections表示当前活跃连接数server accepts handled requests表示总共接受的连接数和处理的请求数reading表示正在读取请求头的连接数writing表示正在写响应的连接数waiting表示keepalive长连接的空闲数。如果你用Zabbix监控Nginx就可以采集这个状态页的数据。Zabbix的模板里一般有现成的nginx监控项只要在agent端配置好URL访问权限即可。这里提醒一句状态页一定不要暴露给公网不然后果不是性能问题而是安全问题。用allow和deny限定本机或内网就够用了。Zabbix监控Nginx时我最看重两个指标active connections的绝对值以及reading和writing的比例。如果reading长期很高说明客户端请求还没读完可能有人在利用慢速连接攻击如果writing长期很高说明响应堆积了要么带宽不够要么后端响应太慢。这两个状态值都是判断Nginx是否处于亚健康状态的重要信号。5.2 ab 压测时到底该看什么压测工具首选ab或者wrkab简单直接。我一般用ab -n 100000 -c 1000 http://你的域名/来测单接口。这里我要强调压测结果出现Time per request和Requests per second之后不要只看QPS数字还要看Failed requests和Non-2xx responses。如果失败数不为0哪怕QPS再高这个配置也是不合格的。压测的时候请关闭本机影响项比如你本机开着浏览器、下载、视频播放测试结果都会波动。更专业的做法是压测机和服务机分开。第一次压测可以先测静态文件验证Nginx自身性能第二次再测反向代理接口对比QPS下降了多少。如果代理接口的QPS只有静态文件的三分之一甚至更低那说明瓶颈大概率在后端服务而不是Nginx本身。压测时还要注意观察TIME_WAIT状态。你可以用netstat -anp | grep TIME_WAIT | wc -l统计。如果TIME_WAIT数量飙升到几万说明短连接太多需要检查upstream keepalive有没有生效以及tcp_tw_reuse这个内核参数有没有开启。注意tcp_tw_reuse只对客户端连接有效Nginx主动向后端建连时这个参数能帮忙复用TIME_WAIT状态的连接效率提升明显。5.3 容器化部署中的 conf.d 挂载与大并发限制Kubernetes里部署Nginx和传统部署最大的区别在于配置挂载方式。很多人把Nginx容器跑起来后发现配置根本没生效可能是挂载路径不对或者没遵守命名规则。Nginx官方镜像中/etc/nginx/conf.d/下的*.conf文件会被自动加载。如果你挂载的是整个目录就必须保证目录里的配置文件以.conf结尾否则Nginx会忽略它们。这个坑在Alpine镜像上特别明显。Nginx的Alpine镜像本身是最小化构建/etc/nginx/conf.d/default.conf在这个镜像里可能不存在。你挂载整个目录进去如果只放了自己的站点配置就不要再认为默认80端口服务会自动存在。另外挂载文件的权限也很关键Nginx worker是以nginx用户运行的挂载卷如果权限是700或者root所有worker可能因为读不到配置而启动失败。如果明明挂载了文件但容器一启动就报open() /etc/nginx/conf.d/xxx.conf failed (13: Permission denied)多半就是权限问题。Kubernetes下更推荐用ConfigMap挂载配置文件到/etc/nginx/conf.d/但ConfigMap挂载默认是目录挂载会覆盖整个conf.d目录。如果你需要Nginx默认配置存在就必须把默认配置也放进ConfigMap。我个人的做法是业务站点配置放一个ConfigMapnginx.conf主配置单独挂载不让Nginx去读镜像里的默认模板所有行为都显式声明这样最可控。至于容器里的并发数限制还要注意Nginx容器运行时的文件描述符限制。Docker默认的ulimit可能不够高要在docker run或者Kubernetes的securityContext里显式调大。否则你配置里写了worker_connections 10240但容器内文件描述符只允许到4096照样扛不住高并发。6. 性能调优的边界与经验体会6.1 我实际压测过的一组配置样本有些话光说不练没用我把自己在一台2核4G机器上压测的一组配置参数贴出来供你做参考基准。这台机器只跑Nginx纯净服务后端是一个简单的静态文件接口。worker_processes 2; worker_rlimit_nofile 65535; events { use epoll; worker_connections 10240; } http { sendfile on; tcp_nopush on; tcp_nodelay on; keepalive_timeout 65; keepalive_requests 1000; gzip on; gzip_comp_level 5; gzip_min_length 1k; open_file_cache max5000 inactive30s; upstream backend { server 127.0.0.1:8080; keepalive 32; } server { listen 80; server_name example.com; location /static/ { alias /data/static/; } location /api/ { proxy_pass http://backend; proxy_http_version 1.1; proxy_set_header Connection ; proxy_buffering on; } } }这套配置下我用ab跑一次100并发持续1分钟的压测静态文件的QPS能到2万以上代理接口QPS取决于后端服务的响应速度基本是后端的极限减去代理开销。如果代理接口QPS明显低于后端自身压测的QPS那就要回头检查upstream keepalive和proxy_buffering配置了。6.2 一个高效排查问题的顺序处理Nginx性能或报错问题时我的排查顺序基本固定在这里分享给你。第一步先看错误日志路径一般是/var/log/nginx/error.log。日志里如果有worker_connections are not enough是连接数上限问题有upstream timed out是后端超时有SSL_do_handshake相关错误是证书或TLS配置问题。第二步用nginx -t检查配置语法同时看warning信息很多隐患其实在这里就会暴露。第三步用curl -v带上Host和自定义header请求观察响应码和返回头处理CORS类问题这一步最有效。最后一步才上压测工具和抓包工具因为前面几步能解决的问题不该浪费压测时间。这个排查顺序我踩过不少坑才总结出来。以前遇到线上报错第一反应就去翻内核网络参数结果折腾半天发现只是worker连接数不够日志里一行就写明白了。遇到性能问题先看日志这个习惯能帮你省掉至少一半的排查时间。6.3 调优的本质是找到真正的瓶颈文章最后聊点实际的。Nginx高性能不是一个孤立配置它是操作系统、Nginx配置、后端服务三者协同的结果。很多人在worker_connections和gzip上反复折腾却忽略了真正的瓶颈可能是后端PHP-FPM的进程池太小也可能是数据库连接池被打满。这时候你调Nginx调到天花板也解决不了问题。我的体会是每次调完一个参数都要带着压测数据去判断有没有效果不要凭感觉。两个版本之间如果QPS和延迟没有明显变化说明这个参数不是当前瓶颈调它没有意义。性能优化是不断逼近真实瓶颈的过程而不是把网上所有优化参数都堆到配置里。好的Nginx配置应该是能讲清楚每个参数是为什么而存在的这比一个写满几十行“优化建议”的配置文件靠谱得多。如果你现在正准备去调一台Nginx的性能我的建议是从worker_rlimit_nofile和worker_connections这对组合开始这几乎是高并发场景下性价比最高的两个参数。改完压测一轮观察连接数和QPS的变化再决定要不要动keepalive和gzip。调优的路子对了后面就顺了。