☰
Nginx反向代理、负载均衡与高并发调优实战指南
2026/9/26 13:53:32 网站建设 项目流程

前阵子线上有个服务一到高峰期就卡死,后面接的 Tomcat 线程全部被占满,用户请求排队排到超时。当时第一反应是加机器,结果加完发现瓶颈根本不在 CPU 和内存,而是连接全阻塞在上游响应上,加再多的机器也只是把问题往后挪。后来把 Nginx 架在最前面做反向代理,静态资源直接由 Nginx 返回,动态请求才转发给 Tomcat,整个系统的并发能力直接翻了几倍,那之后我才算真正体会到 Nginx 的价值。

这篇文章把我在生产环境里用过、踩过坑、最后沉淀下来的 Nginx 核心功能讲清楚,包括反向代理、负载均衡、静态资源服务、高并发调优、平滑升级、常见报错排查这些。内容会尽量贴合实际运维场景,适合刚接触 Nginx 的运维、后端开发,也适合准备面试时想快速梳理知识体系的朋友。

1. 为什么大家都在用 Nginx:它到底解决了什么问题

先放下那些高大上的概念,用大白话说,Nginx 就是一个非常能扛并发、功能又很灵活的 Web 服务器和反向代理服务器。它最初就是为高并发而生的,和传统的 Apache 那种“一个连接一个进程”的模式完全不同,Nginx 用的是事件驱动模型,用少量进程就能处理海量连接。这也是为什么很多高流量网站前面都会架一层 Nginx。

1.1 反向代理和正向代理的区别

很多新手容易把反向代理和正向代理搞混。正向代理是替客户端去访问服务器,比如你在浏览器里配置代理访问被限制的网站,那个代理就是正向代理,服务器不知道真实的客户端是谁。反向代理则正好反过来,它替服务器接收客户端的请求,客户端不知道真实响应它的服务器是哪台,只知道自己在访问 Nginx 的地址。

放到实际架构里看:你的服务端有多台 Tomcat、多台 Node.js 实例,客户端想要访问这些服务,不可能把每台机器的 IP 都暴露给用户,这时候 Nginx 作为统一入口接收请求,再按规则转发到内部的某个节点,这个“统一入口 + 内部转发”就是反向代理最常见的用法。

1.2 一个请求在 Nginx 里走什么样的流程

理解了代理方向之后,我们看下完整请求链路。用户发起一个 HTTP 请求,先到 Nginx 的监听端口,Nginx 会做一系列处理:先根据域名匹配对应的 server 块,再根据 URL 匹配 location 块,如果命中了静态资源规则就直接返回本地文件;如果命中了反向代理规则,就把请求转发给 upstream 或 proxy_pass 指定的后端地址。

这一步理解清楚后,很多配置就好懂了。Nginx 所有配置都在解决一个问题:什么样的请求,走什么样的处理方式。可以是返回静态文件、转发给后端、重定向到另一个域名、返回错误码,甚至做限流和鉴权。核心就是 server 和 location 这两层匹配逻辑。

2. 核心功能拆解:反向代理、负载均衡和静态资源服务

2.1 反向代理配置的核心逻辑

反向代理最常见的配置就是 proxy_pass 指令,简单到几行就能搞定:

server { listen 80; server_name api.example.com; location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }

上面这个配置把所有访问 api.example.com 的请求都转发到本机 8080 端口。关键点在于 proxy_set_header,因为后端服务很多时候需要判断客户端真实 IP,如果不在 Nginx 层把原始 IP 传过去,后端拿到的一律都是 Nginx 的 IP,日志排障和用户定位都会很痛苦。

配置 proxy_pass 时有个容易踩的坑:proxy_pass 后面带不带 URI(路径)会导致完全不同的转发行为。比如 proxy_pass http://127.0.0.1:8080; 表示原来请求什么路径就转发什么路径,而 proxy_pass http://127.0.0.1:8080/api; 则会把匹配到 location 的路径替换掉。举个例子,location /user/ 匹配到 /user/abc,转发时如果 proxy_pass 没带路径就还是 /user/abc,如果带了 /api 就变成 /api/abc。这块我是吃过亏的,建议拿到新环境先做一次带路径的请求测试再上线。

