网关层的坑都在细节里
Nginx 的配置看起来简单,出问题的地方却很集中:location 优先级、proxy_pass 的斜杠、add_header 的继承。这三处答不清,说明只抄过配置没排过障。
01先讲模型再讲配置多进程事件循环决定了它能扛多少并发、什么情况下会退化。
02三处高频陷阱要能背location 优先级、斜杠、add_header 继承,几乎每场都问。
03故障按码定性502 查可达性,504 查耗时,先看 error.log 再猜。
1
Nginx 为什么能扛住高并发★
不是"用了 epoll"这么简单
Nginx 的高并发来自三件事的组合:多进程 + 每进程单线程事件循环 + 非阻塞 IO。单线程意味着没有线程切换和锁开销,事件循环让它用极少的资源管理几万个连接。反过来,任何阻塞操作都会卡住整个 worker。
传统模型:一连接一线程
- 每个连接一个线程/进程
- 并发数受内存和线程切换限制
- 上万连接基本撑不住
- 阻塞 IO:读不到数据就干等
Nginx:事件驱动
- 少量 worker,每个都是单线程事件循环
- 一个 worker 管理数万连接
- 非阻塞 IO:没数据就先去处理别的
- 没有锁竞争,CPU 缓存友好
一个数字对比(量级示意)Apache prefork(一连接一进程) 并发几百到几千就吃满内存
Nginx(事件驱动) 单机几万连接是常见水平
原因不在"代码写得好",而在模型:
等待数据的时间被复用来处理其他连接,而不是用来阻塞线程
- worker 数一般等于 CPU 核数:多了会有进程切换和缓存失效,少了浪费核心。配置
worker_processes auto;就是按核数来。 - 为什么单线程反而快:省掉了线程创建、上下文切换和锁的开销。代价是不能有阻塞调用,一个磁盘 IO 卡住就会拖慢这个 worker 的所有连接。
- 哪些情况会退化成阻塞:访问慢速磁盘、用第三方模块同步读文件、大量小文件随机读。这时应该用
aio、加缓存层,或者干脆上 CDN。
面试怎么答Nginx 是 master 加多个 worker 的多进程模型,每个 worker 是单线程事件循环,用 epoll 做非阻塞 IO。高并发不是靠线程多,而是靠等待数据的时间被复用来处理其他连接。代价是 worker 里不能有阻塞操作,磁盘慢或者用同步模块都会拖垮整个 worker。
2
master / worker 与平滑重启★
改配置为什么不用停服务
master 只做管理(读配置、拉起 worker、处理信号),worker 真正处理请求。
reload 时 master 拉起新 worker,等旧 worker 把手上的请求处理完再退出,所以能做到零中断。master 启动读配置,创建监听 socket
→
fork worker多个 worker 共享同一个监听端口
→
worker 接请求由内核决定哪个 worker 拿到连接
→
nginx -s reloadmaster 校验新配置
→
新 worker 起用新配置开始接新连接
→
旧 worker 退处理完在途请求后优雅退出
- 多个 worker 能监听同一端口:靠的是 Linux 的
SO_REUSEPORT,内核在多个进程间分发连接,避免惊群。 - reload 不断连接:旧 worker 会先关闭监听、把在途请求做完。所以长连接(WebSocket)会一直挂着,直到超时。
- 改了 worker_processes 要重启不是 reload:worker 数量这类全局参数,reload 不生效,得
systemctl restart。
面试怎么答master 进程只负责管理:读配置、fork worker、处理信号。worker 才是真正接请求的。reload 时 master 校验新配置后拉起新 worker 接新连接,旧 worker 处理完在途请求再退出,所以不断服务。多个 worker 共享监听端口靠的是 SO_REUSEPORT,由内核分发。
3
配置结构与继承规则
上下文决定指令能不能写
Nginx 配置是树形上下文:main → events / http → server → location。每个指令只能在特定上下文里出现,写错位置会直接报
directive is not allowed here。继承规则是内层覆盖外层,同层不合并。四个关键上下文能写什么main worker_processes、user、pid、error_log(全局级)
events worker_connections、use epoll
http mime、gzip、upstream、log_format、include
server listen、server_name、ssl、location、root
location proxy_pass、try_files、add_header、rewrite
面试怎么答Nginx 配置是树形上下文,main、events、http、server、location 逐层嵌套,每个指令有它能出现的层,写错位置会直接报错。继承原则是内层覆盖外层,但同层的多个指令不会合并——这一点在 add_header 上最容易踩坑。
4
location 匹配优先级★
这道题几乎每场都问
优先级从高到低:= 精确 → ^~ 前缀且不再查正则 → ~ / ~* 正则(按配置顺序) → 普通前缀(最长匹配)。记住一条主线:精确最优先,正则次之,普通前缀最后。
◐ 可交互:输一个 URI,看哪条 location 赢
先找 =有完全相同的就立刻用,结束
→
再找 ^~最长前缀命中且带 ^~,直接用,不查正则
→
再按顺序试正则第一个匹配的正则就是结果
→
最后用最长前缀没有正则命中时,用最长的普通前缀
- 正则是有序的,前缀是无序的:多个
~之间按出现顺序取第一个匹配;多个前缀之间按长度取最长。这个区别常被答错。 - ^~ 的意义就是"别再查正则":不加它,
/static/a.png会被后面的\.(png)$抢走。 - 嵌套 location 有额外规则:内层 location 匹配的是去掉外层前缀后的剩余路径,容易写出意料之外的结果,能不用就不用。
面试怎么答优先级是先精确匹配等于号,然后是最长前缀带 ^~(命中就不再查正则),然后是正则按书写顺序取第一个匹配,最后才是普通前缀取最长。前缀之间比长度,正则之间比顺序,这两个规则不一样。^~ 的作用就是防止被后面的正则抢走。
5
proxy_pass 的斜杠★
多一个斜杠,路径就变了
规则只有一条:proxy_pass 后面带 URI(有斜杠或有路径)时,会把 location 匹配到的前缀替换掉;不带 URI 时,原样转发完整路径。这是反向代理最常见的 404 原因。
四种写法的实际转发结果请求: GET /api/user/1
location /api/ { proxy_pass http://backend; } -> /api/user/1
location /api/ { proxy_pass http://backend/; } -> /user/1
location /api/ { proxy_pass http://backend/v1; } -> /v1user/1 ← 注意这里
location /api { proxy_pass http://backend/v2/; } -> /v2/user/1
- "带 URI"指的是有路径部分:
http://backend/末尾那个斜杠也算 URI(等于根路径),所以它会把/api/替换成/。 - location 和 proxy_pass 的斜杠要配对:
location /api/配proxy_pass http://b/,替换后是/user/1;一边有斜杠一边没有就会出现/v1user/1这种拼接错误。 - 排查方法:看上游的 access log 里实际收到的路径,比盯着配置猜快得多。
面试怎么答proxy_pass 后面如果带 URI(包括单独一个斜杠),Nginx 会把 location 匹配到的前缀替换成这个 URI;不带 URI 就原样转发完整路径。所以 location 和 proxy_pass 的斜杠要么都有要么都没有,一边有一边没有就会拼出错误路径,这是 404 最常见的原因。
6
负载均衡策略
五种算法各自的适用场景
默认是轮询;weight 按权重分流;ip_hash 保证同一 IP 落到同一台(解决 session);least_conn 优先给连接数少的(适合长连接和请求耗时差异大);hash 按自定义 key 分片。
选型判断无状态服务,机器同配置 轮询
机器配置差异大 weight
有会话且没做集中存储 ip_hash(或直接的粘性 cookie)
请求耗时差异大 / 长连接 least_conn
需要按用户或租户分片 hash $key consistent
面试怎么答默认用轮询;机器配置不同用 weight 按能力分配;没做集中式 session 时用 ip_hash 保证同一 IP 打到同一台,但要注意 NAT 后面很多人会共用一个 IP;请求耗时差异大或长连接多用 least_conn。另外要配 keepalive 减少和上游的握手开销。
7
静态资源与缓存★
强缓存和协商缓存怎么配
两类缓存要分开:带 hash 的产物用强缓存(
expires 1y + immutable),入口 HTML 用协商缓存(no-cache,每次都问)。配反了就会出现"改了代码用户看不到"或"每次都全量下载"。两种缓存的行为差异强缓存(Cache-Control: max-age=31536000)
浏览器直接用本地副本,不发请求
适合:内容变了文件名也会变的产物
协商缓存(Cache-Control: no-cache + ETag/Last-Modified)
每次问服务器"变了吗",没变回 304
适合:index.html 这种地址固定但内容会变的文件
面试怎么答按文件名是否带 hash 来区分。带 hash 的 JS/CSS 配一年强缓存加 immutable,内容变了文件名也变,不会读到旧文件;入口 HTML 必须用 no-cache 走协商缓存,否则发版后用户还在用旧的 HTML,引用的还是旧 hash 的资源。这是前端发布最经典的一套配置。
8
压缩:gzip 与 brotli
开之前先看这两个参数
压缩能省 60%~80% 的传输体积,但配置错会有两个坑:对图片视频也压(无效且浪费 CPU)、gzip_comp_level 开太高(CPU 换来的收益很小)。另外压缩和 sendfile 一起用要开
gzip_static。- 不要对已压缩格式再 gzip:jpg、png、mp4、woff2 本身就是压缩过的,再压一遍体积几乎不变,纯烧 CPU。
- gzip_vary 必须开:不开的话,CDN 或代理可能把压缩版本发给不支持压缩的客户端,或者反过来,出现乱码。
- 更好的做法是构建时预压缩:产出
.gz/.br文件,Nginx 用gzip_static on直接发,省掉每次请求的压缩开销。
面试怎么答gzip 的性价比拐点在 level 5 左右,再往上 CPU 开销大增但体积几乎不降。要设 gzip_min_length 跳过小文件,开 gzip_vary 避免 CDN 缓存串版本,并且只压文本类型,图片视频不要再压。更彻底的做法是构建时就生成 .gz 和 .br,用 gzip_static 直接发。
9
HTTPS 与 HTTP/2
证书、协议、跳转三件事
HTTPS 配置本身不复杂,容易出问题的是三处:证书链不完整(浏览器报不受信任)、HTTP 没跳 HTTPS、开了 HTTP/2 但没配 ALPN。TLS 版本要禁用 1.0 / 1.1。
- fullchain 不是 cert:只配站点证书不配中间证书,PC 上可能正常、手机上就报不受信任,因为有的系统会自动补链、有的不会。
- HSTS 有"回不去"的风险:一旦下发 max-age,浏览器在有效期内强制走 HTTPS,证书出问题用户也进不来。第一次上线建议先用小 max-age。
- HTTP/2 对 HTTPS 几乎是必配:多路复用能显著减少并发请求的握手开销,但服务端推送已被主流浏览器废弃,不要再基于它做优化。
面试怎么答HTTPS 要配完整证书链,只配站点证书会导致部分客户端报不受信任;TLS 只留 1.2 和 1.3;80 端口统一 301 跳到 443。开 HTTP/2 能多路复用减少握手,但服务端推送已经废弃。HSTS 要谨慎,一旦下发在有效期内无法回退。
10
跨域与 add_header 的继承陷阱★
配了却没生效,多半是这个原因
add_header 的继承规则很反直觉:子级一旦有自己的 add_header,就会完全覆盖父级的全部 add_header,不是合并。所以 location 里加了一条,server 层的 CORS 头就全没了。- always 参数很重要:不加
always,4xx 和 5xx 响应不会带上这些头,前端拿到的错误就变成"跨域失败",掩盖了真实错误码。 - 带 Cookie 时不能用 *:
Allow-Origin: *与Allow-Credentials: true不能共存,必须回显具体的$http_origin。 - 如果后端自己也设了 CORS 头:两边都设会出现重复头,浏览器直接报错。选一边做,通常交给网关或后端更灵活。
面试怎么答add_header 的继承是"覆盖"不是"合并":子级 location 里只要写了自己的 add_header,父级 server 上的全部 add_header 都会失效。所以要么每个 location 写全,要么用 include 抽成公共文件。另外要加 always 参数,否则错误响应不带 CORS 头,前端会误报成跨域问题。
11
rewrite 与 return
能用 return 就别用 rewrite
两者都能改地址,但用途不同:return 直接返回状态码或跳转,性能最好且行为明确;rewrite 会重写 URI 并继续走 location 匹配,容易产生意料之外的循环。能定死就用 return。
常见场景的正确写法整站跳 HTTPS return 301 https://$host$request_uri; ← 用 return
旧域名跳新域名 return 301 https://new.com$request_uri;
去掉末尾斜杠 rewrite ^/(.*)/$ /$1 permanent;
老路径映射到新路径 rewrite ^/old/(.*)$ /new/$1 permanent;
内部转发不改 URL rewrite ^/api/(.*)$ /internal/$1 last;
- rewrite 写在 server 里用 last 可能死循环:重写后的路径又命中了同一条规则,Nginx 会报
rewrite or internal redirection cycle。 - if 是"邪恶的":Nginx 官方文档明确说
if在 location 里有反直觉行为(比如 if 里配 proxy_pass 会继承外层)。能用 map、try_files、return 解决的都别用 if。 - 301 会被浏览器长期缓存:调试跳转规则时先用 302,确认无误再换成 301,否则改动后用户端还在跳旧地址。
面试怎么答return 直接返回状态码或跳转,性能最好、行为最确定,整站跳转和域名迁移都用它。rewrite 会重写 URI 并继续走匹配流程,last 是重新匹配 location、break 是不再匹配,用错会死循环。原则是能用 return 就不用 rewrite,能不用 if 就不用 if。
12
限流:limit_req 与 limit_conn
漏桶和并发数两道闸
limit_req 限速率(漏桶,按请求数),limit_conn 限并发(按连接数)。两者针对的问题不同:前者防刷接口,后者防大文件下载把连接占满。两个参数的含义rate=10r/s 平均每秒 10 个请求
burst=20 允许多出 20 个排队(超过就 503/429)
nodelay 排队的立刻处理,只限平均速率
不加 nodelay 则严格匀速放行,排队的要等
- rate 是全局限流不是单用户:
$binary_remote_addr决定按什么维度限,想按用户限就用$http_x_user_id之类,但要防伪造。 - NAT 后面会误伤:一个公司出口 IP 相同,按 IP 限流会把整个公司当一个人,需要调大或改用 token 维度。
- 分布式部署时单机限流不够:三台机器的 limit_req 各自独立,实际总速率是三倍。要么在入口层统一限,要么用 Redis 做全局限流。
面试怎么答limit_req 限速率,是漏桶模型,rate 控制平均速率、burst 控制允许的突发排队数,加 nodelay 让排队的立刻处理否则会严格匀速放行。limit_conn 限单 IP 的并发连接数,用来防大文件下载占满连接。多机部署时单机限流会失效,要在入口统一限或用 Redis。
13
SPA 的 try_files
刷新 404 的标准解法
单页应用的路由是前端管的,服务器上不存在
/order/123 这个文件。所以要把"找不到的路径"统统交给 index.html——这正是 try_files 做的事。try_files 的求值顺序try_files $uri $uri/ /index.html;
1. 找 root 下是否有这个文件 -> 有就返回
2. 加上斜杠看是否是目录 -> 是目录就找 index
3. 都没有,内部转发到 /index.html(状态码 200,不是 302)
面试怎么答SPA 的路由在前端,服务器上没有对应文件,所以刷新会 404。解决方法是 location / 里用 try_files 先找文件、再找目录、最后回落到 index.html。注意静态资源目录要单独配 try_files $uri =404,否则资源 404 也会返回 HTML,报错信息会变得很难懂。
14
502 / 504 怎么查★
这两个码指向完全不同的原因
502 Bad Gateway 是上游返回了非法响应或连接被拒(进程挂了、端口不通、协议不对);504 Gateway Timeout 是上游没在规定时间内响应(慢查询、死锁、超时时间设小了)。先分清楚是哪一个,排查方向完全不同。
502 的方向
- 后端进程是否活着(端口有无监听)
- proxy_pass 的地址端口对不对
- 后端是不是崩了、OOM 被杀
- 协议不匹配(HTTP 打到 HTTPS 端口)
- 响应头过大(buffer 不够)
504 的方向
- 后端接口本身慢(慢 SQL、外部依赖)
- proxy_read_timeout 是否太小
- 负载高、线程池被占满
- 数据库锁等待
定位命令看错误日志 tail -f /var/log/nginx/error.log
"connect() failed (111: Connection refused)" -> 后端没起
"upstream timed out" -> 504,后端慢
"upstream prematurely closed" -> 后端主动断开
查端口 ss -lntp | grep 8080
直连测试 curl -v http://10.0.0.11:8080/health
看进程 ps aux | grep java / systemctl status app
- 先看 error.log 再猜:Nginx 的错误日志会明确写
connect() failed还是upstream timed out,这一条就能定性。 - 502 有一类特殊原因:后端返回的响应头超过
proxy_buffer_size,Nginx 会当成非法响应,日志里是upstream sent too big header。加个大 buffer 就好。 - 504 不要只调大超时:调大只是把问题推后,真正要查的是后端为什么慢。超时时间应该比后端 P99 略大,而不是随手设 300 秒。
面试怎么答502 是上游不可达或返回了非法响应,先查后端进程是否活着、端口通不通、协议对不对;504 是上游太慢,先看后端接口耗时和超时配置。最快的一步是看 Nginx 的 error.log,里面会直接写 connect failed 还是 upstream timed out,据此定性再往下查。
15
速答清单
十句话收口
01Nginx 为什么高并发?
多进程加每进程单线程事件循环,用 epoll 做非阻塞 IO。等待数据的时间被复用来处理其他连接,所以少量 worker 能扛几万连接。
02reload 为什么不断服务?
master 拉起新 worker 接新连接,旧 worker 处理完在途请求后退出。多 worker 共享端口靠 SO_REUSEPORT。
03location 的匹配顺序?
精确 = 优先,然后是最长前缀带 ^~(命中不查正则),再按顺序试正则,最后用最长普通前缀。
04^~ 有什么用?
前缀命中后不再尝试正则匹配,防止静态资源被后面的正则 location 抢走。
05proxy_pass 带不带斜杠的区别?
带 URI(含单独斜杠)会替换掉 location 的前缀,不带则原样转发完整路径。两边斜杠要配对,否则拼出错误路径。
06add_header 的坑是什么?
子级只要有自己的 add_header,父级的全部失效,是覆盖不是合并。另外不加 always,错误响应不会带这些头。
07带 hash 的静态资源怎么配缓存?
expires 1y 加强缓存加 immutable。入口 index.html 必须 no-cache 走协商缓存,否则发版不生效。
08SPA 刷新 404 怎么解决?
location / 里用 try_files $uri $uri/ /index.html。静态资源目录单独配 try_files $uri =404。
09502 和 504 的本质区别?
502 是上游不可达或响应非法(进程挂了、端口不通、header 过大);504 是上游超时(接口慢、负载高)。
10limit_req 的 burst 和 nodelay?
burst 是允许的突发排队数,nodelay 让排队的立刻处理只限平均速率;不加 nodelay 会严格匀速放行,排队的要等。