目录

Nginx-14 高频面试题汇总

前置阅读:Nginx-13 实战案例(Go 服务 + Docker)

本篇是整个系列的收尾。前 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_mutexEPOLLEXCLUSIVE(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_connip_hashhash 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_connectionsulimit -nListenOverflows 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 -tnginx -s reloadnginx -t 是纪律不是建议:配置语法错误时 reload 会保留旧配置继续跑,服务不挂但你的改动一个字都没生效——这种「静默失败」最浪费排查时间。

Q:nginx -s stopquit 的区别?

stopTERM,立即关闭、中断正在处理的请求;quitQUIT,处理完当前请求再退出。生产下线必须用 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 -nfs.file-maxip_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_headerproxy_set_headeraccess_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_reqproxy_pass 在配置里的先后顺序会影响执行顺序吗?

不会。执行顺序由阶段决定:limit_req 在 PREACCESS(第 6 阶段),proxy_pass 在 CONTENT(第 10 阶段),无论怎么写都是限流先执行。

同理 deny 在 ACCESS(第 7),所以顺序永远是「限流 → 鉴权 → 生成内容」。这个设计是对的:让廉价的限流先挡住流量,避免昂贵的鉴权被打爆。

Q:配了 keepalive 64 但上游还是几千个 TIME_WAIT,排查思路?

按顺序查四点:

  1. 漏了 proxy_http_version 1.1;(Nginx 默认用 HTTP/1.0,不支持持久连接)
  2. 漏了 proxy_set_header Connection "";(Nginx 默认发 Connection: close
  3. 子 location 里写了其他 proxy_set_header,数组式继承把父级的 Connection "" 整体冲掉了 ← 最隐蔽
  4. 后端自己的 keepalive 没开,或者它的空闲超时比 Nginx 短

验证方法:

ss -tan | grep :8080 | awk '{print $1}' | sort | uniq -c
# 期望:ESTAB 数量稳定在 keepalive 值附近,TIME-WAIT 个位数

Q:rate=10r/s burst=20rate=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 回收僵尸进程,完成

三个坑:

  1. 长连接会拖住老 worker(WebSocket/SSE/大文件下载),必须配 worker_shutdown_timeout 30s
  2. 频繁 reload 会累积老 workerps -ef | grep "shutting down" 能看到,进程数和内存越堆越多。
  3. 共享内存 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+ 直接开 reuseportaccept_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_filesauth_requestmirrorlimit_reqrealip 分别在哪个阶段?为什么这样排?

指令/模块 阶段 为什么在这里
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_filtergzip 之前。如果上游返回的已经是 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

两个关键点:

  1. worker_connections 是启动时就分配的——设 100 万意味着启动就吃掉几百 MB 的 ngx_connection_tngx_event_t 结构体。所以要按实际并发算,不能拍脑袋。
  2. 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;      # 便于验证和分池统计
}

关键论点(面试官想听的):

  1. 不要用 upstream 的 weight 做灰度。同一用户的连续请求会在新旧版本间跳,可能出现「刚提交的数据看不到」这类问题。
  2. 必须按用户稳定分流(uid 尾号或 split_clients 哈希),保证同一用户永远在同一侧。split_clients 对输入做 MurmurHash2,同输入必然同结果。
  3. 可观测性是灰度的前提:日志里带 X-Pool$upstream_addr,能分池对比 5xx 率和 P99 延迟。没有分池监控的灰度等于盲测。
  4. 回滚必须是秒级的:改 map 比例 + reload,不需要重新部署。
  5. 数据库兼容性:灰度期间新旧代码同时读写同一个库,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_cachesendfilessl_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 性能」

先讲方法论,再讲具体项

  1. 建立基线 + 明确目标(QPS 多少、P99 多少),不要盲目抄参数
  2. 分层压测定位瓶颈return 200(测极限)→ 静态文件 → 直压后端 → 通过 Nginx 压后端。四组数据一对比就知道瓶颈在哪层
  3. 强调一个前提:绝大多数「Nginx 性能问题」病根不在 Nginx。判断依据是 $upstream_response_time / $request_time 的比例——如果 90% 时间在等后端,调 Nginx 是白费功夫
  4. 一次只改一项并压测验证,否则不知道是哪项起作用、有没有某项被掩盖的负作用
  5. 按收益排序说具体项:upstream keepalive(最大)> TLS 会话复用 > gzip_static > reuseport > 日志 buffer > 缓冲区调整

5.2 「说说你对 Nginx 架构的理解」

按四层展开:

  1. 进程模型:master(root,管配置和进程,不处理请求)+ worker(低权限,干活)+ cache manager/loader。worker 间无共享无锁,一个崩了 master 重启它
  2. I/O 模型:epoll ET + 全非阻塞 socket + 状态机。请求被拆成一串「让出-回来」的片段,一个 worker 单线程管数万连接
  3. 模块化:11 个阶段的 handler 链 + 两条 filter 链(header/body),阶段和链的顺序编译期固定。第三方模块通过往阶段注册 handler 参与处理
  4. 内存管理:三种生命周期的内存池(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 面试题汇总 本篇

上一篇:Nginx-13 实战案例(Go 服务 + Docker)