Nginx-14 高频面试题汇总
本篇是整个系列的收尾。前 13 篇每篇末尾都有对应主题的面试题,这里做跨主题的汇总:先给一张速查表用于突击复习,然后是分难度的问答,最后是几道综合设计题和答题框架。
1. 三十秒速查表
面试前扫一遍,能想起答案就跳过,想不起来的回去看对应篇目。
1.1 概念与原理
| 问题 | 一句话答案 | 详见 |
|---|---|---|
| Nginx 为什么快 | 多进程+单线程事件循环无锁、epoll 只关心活跃连接、内存池、sendfile 零拷贝 | 03 |
| 正向 vs 反向代理 | 正向代理隐藏客户端(客户端主动配),反向代理隐藏服务端(客户端无感知) | 01 |
| master 干什么 | 读配置、创建监听 socket、fork/监控 worker、接信号;不处理请求 | 03 |
| 多 worker 怎么共享 80 端口 | master 在 fork 前完成 listen,worker 继承 fd;或用 SO_REUSEPORT 各自 bind |
03 |
| 惊群怎么解决 | 三代方案:accept_mutex → EPOLLEXCLUSIVE(4.5+,1.11.3+ 自动)→ listen ... reuseport(最优) |
03 |
| epoll 为什么快 | fd 常驻内核红黑树不用每次全量拷贝、回调把就绪 fd 挂链表不用 O(n) 遍历、只拷贝就绪的 | 03 |
| Nginx 是单线程吗 | 每个 worker 单线程事件循环,整体多进程,可选 aio threads 线程池卸载磁盘 I/O |
03 |
| 请求处理有几个阶段 | 11 个,顺序编译期固定,与配置书写顺序无关 | 04 |
| 内存池解决什么 | 按生命周期批量分配、请求结束一次性销毁,零碎片且不会漏 free | 03 |
| reload 会丢请求吗 | 不会。老 worker 停 accept、处理完存量再退;但配置语法错会静默失效 | 01 |
| 热升级原理 | USR2 → 老 master 通过环境变量把 listen fd 传给新 master,端口从未释放 |
01 |
1.2 配置语义
| 问题 | 一句话答案 | 详见 |
|---|---|---|
| location 优先级 | = > ^~(最长) > 正则(按书写顺序) > 普通前缀(最长) |
05 |
^~ 干什么 |
前缀命中且最长时直接使用,跳过所有正则 | 05 |
root vs alias |
root 拼接(root + 完整 URI),alias 替换(换掉 location 匹配的部分) |
02 |
$uri vs $request_uri |
$uri 解码规范化、无查询串、随 rewrite 变;$request_uri 是原始完整值 |
02 |
proxy_pass 斜杠 |
后面有 URI 就替换,没 URI 就透传(一个 / 也算 URI) |
05 |
last vs break |
last 触发内部重定向重新匹配 location(≤10 次);break 不重匹配,留在当前 location |
05 |
| 为什么 “If is Evil” | if 会创建匿名嵌套 location,数组式指令父级配置全丢;只有 return/rewrite 安全 |
05 |
add_header 为什么消失 |
数组式继承「全有或全无」——子层写一条,父层同名全不继承 | 02 |
add_header 为什么要 always |
不加只在 2xx/3xx 生效,4xx/5xx 不带头 | 09 |
internal 干什么 |
只允许内部访问(子请求/内部重定向/X-Accel-Redirect),客户端直接访问 404 |
04 |
X-Accel-Redirect |
后端只鉴权、返回这个头,文件传输交给 Nginx 的 sendfile,后端立即释放 |
04 |
上游 502 为什么 error_page 不生效 |
默认透传上游错误,需要 proxy_intercept_errors on |
05 |
1.3 代理与负载均衡
| 问题 | 一句话答案 | 详见 |
|---|---|---|
为什么必须设 Host $host |
默认是 $proxy_host(upstream 名字),后端按 Host 路由会错 |
06 |
| 有哪些负载均衡算法 | 轮询、加权、least_conn、ip_hash、hash key [consistent]、random two least_conn |
06 |
ip_hash 有什么问题 |
IPv4 只哈希前三段(NAT 用户全落一台)、增删节点大面积失效、不兼容其他策略 | 06 |
| 一致性哈希解决什么 | 普通取模在 N→N+1 时几乎所有 key 重分布;consistent 只影响 1/N。缓存分片必加 |
06 |
random two least_conn 为什么存在 |
多个 Nginx 各自算 least_conn 会一起打向同一台(羊群效应),随机性打破同步 |
06 |
| 开源版怎么健康检查 | 只有被动:max_fails+fail_timeout+proxy_next_upstream。主动需 Plus/第三方模块/OpenResty |
06 |
non_idempotent 什么风险 |
POST 也会重试,后端已处理但响应超时时会重复下单/重复扣款 | 06 |
| upstream keepalive 三件套 | keepalive N + proxy_http_version 1.1 + proxy_set_header Connection "",缺一不生效 |
06 |
proxy_buffering 何时关 |
流式响应(SSE/实时日志/边下边传);也可让上游返回 X-Accel-Buffering: no |
06 |
gRPC 为什么用 grpc_pass |
HTTP/2 多路复用,四层代理只能按连接分发导致负载全落一台 | 06 |
1.4 静态、缓存、限流、TLS
| 问题 | 一句话答案 | 详见 |
|---|---|---|
| 零拷贝原理 | sendfile 让数据在内核态从页缓存直达 socket;SG-DMA 下连这次拷贝也省 |
07 |
| 开了 gzip 还有零拷贝吗 | 没有。压缩必须把数据读到用户态,两者互斥。用 gzip_static 兼得 |
07 |
no-cache vs no-store |
no-cache 可存但用前必须验证(能走 304);no-store 完全不存 |
07 |
| 发版后用户还是旧页面 | index.html 被缓存了。入口 HTML no-cache + 资源名带 hash + immutable |
07 |
SPA try_files 的隐患 |
不存在的 .js 也返回 index.html(200+HTML),浏览器报 Unexpected token '<'。静态资源单独 =404 |
07 |
levels=1:2 为什么要分层 |
单目录几百万文件时文件系统查找急剧变慢;4096 个子目录,字符取 MD5 末尾 | 08 |
use_temp_path=off 为什么重要 |
跨文件系统的 rename 会退化成完整复制,翻倍磁盘 I/O |
08 |
| 缓存击穿怎么防 | proxy_cache_lock on + use_stale updating + background_update on |
08 |
cache_bypass vs no_cache |
前者控制读(跳过缓存但仍写入),后者控制写(不存入)。全绕过要两个都配 | 08 |
rate=10r/s 是什么意思 |
每 100ms 1 个,不是每秒 10 个。同毫秒来 2 个第二个就被拒 | 09 |
burst + nodelay |
burst 定桶容量(超额排队延迟处理),nodelay 让队列内立即处理但槽位按 rate 释放。生产用这个组合 |
09 |
为什么用 $binary_remote_addr |
二进制 IPv4 只 4 字节,字符串 7-15 字节,共享内存省一半以上 | 09 |
| 怎么让白名单不限流 | map 把 key 映射成空字符串,limit_req 遇空 key 直接跳过 |
09 |
| Slowloris 怎么防 | 核心是 client_header_timeout(10s),配 client_body_timeout+limit_conn+reset_timedout_connection |
09 |
CORS 为什么要 Vary: Origin |
不加的话 CDN 会把 origin A 的响应(带 Allow-Origin: A)返给 origin B |
09 |
| 只放服务器证书会怎样 | PC 可能正常(自动补全/缓存),移动端和客户端库报错。必须用 fullchain | 10 |
| TLS 优化优先级 | 会话复用(最大)> ECDSA 证书 > TLS 1.3 > OCSP Stapling > 长 keepalive | 10 |
builtin vs shared |
builtin 每 worker 独立、命中率极低;必须 shared。不配则默认 none(完全无复用) |
10 |
| ticket key 为什么要轮换 | key 泄露能解密所有用它加密过的历史会话,削弱前向保密。每天轮换、保留新旧两个 | 10 |
OCSP Stapling 为什么要 resolver |
Nginx 要解析 OCSP 服务器域名,不配就静默失效(最常见遗漏) | 10 |
| 0-RTT 什么风险 | Early Data 可被重放,只能用于幂等请求。非幂等应返回 425 | 10 |
1.5 运维与排障
| 问题 | 一句话答案 | 详见 |
|---|---|---|
$request_time vs $upstream_response_time |
前者含客户端网络传输;两者差距大说明客户端慢或响应体大 | 11 |
为什么要 escape=json |
不加则 UA 里的引号会产生非法 JSON,甚至可被构造来注入伪造字段 | 11 |
logrotate 为什么要 kill -USR1 |
rename 不影响已打开的 fd,worker 会继续往改名后的文件写,新文件永远是空的 |
11 |
accepts != handled |
有连接被丢弃。查 worker_connections、ulimit -n、ListenOverflows |
11 |
Reading 异常高 |
大量连接卡在读请求头 = Slowloris 特征。正常应是个位数 | 11 |
| 怎么做全链路追踪 | $request_id 传后端 + 回给客户端;或 1.25.3+ 的 ngx_otel_module |
11 |
limits.conf 改了不生效 |
systemd 服务不读 limits.conf,必须配 LimitNOFILE 并验证 /proc/<pid>/limits |
12 |
gzip_comp_level 9 好吗 |
不好。5→9 压缩率只多 2-3%,CPU 涨 3 倍。5 是拐点 | 12 |
TIME_WAIT 多怎么办 |
开 upstream keepalive + tcp_tw_reuse + 扩端口范围。不要用 tcp_tw_recycle(4.12 已移除) |
12 |
CLOSE_WAIT 堆积 |
应用层 bug(收到 FIN 没 close),调内核参数无效,必须修代码 | 12 |
| upstream keepalive 超时怎么设 | 必须短于后端的 IdleTimeout,让 Nginx 主动淘汰连接,否则偶发 502 | 12 |
| K8s 滚动更新为什么 502 | 发 SIGTERM 与移除 Endpoint 是并行的。preStop: sleep 10 让 Endpoint 先更新 |
13 |
2. 分难度问答
2.1 初级(必须答对)
Q:Nginx 常用命令有哪些?改完配置的标准流程是什么?
nginx -t # 测试语法
nginx -T # 测试并打印合并后的完整配置(排查 include)
nginx -V # 版本 + 编译参数(查模块在不在)
nginx -s reload # 平滑重载
nginx -s quit # 优雅停止
nginx -s reopen # 重开日志(配合 logrotate)
标准流程是改配置 → nginx -t → nginx -s reload。nginx -t 是纪律不是建议:配置语法错误时 reload 会保留旧配置继续跑,服务不挂但你的改动一个字都没生效——这种「静默失败」最浪费排查时间。
Q:nginx -s stop 和 quit 的区别?
stop 发 TERM,立即关闭、中断正在处理的请求;quit 发 QUIT,处理完当前请求再退出。生产下线必须用 quit。
Q:worker_processes 设多少?
auto(= CPU 核心数)。worker 是单线程事件循环,核数就是并行上限,超出只增加上下文切换和 CPU 争抢。磁盘 I/O 阻塞应该用 aio threads 解决而不是加 worker。
Q:Nginx 单机最大并发连接数怎么算?
理论上限 worker_processes × worker_connections。但反向代理时每个请求占 2 个连接(客户端侧 + 上游侧),实际可服务客户端数要除以 2。还受 worker_rlimit_nofile、系统 ulimit -n、fs.file-max、ip_local_port_range 限制。
Q:怎么配置一个最基本的反向代理?
location /api/ {
proxy_pass http://backend;
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 后端收到的是 upstream 名字;不设后三个后端拿不到真实客户端信息,生成的绝对 URL 也会是 http://。
2.2 中级(区分度最高)
Q:一个请求 /api/config.js,配置里有 location /api/ { proxy_pass ...} 和 location ~* \.js$ { root ...},会走哪个?为什么?怎么修?
走正则那个,去 /var/www/static/api/config.js 找文件然后 404。因为正则优先于普通前缀匹配。
修复是给前缀加 ^~:
location ^~ /api/ { proxy_pass http://backend; }
location ~* \.js$ { root /var/www/static; }
规律:只要一个前缀 location 里配了 proxy_pass,且同时存在按扩展名匹配的正则 location,就应该给这个前缀加 ^~。
Q:为什么我在 location 里加了一条 add_header,server 层的安全头全没了?
add_header、proxy_set_header、access_log 这类可写多条的数组式指令,继承规则是「全有或全无」:子层级只要出现任意一条同名指令,父层级的所有同名指令一律不继承。
解法:子层重复写全,或抽成 include snippets/security-headers.conf 片段复用。
这个坑最隐蔽的形式是它会悄悄破坏 upstream keepalive:
http {
proxy_http_version 1.1;
proxy_set_header Connection ""; # 全局设置
server {
location /api/ {
proxy_set_header X-Foo bar; # ⚠️ 这一行让上面的 Connection "" 失效
proxy_pass http://backend;
}
}
}
结果是 keepalive 64 完全失效、大量 TIME_WAIT,而配置看起来毫无问题。
Q:limit_req 和 proxy_pass 在配置里的先后顺序会影响执行顺序吗?
不会。执行顺序由阶段决定:limit_req 在 PREACCESS(第 6 阶段),proxy_pass 在 CONTENT(第 10 阶段),无论怎么写都是限流先执行。
同理 deny 在 ACCESS(第 7),所以顺序永远是「限流 → 鉴权 → 生成内容」。这个设计是对的:让廉价的限流先挡住流量,避免昂贵的鉴权被打爆。
Q:配了 keepalive 64 但上游还是几千个 TIME_WAIT,排查思路?
按顺序查四点:
- 漏了
proxy_http_version 1.1;(Nginx 默认用 HTTP/1.0,不支持持久连接) - 漏了
proxy_set_header Connection "";(Nginx 默认发Connection: close) - 子 location 里写了其他
proxy_set_header,数组式继承把父级的Connection ""整体冲掉了 ← 最隐蔽 - 后端自己的 keepalive 没开,或者它的空闲超时比 Nginx 短
验证方法:
ss -tan | grep :8080 | awk '{print $1}' | sort | uniq -c
# 期望:ESTAB 数量稳定在 keepalive 值附近,TIME-WAIT 个位数
Q:rate=10r/s burst=20 和 rate=10r/s burst=20 nodelay 有什么区别?瞬间来 25 个请求分别什么结果?
漏桶算法下 rate=10r/s 实际是「每 100ms 放行 1 个」。
- 只有
burst:第 1 个立即处理,第 2-21 个进队列分别延迟 100ms…2000ms 处理,第 22-25 个返回 503。问题是第 21 个用户等了 2 秒。 burst+nodelay:第 1-21 个全部立即处理(占满槽位),第 22-25 个 503;之后槽位按每 100ms 释放 1 个。
生产用 nodelay:用户打开页面瞬间发 10 个 API 请求是正常行为,不该被延迟。它的语义是「允许合理突发,长期速率受控」。
Q:proxy_next_upstream 该配哪些值?为什么不能加 http_500?
推荐 error timeout http_502 http_503 http_504,并配 proxy_next_upstream_tries 2。
error:连接/发送都失败,后端没收到,安全502/503/504:通常是后端进程没起或队列满,请求未被业务逻辑处理,相对安全http_500不安全:500 意味着后端已经执行了请求只是过程出错,重试会造成重复副作用non_idempotent更危险:POST 也会重试,后端已成功但响应超时时会重复下单/重复扣款
根本解法是业务层幂等(唯一请求 ID 去重),而不是在 Nginx 层纠结。另外 proxy_next_upstream_tries 不设默认是 0(不限次数),会把所有后端试一遍,后端多时一个请求能拖很久。
Q:$request_time = 3.5s 但 $upstream_response_time = 0.05s,问题在哪?
问题不在后端。$request_time 是 Nginx 视角的完整耗时,包含客户端网络传输时间。3.5s 里有 3.45s 花在了客户端接收数据上。
可能原因:响应体太大(看 $body_bytes_sent)、客户端网络差、没开压缩。处理方向是压缩、CDN、分页减小响应体——调 Nginx 或后端都没用。
反过来如果 upstream_connect_time 大,说明后端 accept 队列满、网络问题或没开 upstream keepalive。
Q:怎么防止缓存击穿?完整配置是什么?
proxy_cache_lock on; # 同 key 只让一个请求回源
proxy_cache_lock_timeout 5s;
proxy_cache_use_stale error timeout http_5xx updating; # 后端挂了用旧数据顶
proxy_cache_background_update on; # 过期时立即返旧值,后台异步刷新
proxy_cache_revalidate on; # 带条件头回源,304 时不重传体
后两行是性价比最高的两行,实现了 stale-while-revalidate 语义:过期时用户零等待拿到旧数据、后台静默更新;后端全挂时用户仍能看到内容而不是 502。
Q:proxy_buffering on 有什么价值?什么时候必须关?
开启时 Nginx 尽快把上游响应读完缓存起来,后端进程立刻释放去处理下一个请求,慢速客户端由 Nginx 扛着。这是保护后端的核心机制——尤其对 PHP-FPM、同步框架这类每请求占一个进程/线程的后端。
必须关的场景:SSE、实时日志推送、大文件边下边传。也可以让上游返回 X-Accel-Buffering: no 来动态关闭,比在 Nginx 里写死更灵活。
2.3 高级(深度题)
Q:详细说说 reload 的完整流程,以及有什么坑。
1. nginx -s reload → 新起一个进程读 pid 文件,给 master 发 SIGHUP,然后退出
2. master 重新解析配置
→ 失败:打日志、保留旧配置继续跑,reload 静默失效 ⚠️
→ 成功:继续
3. master 创建新 listen socket(配置未变则复用旧 fd)
4. master fork 新 worker(用新配置)
5. master 给所有老 worker 发 SIGQUIT
6. 老 worker:从 epoll 移除 listen fd(停 accept)→ 处理完存量连接 → 销毁池 → 退出
超过 worker_shutdown_timeout 则强制关闭连接后退出
7. master 回收僵尸进程,完成
三个坑:
- 长连接会拖住老 worker(WebSocket/SSE/大文件下载),必须配
worker_shutdown_timeout 30s。 - 频繁 reload 会累积老 worker,
ps -ef | grep "shutting down"能看到,进程数和内存越堆越多。 - 共享内存 zone 会重建——限流计数器、
upstream的 zone 状态、缓存 key 索引全部清零。所以别把 reload 当常规操作反复执行(比如每次服务注册变化就 reload)。
Q:Nginx 的惊群问题有几代解决方案?各自的取舍?
| 方案 | 机制 | 问题 |
|---|---|---|
accept_mutex on |
跨进程互斥锁串行化 accept,配合负载均衡(连接数超 7/8 主动让锁) | 锁本身是串行瓶颈;accept_mutex_delay 默认 500ms,低并发时延迟毛刺明显 |
EPOLLEXCLUSIVE(Linux 4.5+) |
内核标志,一个事件只唤醒一个等待进程。1.11.3+ 自动使用,accept_mutex 默认改为 off |
仍是单个 listen fd 上的竞争 |
listen ... reuseport(Linux 3.9+) |
每个 worker 独立 listen fd + 独立 accept 队列,内核按四元组 hash 分发 | reload 时分发短暂抖动;某 worker 卡住时它队列里的连接不会被别人接走 |
生产建议:Linux 3.9+ 直接开 reuseport,accept_mutex off。官方压测显示长尾延迟明显改善。
Q:什么操作会阻塞 worker?后果多严重?怎么解决?
事件驱动模型的致命前提是回调绝不能阻塞。一旦卡住,这个 worker 上的所有连接(可能上万个)全部停摆,表现为部分请求突然变慢或超时——而且是随机的一批用户受影响,排查时容易误判为「偶发抖动」。
| 阻塞源 | 解决 |
|---|---|
磁盘冷读(read/sendfile 数据不在 page cache) |
aio threads 线程池 + directio 8m |
同步 DNS 解析(proxy_pass 写死域名且未配 resolver) |
配 resolver + 变量式 proxy_pass http://$var$request_uri |
| 第三方模块的同步调用 | 换异步实现 |
OpenResty 里的阻塞函数(os.execute、阻塞 socket 库) |
用 resty.* 系列的非阻塞 API |
关于线程池:官方测试在大文件随机读(数据集远大于内存)场景吞吐提升 9 倍,但在文件全命中 page cache 的场景是负优化(白增调度开销)。所以它是专用优化不是万能开关。
Q:proxy_pass 里写域名和写变量有什么本质区别?
写死域名只在启动/reload 时解析一次,IP 缓存到进程生命周期结束。上游 IP 变了(K8s Pod 重建、云 SLB 换 IP、Docker 容器重建)就持续 502。
含变量时 Nginx 才会在运行时用自己的异步 resolver 解析:
resolver 127.0.0.11 valid=10s ipv6=off; # Docker 内嵌 DNS
set $backend "api.example.com";
proxy_pass http://$backend$request_uri; # 必须显式带 URI,变量式不自动传递
副作用是 URI 不再自动传递,要手写 $request_uri。另外 1.27.3+ 开源版支持 upstream 里的 server ... resolve 参数,是更干净的方案。
Q:11 个阶段里,try_files、auth_request、mirror、limit_req、realip 分别在哪个阶段?为什么这样排?
| 指令/模块 | 阶段 | 为什么在这里 |
|---|---|---|
realip |
1 POST_READ | 必须最先执行,后面所有阶段(限流/ACL/日志)才能拿到真实 IP |
limit_req/limit_conn |
6 PREACCESS | 在鉴权之前,让廉价的限流先挡住流量,避免昂贵的鉴权被打爆 |
allow/deny/auth_basic/auth_request |
7 ACCESS | 内容生成之前完成授权 |
try_files/mirror |
9 PRECONTENT | 决定「用哪个内容源」,在实际生成之前 |
proxy_pass/root |
10 CONTENT | 真正产生响应 |
realip 在第 1 阶段是关键设计。如果它在限流之后,limit_req 拿到的就是代理的 IP 而不是真实客户端 IP,所有基于 IP 的限流全部失效——所有用户会共享同一个限流配额。
Q:sub_filter 对某些页面不生效,为什么?
过滤器链的顺序是编译期固定的:sub_filter 在 gzip 之前。如果上游返回的已经是 gzip 压缩的内容,sub_filter 拿到的是压缩后的字节流,无法做字符串匹配。
location / {
proxy_pass http://backend;
proxy_set_header Accept-Encoding ""; # 让上游返回未压缩内容
sub_filter 'old' 'new';
sub_filter_once off;
sub_filter_types text/html text/css application/json; # 默认只处理 text/html
gzip on; # 由 Nginx 自己压缩
}
另一个常见原因是 sub_filter_types 默认只有 text/html。
Q:Nginx 的内存到底花在哪?怎么估算?
总内存 = worker_processes × 每 worker 内存 + 共享内存(只算一次)
每 worker 内存 =
固定开销(代码段 + 共享内存映射)
+ worker_connections × 约 500 字节 ← 启动时一次性预分配!
+ 活跃连接数 × (proxy_buffers 总大小 + request pool)
共享内存 = proxy_cache 的 keys_zone
+ limit_req_zone + limit_conn_zone
+ ssl_session_cache
+ upstream 的 zone
两个关键点:
worker_connections是启动时就分配的——设 100 万意味着启动就吃掉几百 MB 的ngx_connection_t和ngx_event_t结构体。所以要按实际并发算,不能拍脑袋。proxy_buffers是「每个活跃连接」的——proxy_buffers 16 64k(1MB)× 1 万并发 = 理论 10GB。调大要算清楚。
3. 综合设计题
这类题考的是能否把知识串成体系。答题时先讲思路和分层,再落到具体指令。
3.1 设计一个日均 1 亿 PV 的网站接入层
回答框架:分层 → 每层职责 → 关键配置 → 容量估算 → 监控。
DNS(智能解析/GSLB)
↓
CDN(静态资源 + 部分 API 缓存)
↓
四层 LB(LVS/云 SLB,做 VIP 和跨机房)
↓
Nginx 集群(本层重点)
↓
业务服务集群
Nginx 层的职责与配置:
# 容量估算:1亿 PV/天 ≈ 平均 1157 QPS,峰值按 5 倍算 ≈ 6000 QPS
# 单机 8 核能扛 3-5 万 QPS(反代),4 台机器有充足余量 + 容灾冗余
worker_processes auto;
worker_cpu_affinity auto;
worker_rlimit_nofile 65535;
events {
worker_connections 20480; # 按峰值并发 × 2 / worker 数算
accept_mutex off;
}
http {
# ① 静态资源:CDN 回源,本层强缓存
# ② API:短 TTL 缓存 + 防击穿
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=api:500m
max_size=50g inactive=2h use_temp_path=off;
# ③ 上游长连接(最关键的优化)
upstream backend {
least_conn;
server 10.0.1.1:8080 max_fails=3 fail_timeout=30s;
# ... 更多节点
keepalive 64;
keepalive_timeout 30s; # 短于后端 IdleTimeout
}
# ④ 分层限流
limit_req_zone $binary_remote_addr zone=perip:50m rate=50r/s;
limit_req_zone $server_name zone=global:10m rate=20000r/s;
# ⑤ TLS 性能(HTTPS 站点的最大开销)
ssl_session_cache shared:SSL:200m;
ssl_session_timeout 1d;
ssl_session_tickets on; # 多机共享 ticket key
# ECDSA + RSA 双证书
# ⑥ 日志(JSON + buffer)
access_log /var/log/nginx/access.json.log json buffer=64k flush=5s;
}
要主动提到的点(体现深度):
- 容量估算过程,而不是空谈「加机器」
- 多台 Nginx 必须共享
ssl_session_ticket_key,否则会话复用率极低 - 多台 Nginx 各自算
least_conn会有羊群效应,考虑random two least_conn - 缓存分片如果用
hash必须加consistent,否则扩容一次缓存全失效、后端被击穿 - 单点问题:Nginx 层要 ≥ 2 台 + 四层 LB 或 keepalived
- 监控:5xx 率、P99 延迟、
accepts != handled、缓存命中率、证书过期
3.2 设计一个灰度发布方案
回答框架:分流维度 → 一致性要求 → 可观测 → 回滚。
# 优先级:请求头(测试)> Cookie(用户主动)> 用户 ID 尾号(稳定)> hash(无登录态)
map $http_x_canary $by_header { default ""; "1" "canary"; "0" "stable"; }
map $cookie_uid $uid_tail { default ""; "~(\d)$" $1; }
map $uid_tail $by_uid { default ""; "7" "canary"; } # 约 10%
split_clients "${remote_addr}${http_user_agent}" $by_hash { 5% "canary"; * "stable"; }
map "$by_header|$by_uid|$by_hash" $pool {
default "app_stable";
"~^canary" "app_canary";
"~^\|canary" "app_canary";
"~^\|\|canary" "app_canary";
}
location /api/ {
proxy_pass http://$pool;
proxy_set_header X-Pool $pool;
add_header X-Pool $pool always; # 便于验证和分池统计
}
关键论点(面试官想听的):
- 不要用 upstream 的
weight做灰度。同一用户的连续请求会在新旧版本间跳,可能出现「刚提交的数据看不到」这类问题。 - 必须按用户稳定分流(uid 尾号或
split_clients哈希),保证同一用户永远在同一侧。split_clients对输入做 MurmurHash2,同输入必然同结果。 - 可观测性是灰度的前提:日志里带
X-Pool或$upstream_addr,能分池对比 5xx 率和 P99 延迟。没有分池监控的灰度等于盲测。 - 回滚必须是秒级的:改
map比例 +reload,不需要重新部署。 - 数据库兼容性:灰度期间新旧代码同时读写同一个库,schema 变更必须向前兼容(先加字段不删字段、先双写后切读)。这是灰度真正的难点,不在 Nginx 层。
3.3 线上突然大量 502,排查思路
回答框架:先看日志定性 → 再分层验证 → 最后处置。
# ① error.log 定性(502 的具体原因就写在里面)
tail -200 /var/log/nginx/error.log | grep -oP '\) \K[a-z ]+' | sort | uniq -c
| error.log 内容 | 原因 | 处置 |
|---|---|---|
Connection refused (111) |
后端进程没起/端口错 | 查后端进程、端口 |
No route to host (113) |
网络不通 | 查安全组/防火墙/路由 |
upstream prematurely closed connection |
后端崩了/OOM/主动关连接 | 查后端日志 + dmesg | grep -i oom |
no live upstreams |
所有后端被被动健康检查踢出 | 查后端健康、max_fails 是否太激进 |
upstream sent too big header |
响应头超缓冲区 | 调大 proxy_buffer_size |
connect() failed + SELinux |
SELinux 拦截 | setsebool -P httpd_can_network_connect 1 |
# ② 分层验证:从 Nginx 机器上直连后端,排除 Nginx 自身
curl -v http://10.0.0.1:8080/health
# ③ 看是全部后端还是部分
jq -r 'select(.status==502) | .uaddr' access.json.log | sort | uniq -c
# 全部节点都有 → 后端整体故障 / 依赖(DB/Redis)挂了
# 只有某个节点 → 单机问题,先摘掉它
# ④ 看时间分布:是持续还是尖峰
jq -r 'select(.status==502) | .time[0:16]' access.json.log | uniq -c
# ⑤ 是不是刚发布/刚 reload
docker compose ps # 或 kubectl get pods -w
git log --oneline -5
一个高频的隐蔽原因(答出来加分):Nginx 的 upstream keepalive_timeout 长于后端的 IdleTimeout。后端先关闭空闲连接,Nginx 恰好在这一瞬间往这条连接上发请求,就会 upstream prematurely closed connection。特征是502 数量少但持续存在,后端却没有任何错误日志。修复是让 Nginx 侧超时更短(Nginx 30s,后端 90s)。
K8s 环境的高频原因:滚动更新时 SIGTERM 和移除 Endpoint 并行,preStop: sleep 10 可以消除这个窗口。
处置顺序:先止损(摘掉故障节点/回滚发布/开 proxy_cache_use_stale 顶住),再定位根因。不要在流量还在打的时候慢慢调试。
3.4 静态资源加载慢,怎么优化
回答框架:先量化 → 分层优化 → 验证。
# 量化:先搞清慢在哪一段
curl -o /dev/null -s -w "dns:%{time_namelookup} tcp:%{time_connect} tls:%{time_appconnect} ttfb:%{time_starttransfer} total:%{time_total} size:%{size_download}\n" \
https://example.com/app.js
| 慢在哪段 | 优化方向 |
|---|---|
dns 大 |
DNS 预解析、减少域名数(HTTP/2 下域名分片是负优化) |
tcp 大 |
CDN 就近接入 |
tls 大 |
会话复用、ECDSA、TLS 1.3、OCSP Stapling |
ttfb 大 |
open_file_cache、sendfile、ssl_buffer_size 4k |
total - ttfb 大 |
压缩(Brotli/gzip)、减小体积、限速是否设太低 |
size 大 |
Brotli 比 gzip 小 15-25%,构建时预压缩 |
Nginx 侧的具体配置:
# ① 压缩:构建时预压缩,运行时零 CPU
gzip_static on;
brotli_static on;
# ② 缓存分层:带 hash 的永久缓存
location ~* \.[0-9a-f]{8,}\.(js|css|woff2)$ {
expires 1y;
add_header Cache-Control "public, max-age=31536000, immutable";
access_log off;
try_files $uri =404; # 不能回退到 index.html
}
# ③ 入口 HTML 绝不缓存(否则发版后用户还是旧页面)
location = /index.html {
add_header Cache-Control "no-cache, no-store, must-revalidate";
expires -1;
}
# ④ 传输
sendfile on;
tcp_nopush on;
open_file_cache max=100000 inactive=60s;
ssl_buffer_size 4k; # 降低首字节延迟
# ⑤ HTTP/2 或 HTTP/3
http2 on;
要提到的关键论点:真正的大头往往不在 Nginx——静态资源优化的收益顺序是 CDN > 浏览器缓存策略 > 压缩 > Nginx 传输参数。前三项是数量级的改善,最后一项是百分比级的。
4. 易错点速查
面试时说错这些会直接暴露没有实操经验。
| ❌ 错误说法 | ✅ 正确 |
|---|---|
| location 从上到下第一个匹配的赢 | 有明确优先级:= > ^~ > 正则(按顺序)> 前缀(最长) |
| 正则 location 之间比谁更精确 | 正则之间只比书写顺序,第一个匹配的赢 |
rate=10r/s 是每秒 10 个 |
是每 100ms 1 个,同毫秒来 2 个第二个就被拒 |
worker_connections 越大越好 |
启动时一次性预分配,设 100 万白吃几百 MB |
gzip_comp_level 越高越好 |
5→9 只多压 2-3%,CPU 涨 3 倍 |
tcp_tw_recycle = 1 能解决 TIME_WAIT |
4.12 已移除,NAT 环境会丢弃合法连接 |
proxy_read_timeout 是响应总超时 |
是两次读操作之间的间隔上限 |
sendfile 对反向代理也有效 |
只对「文件 → socket」有效,代理的数据来自上游 socket |
| 开 gzip 和 sendfile 可以同时生效 | 对同一响应互斥,压缩必须把数据读到用户态 |
add_header 在所有响应上生效 |
默认只在 2xx/3xx,4xx/5xx 需要 always |
ssl_session_cache builtin 就够了 |
每 worker 独立,命中率极低,必须 shared |
if 可以放心用 |
会创建匿名嵌套 location,只有 return/rewrite 安全 |
if 支持 && |
不支持 &&/` |
| Nginx 能主动健康检查 | 开源版只有被动(max_fails),主动需 Plus/第三方模块 |
reload 一定会生效 |
配置语法错时会静默保留旧配置,所以必须先 nginx -t |
改 limits.conf 就能提高 fd 上限 |
systemd 服务不读它,必须配 LimitNOFILE |
CLOSE_WAIT 多要调内核参数 |
一定是应用层没 close,必须修代码 |
$uri 可以用来防路径穿越 |
$uri 已被规范化,.. 早被处理掉了 |
用 weight 做灰度就行 |
同一用户会在新旧版本间跳,要按用户稳定分流 |
Access-Control-Allow-Origin: * 通用 |
与 Allow-Credentials: true 不兼容,且任何站点能读你的接口 |
5. 答题框架
被问到开放性问题时,用这几个框架组织回答,比零散罗列配置项显得体系化得多。
5.1 「你怎么优化 Nginx 性能」
先讲方法论,再讲具体项:
- 建立基线 + 明确目标(QPS 多少、P99 多少),不要盲目抄参数
- 分层压测定位瓶颈:
return 200(测极限)→ 静态文件 → 直压后端 → 通过 Nginx 压后端。四组数据一对比就知道瓶颈在哪层 - 强调一个前提:绝大多数「Nginx 性能问题」病根不在 Nginx。判断依据是
$upstream_response_time / $request_time的比例——如果 90% 时间在等后端,调 Nginx 是白费功夫 - 一次只改一项并压测验证,否则不知道是哪项起作用、有没有某项被掩盖的负作用
- 按收益排序说具体项:upstream keepalive(最大)> TLS 会话复用 >
gzip_static>reuseport> 日志 buffer > 缓冲区调整
5.2 「说说你对 Nginx 架构的理解」
按四层展开:
- 进程模型:master(root,管配置和进程,不处理请求)+ worker(低权限,干活)+ cache manager/loader。worker 间无共享无锁,一个崩了 master 重启它
- I/O 模型:epoll ET + 全非阻塞 socket + 状态机。请求被拆成一串「让出-回来」的片段,一个 worker 单线程管数万连接
- 模块化:11 个阶段的 handler 链 + 两条 filter 链(header/body),阶段和链的顺序编译期固定。第三方模块通过往阶段注册 handler 参与处理
- 内存管理:三种生命周期的内存池(cycle/connection/request),小块分配只是移动指针,请求结束一次性销毁全部回收
然后主动引出一个深度点,比如:「这个设计的代价是回调绝不能阻塞——一旦某个 worker 卡在磁盘冷读或同步 DNS 解析上,它上面的上万个连接全部停摆,表现为随机一批用户突然变慢。这也是 aio threads 线程池和 resolver 配置存在的原因。」
5.3 「线上出问题你怎么排查」
固定动作,按顺序说:
# ① error.log 定性(Nginx 的报错信息质量很高,多半直接告诉你原因)
tail -200 /var/log/nginx/error.log
# ② access_log 定量(范围、时间分布、涉及哪些节点)
jq -r 'select(.status>=500) | "\(.time[0:16]) \(.uaddr) \(.uri)"' access.json.log | sort | uniq -c
# ③ 耗时拆解(Nginx 慢还是后端慢)
jq -r 'select(.rt>1) | "\(.rt) \(.urt) \(.uri)"' access.json.log | sort -rn | head -20
# ④ 连接状态(队列溢出、TIME_WAIT、CLOSE_WAIT)
ss -s; nstat -az | grep -E "ListenOverflows|ListenDrops"
# ⑤ 系统资源
top; iostat -x 1; free -h
# ⑥ 配置确认(include 展开后的真实配置)
nginx -T | grep -A5 "location /api"
# ⑦ 只对自己的 IP 开 debug(不影响其他用户)
# events { debug_connection 1.2.3.4; } error_log ... debug;
强调两点:
- 止损优先于定位:先摘故障节点/回滚发布/用
proxy_cache_use_stale顶住,再慢慢查根因 $request_id是最高效的工具:用户提供一个 ID,就能在 Nginx 日志、应用日志、SQL 日志里精确定位同一次请求的全部记录。没有它只能靠时间戳 + IP 模糊匹配,高并发下几乎不可行
6. 系列回顾
| 篇目 | 核心内容 |
|---|---|
| 01 入门与安装 | 角色定位、编译选项、信号、平滑升级 |
| 02 配置结构与核心指令 | 块层级、两种继承规则、内置变量、map |
| 03 进程模型与事件驱动 | master-worker、epoll、惊群三代方案、内存池 |
| 04 请求处理 11 个阶段 | 阶段顺序、filter 链、子请求、X-Accel-Redirect |
| 05 location 与 rewrite | 匹配优先级、proxy_pass 斜杠、last/break、if 陷阱 |
| 06 反向代理与负载均衡 | 请求头、六种算法、健康检查、重试幂等、upstream keepalive |
| 07 动静分离与静态优化 | 零拷贝、压缩、缓存分层、SPA 部署、防盗链 |
| 08 缓存机制 | key 设计、防击穿、stale-while-revalidate、主动清除 |
| 09 限流与安全 | 漏桶与 burst/nodelay、CC 与慢速攻击、CORS、WAF |
| 10 HTTPS 与 TLS | 证书链、会话复用、OCSP、HTTP/2、HTTP/3、mTLS |
| 11 日志与监控 | 变量大全、JSON 日志、切割、分析命令、Prometheus、追踪 |
| 12 性能调优 | 方法论、内核参数、压测、瓶颈定位、六个真实案例 |
| 13 实战案例 | Go 多实例、灰度、WebSocket、大文件、蓝绿、K8s Ingress |
| 14 面试题汇总 | 本篇 |
xingliuhua