作为一名常年跟线上服务打交道的人,我对 Nginx 的感情一直很复杂。一方面,静态部署、反向代理、负载均衡这几件事,随便拿出来一个都不算难,网上教程一把一把的;另一方面,真到了线上出问题的时候——SSL 证书换了不生效、浏览器报net::err_cert_common_name_invalidd、后端接口一会儿通一会儿不通——很多人对着配置文件就是一脸懵。经常有人问我:配置都照抄了,为什么还是不行?问题多半出在你根本没理解 Nginx 处理一个请求时走的是哪条链路。这篇文我不打算给你念文档,而是把从静态部署到反向代理再到负载均衡这条线的核心知识点拆开讲透,顺便把我踩过的坑、排查过的线上事故一起交代清楚。适合刚入门想系统搞懂 Nginx 的开发者,也适合被线上故障逼着来补课的同学。
1. 先搞懂 Nginx 收到请求后到底做了什么
很多人配置 Nginx 全凭记忆,server、location、proxy_pass这些指令都会写,但从来没人告诉他这些指令之间是怎么协作的。结果就是配置能跑,但稍微改一点就翻车。这一节我们先把这个地基打牢——理解 Nginx 的请求处理模型,后面所有配置和排错才有依据。
1.1 server 块:监听端口与虚拟主机匹配
一个请求到达 Nginx 时,第一件事是看listen。这个指令决定了 Nginx 在哪个 IP 和端口上接收流量,比如listen 80;就是监听所有网卡的 80 端口,listen 443 ssl;是监听 443 并启用 SSL。同一台机器上可以写很多个server块,每个server监听不同的端口或 IP,也可以监听同一个端口但通过server_name区分——这就是虚拟主机(Virtual Host)的概念。
server_name的匹配规则很多人搞不清楚,其实优先级是:精确匹配 > 左侧通配符(*.example.com)> 右侧通配符(www.example.*)> 正则匹配(~^www\.\d+\.example\.com$)> 默认 server。如果所有server_name都没匹配上,Nginx 会使用这个端口上第一个定义的server块,或者显式声明listen 80 default_server;的那个。这个"默认"规则很容易被忽略,我遇到过不止一次:新增了一个server块后流量全部跑到了旧站点,就是因为新块写在了前面,成了隐式 default_server。
再说一个容易被带偏的点:server_name如果你写了localhost,那就只有浏览器地址栏输入 localhost 才会命中;你拿本机 IP 访问,会直接落到默认 server。所以本地开发要多站点调试(比如热搜里那个"本地+虚拟机 多端口 nginx 开发环境多站点自定义域名配置"),本质上就是靠listen不同的端口或server_name不同的域名来区分站点,然后在 hosts 文件里把自定义域名指到 Nginx 所在机器。你看到 1panel 这类面板图形界面上配置"多个网站反向代理",底层生成的就是一组server块,没什么神秘可言。
1.2 location 匹配:不是"从上到下找第一个",而是"按规则优先级"
location是 Nginx 配置里最容易被低估的指令。很多初学者的理解是"请求进来后,从上到下找第一个匹配的 location 就完事了",这个理解在部分场景下凑巧成立,但完全不是 Nginx 的规则。真正的匹配分了两轮:先比普通前缀,再比正则。
普通前缀分两种:不带修饰符的location /api和带^~修饰的location ^~ /api;正则也有两种:区分大小写的location ~ /api和不区分大小写的location ~* /api;还有精确匹配location = /api。匹配顺序是这样的:
| 优先级 | 匹配类型 | 说明 |
|---|---|---|
| 1 | =精确匹配 | 完全一致才命中,优先级最高,命中后立即停止 |
| 2 | ^~前缀匹配 | 普通前缀中找到最长匹配项,且命中后不再检查正则 |
| 3 | 正则匹配~/~* | 按配置文件里的书写顺序逐个匹配,第一个命中的生效 |
| 4 | 普通前缀匹配 | 取最长匹配项,但仍会继续检查正则;若正则没命中,使用该前缀 |
| 5 | /兜底 | 以上都未命中时使用 |
看到没?普通前缀并不是"哪个写在前面用哪个",而是"哪个匹配得长用哪个";而正则恰恰相反,是"哪个写在前面用哪个"。我举个例子你就明白了:请求GET /static/js/app.js,配置里有location /和location /static/两个普通前缀,Nginx 会选/static/,因为它更长、更具体;如果你还配了location ~* \.js$,那正则会在前缀匹配之后参与竞争,一旦正则命中,就用正则的结果。
这个机制带来的坑是:正则 location 一旦写多了,顺序就变成了一件极其敏感的事。比如有人写了location ^~ /api/又写了location ~ /api/v1/,本来前缀优先,但如果在后面配正则时会继续检查正则……不对,^~命中后连正则都不看了。如果想让某个前缀彻底隔离正则干扰,^~就是你要的修饰符。
1.3 一个请求最终落到了哪个处理分支
把 1.1 和 1.2 串起来,一个请求在 Nginx 内部的完整路径大概是:先按listen找到对应的监听端口,然后在监听端口下按server_name找到合适的server块,接着在server块里按location匹配规则找到对应的处理分支。这个分支要么是静态文件指令(root、alias、try_files),要么是反向代理指令(proxy_pass),也可能是返回指令(return)。
我个人建议你在排查任何 Nginx 问题时,都在心里过一遍这三步:请求落在哪个 server?落在哪个 location?这个 location 里执行的是静态还是代理逻辑?能回答这三个问题,至少一半的 Nginx 疑难杂症你已经能自己看出来。比如"访问域名打不开"——先 curl 一下看是不是连到了 Nginx(排除 DNS、防火墙),再确认是不是有匹配的 server_name,再看默认 server 是谁,路径基本就出来了。接下来两章我们就把静态和代理这两大分支分别讲透。
2. 静态部署:把配置文件写清楚,少踩 root/alias 的坑
静态部署是 Nginx 最朴素的应用,但恰恰是"朴素"两个字让人放松警惕。root、alias、index、try_files,这几个指令单看文档都能懂,组合到一起就全是戏。
2.1 root 与 alias:看起来差不多,行为差很远
这两个指令的区别是静态配置里最经典的问题。简单说:root会把完整的请求 URI 拼接到文件路径后面,alias则是把 location 匹配到的部分替换成自己指定的路径。
拿具体例子对比一下。配置:
location /static/ { root /var/www/html; }这时请求GET /static/css/app.css,Nginx 拼出的磁盘路径是/var/www/html/static/css/app.css——也就是root的值加上完整 URI。再看:
location /static/ { alias /var/www/html/static/; }同样的请求,拼出的路径是/var/www/html/static/css/app.css——/static/这个前缀被替换成了 alias 的值,后面的/css/app.css原样接上。在这个场景下两者结果一样,很容易让人以为差不多。但如果你把 alias 写成/var/www/static(末尾没有斜杠),或者 location 不是/static/而是/static(不带斜杠),拼接结果立刻变得诡异,最常见的 404 就是这么来的。
说一个我实际踩过的坑:location /static/ { root /var/www/site; }其实会访问/var/www/site/static/下的文件,而很多人以为 root 和 alias 一样,配了root /var/www/site/static/;,结果路径变成了/var/www/site/static/static/,访问全 404。所以我的习惯是:静态文件场景优先用root,并且root后面不要想当然地加上 location 的路径;只有当你明确想改变 URL 与磁盘目录的映射关系时,才使用alias,并且记住alias路径末尾的/要和 location 的/对应——location 以/结尾,alias 也以/结尾,否则拼接就会错位。
2.2 一个生产可用的静态站点配置长什么样
静态部署不只是root + index两行。真正能用、性能还过得去的静态站点,至少要考虑到缓存、压缩、目录安全和 SPA 路由回退。下面这套配置我在多个项目里直接用过,可以当模板:
server { listen 80; server_name example.com www.example.com; root /var/www/example; index index.html; gzip on; gzip_types text/plain text/css application/javascript application/json image/svg+xml; gzip_min_length 1024; location / { try_files $uri $uri/ /index.html; } location /static/ { expires 7d; add_header Cache-Control "public, no-transform"; } location ~* \.(php|sh|env)$ { deny all; } autoindex off; }几个关键点说一下。try_files $uri $uri/ /index.html;是 SPA(单页应用)的标准写法:先找真实文件,再找目录,都找不到就回退到入口页,把路由交给前端框架处理。这行配置是很多前后端分离项目"刷新页面就 404"的根治方案。location /static/里那两行是给静态资源加缓存头,让浏览器对该目录下的文件缓存 7 天,减少重复请求。deny all那段正则,是防止敏感文件被直接下载——很多人的.env、备份脚本就是这么泄露出去的。autoindex off;关闭目录列表,防止别人直接浏览你的文件目录结构。
2.3 多站点多端口:一个 Nginx 多个网站的配置思路
热搜里那组"本地+虚拟机 多端口 nginx 开发环境多站点自定义域名配置"我单独拎出来说一下。做本地开发经常需要同时跑好几个前端项目,每个项目一个端口甚至一个自定义域名,用 Nginx 统一入口比一个个起静态服务要方便得多。思路很简单:Nginx 给你开好几个server,每个server监听不同端口或者用不同的server_name,然后你在系统 hosts 文件里把这些域名指向 127.0.0.1(或虚拟机 IP)。
server { listen 8081; server_name site1.dev; root /home/dev/site1/dist; index index.html; location / { try_files $uri $uri/ /index.html; } } server { listen 8082; server_name site2.dev; root /home/dev/site2/dist; index index.html; location / { try_files $uri $uri/ /index.html; } }hosts 文件里写127.0.0.1 site1.dev site2.dev,浏览器访问http://site1.dev:8081就会打到 site1,http://site2.dev:8082打到 site2。这套逻辑跟 1panel 面板里"配置反向代理、多个网站"的原理完全一样:面板只是可视化地帮你生成server块,踩坑点依然绕不开listen、server_name、root这几个字段的匹配关系。把底层原理搞懂,你用不用面板都不会慌。
2.4 静态站 403/404 的排查顺序
静态站点最容易出两个状态码:403 和 404。403 多数是权限问题,404 多数是路径映射问题,排查顺序可以固定成下面的流程。
403 的排查:先看 Nginx 进程用户(通常是www-data或nobody)对目标目录有没有读权限和执行权限——目录至少要有r-x,文件至少要有r--;再确认目录下有没有index指定的入口文件;然后确认autoindex是不是关闭了但目录下又没有可访问的 index 文件;最后看一眼有没有被deny规则命中。用命令ls -l /path/to/dir和sudo -u www-data cat /path/to/file可以快速定位。
404 的排查:核心是确认 Nginx 拼接出来的磁盘路径跟实际文件路径是否一致。我强烈建议在server块里临时加一行error_log /tmp/nginx_debug.log debug;,重载后看日志里打印的实际文件路径,比瞎猜快得多。除此之外,root后面多拼了一层、alias末尾缺斜杠、location 前缀与目录结构不一致,都是高频原因。记住一个心法:Nginx 的 404 绝大多数时候不是文件不存在,而是你告诉它找的路径和你以为的路径不一致。
3. 反向代理:核心不是 proxy_pass 一行,而是你把握住了什么
反向代理是 Nginx 最核心的用途。很多人觉得反向代理就是写一行proxy_pass http://后端地址;,其实这一行的背后藏着 URI 传递规则、header 处理、SSL 回源、超时与长连接等一系列问题。搜索引擎里"nginx 反向代理 不生效""nginx 反向代理 ollama""net::err_cert_common_name_invalidd"这些热搜词,全是这些细节没掌握导致的。
3.1 proxy_pass 的 URI 继承规则:最容易翻车,没有之一
proxy_pass后面写不带路径和带路径,行为完全不同。这绝对是 Nginx 反向代理配置里翻车率最高的知识点。先说结论,再上例子。
规则是:如果proxy_pass后面带了 URI(哪怕只是一个/),那么 Nginx 会用这个 URI 替换掉 location 匹配到的部分,然后把剩余路径接在后面;如果proxy_pass后面不带 URI,那么原始请求 URI 会被原样转发给后端。三个例子你就懂了:
| 配置写法 | 请求 | 后端实际收到的 URI |
|---|---|---|
proxy_pass http://10.0.0.2; | GET /api/user | /api/user |
proxy_pass http://10.0.0.2/; | GET /api/user | /user |
proxy_pass http://10.0.0.2/server/; | GET /api/user | /server/user |
看到区别了吗?第一行原样转发,第二行把/api替换成了/,第三行把/api替换成了/server。很多人的后端接口写着/api/user,Nginx 配了proxy_pass http://backend/;,结果后端收到的是/user,接口 404,然后开始怀疑后端代码有 bug,折腾大半天——这个场景我见过不下五次。
还有两个衍生坑要记住。第一,如果 location 用的是正则匹配(~、~*),proxy_pass后面不允许带 URI,带了 Nginx 直接报错并拒绝启动,因为正则匹配的不确定性让 Nginx 无法确定从哪里开始替换。第二,proxy_pass里的域名后面跟了 URI 时,nginx 会忽略 location 前缀本身,你配置里的 location 前缀在转发时会被"啃掉",所以凡是希望"保留原始路径"的场景,proxy_pass的 URL 就不要带尾斜杠,也不要带任何路径。
3.2 头部信息:Host 与 X-Forwarded-* 为什么必须配
反向代理的本质是"中间人",后端服务默认只知道请求来自 Nginx 的 IP,不知道真实客户端是谁,也不知道客户端访问的是哪个域名。一旦涉及日志分析、IP 限流、HTTPS 跳转、多域名共用一个后端,你就必须显式传递头部信息。我常用的四行配置如下:
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_set_header X-Forwarded-Proto $scheme;逐个解释。Host $host;把客户端请求里的 Host 传给后端,否则后端拿到的 Host 是proxy_pass里的 IP:端口,很多基于域名的路由(比如 Spring Cloud Gateway、Kubernetes Ingress 后端)会直接路由失败。X-Real-IP $remote_addr;是 Nginx 这一跳看到的最真实客户端 IP,因为 Nginx 就是直接面对客户端的那个。X-Forwarded-For $proxy_add_x_forwarded_for;会在已有 XFF 基础上追加当前$remote_addr,这样后端拿到的就是一整条代理链上的客户端 IP 记录。X-Forwarded-Proto $scheme;告诉后端"用户原本用的是 http 还是 https"——如果你不配这一行,后端做强制 HTTPS 跳转时看到的一直是 http,就会陷入无限重定向。
这里顺带纠正一个高频率的误解:$host和$http_host不一样。$host优先取请求行里的主机名,如果没有则取 HOST 头,且会去掉端口号;$http_host百分百取 HOST 头原值,带端口就带端口。在绝大多数场景下用$host更符合直觉,不容易搞出端口不匹配的问题。
3.3 HTTPS 回源:证书校验与 SNI 的那些事
反向代理遇到 HTTPS,坑一下就变多了。最常见的场景有两种:一种是你自己对外提供 HTTPS 服务,收到 443 的 HTTPS 请求后反代给内网的 HTTP 服务——这种只需把证书配置在前端 server 块,回源不需要加密;另一种是你的后端服务本身也是 HTTPS,Nginx 需要以 HTTPS 客户端去访问后端,这种就要处理证书校验和 SNI。
先讲第二种,因为它和热搜里的net::err_cert_common_name_invalidd直接相关。Nginx 默认用http://访问上游时不会去校验上游证书,但这不代表它会把 SNI(Server Name Indication,TLS 握手里告诉服务器"我要访问哪个域名"的字段)正确发过去。如果你的proxy_pass写的是 IP(比如https://192.168.1.10:8443),而后端服务器上的证书是为某个域名签发的,TLS 握手时 Nginx 没有发送 SNI,后端就不知道应该下发哪个证书,可能发了一个默认的、跟实际访问域名不匹配的证书,浏览器立刻报err_cert_common_name_invalidd。解法是加上:
location /api/ { proxy_pass https://192.168.1.10:8443; proxy_ssl_server_name on; proxy_ssl_name api.internal.example.com; }proxy_ssl_server_name on启用 SNI,proxy_ssl_name指定 SNI 里应该带的名字,也就是后端证书的域名。如果是自签名证书要跳过校验,再加proxy_ssl_verify off;。这套配置也是本地跑 Ollama 这类 AI 服务做反向代理时绕不开的细节——你通过 Nginx 暴露 Ollama 服务、在 CherryStudio 这类客户端里填 API Key 时,如果遇到证书校验失败,先检查的就是这几个参数。
3.4 超时与长连接:流式接口为什么总是断
反向代理场景下,"请求超时"三个字背后可能是完全不同的阶段。需要分清三个超时参数:
| 参数 | 作用 | 默认值 | 合理调整 |
|---|---|---|---|
proxy_connect_timeout | 与上游建立 TCP 连接的超时 | 60s | 内网可保持默认 |
proxy_send_timeout | 向下游(客户端)发送数据的超时 | 60s | 大文件下载可调大 |
proxy_read_timeout | 等待上游响应的超时 | 60s | 流式接口建议调到 300s 以上 |
AI 推理、流式返回、长轮询这类接口最典型的故障就是:前端页面转圈转着转着就报 504,Nginx error.log 里出现upstream timed out。原因多半是默认的proxy_read_timeout只有 60 秒,而上游模型推理超过 60 秒没有产生任何响应,Nginx 就主动断开了。流式场景建议把proxy_read_timeout调到 300s 甚至更高。另外注意proxy_buffering off;在流式场景里的作用——它的本意是让 Nginx 不要缓冲整个上游响应,而是边收边传给客户端,对 SSE(Server-Sent Events)类接口几乎是必选项,否则客户端收不到实时事件。
还有一个热搜词是"nginx mirror 超时时间"。mirror指令可以把请求复制一份发给另一个上游做流量镜像或录制,重点是:镜像请求的超时或失败不应该影响主链路。如果配置了 mirror 后主接口变慢或挂掉,先查 mirror 上游能不能在规定时间内响应——镜像请求默认也在走代理超时逻辑,你把proxy_read_timeout调大了,镜像请求同样会跟着放大等待时间。
4. 负载均衡:从轮询到真实流量的分配
负载均衡可以说是 Nginx 最出圈的能力之一。upstream模块写起来就那么几行,但"把流量分发给多个后端"这件事,策略选错、健康检查缺失、连接数算错,都会从隐性小坑变成线上大事故。这一节把 upstream 的常用配置和决策逻辑给你讲透。
4.1 upstream 基础配置与转发机制
一个标准的负载均衡配置分为两部分:先定义上游服务器组,再在location里通过proxy_pass引用它。看这个例子:
upstream web_cluster { server 192.168.1.10:8080 weight=3 max_fails=2 fail_timeout=10s; server 192.168.1.11:8080 weight=2 max_fails=2 fail_timeout=10s; server 192.168.1.12:8080 backup; } server { listen 80; server_name example.com; location / { proxy_pass http://web_cluster; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }upstream块里每个server可以带一系列参数:weight表示权重,默认 1;max_fails和fail_timeout组合实现故障摘除——比如max_fails=2 fail_timeout=10s的意思是 10 秒内失败 2 次就把该节点标记为不可用,10 秒后再试探恢复;backup表示备用节点,正常节点全部不可用时才接管流量;down表示永久下线。注意这些参数是配合"被动健康检查"工作的,也就是 Nginx 是在转发请求发现失败后才摘除节点,而不是主动探测。
4.2 四种均衡策略怎么选
upstream默认的负载策略是轮询(round-robin):请求依次分发给每个后端,权重高的多分发。但真实流量并不是永远均匀的,于是 Nginx 又给了几种策略:
| 策略 | 写法 | 适用场景 |
|---|---|---|
| 轮询 | 默认 | 后端无状态、机器性能均衡 |
| 加权轮询 | server ... weight=3 | 机器性能差异明显时 |
| 一致性哈希 | hash $request_uri consistent; | 需要将同一请求固定到同一后端,比如缓存命中友好 |
ip_hash | ip_hash; | 需要会话保持,但注意前端统一入口时会分发不均 |
| 最少连接 | least_conn; | 后端处理耗时差异大、长连接请求多 |
ip_hash是我最想提醒的一个。它按客户端 IP 的哈希结果分发,看起来能解决会话保持,但如果你前面还挂了一层 CDN 或另一个负载均衡,所有请求的$remote_addr都来自同一个出口 IP,ip_hash就退化成了"所有请求全打到一个后端",负载完全失衡。真正要解决会话保持,优先考虑让应用层用 Redis 存 session,或者后端自己做粘性会话,Nginx 层的ip_hash只适合小规模、纯直连的场景。
least_conn适合后端处理耗时差异大的场景。比如三个后端里一个处理慢、两个处理快,轮询会让慢节点堆积请求,least_conn会优先把新请求分给当前活跃连接最少的节点,整体吞吐更均衡。
4.3 健康检查与故障摘除:被动检查的局限
前面说了max_fails和fail_timeout属于被动健康检查。它的优点是零额外成本,缺点是只有"转发到坏节点失败"之后才知道节点坏了,而且失败次数和摘除时间都需要靠配置调优。比如某个接口偶发超时,如果你的proxy_next_upstream配置比较激进,Nginx 会把同一个请求重试给下一个节点,这虽然提高了成功率,但也意味着后端可能收到重复请求。
proxy_next_upstream的默认值是error timeout,也就是只有连接错误和超时才尝试下一个节点。如果后端返回 502、503、504,默认不会重试。你可以显式扩展,但一定要想清楚重试的幂等性:
location /api/ { proxy_pass http://web_cluster; proxy_next_upstream error timeout http_502 http_503 http_504; proxy_next_upstream_tries 2; }POST 请求尤其小心:如果你的请求已经到达后端并且后端已经开始处理,此时发生了超时,Nginx 重试到下一个节点,下一个节点又处理一遍,就可能出现重复下单、重复扣款这类事故。幂等接口可以放心配重试,非幂等接口尽量把重试范围缩小,甚至不配。
4.4 worker 与连接数:反向代理的 TCP 最大连接数怎么算
热搜里有"nginx 作为反向代理 tcp 最大连接数",这是性能调优绕不开的问题。先给结论:单个 worker 能维持的最大并发连接数由worker_connections决定,整个 Nginx 的最大并发连接数约等于worker_processes * worker_connections,但在反向代理模式下,每个请求会占用两条连接——一条是客户端到 Nginx,一条是 Nginx 到上游——所以能处理的并发请求数约为这个数字的一半。
worker_processes一般设成auto或 CPU 核心数,worker_connections默认 1024,一般会调大到 4096 或 10240。但调大之前要确认两件事:一是系统文件描述符限制,Linux 上用ulimit -n查看,Nginx 的 worker 进程实际能打开的文件描述符受这个值约束;二是内核参数,尤其是net.core.somaxconn和net.ipv4.ip_local_port_range,后者决定 Nginx 作为客户端向上游发起连接时可用的本地端口范围,如果同时并发连接数过高,端口耗尽就会出现Cannot assign requested address。
在上游配置里加keepalive也是减少连接开销的关键手段:
upstream web_cluster { server 192.168.1.10:8080; keepalive 32; } server { location / { proxy_http_version 1.1; proxy_set_header Connection ""; proxy_pass http://web_cluster; } }这里有两步缺一不可:proxy_http_version 1.1让 Nginx 用 HTTP/1.1 与上游通信(HTTP/1.0 默认不支持长连接),proxy_set_header Connection ""是为了清空请求头里的Connection: close,让上游不要主动关闭连接。两个一起配,Nginx 到上游的 TCP 连接才能复用,高并发场景下的性能差距非常明显。
5. 高频故障排查:SSL 不生效、CORS 报错、超时问题
最后这一章我把搜索引擎里反复出现的几类 Nginx 故障集中讲一下。别看它们症状五花八门,根因基本都是配置细节没对上。
5.1 替换 SSL 证书不生效:先分清是没加载、还是没生效
"我把新证书放到服务器上,改了ssl_certificate的路径,也nginx -s reload了,访问还是旧证书。"这个场景的排查链路,我建议按下面来。
第一步确认 Nginx 到底加载的是哪个证书。执行nginx -T(注意大写 T)可以把当前生效的完整配置打印出来,然后 grep 一下ssl_certificate,看看实际生效的路径是不是你以为的那个路径。这一步非常关键,因为有些人改的是conf.d/下某个文件,但实际生效的配置可能来自/etc/nginx/nginx.conf里的 include 顺序,后 include 的文件覆盖了先 include 的。第二步看证书链是否完整。浏览器要求服务器返回的证书链必须包含站点证书和中间证书,如果你的ssl_certificate文件只放了站点证书而没有中间证书,浏览器会报"证书链不完整",表现也是"证书没生效"。正确做法是cat 站点证书 中间证书 > fullchain.pem,顺序不能反。第三步确认 reload 是不是真的成功了。nginx -t只检查语法,nginx -s reload让 worker 进程平滑重载配置,但如果你的 worker 进程因为某些原因没有重载成功,配置其实没生效,ps -ef | grep nginx看一下 master 和 worker 的启动时间可以判断。第四步,如果你用了 1panel 这类面板,替换证书后还要检查站点配置里绑定的证书 ID 是否切换到了新证书——面板生成的配置里,证书路径往往是动态注入的,页面上的勾选没跟上,磁盘上的文件换了也没用。
5.2 net::err_cert_common_name_invalidd 的完整排查链路
这个浏览器报错含义很明确:证书里的域名和你在地址栏敲的域名对不上。但"对不上"可能发生在三条不同的链路上。
链路一是客户端直接到 Nginx。你用https://example.com访问,Nginx 给的是证书里只有www.example.com没有example.com,就会报这个错。这最常见,也最好查:用openssl s_client -connect example.com:443 -servername example.com看看实际返回的证书主体是不是你要访问的域名。链路二是 Nginx 作为客户端访问 HTTPS 上游,前面 3.3 节说过,proxy_pass用 IP、未开启proxy_ssl_server_name,上游下发了错误的证书,浏览器看到的是"Nginx 转发链路里有一跳证书不匹配"——报错也是这个。链路三是访问了旧 IP、旧域名,比如你换了证书但 CDN 或 hosts 缓存还没刷新,浏览器把请求打到了旧的接入点。
给一个实用的排查命令组合:
curl -vI https://example.com 2>&1 | grep -i "SSL connection" openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null | openssl x509 -noout -subject -issuer -dates顺便说一句:2020 年后主流浏览器都强制要求证书必须包含 SAN(Subject Alternative Name)字段,以前那种只写 CN 的老证书,即使浏览器地址栏域名跟 CN 一致,也会报这个错。所以新申请证书时,务必确认 SAN 里把你要用的每个域名都列上。
5.3 invalid cors request:CORS 到底是在谁身上配
invalid cors request这个报错经常出现在跨域场景。前端在http://localhost:3000,后端接口在http://api.example.com,浏览器发起跨域请求时先发一个 OPTIONS 预检请求,预检通过才发真正的请求。如果这个 PR 预检在 Nginx 层面处理得不对,就会出现invalid cors request。
处理思路是在反向代理的 location 里显式处理 OPTIONS 和响应头。我常用的模板:
location /api/ { if ($request_method = OPTIONS) { add_header Access-Control-Allow-Origin $http_origin always; add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always; add_header Access-Control-Allow-Headers "Authorization, Content-Type, X-Requested-With" always; add_header Access-Control-Max-Age 86400 always; add_header Access-Control-Allow-Credentials true always; return 204; } add_header Access-Control-Allow-Origin $http_origin always; add_header Access-Control-Allow-Credentials true always; proxy_pass http://backend; }几个要点说明。$http_origin是请求里的 Origin 头,把它原样回显到Access-Control-Allow-Origin,可以做到"谁请求就允许谁",但如果你要限制跨域来源,就不能这么写,要改成具体的域名白名单。always参数是一定要加的,它的作用是即使响应码不是 200 也会带上这些响应头,否则 4xx、5xx 响应里会丢失 CORS 头,浏览器一样会报错。还有一点,add_header指令在 location 里一旦写了,会覆盖从 server 层继承下来的 add_header,如果你之前上级配过 CORS 头,内层不要漏配。
再说一下"谁身上配"的问题。Nginx 只是第一层,真正要做严谨的 CORS 校验,后端应用也要处理。Nginx 层的 CORS 能解决 80% 的开发调试问题,但如果你的接口涉及敏感操作、需要严格控制来源,后端必须自己校验 Origin。
5.4 nginx -t 通过但服务异常时的通用排查步骤
nginx -t通过只代表语法没问题,逻辑问题它一概不管。当nginx -t通过但线上服务还是异常时,我习惯按下面这张清单逐步排查:
| 症状 | 检查点 | 常见结论 |
|---|---|---|
| 配置改了不生效 | nginx -T看实际配置、检查 worker 进程时间 | 改的文件没被 include、没 reload |
| 访问返回 502 | 上游是否存活、proxy_pass的上游地址是否可达 | 上游挂了、端口写错、防火墙拦截 |
| 访问返回 499 | 客户端提前断开,看是不是超时设置太短 | 与后端处理时长不匹配 |
| 错误日志无输出 | 确认error_log级别和路径 | 默认 error.log 在/var/log/nginx/ |
| 容器内挂载配置报错 | 检查挂载目录是否存在、权限是否正确 | alpine 镜像conf.d目录需要先存在,挂载时注意subPath语义 |
容器场景我再补一句:用 nginx 官方 alpine 镜像时,很多人把宿主机配置目录直接挂到/etc/nginx/conf.d,结果容器启动报错说目录不存在或没有配置。原因是官方镜像里/etc/nginx/conf.d是符号链接,直接挂载会覆盖掉链接本身。常见的做法是先把配置目录挂到其他位置,再用自定义入口脚本或直接修改主配置里的 include 路径。这个坑在 Kubernetes 部署 Nginx 时尤其常见——你以为是 Yaml 写错了,其实就是目录挂载语义没搞清。
另外,如果你用 zabbix 这类监控工具盯着 Nginx,记得在主配置里开启stub_status模块,暴露/nginx_status端点,zabbix 才能采集到活跃连接数、请求总数这些指标。这也是很多人配置了监控模板却拿不到数据的常见原因——模板没问题,是 Nginx 没把状态页开出来。
我个人的习惯是,线上环境改动任何 Nginx 配置之前,先做三件事:备份原配置、nginx -t验语法、想清楚这次改动会影响哪个 server、哪个 location。改完之后不是直接看页面,而是先curl -I看响应头对不对。三次事故下来你就发现,Nginx 的问题基本不是"不会配",而是"配置的生效链路没搞清"。把这篇文章里讲的请求匹配顺序、URI 传递规则、SSL/SNI 的握手逻辑串起来,再遇到搜索记录里那些高频报错,你至少能知道该去看哪个文件、哪一行了。