前阵子线上有个服务一到高峰期就卡死,后面接的 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 reloadnginx -t 会检查所有配置文件语法,只有没问题了再用 nginx -s reload 平滑重载配置。reload 不会中断正在处理的请求,Nginx 会重新读取配置、创建新的 worker 进程,然后优雅地停掉旧 worker,所以生产环境改配置后用 reload 而不是 restart。只有在修改了监听端口、需要加载新模块这种级别的大改动时,才需要重新 start 或 restart。
5. 平滑升级:在不中断服务的情况下换版本
5.1 为什么要平滑升级
生产环境运行的 Nginx 版本如果出现安全漏洞,比如之前 F5 Nginx 的 CVE-2022-41742 缓冲区错误漏洞,影响范围很大,攻击者可能利用它导致进程异常。这时升级就是必须的,但直接停服务再换二进制,线上业务会中断。好在 Nginx 提供了平滑升级能力,整个过程几乎不影响在线请求。
5.2 平滑升级的具体步骤
我的操作流程大致是这样的:
- 先准备新版本源码,并编译到新的安装路径,或者直接准备好新版本二进制文件。
- 查看当前 Nginx 的编译配置参数,用
nginx -V输出。新的版本编译参数要和原来保持一致,否则升级后某些模块行为会不一致。 - 确认当前运行的 master 进程 PID,通常可从 /usr/local/nginx/logs/nginx.pid 读取。
- 向旧 master 进程发送 USR2 信号,它会启动新的 master 和新的 worker 进程,新旧版本会短暂并行。
- 向旧 master 进程发送 WINCH 信号,它会优雅地关闭旧 worker 进程,保留旧 master,方便回滚。
- 验证新版本运行正常后,向旧 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 这些上层工具,一步一个脚印,比拿到文档硬啃效率高很多。