做后端开发这些年,几乎每个项目都会和 Nginx 打交道。它不复杂,但初学者往往一上来就被一堆名词和配置项搞得头晕:什么叫反向代理?location匹配规则为什么这么多?nginx -s reload和 restart 到底有什么区别?配置文件里那几十行到底哪些必须改、哪些不用管?
我最初学 Nginx 也是靠硬背,后来把名词、命令、配置文件三个部分拼成一张“地图”,才真正把 Nginx 用明白。这篇就用图解的思路,把它拆开揉碎讲一遍。适合刚开始用 Nginx 的后端、运维和前端同学,也适合那些配过几次但一直“知其然不知其所以然”的人。
1. Nginx 核心名词图解:先把几个概念放到正确的位置
1.1 进程模型:master 和 worker 各自在干嘛
Nginx 启动后会同时存在两种进程:一个 master 进程和多个 worker 进程。看下大概的结构:
master(主进程) ├── 读取配置、管理worker生命周期 ├── worker(子进程1) → 处理请求A、请求B、请求C... ├── worker(子进程2) → 处理请求D、请求E... └── worker(子进程N) → 处理请求...master 进程不直接处理业务请求,它负责干这几件事:启动时读取配置文件、创建/终止 worker 进程、接收管理员发的信号(比如 reload、stop)、记录错误日志。真正干活的是 worker 进程,每个 worker 都是独立进程,互相不共享内存,事件循环各自处理进来的连接。
这个模型最关键的优点是:worker 崩了不影响其他 worker,master 会重新拉起一个新的。我之前遇到过某个业务模块导致 worker 内存暴涨被系统杀掉,其他 worker 一点事没有,请求只是短暂变慢,不会整体挂掉。这就是多进程模型的价值,也是 Nginx 在同类软件里稳定性口碑好的重要原因。
还有个常见问题:worker_processes到底配几个?常规做法是等于 CPU 核心数。可以在服务器上跑nproc或者grep -c ^processor /proc/cpuinfo查看。配少了浪费 CPU,配多了反而会因为进程上下文切换增加开销,一般就是核心数,不用盲目调大。
1.2 正向代理和反向代理:方向决定本质
这个几乎是面试必问,也是初学最容易混的点。我用一句话区分:代理服务放在客户端那边,帮客户端访问它访问不到的资源,叫正向代理;代理服务放在服务器那边,帮服务器接收并转发请求,叫反向代理。
看下面的对比:
正向代理: 客户端 → 正向代理服务器 → 目标服务器(比如某个外部网站) (客户端知道自己在用代理,代理替客户端转发请求) 反向代理: 客户端 → 反向代理服务器(Nginx) → 后端服务器(比如Tomcat、Node服务) (客户端不知道自己连的是代理,它以为Nginx就是最终服务器)Nginx 最核心的用途就是反向代理。外部客户端请求到 Nginx,Nginx 按配置把请求转发给内网里的业务服务器,再把响应返回给客户端。这样做有实际好处:后端服务器不用暴露公网 IP,便于统一做拦截、限流、日志、HTTPS 证书管理,还能顺手做负载均衡。前端同学常见的“跨域问题”,多数时候也可以用 Nginx 反向代理解决,原理就是让浏览器以为请求发给了同源的 Nginx,再由 Nginx 转发给真正的后端接口。
1.3 负载均衡的几种策略与选择逻辑
反向代理最常见的配套能力就是负载均衡,也就是把请求按一定策略分发给多台后端服务器。Nginx 原生支持的策略主要有以下几种:
| 策略 | 默认/配置方式 | 特点与适用场景 |
|---|---|---|
| 轮询 | 默认 | 请求依次分发给每台后端,适合后端配置相同的场景 |
| 加权轮询 | weight=2 | 按权重分配比例,适合机器性能有差异的场景 |
| ip_hash | ip_hash; | 按客户端IP计算哈希,同一IP固定落到同一台后端,适合需要会话保持的场景 |
| least_conn | least_conn; | 优先分给当前活跃连接数最少的后端,适合后端负载不均明显的场景 |
| fair(第三方) | fair; | 按后端响应时间分配,响应快的多分,需要额外模块支持 |
我实际项目中用最多的是加权轮询和ip_hash。比如两台后端机器性能差别不大就默认轮询,其中一台是新上线的机器,先给个weight=3让它多承担一些流量观察稳定性。涉及登录态的旧项目没有做共享 Session,就用了ip_hash,简单粗暴解决会话一致性问题。后来改造完会话集中存储,又切回了轮询。
需要注意:ip_hash并不一定平均分配,某些大出口IP下会有“倾斜”效果,不要把它当成严格均衡方案。
1.4 server、location、upstream:配置语境里的三个“容器”
这三个词常被混在一起提,但它们承担的角色完全不一样。我习惯用“城市-小区-房间”来类比:
server是虚拟主机,对应一个“小区”。它靠listen(端口)和server_name(域名)区分不同站点,比如一个 Nginx 可以同时托管aaa.com和bbb.com,各自写一个server块。location是server内部的路径匹配规则,相当于“房间”。它专门负责把不同的 URL 路径分流到不同的处理方式,比如/api/走代理、/static/走本地文件。upstream是后端服务器组,对应“通信录里的联系人列表”。它定义一组后端地址,供proxy_pass调用。
举个例子:
upstream backend_servers { server 127.0.0.1:8081; server 127.0.0.1:8082; } server { listen 80; server_name api.example.com; location /api/ { proxy_pass http://backend_servers; } }这里upstream定义了两个后端地址,server定义了对外服务的虚拟主机,location负责把/api/开头的请求交给proxy_pass转发。三个容器各管一段,理解了这一点,配置文件的逻辑就清晰了一大半。
2. 常用命令:不要只记得nginx -s reload
2.1 启动、停止与重启的信号逻辑
Nginx 的命令行设计是基于信号机制的,很多人只会nginx -s reload,但理解信号后所有命令都好记了。先看最常用的几个:
nginx # 启动,读取默认配置文件(/etc/nginx/nginx.conf) nginx -c /path/to/nginx.conf # 指定配置文件启动 nginx -s stop # 立即停止,等同于强制退出 nginx -s quit # 优雅停止,处理完当前请求再退出 nginx -s reload # 平滑重载配置 nginx -s reopen # 重新打开日志文件stop和quit的区别值得单独说。stop是直接终止进程,正在处理的请求会断掉;quit会等 worker 进程把当前连接处理完再退出。生产环境尽量用quit,但stop在配置文件写法有问题、急需止血时反而更快。
实际工作中,reload是最常用的运维操作。它不中断服务,原理是:master 进程收到 reload 信号后,重新读取配置文件并创建新的 worker 进程;新旧 worker 短暂共存,旧 worker 处理完手头请求后自动退出。所以你在线上新增一个location或修改反向代理地址后,直接nginx -s reload就行,不用停服,用户体验完全不受影响。
2.2 配置检查:nginx -t是好习惯,-T是排错利器
修改完配置直接 reload 是有风险的。语法错误会导致 reload 失败,更麻烦的是如果用自己的方式在错误配置文件下启动,可能整个服务起不来。所以不管改多小的内容,都建议先检查再重载:
nginx -t # 检查默认配置文件的语法 nginx -t -c /path/to/nginx.conf # 检查指定配置文件 nginx -T # 显示所有配置内容并做语法检查nginx -t输出syntax is ok和test is successful就说明语法没问题,可以放心 reload。如果报错,比如unknown directive "xxxx",说明你某个指令写错了或者缺少对应模块,先检查拼写,再看这个指令是不是被编译进当前 Nginx。
nginx -T我强调一下:它会输出 Nginx 完整加载后的配置,包括所有 include 进来的子文件内容。配置文件多且分散时,可以直接用一条命令看到最终生效的全体配置,排查“我明明改了为什么没生效”这类问题特别有用。我之前排查过一个诡异现象:改的是/etc/nginx/conf.d/下的某个文件,但实际 Nginx 加载的是/usr/local/nginx/conf/里的另一个同名文件,就是靠-T一眼看出加载来源不对。
2.3 日志重开:日志切割后必须要做的事
nginx -s reopen是很多人忽略的命令,但它对应一个非常重要的场景——日志切割。
假设你按天归档日志,到凌晨 00:00 把access.log重命名成access_20241120.log,此时 Nginx 进程仍然持有原来那个文件的文件描述符,会继续往已改名的文件里写日志,新的access.log文件里反而什么都没有。这时候执行:
nginx -s reopenmaster 进程会重新打开access.log并写入。用 crontab 做日志切割的同学,脚本里一定记得把nginx -s reopen加上,否则日志永远切不干净。
2.4 systemd 语境下的命令与直接信号的区别
如果你用的是systemctl管理体系(现在大多数发行版默认如此),更推荐习惯这样:
systemctl start nginx systemctl stop nginx systemctl restart nginx systemctl reload nginx systemctl status nginxsystemctl reload nginx和nginx -s reload本质上效果一样,都是平滑重载。区别在于 systemd 会跟踪服务状态,restart是停掉再启动,存在短暂中断;systemctl reload则走平滑路径。另外,直接用nginx -s reload的前提是你能直接执行这个命令,而且 nginx 二进制在 PATH 里;用 systemd 管理时,配置文件和二进制路径通常更可控。
还有一种是手动用 kill 发送信号:
kill -HUP $(cat /var/run/nginx.pid) # 平滑重载 kill -TERM $(cat /var/run/nginx.pid) # 优雅退出Nginx 启动后会把 master 进程的 PID 写入 pid 文件(通常是/var/run/nginx.pid),用kill配合信号本质上和nginx -s是一样的。理解这点后,即使哪天nginx -s reload报找不到 pid 文件,你也能自己按信号来处理。
3. 配置文件逐块拆解:一条主线读明白nginx.conf
Nginx 配置文件表面看是一层层的花括号嵌套,实际可以按这条主线去读:全局配置 → events → http → server → location。每一层只关心自己的职责,别混在一起理解。
一个典型的配置骨架长这样:
user nginx; # 全局块:worker进程运行身份 worker_processes auto; # 全局块:worker进程数 events { # events块:连接模型设置 worker_connections 1024; } http { # http块:所有HTTP相关配置 include mime.types; sendfile on; server { # server块:虚拟主机 listen 80; server_name example.com; location / { # location块:路径匹配 root html; index index.html; } } }下面逐块看每个关键项的含义和为什么这么配。
3.1 全局块:运行身份和 worker 数量的起点
全局块是配置文件最外层、events之前的那些指令。最需要关注的三个:
user:指定 worker 进程以什么系统用户运行。默认可能是nobody或nginx。这里藏着一个常遇到的坑:master 以 root 启动,worker 以 user 指定的身份运行。如果你的root目录或者日志文件权限只有 root 能读,而user nginx没权限读取,访问就会出现 403。我之前就遇到过:root /data/www;里的文件权限是700,属主是 root,Nginx worker 没有读权限,页面一直 403,排查了半小时才想起权限问题。
worker_processes:进程数。建议设为auto让 Nginx 自动按 CPU 核心数创建,或者手动填核心数。这个值不是越大越好,过大会消耗内存(每个 worker 都要复制一份配置和模块),且无谓增加调度开销。
pid:master 进程号写入路径,默认/var/run/nginx.pid。改动它要谨慎,因为 systemd、日志切割脚本、监控脚本往往都靠这个文件找进程。
error_log:全局错误日志路径和级别,常见级别debug | info | notice | warn | error | crit。生产环境建议warn或error,排查问题临时调到notice甚至debug可以获得更多信息,但 debug 会产生海量日志,别长期开启。
3.2 events 块:连接模型与并发上限
events块主要影响 Nginx 如何处理网络连接。核心是两个指令:
worker_connections:每个 worker 进程能同时保持的最大连接数,默认 1024。Nginx 的最大并发连接数大致是:
最大并发 ≈ worker_processes × worker_connections但这个公式只对“Nginx 直接返回静态文件、不做反向代理”基本成立;如果做反向代理,一个请求往往要占用两个连接(客户端到 Nginx,Nginx 到后端),实际并发能力要打个折扣。比如worker_processes 4、worker_connections 1024,则理论上限约 4096,反向代理场景下可能实际 2000 左右。真要支撑上万的并发,要么调大worker_connections,要么增加 worker 数量,还要检查系统级文件描述符限制,比如ulimit -n,否则worker_connections配置再大也被系统限制卡住。
use epoll:在 Linux 上用 epoll 事件模型处理连接,是高效并发的关键。现代 Nginx 大多会自动选最优模型,一般不用手动写,但了解它有助于理解“为什么 Nginx 的并发能力比 Apache 默认模式下强很多”——本质上就是事件驱动 + 非阻塞 I/O,而不是每个连接一个进程。
3.3 http 块:公共功能开关区
http块是整个配置最庞大的部分,所有server的公共设置都在这里。常见项逐个记:
include mime.types;:引入 MIME 类型映射表,核心作用是让浏览器正确识别文件类型。少了它,访问.css、.js文件时浏览器可能当成纯文本下载或直接显示乱码。
sendfile on;:开启内核态零拷贝,把文件从磁盘发给客户端时减少用户态和内核态之间的数据拷贝,大文件传输性能明显提升。静态资源站点基本都会开。
keepalive_timeout 65;:客户端连接的空闲超时时间。设太短,频繁建立新连接增加开销;设太长,空闲连接占用资源。默认 60 秒左右够用。
gzip on;和gzip_types:开启响应压缩,对文本类资源效果显著,CSS/JS 经常能压到原来的三分之一。但要注意:图片、视频类文件本身已压缩,开了反而浪费 CPU,所以gzip_types只列文本类就好。
log_format:定义访问日志格式。我常用的精简格式是这样的:
log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent"';然后在server里access_log /var/log/nginx/access.log main;引用。日志字段不是越多越好,字段多了日志文件膨胀快、清洗也麻烦,按团队实际需求取舍。
3.4 server 块与 location 块:虚拟主机与匹配规则
server块里核心是listen和server_name的配合。listen 80;监听的端口,server_name绑定域名。一个 Nginx 可以写多个server,靠这两个字段区分不同的网站入口。当多个server的listen相同,Nginx 会按server_name的匹配优先级选虚拟主机。
location的匹配规则是另一个容易出问题的地方。我尽量用“匹配优先级”来总结:
location = /xxx精确匹配,优先级最高,匹配到后不再继续。location ^~ /static/前缀匹配,且命中后不再检查正则。location ~ /xxx和location ~* /xxx正则匹配,按配置顺序匹配,区分大小写/不区分大小写。- 普通字符串前缀匹配,按最长匹配优先。
location = /healthz { return 200 'ok'; # 精确匹配健康检查路径 } location ^~ /static/ { alias /data/static/; # 前缀匹配静态资源,命中后不再走正则 } location ~* \.(png|jpg|css|js)$ { expires 30d; # 正则匹配常见静态文件,设置长缓存 } location /api/ { proxy_pass http://backend_servers; proxy_set_header Host $host; }其中alias和root的区别非常容易踩坑。root会把完整的 URL 路径拼在指定目录后面,比如:
location /static/ { root /data/www; }请求/static/a.css实际查找的是/data/www/static/a.css,注意路径里带了/static/。而alias是替换匹配的部分:
location /static/ { alias /data/files/; }请求/static/a.css实际查找的是/data/files/a.css,/static/被替换掉了。很多人把两者混用,导致文件 404 或者路径不对,学会先看“URL 路径是否保留”来判断该用哪个。
4. 配置中最容易翻车的几个点与排查思路
配置文件不报错不等于配置正确,业务的问题往往发生在运行期。以下是我实际踩过或者帮别人排查过的高频问题。
4.1proxy_pass末尾带不带斜杠,效果完全不同
这是反向代理里最容易犯的错。看两个写法:
# 写法A:不带URI location /api/ { proxy_pass http://backend_servers; }请求/api/user/info转发后,后端收到的路径还是/api/user/info。
# 写法B:带 URI(末尾斜杠) location /api/ { proxy_pass http://backend_servers/; }请求/api/user/info转发后,Nginx 会把location匹配到的/api/部分替换成/,后端收到的是/user/info。
简单说:proxy_pass后面带路径(含/)就会替换匹配部分,不带路径就保留原始 URI。但如果是location = /xxx这类精确匹配,或者location用正则匹配,则proxy_pass通常不能带 URI。实际项目里出现“接口 404”“路径多了一层”基本都是这里没理清楚。排查方法:后端接口用假路由打印一下收到的 URL,或者直接看 Nginx access log 里的$request字段,能明显看到转发前后路径差异。
4.2reload没生效:检查 include 和缓存
改完配置文件执行nginx -t通过、nginx -s reload成功,但访问效果没变。这种情况最常见的两个原因:
第一个是主力配置文件的include被覆盖。比如你改的是/etc/nginx/conf.d/xxx.conf,但主配置里 include 的是另一个目录;或者有两份配置文件同时定义了同一个server_name,Nginx 按加载顺序选了第一份。用nginx -T一次性看全量配置,快速定位实际加载的是哪一份。
第二个是浏览器缓存。静态资源返回了expires 30d;之类的长缓存头,后端文件已经换了,浏览器还直接读本地缓存,看起来像“Nginx 没生效”,其实是缓存策略太激进。排查时先强制刷新(Ctrl+Shift+R),再用curl -I看响应头里的Cache-Control、Last-Modified字段确认。
4.3 端口冲突
启动或者 reload 时收到bind() to 0.0.0.0:80 failed (98: Address already in use),说明端口被占用。先用ss -lntp | grep 80或lsof -i:80找到占用进程,要么停掉它,要么换个端口。在容器环境里,尤其要确认多个 Nginx 容器是否映射了同一个宿主机端口。
4.4 502 与 504 的排查方向
502 Bad Gateway通常是 Nginx 连不到后端,504 Gateway Timeout是连到了但后端超时没返回。
排查 502 的顺序:先检查后端进程是否存活,再看 Nginx 到后端的网络连通性(curl一下后端地址),再确认proxy_pass里写的地址和端口与后端实际监听是否一致。我碰到过一次很隐蔽的 502:后端进程还在,但监听的 IPv6 地址,Nginx 配置里写的是 IPv4 地址,连不上。后来在upstream里改成server [::1]:8080;才解决。
排查 504 则要看后端处理耗时。如果是大文件上传或者某些慢查询接口,需要调整proxy_read_timeout、proxy_send_timeout。默认 60 秒往往不够,要结合业务场景调大,但不能无限调大,因为后端真卡死时也得靠超时触发告警。
4.5 403 不只是权限问题,也可能是目录索引被关
遇到 403,先检查user配置的用户对root或alias目录有没有x权限。Linux 对目录的 x 权限代表“能否进入目录”,没有 x 权限连读取文件列表都不行。可以用ls -ld /data/www先看一眼。另外,如果你的location /配置了autoindex on;,但当前目录没有索引文件(index.html)也没开启目录列表,也会出现 403。此时要么补一个索引文件,要么开启autoindex on;允许浏览目录。
4.6 日志是排错的第一入口,其次是curl -I
排查任何 Nginx 问题,我建议的顺序永远是:看错误日志 → 看访问日志 → 抓请求响应。错误日志默认位置是/var/log/nginx/error.log,这里有启动失败、权限问题、worker 异常、上游连接失败等关键信息。访问日志能确认“请求到底有没有到 Nginx、返回了哪个状态码”。
curl -I http://你的域名/xxx能帮你快速复现问题,而且能直接看到响应头。比如返回的Content-Type不对、Location跳转目标不对、缓存头异常,一下就能定位到是哪一层配置出的问题。实在搞不定,临时把错误日志级别调到debug看 Nginx 每步处理的详细过程,但用完记得调回来,不然磁盘撑不住。
5. 我的一些配置习惯,可能对你有用
关于配置文件管理,我的习惯是:主配置只保留全局设置和include,所有业务相关的server全部拆到/etc/nginx/conf.d/下的独立文件里,命名按业务来,比如shop.conf、pay.conf。这样改一个站点不会影响其他站点,reload 出问题也容易回滚,直接用 Git 管理整个/etc/nginx/目录,每次改动都留痕。这个习惯看起来不起眼,但在多业务共用一个 Nginx 的时候能省很多事。
如果刚开始接触 Nginx,建议先在自己电脑上装一个,用nginx -T和nginx -t反复改配置看结果,亲手复现一遍“改错配 403”“reload 不生效”这类问题,比看十篇教程都有用。等能熟练看懂error.log里的每一行报错,Nginx 基本就算真正掌握了。