server { listen 80; server_name example.com; location /api/ { proxy_pass http://backend_server:8080; 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_connect_timeout 5s; proxy_read_timeout 10s; } }

2.2 负载均衡的几种策略

如果后端有多台机器,Nginx 可以通过 upstream 定义一组后端节点,然后在 location 里用 proxy_pass http://upstream_name 引用这组节点,请求会被分摊到不同机器上。

upstream backend_cluster { server 192.168.1.10:8080 weight=3; server 192.168.1.11:8080 weight=1; server 192.168.1.12:8080 backup; } server { listen 80; server_name app.example.com; location / { proxy_pass http://backend_cluster; proxy_set_header Host $host; } }

这里用了 weight 参数,weight=3 的机器会被分配更多请求,适合后端机器配置不一致的场景。backup 表示备用节点,只有前面两台都不可用时才接管请求。除了默认的轮询策略,还有最少连接、IP 哈希等策略:

策略指令适用场景
轮询默认后端机器配置基本一致
权重轮询weight机器性能不同,按比例分流量
最少连接least_conn长连接、耗时差异大的请求
IP 哈希ip_hash需要把同一用户固定到同一后端

同一个用户固定到同一台后端,这种需求常见于会话没有做统一缓存的老系统。如果用了 IP 哈希,就不要再叠 weight 做动态调整了,否则哈希结果会被打乱。

2.3 静态资源服务与 autoindex 在线文件浏览

Nginx 处理静态文件的能力比后端应用服务器强很多,因为它在读取本地文件时直接用 sendfile 系统调用,数据从磁盘到网卡基本是零拷贝,CPU 占用非常低。所以像前端打包后的 dist 目录、图片资源、文件下载目录,都建议直接交给 Nginx。

server { listen 80; server_name static.example.com; root /data/wwwroot; index index.html; location / { try_files $uri $uri/ /index.html; } location /download/ { alias /data/files/; autoindex on; autoindex_exact_size off; autoindex_localtime on; charset utf-8; } }

这里重点说下 autoindex,它可以把某个目录列表直接展示出来,做成一个简单的在线文件浏览页面。autoindex_exact_size off 表示文件大小显示为可读格式(KB、MB 这样),autoindex_localtime on 表示用本地时间显示文件修改时间。我经常用这个功能在公司内网搭一个共享文件的下载站点,比搭 FTP 省事多了。需要注意中文文件名在目录列表中可能出现乱码,所以一定要加上 charset utf-8。

还有一个高频场景是前端路由的 history 模式。如果直接用 Nginx 托管 React/Vue 打包后的页面,刷新一个 /user/123 之类的路由时 Nginx 会去找对应的物理文件,结果返回 404。所以中间加了 try_files $uri $uri/ /index.html,意思是先找真实文件,找不到就回退到 index.html,让前端路由自己去解析。

3. 高并发能力与性能调优经验

3.1 Nginx 为什么能扛高并发

Nginx 的高并发本质上是靠事件驱动 + 异步非阻塞 IO,它不像传统服务那样每来一个连接就起一个线程。Nginx 的 worker 进程数量一般和 CPU 核心数一致,每个 worker 用 epoll 这样的机制同时盯着成千上万个连接,有事件才处理,没有事件就挂起等待,所以少量的 worker 就能吃掉极大的并发连接数。

这种方式和我们平时处理事情的逻辑很像,一个人同时接很多电话,不打电话的时候就干别的,而不是每来一个电话就雇一个新人。这样既省资源,又不会因为线程切换导致性能损耗。

3.2 核心参数调优

先看几个直接影响并发能力的参数:

worker_processes auto; worker_connections 65535; keepalive_timeout 65; keepalive_requests 1000; gzip on; gzip_min_length 1k; gzip_types text/plain application/json application/javascript text/css;

worker_processes auto 会根据 CPU 核心数自动设置进程数。worker_connections 表示每个 worker 进程能同时打开的最大连接数,默认值通常比较保守,生产环境一般会调高到 10240 或者更高,同时要同步调整系统层面的 ulimit,否则连接数限制会卡在操作系统这里。

一个 Nginx 服务能承载的最大并发连接数可以简单估算:worker_processes × worker_connections。比如 4 核机器、worker_connections 设为 10240,理论最大并发约 40960,但这只是理论值,实际还要看带宽、请求大小、后端响应时间。响应越慢,连接被占用越久,真实吞吐量越低,所以 Nginx 层要配合超时时间和 gzip 压缩一起优化。

gzip 压缩对带宽的节省非常明显。我记得之前有个纯 JSON 接口,没开 gzip 时一个响应大概 80KB,开完之后直接降到 8KB,整体访问耗时下降了一大截。但 gzip 对 CPU 有消耗,所以一般只压缩文本类类型,图片和视频这种已经压缩过的就不用再压了。

3.3 上游长连接和 keepalive 配置

高并发场景下还容易忽略一个问题:Nginx 和后端之间的连接复用。如果每个请求都重新建立 TCP 连接,握手开销会消耗大量时间和资源。用 upstream keepalive 可以维护 Nginx 和后端之间的长连接池。

upstream backend_cluster { server 192.168.1.10:8080; keepalive 32; } server { listen 80; location / { proxy_pass http://backend_cluster; proxy_http_version 1.1; proxy_set_header Connection ""; } }

两个关键点不能漏:proxy_http_version 必须设为 1.1,因为 HTTP/1.0 不支持长连接;proxy_set_header Connection "" 用来清掉请求头里的 Connection 字段,让 Nginx 能复用和后端之间的连接。不这么配的话 keepalive 参数写了也是白写,后端还是每次请求都建连。

3.4 关于 QUIC 和 HTTP/3

很多人在关注 Nginx 对 QUIC 的支持,毕竟 HTTP/3 在弱网环境下的表现确实好一些。新版本 Nginx 已经支持 HTTP/3,配置也不算太复杂,可以单独开一个 443 端口监听 HTTP/3 流量。但说实话,目前国内普通业务场景下 HTTP/3 的普及度还没有那么高,中间网络设备、客户端兼容性都需要考虑,建议先在测试环境做压测验证,不要直接在生产环境大规模切过去。做压测时可以用 nghttp3 这类工具,模拟 HTTP/3 请求看下 UDP 443 端口的转发是否正常,同时关注 UDP 丢包和防火墙限制这两个典型的坑。

4. 从安装到上线:一份完整的实操记录

4.1 Linux 环境下安装 Nginx 的几种方式

Yum/Apt 安装

如果用 CentOS、Rocky Linux 或 AlmaLinux,最省事的方式是直接通过包管理器安装。以 AlmaLinux 9 为例:

sudo dnf install -y nginx sudo systemctl enable --now nginx

但有两点要注意:官网的 Nginx 版本往往比发行版自带的版本更新,新版会包含更多安全修复和功能,比如 HTTP/3;另一个是默认仓库的 Nginx 有些模块没有编译进去,如果你需要特定第三方模块,推荐用编译安装。如果你是生产环境用户,建议先配官网提供的 nginx.repo,再装官方版本,这样后续可以用 yum/dnf 持续升级。

编译安装

编译安装的优势是可控性强,可以选择自己需要的模块。步骤大致是:

wget http://nginx.org/download/nginx-1.26.2.tar.gz tar -zxvf nginx-1.26.2.tar.gz cd nginx-1.26.2 ./configure --prefix=/usr/local/nginx \ --with-http_ssl_module \ --with-http_v2_module \ --with-http_stub_status_module \ --with-http_gzip_static_module make -j$(nproc) make install

编译前需要确保系统装了 gcc、pcre-devel、zlib-devel、openssl-devel 这些依赖。编译报错九成是缺依赖,缺哪个装哪个就行。编译安装完没有 systemd 管理脚本,需要自己写一个,或者直接用 nginx 自带的启动脚本。我的习惯是用编译安装的方式部署生产环境,因为可控性最高,后续加模块也知道自己的 Nginx 是哪些组件拼起来的。

国内镜像下载

如果服务器在国外源拉取很慢,或者网络环境受限,可以用国内镜像站下载 Nginx 源码包。有多个镜像源都提供同步,直接替换 URL 里的域名即可,注意选择和自己系统匹配的版本。

AArch64 纯内网离线安装

有的内网环境是 ARM 架构服务器,也就是 aarch64,而且完全隔离,不能连外网,只能把 RPM 包或源码包传到内网装。离线安装的核心思路是:在一台能联网的同架构机器上下载好所有 RPM,然后拷贝到内网用 rpm -Uvh 或 dnf localinstall 安装。

如果是源码方式,需要把 gcc、make、openssl-devel、pcre-devel、zlib-devel 一并拷贝进去,离线编译时同样会用到这些依赖。我记得有个经验:离线安装最费时间的不是在目标机器上安装,而是找齐所有依赖的 RPM,建议用 yumdownloader --resolve 这种方式一次性拉完依赖,省得来回折腾。装完之后顺便要把 worker 进程数和系统 ulimit 一起调了,否则内网高并发压测一样会出问题。

4.2 Windows 安装 Nginx 和踩坑记录

Windows 下安装 Nginx 相对简单,去官网下载 Windows 版压缩包,解压到没有中文和空格的目录,双击 nginx.exe 或者命令行运行即可。有一个非常典型的问题:如果解压路径带中文,比如 D:\测试\nignx,运行时会报各种路径相关的错误;如果目录名有空格,某些配置中的绝对路径也可能解析异常。建议直接用 D:\nginx,老老实实别折腾。

Windows 下还有个容易踩的坑,启动时报错:

[emerg] CreateFile() "D:/phpstudy_pro/www/admin2.com/.nginx.htaccess" failed

这个本质上是 Nginx 启动了但找不到对应的文件或权限不足。常见原因包括:配置文件里定义的路径不存在,或者该路径在 Windows 下没有访问权限。排查思路很简单:看 error.log,根据路径去检查目录是否存在,然后确认 Nginx 是否有该目录的读写权限。很多人在 Windows 上做本地开发会用 phpStudy,里面集成的 Nginx 经常出现这个问题,解决方法通常就是手动创建缺失的路径或修改配置里的 root 地址。

4.3 Nginx Proxy Manager

如果不想手写 Nginx 配置,可以考虑 Nginx Proxy Manager,简称 NPM。它是一个带 Web UI 的 Nginx 管理工具,通过 Docker 一键部署,用界面配置反向代理、SSL 证书、访问控制,生成后会自动维护一套 Nginx 配置。适合小团队或个人项目用,可以极大降低配置成本。它的部署方式不复杂,需要 Docker 环境,或者用项目自带的 Docker Compose 文件,把管理端口映射出来之后就能访问 Web 界面。不过 NPM 的配置能力相比手写 Nginx 还是有限的,复杂的自定义配置落地难度比较大,需要用它的高级自定义配置功能来补充。

4.4 一份能直接用的反向代理和域名配置

主域名和二级域名共存

生产场景中,一台 Nginx 同时托管主域名和多个二级域名非常常见。比如 example.com 是公司官网,api.example.com 是对接接口,files.example.com 放静态资源。核心就是为每个域名配置独立的 server 块:

server { listen 80; server_name example.com www.example.com; root /data/wwwroot/example; index index.html; }
server { listen 80; server_name api.example.com; location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }

这里的关键是理解 server_name 匹配规则:Nginx 收到一个请求后,会用请求的 Host 头去匹配配置文件里的 server_name,匹配到哪个就交给哪个 server 块处理。如果一个请求的域名没有任何 server 块匹配,会落到 listen 80 的默认 server 块,可能是第一个加载的配置,也可能是 default_server 指定的配置,生产环境最好显式声明一个默认返回 444 的 server,避免非法域名打进来还打到业务上。

server { listen 80 default_server; server_name _; return 444; }
Nginx 支持 JSP 吗

经常有新手问“Nginx 支持 JSP 吗”,答案是 Nginx 本身不支持直接执行 JSP。Nginx 是 Web 服务器和反向代理,它只能处理静态文件和转发请求,像 JSP 这种需要容器解析的动态语言,必须交给 Tomcat、Jetty 这样的 Servlet 容器去处理。Nginx 在里面扮演的角色是统一入口:请求进入 Nginx,如果你的 URL 是 /app/* 这种动态路径,就用 proxy_pass 转发给后端的 Tomcat;如果是 /static/* 这种资源路径,Nginx 自己直接返回。很多经典架构就是前端 Nginx + 后端 Tomcat,Nginx 负责静态资源和负载均衡,Tomcat 负责跑 JSP。

4.5 配置修改后的加载与重启

改配置文件最稳妥的验证流程是这样的:

nginx -t nginx -s reload

nginx -t 会检查所有配置文件语法,只有没问题了再用 nginx -s reload 平滑重载配置。reload 不会中断正在处理的请求,Nginx 会重新读取配置、创建新的 worker 进程,然后优雅地停掉旧 worker,所以生产环境改配置后用 reload 而不是 restart。只有在修改了监听端口、需要加载新模块这种级别的大改动时,才需要重新 start 或 restart。

5. 平滑升级:在不中断服务的情况下换版本

5.1 为什么要平滑升级

生产环境运行的 Nginx 版本如果出现安全漏洞,比如之前 F5 Nginx 的 CVE-2022-41742 缓冲区错误漏洞,影响范围很大,攻击者可能利用它导致进程异常。这时升级就是必须的,但直接停服务再换二进制,线上业务会中断。好在 Nginx 提供了平滑升级能力,整个过程几乎不影响在线请求。

5.2 平滑升级的具体步骤

我的操作流程大致是这样的:

  1. 先准备新版本源码,并编译到新的安装路径,或者直接准备好新版本二进制文件。
  2. 查看当前 Nginx 的编译配置参数,用nginx -V输出。新的版本编译参数要和原来保持一致,否则升级后某些模块行为会不一致。
  3. 确认当前运行的 master 进程 PID,通常可从 /usr/local/nginx/logs/nginx.pid 读取。
  4. 向旧 master 进程发送 USR2 信号,它会启动新的 master 和新的 worker 进程,新旧版本会短暂并行。
  5. 向旧 master 进程发送 WINCH 信号,它会优雅地关闭旧 worker 进程,保留旧 master,方便回滚。
  6. 验证新版本运行正常后,向旧 master 发送 QUIT 信号,彻底退出旧 master。

这套流程本质是利用 Nginx 的信号机制做二进制替换,整个过程只需要几个 kill 命令。如果升级后发现新版本有问题,只要旧 master 还没退出,就可以再向旧 master 发送 HUP 信号重新拉起旧 worker,实现回滚。这是 Nginx 非常经典的一个运维操作,值得花时间演练两遍。

5.3 配置热加载的原理

顺带说一下为什么 nginx -s reload 不会中断连接:reload 的过程是重新解析配置文件并启动新的 worker 进程,然后把新请求交给新 worker,旧的 worker 在处理完它手里正在服务的请求之后才退出。所以正在进行的下载、长轮询请求不会因为配置重载而被强制断开。这也是 Nginx 设计上一个很优秀的点。

6. 高频问题的排查经验记录

6.1 访问返回 403:最常踩的三个坑

Nginx 返回 403 是出现频率非常高的一个问题,我在不同环境里遇到过三种原因:

第一是索引文件缺失。如果你访问的目录下没有 index.html 或 index.htm,而配置里没有开启 autoindex,Nginx 会禁止列出目录内容,直接返回 403。这种情况下要么把 autoindex 打开,要么确保目录里有默认首页文件。

第二是权限不足。Nginx 的 worker 进程通常以 nginx 用户运行,它需要对该用户有路径的执行权限。比如 root 目录下的网站目录权限是 700,Nginx 用户进不去,就一定会 403。解决方法是确认目录链路(每一级目录)都要有执行权限,用ls -ld /data /data/wwwroot /data/wwwroot/site逐级检查。

第三是 SELinux 拦截。这个在 CentOS、Rocky Linux、AlmaLinux 上尤其常见,配置和权限都看着没问题,但 Nginx 就是无法访问目录。排查办法很简单,先执行getenforce,如果是 Enforcing,可以看下 AVC 日志确认是否被拦截,或者临时用setenforce 0验证,验证完再恢复。

很多时候 403 不是配置写错,而是文件系统权限和 SELinux 策略在背后搞事,把这些链路排查一遍,基本就能定位。

6.2 upstream keepalive 相关的坑

配了 upstream keepalive 之后反而不稳定了,报错里看到upstream prematurely closed connection这类信息,大多数情况是后端服务不支持 HTTP/1.1 长连接,或者后端在连接空闲后主动断开,但 Nginx 不知道,还在复用这条连接。解决思路:先确认后端是否支持 HTTP/1.1 和 keepalive;如果后端是短连接模型,就不要在 Nginx 层硬开 keepalive;同时配合调短后端空闲超时时间,让两边保持在相近的节奏上。

6.3 静态资源中文文件名乱码

Nginx 默认按 UTF-8 输出页面,但有的系统文件名字符集不是 UTF-8,就会乱码。前面提到的 autoindex 场景需要加 charset utf-8,另外建议文件系统层面统一用 UTF-8,服务器和客户端编码一致才能避免各种显示问题。

6.4 压测时高并发连接数上不去

明明 Nginx 参数都调高了,压测还是发现大量请求失败,先用ab -n 10000 -c 1000这样的工具压一遍,再看系统层面的连接限制。核心检查三件事:

  • ulimit -n是否够大
  • /etc/security/limits.conf是否限制了用户的最大打开文件数
  • Nginx 的 worker_connections 和 worker_rlimit_nofile 参数是否合理

Linux 下每个 TCP 连接都对应一个文件描述符,如果文件描述符上限不够,连接根本建立不起来,这时候加再多的 worker 也没用。生产服务器上我一般把 nofile 设为 65535 或更高,同时把 worker_rlimit_nofile 同步调上去。

6.5 配置了反向代理但总拿到空白页

空白页一般是后端返回了 502 或 504,但 Nginx 没有把错误页面显示出来,或者 Nginx 和后端之间通信失败。建议看 error.log,确认是连不上后端、超时还是后端本身崩溃。排查时可以在服务器上用 curl 直接访问后端的地址,绕开 Nginx 测试后端是否正常,再回到 Nginx 层面检查 proxy_pass 配置、防火墙、DNS 解析这几个环节。

7. 最后再分享两个我实际使用中的小技巧

第一个技巧是给每个 server 块记录独立的访问日志。项目多了以后,所有日志混在一起很难排查,给每个域名一个独立的 access_log 和 error_log,日志归档和分析都会舒服很多,比如 access_log /data/logs/nginx/api.example.com.access.log。

第二个技巧是优先用 nginx -t 检查配置再操作。我在生产环境几乎不碰 restart,都是改完配置先nginx -t,通过之后nginx -s reload,整个过程对线上服务零影响。学会这个习惯之后,会减少非常多没必要的故障。

Nginx 看起来配置量大,但核心其实就那几个模型:server 匹配域名、location 匹配路径、upstream 定义后端集群、proxy_pass 做转发。把这几个理解透了,剩下的模块都是在这些骨架上补充能力。我自己的学习路径是先把反向代理和静态资源吃透,再进阶负载均衡和高并发调优,最后再去接触平滑升级、HTTP/3、Nginx Proxy Manager 这些上层工具,一步一个脚印,比拿到文档硬啃效率高很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询