Appearance
第4章 Nginx 负载均衡与高可用
对应简历:技能第7条「熟练掌握 Nginx + Keepalived 高可用负载均衡(VIP 漂移、健康检查、故障转移验证),熟悉 LVS、HAProxy 及 LNMP 平台搭建」/ 项目一职责「通过 Nginx + Keepalived(VIP 漂移 + 健康检查脚本)升级为多 master 高可用架构并完成故障转移测试」/ 项目一职责「Nginx 1.25.1 源码编译(--with-http_ssl_module),80 端口 301 重定向到 443,443 挂证书服务前端静态页,/prod-api/ 反向代理到本地 8080 后端」
术语约定:Kubernetes(以下简称 K8s,仅正文简写)。内网 IP 一律脱敏为
<VIP>、<master01-ip>等占位符。
4.1 Nginx 是什么:架构与 master/worker 模型 【必背】
标准回答(先结论后展开,可口述 60 秒): Nginx 是高性能的 HTTP 服务器和反向代理服务器,同时承担负载均衡、缓存加速和 SSL 终端的角色。它高并发的根本原因在于"多进程 + 事件驱动 + epoll 异步非阻塞"这套架构。具体拆开看三个角色:master 进程是管理者——读配置、绑定端口、创建和回收 worker 进程,自己不处理业务请求;它支持平滑重载,nginx -s reload 时 master 先校验新配置,然后起新 worker 加载新配置,给老 worker 发信号让它们处理完手上的存量连接再退出,全程服务不断——我在生产上改前端配置都是这么做的。
worker 进程是干活的——数量通常等于 CPU 核数,配 worker_processes auto 自动对齐;worker 之间完全独立、无锁竞争,一个 worker 崩了 master 立刻补一个新的,稳定性好。
第三是 epoll:Linux 内核的 IO 多路复用机制,一个 worker 用单线程同时监听成千上万个连接,谁就绪处理谁,不需要为每个连接开线程,所以几万并发连接只占 MB 级内存——对比 Apache 每连接一线程/进程的模型,资源消耗差一个量级。 追问链:
- 追问1:worker 进程数为什么设成 CPU 核数? → 答:因为 worker 是单线程事件驱动的,一个 worker 最多打满一个核;worker 数等于核数就能把多核吃满又不浪费。设少了核闲着,设多了 worker 之间增加进程切换开销还可能争抢连接。验证方法
grep processor /proc/cpuinfo | wc -l看核数和ps -fC nginx看 worker 数是否对齐。高并发场景可以核数乘二给突发留余量,但我项目里 auto 就是标准配置。 - 追问2(深挖):epoll 和 select/poll 的区别? → 答:核心差异在"谁来遍历连接"。select/poll 每次调用都要把整个连接集合从用户态拷贝到内核态,内核再逐个检查哪些就绪,复杂度 O(n),连接数一大 CPU 全耗在无效遍历上;epoll 在内核里维护一棵红黑树管理所有连接,事件到达时由回调把它挂进就绪链表,进程只从就绪链表里取就绪的连接处理,复杂度接近 O(1)。这块我理解到原理层,生产中我更多是按"epoll + worker_connections"这套参数组合做调优——把原理和调优动作对应起来说最稳。
关联话术:"我在银行数币项目里的 Nginx 1.25.1 是在统信 UOS 的 ARM 环境上源码编译部署的,对 master/worker 模型的体感非常直接:改完前端配置 reload,用户页面零感知;worker 偶发崩溃时 master 自动补起,服务不中断——这些行为我在生产上验证过,不是背架构图。"
4.2 正向代理与反向代理的区别 【理解】
标准回答: 一句话先行:正向代理代理的是客户端,反向代理代理的是服务端——区别在于"代理站在谁那边"。正向代理:客户端主动配置代理地址,由代理替客户端去访问外部资源,目标服务器看到的来源是代理 IP 而不是真实客户端,典型场景是企业统一上网出口、访问控制。反向代理:客户端完全无感知,直接访问代理对外的域名或 VIP,代理把请求转发给后端服务器组,客户端只看到代理、真实服务端被隐藏保护起来,典型场景是负载均衡、SSL 终端(证书挂在代理层,后端免加解密)、静态缓存、安全隔离(后端不直接暴露公网)。
这两个概念在 Nginx 里对应不同的配置块:正向代理配 proxy_pass 指向外部,反向代理配 upstream 加 proxy_pass 指向后端组。反向代理还有一个必须会延伸的点:后端怎么拿真实客户端 IP——靠代理透传 X-Real-IP 和 X-Forwarded-For 请求头。 追问链:
- 追问1:反向代理后后端怎么拿到客户端真实 IP? → 答:代理层透传请求头:
proxy_set_header X-Real-IP $remote_addr;把直连代理的真实 IP 传过去,proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;在已有的转发链头部追加,形成来源链;后端应用从这两个头取值,日志格式也要相应配置。我在数币平台 /prod-api/ 反代里就配了这两个头,后端 jar 的访问日志才能记录真实来源——多级代理时取 X-Forwarded-For 的第一个值,这是我提示开发注意的细节。
4.3 Nginx 的作用与虚拟主机 【了解】
标准回答: Nginx 在我的架构里承担四类角色:一是静态资源服务器,HTML/CSS/JS/图片直出,靠 mmap 和 sendfile 零拷贝机制,比 Apache 快且省内存;二是反向代理与负载均衡,动态请求分发给后端组;三是安全防护入口,HTTPS 终端、limit_req 限流、防盗链、隐藏版本号 server_tokens off;四是动静分离和轻量 API 网关,按 location 把不同路径路由到不同处理方式。
虚拟主机解决"一台 Nginx 跑多个站点"的问题:一个 server 块就是一个虚拟主机,三种区分方式——基于域名(server_name 不同,最常用,靠 HTTP Host 头区分)、基于端口(listen 不同端口)、基于 IP(一台机器多个 IP)。每个 server 块有独立的 location 规则、证书和日志,互不干扰。
我在项目里主要用基于域名的虚拟主机,测试环境也用过基于端口给不同业务分入口。 追问链:
- 追问1:基于域名的虚拟主机靠什么区分? → 答:靠 HTTP 请求头里的 Host 字段。Nginx 收到请求后按 server_name 匹配 server 块,匹配顺序是:先精确匹配,再前缀通配(*.example.com),再正则匹配,全都命不中就交给 default_server(显式标记或配置文件第一个)。所以同一 IP 同一端口可以挂几十个域名各自独立配置——这也是共享主机和 Ingress 的基本原理,我讲 K8s Ingress 时会回指这个机制。
4.4 负载均衡调度策略(RR/加权/IP hash/least_conn) 【必背】
标准回答: Nginx 的 upstream 默认策略是轮询——请求依次分给每台后端,适合后端性能相近的场景;加权轮询在后端配置不均时用 weight 指定比例,比如 8 核机器 weight 3、4 核机器 weight 1,让能力强得多干活;ip_hash 按客户端 IP 哈希把同一客户端固定到同一后端,解决 session 存在本机时的会话保持;least_conn 优先把请求给当前活跃连接最少的后端,适合请求处理时长差异大的长连接服务;还有第三方的 url hash 按 URI 哈希,用于提高缓存命中。
每个后端节点还可以加两个状态参数:backup 标记备份节点,正常节点全挂了才启用;down 永久摘除不参与调度。健康检查方面开源版是"被动"的:靠 max_fails 加 fail_timeout(默认 1 次失败 10 秒),失败次数达到就在 fail_timeout 时间窗内把它摘除——必须理解这不是主动探测,主动探测要 Nginx Plus 或第三方模块。
| 策略 | 写法 | 逻辑 | 适用场景 |
|---|---|---|---|
| 轮询 RR(默认) | 什么都不写 | 依次分配 | 后端性能相近 |
| 加权轮询 | weight=3 | 权重越大分得越多 | 后端配置不均 |
| IP hash | ip_hash; | 按客户端 IP 哈希固定到同一后端 | 会话保持(session 在本机时) |
| 最少连接 | least_conn; | 优先给当前连接数最少的后端 | 请求处理时长差异大的服务 |
| URL hash(三方/了解) | hash $request_uri consistent; | 按 URI 哈希 | 缓存命中场景 |
追问链:
- 追问1:ip_hash 有什么缺陷? → 答:两个结构性缺陷。第一,客户端经过 NAT 出口(公司、校园网、移动网络)时大量用户共享同一个出口 IP,会被哈希到同一台后端造成严重倾斜;第二,后端节点增减时哈希空间重排,大量会话重新分布。更稳的会话保持方案:用 sticky cookie 的第三方模块,或者干脆把 session 外置到 Redis——数币平台就是 Redis 存 session,负载层不需要做任何会话保持,架构上更干净。我会主动把这个演进讲出来。
- 追问2(深挖):健康检查为什么是"被动"的? → 答:开源版 Nginx 没有主动探测后端健康的能力,它只在实际转发失败时按 max_fails 计数,达到阈值后在 fail_timeout 窗口内不再分发流量给这台后端——也就是"死了才发现、试出来才摘除"。主动健康检查需要 Nginx Plus 的 health_check 指令、淘宝的 nginx_upstream_check_module 或改用 OpenResty。我们的生产规模下"被动检查加 Keepalived 服务级检查"已经够用,因为后端数量少、故障切换秒级可见。
关联话术:"银行项目 apiserver 的四层代理用的就是默认轮询——三台 master 完全对等,谁处理请求结果一致,轮询就是最合理也最简单的策略,不需要引入任何状态。这个例子我很爱讲:'策略选择的前提是理解后端有没有状态',一句话把选型思维展示出来。"
4.5 七层负载均衡配置五步(upstream) 【必背】
标准回答(可当口诀背:定义组 → 配策略 → 加健康检查 → 建 server → 写 location): 我配七层负载均衡的固定五步:第一步在 http 块里用 upstream 定义后端服务器组,起一个组名;第二步在组内声明调度策略,weight、ip_hash 或 least_conn,不写就是轮询;第三步给每个 server 节点配 max_fails、fail_timeout 做故障摘除,重要的备份节点标 backup;第四步建 server 块监听端口、写 server_name;第五步在 location 里 proxy_pass http://组名 把流量转发过去,同时透传 Host、X-Real-IP、X-Forwarded-For 三个头,再按业务配 proxy_connect_timeout、proxy_read_timeout 和 proxy_next_upstream(失败自动切下一台)。这五步我在数币平台和课程 LNMP 实验里反复写过,面试手写就是默写这个模板,五分钟内能写完整。
实操模板(七层标准模板,可直接手写):
nginx
http {
upstream backend { # 1 定义组
least_conn; # 2 策略
server <web1-ip>:8080 weight=3 max_fails=2 fail_timeout=10s; # 3 健康检查
server <web2-ip>:8080 max_fails=2 fail_timeout=10s;
server <web3-ip>:8080 backup; # 备份节点
}
server { # 4 server 块
listen 80;
server_name www.example.com;
location / { # 5 转发
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_connect_timeout 5s; # 连后端超时
proxy_read_timeout 60s; # 读响应超时
proxy_next_upstream error timeout; # 失败自动切下一台
}
}
}追问链:
- 追问1:怎么观察流量实际分到哪台? → 答:两个手段。一是日志:自定义 log_format 加入
$upstream_addr变量,它会记录每个请求实际命中的后端地址,grep 统计就能看出分布是否符合权重要求;二是状态页:开 stub_status 模块,看 Active connections 和 Reading/Writing/Waiting 三态分布,判断连接是否堆积。我做变更后的验证就是这套:改配置 reload → 观察 upstream_addr 分布 → 确认策略生效,这在生产上是标准动作,不是可选项。
4.6 root 与 alias 的区别 【必背】
标准回答: root 和 alias 都能把 location 映射到文件系统目录,但拼接规则不同,一句话:root 是"root 路径 + 完整 URI",alias 是"alias 路径 + URI 去掉 location 前缀的部分"。
举例对比同一个请求 /ui/a.js:location /ui/ { root /opt/app; } 找的文件是 /opt/app/ui/a.js——location 的路径 /ui/ 被保留拼进结果;location /ui/ { alias /opt/app/dist/; } 找的文件是 /opt/app/dist/a.js——/ui/ 前缀被 alias 整段替换掉了。两个约束也要记:alias 只能写在 location 里(root 可以写在 http、server、location 任何层);location 用正则时 alias 必须带捕获组来拼路径,否则语法混乱。
这个知识点是手写配置题的高频坑:把前端文件放在目录名和访问路径不一致的地方,root 就会 404,换 alias 立刻就好——我用实际踩坑经历答这题。 追问链:
- 追问1:前端放 /opt/dcepscapi/ui,用 root 怎么写? → 答:我数币平台就是这么配的:
location / { root /opt/dcepscapi/ui; index index.html; }。因为 location 是 /,URI 原样拼在 root 后面,请求 / 就落到 /opt/dcepscapi/ui/index.html,路径正好对齐,这时用 root 最自然。什么时候必须换 alias?当访问路径前缀和磁盘目录名不一致的时候,比如访问 /static/ 但文件在 /opt/app/assets/——这种情况 root 写不出来,只能 alias 替换前缀。能说出"什么时候用哪个",这题才算真懂。
4.7 location 匹配规则与优先级 【理解】
标准回答(优先级从高到低,必背): location 匹配优先级四档:第一档 location = /path 精确匹配,命中立即停止不再看其他;第二档 location ^~ /static/ 前缀匹配,命中后不再检查正则——专门用来"锁住"某些路径防止被后面的正则抢走;第三档 location ~ 正则 和 ~* 正则(区分/不区分大小写),多个正则按配置文件出现顺序,第一个命中的生效;第四档普通 location /path 前缀匹配,取最长前缀的那条——注意它只作为"备胎缓存",正则命不中时才用它。记忆口诀一句话:精确 > ^~ > 正则(按顺序)> 普通前缀(取最长)。
这套规则决定了多 location 共存时的实际行为,配错顺序就会出现"静态文件请求被 / 的反代规则吃掉"这类诡异现象。 追问链:
- 迭问1:
location /prod-api/和location ~ \.php$谁优先? → 答:正则优先——只要请求同时匹配两者(比如 /prod-api/x.php),正则块生效,普通前缀只有在没有任何正则命中时才接管。这就是为什么静态站配置里常见location ^~ /static/先行:用 ^~ 把静态目录锁死,防止被后面的 .php、.js 正则误抢。理解了"正则压普通前缀",一堆 location 的配置行为就能推理出来,不用死记。 - 迭问2:rewrite 怎么用? → 答:语法
rewrite 正则 替换 标志位,四个标志位要分清:last 终止当前轮匹配、用新 URI 重新走一遍 location(容易死循环要小心);break 终止后续 rewrite 和匹配,在当前 location 内处理;redirect 和 permanent 分别是 302 临时和 301 永久跳转。我项目里 80 跳 443 用的就是rewrite ^/(.*)$ https://$host/$1 permanent;——把路径捕获下来拼进 HTTPS 地址,301 让浏览器缓存跳转关系,减少后续请求的往返。
4.8 四层 stream 与七层 http 的区别 【必背】(本人真实高频题)
标准回答: 这两者我都真实配过,按"工作在哪一层、能看懂什么"来区分。七层负载在 http 块:Nginx 完整解析 HTTP/HTTPS 协议,所以能按 URL 路径、Header、Cookie 做路由,支持改写、缓存、限流——功能强但要解协议,性能略低。四层负载在 stream 块:只看 IP 和端口,做纯 TCP/UDP 流量转发,不解应用层内容,性能更高,适合 MySQL、Redis、SSH 这类非 HTTP 服务,以及 K8s apiserver——apiserver 是 HTTPS 但不需要按 URL 路由,四层转发就够了。
stream 是独立模块,编译时必须带 --with-stream,用 nginx -V 能查到。本人真实案例必须讲成自己的:银行项目里我给三台 master 的 apiserver 做四层代理——Nginx stream 块里 upstream k8s-apiserver 挂三台 master 的 6443,listen 16443 然后 proxy_pass 过去。
这里有个关键细节:因为 Nginx 和 master 复用同一台机器,监听端口绝不能是 6443(会撞本地 apiserver),所以统一用 16443,外部所有 kubectl 和组件都连 VIP:16443。 追问链:
- 迭问1:怎么确认 Nginx 支持 stream? → 答:
nginx -V 2>&1 | grep with-stream看编译参数里有没有;没有的话要重新./configure --with-stream再 make 编译。我在 ARM 环境编译 Nginx 1.25.1 时把 ssl、stub_status、stream 该带的模块一次性都带上了——生产环境编译要"一次配齐",二次补编译是要停服的,这个意识很重要。 - 迭问2(深挖):四层健康检查怎么做? → 答:两个层面。stream 块内配 proxy_connect_timeout,连接后端超时就判定不可用、自动摘除该后端——这是转发层面的被动检查;服务级存活由 Keepalived 的 vrrp_script 检测 16443 端口是否有进程监听,检测失败就触发 VIP 漂移(配置在 4.17)。两层检查的分工要说清:Nginx 管"三台后端哪台可用",Keepalived 管"当前这台 Nginx 所在机器的入口服务还活着没"。
- 迭问3:LVS 能做基于 URL 的转发吗? → 答:不能。LVS 工作在内核态传输层,只认 IP 加端口(IPVS),根本看不到 URL——需要 URL 路由必须交给 Nginx 或 HAProxy 的七层模式。这也是 LVS 和 Nginx 的功能分界:LVS 用性能换功能,Nginx 用一点性能换完整的七层能力,详见 4.18 的三款对比。
4.9 HTTPS/SSL 配置与 80→443 重定向 【理解】
标准回答(流程 + 配置两段式): HTTPS 部署我的标准流程四步:第一步确认编译支持——nginx -V 查有 --with-http_ssl_module,没有就要带参数重编译,ARM 环境还要提前装齐 openssl-devel 依赖否则 configure 直接报错;第二步准备证书 crt 和 key 文件(CA 签发或客户提供,银行项目用的是客户内部 CA 的证书);第三步 server 块 listen 443 ssl 挂证书并配会话复用、加密套件和协议版本——ssl_session_cache 共享缓存减少重复握手、ssl_protocols 限 TLSv1.2/1.3 禁掉老旧协议;第四步 80 端口的 server 块做 301 永久重定向强制跳 443,用 rewrite permanent 把原路径带过去。
这套配置我在数币平台完整落地过:80 全部 301 到 443,443 挂证书服务前端静态页,/prod-api/ 反代到本地 8080 的后端 jar。 实操模板(数币平台真实配置结构,占位符替换):
nginx
# 80 强制跳 443(301 永久重定向)
server {
listen 80;
server_name localhost;
rewrite ^/(.*)$ https://<nginx-ip>:443/$1 permanent;
}
# HTTPS 主站
server {
listen 443 ssl;
server_name localhost;
ssl_certificate /usr/local/nginx/cert/server.crt;
ssl_certificate_key /usr/local/nginx/cert/server.key;
ssl_session_cache shared:SSL:1m; # 会话复用,减少握手开销
ssl_session_timeout 5m;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
ssl_protocols TLSv1.2 TLSv1.3; # 禁用老旧协议
location / {
root /opt/dcepscapi/ui;
try_files $uri $uri/ /index.html; # 前端路由刷新 404 兜底(见 4.14)
index index.html index.htm;
}
location /prod-api/ { # API 反代到本地 8080 后端
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://localhost:8080/;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html { root html; }
}追问链:
- 迭问1:为什么 ARM 环境编译 Nginx 要特别注意 ssl 模块? → 答:因为默认 configure 不带 ssl 模块,漏了的话 listen 443 ssl 语法直接报错,而重新编译要停服。我在统信 UOS ARM 上编译 1.25.1 的完整命令是
./configure --prefix=/usr/local/nginx --with-http_ssl_module --with-http_stub_status_module --with-stream && make && make install,编译前还确认了 gcc、openssl-devel、pcre-devel 这些 ARM 依赖库都装齐——ARM 仓库里依赖不一定全,这是我真实踩过并写进部署指南的。 - 迭问2:
/prod-api/末尾的斜杠有讲究吗? → 答:非常有讲究,是 proxy_pass 的高频坑。proxy_pass http://localhost:8080/;带尾斜杠时,/prod-api/ 这个前缀会被剥掉再拼接——请求 /prod-api/user/login 到后端变成 /user/login;不带尾斜杠则完整 URI 原样传给后端,变成 /prod-api/user/login。到底带不带,取决于前端约定的 baseURL 和后端服务的 context-path:数币平台前端 baseURL 是 /prod-api、后端是根路径,所以我用了带尾斜杠的写法剥掉前缀。配错了现象就是接口全 404——这个坑我给团队新人讲过不止一次。
4.10 gzip 与 expires 静态优化 【了解】
标准回答: 静态资源优化我做两件事:传输层 gzip 压缩和端侧 expires 缓存。gzip:gzip on; gzip_types text/plain text/css application/javascript application/json; gzip_comp_level 5; gzip_min_length 1k;——纯文本类压缩率能到 60% 到 70%,明显省带宽提速;但图片视频本身已是压缩格式,再压只浪费 CPU,不要放进 gzip_types;压缩级别 1 到 9,5 是 CPU 和压缩率的平衡点。
expires:location ~* \.(jpg|png|css|js)$ { expires 30d; add_header Cache-Control "public"; } 让浏览器把静态资源缓存在本地,回源量大幅下降。两者组合一句话:gzip 管"传输过程变小",expires 管"第二次不用传",是静态站优化的标准两板斧。 追问链:
- 迭问1:expires 设多久合适? → 答:看资源有没有版本化。带版本号的静态资源(app.v3.js 这种文件名带哈希或版本号的)可以放心 expires 30d——发版换文件名,旧缓存自然失效;不带版本号的资源要设短(几分钟)或不缓存,否则发版后用户浏览器还拿着旧文件,出现"我改了怎么不生效"的经典工单。我在数币平台配的是带版本号资源长缓存、index.html 不缓存——入口文件必须每次拿最新的。
4.11 limit_req / limit_conn 限流 【理解】
标准回答: Nginx 原生两个限流模块按维度区分:limit_req 限"请求速率",基于漏桶算法——http 块先用 limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s; 定义(10m 共享内存大约能记 16 万个 IP 会话,$binary_remote_addr 用二进制存 IP 更省内存),location 里 limit_req zone=api burst=20 nodelay; 应用——burst 20 允许突发排队 20 个请求,nodelay 让这批突发立即处理不排队,超出就返回 503。
limit_conn 限"并发连接数":limit_conn_zone $binary_remote_addr zone=conn:10m; 加 limit_conn conn 20;,限制单 IP 同时保持 20 个连接。典型场景是 API 防刷、防 CC 攻击。
答这题的加分点是讲清漏桶和令牌桶的区别:漏桶恒定速率流出,做平滑整形;令牌桶按速率补充令牌、桶里有存量就允许突发——Nginx 的 burst 加 nodelay 组合实质是"允许短突发"的折中实现。 追问链:
- 迭问1:漏桶和令牌桶的区别? → 答:漏桶算法把所有进来的请求倒进一个桶,以恒定速率从桶底流出处理——不管进来多猛,出去永远是匀速,彻底整形但会牺牲突发体验;令牌桶按固定速率往桶里放令牌,请求拿到令牌才处理,桶里攒的令牌允许短时间突发超速。适用取向:保护脆弱后端用漏桶(Nginx 实现),允许正常业务波峰用令牌桶(Guava、网络设备 QoS 常见)。Nginx 的 burst 参数相当于给漏桶加了个缓冲区,nodelay 再让缓冲区的请求即时处理——本质是两种算法间的工程折中,这个理解层次答出来就超过背参数了。
4.12 worker_processes / worker_connections 等调优 【必背】
标准回答: Nginx 性能参数我按五组背:进程与连接——worker_processes auto 让 worker 等于 CPU 核数、worker_rlimit_nofile 65535 抬进程级文件描述符上限、events 块里 use epoll 选事件模型、worker_connections 10240 单 worker 最大连接数、multi_accept on 批量接收新连接。传输层——sendfile on 零拷贝发静态文件、tcp_nopush 配合 sendfile 合并发包、tcp_nodelay 让 keepalive 连路低延迟。
连接管理——keepalive_timeout 65s 长连接超时、client_header_timeout 15s 和 client_body_timeout 15s 防慢连接攻击。理论并发等于 worker_processes 乘 worker_connections,4 核乘 10240 约 4 万;反向代理场景实际并发要除以 2,因为每个客户端连接对应一条到后端的连接。
系统层必须配套:ulimit(/etc/security/limits.conf)和 sysctl 的 somaxconn 不抬,Nginx 配再大也接不住——这呼应第 1 章的句柄知识点。调优方法论要说:参数不是抄的,是"压测(ab -n -c)→ 观察 stub_status → 定位瓶颈 → 调参 → 复测"的闭环。
实操模板(必背参数组):
nginx
worker_processes auto; # 1. worker = CPU 核数
worker_rlimit_nofile 65535; # 2. 进程文件描述符上限
events {
use epoll; # 3. Linux 用 epoll 事件模型
worker_connections 10240; # 4. 单 worker 最大连接数
multi_accept on; # 批量接受新连接
}
http {
sendfile on; # 5. 零拷贝传静态文件
tcp_nopush on; # 配合 sendfile 合并包
tcp_nodelay on; # keepalive 低延迟
keepalive_timeout 65s; # 6. 长连接超时
client_header_timeout 15s; # 头/体超时防慢连接攻击
client_body_timeout 15s;
}追问链:
- 迭问1:参数值为什么这么设? → 答:逐条给理由:worker 等于核数是避免进程切换浪费多核资源;10240 乘核数远超业务峰值,是给突发和演进留余量——容量按峰值的两到三倍配是通用原则;65 秒 keepalive 是平衡 TCP 复用收益和后端连接占用,太短复用率低、太长后端连接积压。但我一定会补方法论那句:"这些值是起点不是终点,生产要以压测和 stub_status 观测为准"——说清"怎么定的"比背数字更可信。
4.13 日志分析与排障(access/error) 【理解】
标准回答: Nginx 两类日志分工:access_log 记录每个请求——默认格式包含客户端 IP、时间、请求行、状态码、响应字节数、referer 和 user-agent;排障和统计时我会自定义 log_format 追加三个变量:$upstream_addr 记录实际命中的后端(定位哪台机器出问题)、$request_time 请求总耗时(区分慢在哪段)、$http_x_forwarded_for 多级代理下的真实客户端 IP。
error_log 记录 Nginx 自身错误,error_log 路径 warn; 级别从 debug 到 crit,排障临时调到 debug 看详细信息,平时 warn 足够避免日志爆炸。日志分析的高频命令组(TOP IP、状态码分布、5xx 统计)是我用 awk 管道固定的三件套,见模板。图形化工具 GoAccess 可以实时出 HTML 报表,适合给管理层看。 高频命令(Top 100 IP 必背):
bash
awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -nr | head -n 100 # 访问量前100 IP
awk '{print $9}' access.log | sort | uniq -c | sort -nr # 状态码分布
awk '$9>=500{print $9}' access.log | wc -l # 5xx 数量
awk '{print $7}' access.log | sort | uniq -c | sort -nr | head # 热点 URL追问链:
- 迭问1:日志里全是代理 IP 看不到真实客户端? → 答:反代场景的必然现象——后端日志记的是直连者即代理的 IP。解决两步:代理层配透传头(X-Real-IP、X-Forwarded-For,见 4.2);后端的 log_format 改用 $http_x_forwarded_for 取透传值。多级代理时 X-Forwarded-For 是逗号分隔的链,取第一个才是原始客户端——我在数币平台配过后端日志格式,这类细节是实际配过的人才会注意到的。
4.14 502/504 排查链路 + 前端刷新 404 【必背】
标准回答: 这三个是我处理过的真实高频故障,每个有固定排查链。502 Bad Gateway 含义是"后端无效响应或连不上":排查顺序——后端进程在不在(ps/jps 看进程、ss -lntp 看端口)→ 后端是拒绝连接还是崩溃退出 → Nginx 到后端的网络和防火墙 → SELinux 权限拒绝 → 后端自身的崩溃日志。经验上"后端没起或崩了"占 502 的大多数。
504 Gateway Timeout 含义是"后端活着但太慢":排查顺序——应用慢日志和慢 SQL → 按需调大 proxy_read_timeout → 长任务异步化 → 后端资源层(CPU、内存、GC)。一句话区分:502 是死、504 是慢,处理方向完全不同。第三个是前端刷新 404:Vue/React 用 history 路由模式时,用户在 /user/list 刷新,Nginx 拿这个路径去找文件当然找不到。
解法一行:try_files $uri $uri/ /index.html;——先找真实文件、再找目录、都失败回退到 index.html 交给前端路由处理。这个坑我在数币平台上线时真实处理过。 追问链:
- 迭问1:502 和 504 一句话区分? → 答:502 是 Nginx 连不上后端或后端给了非法响应——"后端死了";504 是连接建立了但在 proxy_read_timeout 内没等到完整响应——"后端慢"。排障入口因此不同:502 第一步查后端进程和端口,504 第一步查后端为什么慢(慢日志、GC、锁)。我处理这类工单时会先看一眼日志里的状态码分布,把死和慢分开派单。
- 迭问2:怎么快速定位是哪台后端 502? → 答:靠日志里的 $upstream_addr 变量——它记录了请求实际打到的后端地址,grep 出 502 的请求看后端分布,就能锁定具体机器;再到那台机器上 curl 后端端口复现验证。没有配 upstream_addr 的日志就要逐台后端 curl 排除——所以我的习惯是 log_format 里永远带着这个变量,"日志为排障而设计"。
关联话术:"数币平台上线时客户报'登录后刷新页面 404',我十分钟定位到是前端 history 路由问题,加 try_files 一行解决,之后这套配置成了我交付前端项目的标配。这个案例我讲起来特别顺:现象、根因(Nginx 按路径找文件 vs 前端路由是 JS 行为)、解法、预防(前端项目标配 try_files)四段齐全,是'小问题见基本功'的最好例证。"
4.15 LNMP 架构与动静分离 【理解】
标准回答: LNMP 是 Linux 加 Nginx 加 MySQL 加 PHP 的经典组合,现在泛化为"Nginx 入口 + 任意后端 + 数据库"。核心分工:Nginx 做统一入口处理静态资源(HTML/JS/CSS/图片)和反向代理,动态请求(.php、/api)转给后端——php-fpm 走 9000 端口、Tomcat 走 8080、Java jar 直接 proxy_pass。
动静分离就是按 location 把两类流量拆开:静态后缀的 location 直接 root 直出配 expires 缓存;动态路径的 location proxy_pass 或 fastcgi_pass 到后端。好处三条:静态走 Nginx 的 sendfile 零拷贝直出,性能比过任何后端高一个量级;后端组可以独立扩容,只扩动态服务;缓存和压缩策略可以按类型分别精细配置。
我在课程 LNMP 实验里用 ab 压测对比过动静分离前后的吞吐差异,数字层面的收益是直观的。 实操模板(动静分离三段式):
nginx
location ~* \.(html|css|js|jpg|png|gif|ico)$ { root /opt/app/static; expires 7d; }
location ~ \.php$ { fastcgi_pass 127.0.0.1:9000; } # PHP 场景
location /api/ { proxy_pass http://backend; } # 通用后端追问链:
- 迭问1:Nginx 和 php-fpm 怎么配合? → 答:通过 FastCGI 协议:Nginx 把 .php 请求经
fastcgi_pass 127.0.0.1:9000转给 php-fpm 进程池处理,php-fpm 执行完把结果返回,Nginx 再响应客户端。与 proxy_pass 的区别在协议本质:fastcgi 是专门为"HTTP 服务器对接后端应用"设计的二进制协议,proxy_pass 是标准的 HTTP 反向代理。php-fpm 侧要理解的参数是进程池模型(pm 设置 static/dynamic)——dynamic 模式按 min/max spare servers 弹性管理 worker,课程实验里我调过这组参数看并发表现。
4.16 Keepalived:VRRP 原理与主备模型 【必背】
标准回答: Keepalived 的核心是 VRRP 协议——虚拟路由冗余协议:把多台机器组成一个虚拟路由器(同一个 virtual_router_id 标识),对外暴露一个虚拟 IP 即 VIP 提供服务。工作模型:priority 优先级最高的竞选为 MASTER(0 到 255),MASTER 持有 VIP 并每 advert_int(默认 1 秒)向组播地址发通告报文;BACKUP 只监听不抢,连续约 3 倍心跳周期(约 3 秒)收不到通告就认为 MASTER 挂了,接管 VIP——这个过程就是 VIP 漂移,对客户端而言入口地址永远不变,这就是它做高可用的原理。
第二个关键机制是 vrrp_script 加 track_script:自定义脚本检测"真实服务"的健康状态(比如 Nginx 端口还在不在),失败时按 weight 降权触发漂移——因为 Keepalived 本身只保证"自己活着",不保证 Nginx 活着,没有这个机制就会出现"Keepalived 在、Nginx 死了、VIP 还在这台死节点上"的假高可用。
验证三板斧:ip addr 看 VIP 在哪台网卡上;主节点关机或杀服务看 VIP 是否漂到备机;客户端持续访问验证业务无感。 追问链:
- 迭问1:state MASTER/BACKUP 和 priority 什么关系? → 答:state 只是初始声明,告诉 Keepalived 自己的期望角色;真正的角色由 priority 竞选决定——即使两台都配 MASTER,priority 高的也会胜出。进阶配置是 nopreempt(抢占模式关闭):主恢复后不自动抢回 VIP,继续由备机服务——好处是避免恢复瞬间的二次切换抖动,代价是流量长期在备机上。是否启用要看业务对切换次数的容忍度,我的项目里主备规格一致,用的默认抢占。
- 迭问2:VRRP 报文不通会怎样? → 答:那就是脑裂的前提条件——BACKUP 收不到通告以为 MASTER 死了自己接管,MASTER 其实活得好好的,两台都持有 VIP,流量紊乱。VRRP 报文不通的常见原因:交换机组播被禁、防火墙拦了 VRRP 协议(协议号 112)、跨交换机部署时组播域不统一。部署时必须验证主备能互相收到 VRRP 报文(tcpdump 能抓到),防范措施见 4.17/4.18。
4.17 vrrp_script 健康检查与 VIP 漂移验证 【必背】(本人真实 check_nginx)
标准回答(结合银行项目讲,全部真实): 这套配置我在银行项目的多 master 改造里从零写过并实测验证。设计逻辑:Keepalived 只保证自己活着,不保证 Nginx 活着,所以必须用 vrrp_script 挂真实服务检测脚本。我脚本检测的是 16443 端口有没有进程监听(ss -antp | grep 16443),数到 0 个进程就 exit 1,Keepalived 按 weight -20 降权;主备 priority 100 和 90 拉开差距——100 减 20 变 80 低于备机 90,VIP 立即漂到备节点。
参数设计有讲究:interval 3 每 3 秒检测一次平衡灵敏度和开销;fall 2 连续失败两次才判定(防单次抖动误切);advert_int 1 秒保证心跳快速感知。
故障转移验证四步是我实测做过的:① ip addr 确认 VIP 初始在主节点;② 主节点 systemctl stop nginx(或直接关机);③ 备节点 ip addr 看到 VIP 漂过来;④ 客户端 curl -k https://<VIP>:16443/version 验证集群正常响应——K8s 场景 kubectl 走 VIP 无感切换。恢复主节点后验证 VIP 是否回切(未配 nopreempt 会抢占回来)。 实操模板(主备 + 脚本,可整段背写):
bash
# /etc/keepalived/check_nginx.sh(主备相同)——检测 16443 是否有进程监听
#!/bin/bash
count=$(ss -antp | grep 16443 | egrep -cv "grep|$$")
if [ "$count" -eq 0 ]; then
exit 1 # 0=正常,非0=异常,触发降权漂移
else
exit 0
finginx
# keepalived.conf —— 主节点(Nginx MASTER)
global_defs { router_id NGINX_MASTER; }
vrrp_script check_nginx {
script "/etc/keepalived/check_nginx.sh"
interval 3 # 每3秒检测一次
weight -20 # 失败降权20,100-20 < 备机90,触发漂移
fall 2 # 连续失败2次才算失败
rise 2
}
vrrp_instance VI_1 {
state MASTER
interface eth0 # 按实际网卡
virtual_router_id 51 # 主备必须一致
priority 100 # 备节点 90
advert_int 1
authentication { auth_type PASS; auth_pass 1111; }
virtual_ipaddress { <VIP>/24; }
track_script { check_nginx; }
}追问链:
- 迭问1:为什么检测端口而不是检测 nginx 进程名? → 答:因为"进程在"不等于"服务可用":配置错误、端口被占、证书加载失败时,nginx 进程可能在但 16443 端口起不来,服务实际不可用。而 16443 是客户端真正访问的入口,检测"入口有没有人监听"才是端到端的可用性视角。这个设计原则可以推广:健康检查要尽量贴近用户真实路径,我后来做 CLB 健康检查、K8s 探针配置都是同一个原则。
- 迭问2:haproxy 版本怎么写? → 答:思路完全一样,只是脚本探测对象换成 haproxy:
killall -0 haproxy—— -0 信号只探测进程存在不真正发信号,退出码反映存活。我项目早期用的 haproxy 方案就是 check 脚本加 killall -0,后来切到 Nginx 方案才改成查端口。两个方案我都配过,对比着讲反而证明我理解的是机制不是模板。
4.18 脑裂与防范 + LVS 四模式 + 三款对比 【必背】
标准回答: 先讲脑裂:主备之间心跳不通、但双方都在对外服务,各自都认为自己是 MASTER,VIP 冲突、流量紊乱甚至数据错乱。防范四条(每条都有项目对应):一是 vrrp_script 检测真实服务,服务挂了主动降权让 VIP 漂走,不要死守;二是 priority 拉开差距(100/90)加 advert_int 1 秒,快速感知快速决出;三是保证 VRRP 报文畅通——管理平面交换机组播放行、防火墙放行协议号 112、跨交换机部署要提前验证;四是监控双 VIP 告警——两台机器同时持有 VIP 就是脑裂的铁证,发现后人工摘除一侧。
再讲 LVS 四种模式:NAT 模式调度器改目标 IP、进出流量都过调度器,简单但调度器易成瓶颈;DR 直接路由模式只改 MAC 地址、响应由 RS 直接回客户端不过调度器,性能最高、生产首选,但要求 RS 与调度器同二层、RS 要绑 VIP 并做 ARP 抑制;TUN 隧道模式 IP 封装支持跨机房,配置复杂逐渐少用;FULLNAT 双向 NAT 是阿里内部扩展,社区 LVS 不原生支持。
三款调度器一句话结论:性能 LVS 大于 HAProxy 大于 Nginx,功能 Nginx/HAProxy 大于 LVS。
| 维度 | LVS | HAProxy | Nginx |
|---|---|---|---|
| 层级 | 仅四层(内核 IPVS) | 四层+七层 | 四层(stream)+七层 |
| 性能 | 百万级,内核态 | 中 | 十万级,用户态 |
| 功能 | 只有转发 | 健康检查精细、会话保持强 | 反代/缓存/rewrite/静态服务,生态最全 |
| 配置 | ipvsadm 偏复杂 | 中等 | 最简单 |
追问链:
- 追问1:脑裂了数据会怎样? → 答:脑裂的真正危险在"两个入口同时接收写请求":两个主各自接受写入,状态分叉,恢复时无法自动合并,可能丢一侧的数据。所以发现脑裂的第一动作是人工干预——摘除一侧 VIP 或手动降权,恢复单主,然后再排查心跳网络。这个答案的层次在"数据视角":很多人只答"IP 冲突",能讲到写冲突和恢复困境才算理解脑裂为什么可怕。
- 迭问2:DR 模式为什么性能最高? → 答:因为它把"响应流量"从调度器上卸掉了:请求进来时调度器只改写帧的目标 MAC 转给 RS(IP 层不动),RS 处理后直接把响应发回客户端,根本不经过调度器。这样调度器只承载请求方向的小流量,出方向的响应大流量由 RS 硬件直接出——对称的流量模型被改造成了不对称的调度模型。代价是部署约束:RS 必须能收到调度器的二层帧(同网段)、RS 要在 loopback 绑 VIP 且做 ARP 抑制避免 ARP 应答混乱。
关联话术:"银行项目里 haproxy 和 Nginx 两个方案我都实际做过 apiserver 前面的四层负载——haproxy 是 frontend bind 16443 加 backend roundrobin 挂三台 master 的 6443 带 check,还有 1080 的 stats 页面;后期客户侧对 Nginx 更熟,标准化到 Nginx stream 加 Keepalived。选型逻辑我能讲:都是 TCP 转发、性能都远超三台 apiserver 的需要,最终看现场运维习惯和统一管理成本——'在性能过剩的场景,可运维性才是决定因素',这是我真实的选型观。"
4.19 HAProxy 四层配置模板(本人真实方案)【必背】
标准回答: haproxy 方案我在银行项目先于 Nginx 实际部署过,作为 apiserver 的四层入口。设计三件套讲清:frontend 定义前端入口——bind *:16443 监听(同样的端口避让逻辑:6443 被本地 apiserver 占用);backend 定义后端组——balance roundrobin 轮询三台 master 的 6443,每个 server 带 check 开启健康检查(默认 tcp 层探活),check inter 2s fall 3 rise 2 控制探测节奏;listen stats 独立监控页——bind *:1080 加 stats enable,浏览器打开就能看每个后端的当前状态、up/down、会话数——这是 haproxy 相对 Nginx stream 的一个体验优势:状态页开箱即用,排障时一眼看到三台 master 的健康状态。后期方案切到 Nginx 是客户侧运维习惯原因(4.18 的话术),两个方案的配置我都背得出。 实操模板(haproxy 四层核心段,可整段背写):
frontend k8s-apiserver
bind *:16443
mode tcp
default_backend k8s-master-api
backend k8s-master-api
mode tcp
balance roundrobin
server master01 <master01-ip>:6443 check inter 2s fall 3 rise 2
server master02 <master02-ip>:6443 check inter 2s fall 3 rise 2
server master03 <master03-ip>:6443 check inter 2s fall 3 rise 2
listen stats
bind *:1080
stats enable
stats uri /stats追问链:
- 迭问1:haproxy 和 Nginx 做四层你实际怎么选? → 答:我的实际经历是两个都上线过、最终按"现场运维习惯"定——银行客户侧对 Nginx 更熟,后期标准化到 Nginx stream 加 Keepalived。技术对比上:纯 TCP 转发性能两者都远超三台 apiserver 的需要,不是决策因素;haproxy 胜在健康检查和状态页开箱即用(stats 页面、更细的 check 参数),Nginx 胜在与业务七层配置统一管理、团队认知成本低。我的结论句:"性能过剩的场景,可运维性才是决定因素"——这句话在面试里说出来的分量,比背一长串参数对比更大。
4.20 长连接与 WebSocket 反代 【理解】
标准回答: Nginx 反代长连接有两个必配点。第一是到后端的长连接:默认 proxy_pass 每个请求都新建一条到后端的 TCP 连接,用完就断——高 QPS 下后端被连接建立开销和 TIME_WAIT 淹没;配置 upstream 里 keepalive 32(连接池大小)加 location 里 proxy_http_version 1.1; 和 proxy_set_header Connection "";(清掉客户端传来的 Connection: close),让 Nginx 与后端复用长连接。
第二是 WebSocket 这类升级协议:WS 客户端先发 HTTP 请求带 Upgrade: websocket 头请求协议升级,Nginx 默认会把这些头丢弃导致升级失败,必须显式透传:proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; 加 proxy_read_timeout 调大(WS 空闲连接要保持住,默认 60 秒超时会把长连接踢掉,我一般给到 300 秒以上)。这两组配置我在课程实验和平台前端场景配过,是"反代能用"到"反代好用"的分水岭。 追问链:
- 迭问1:怎么确认反代后的 WebSocket 升级成功? → 答:三个验证动作:客户端或浏览器开发工具看 101 Switching Protocols 响应码——升级成功的标志;Nginx access_log 里该请求记录的状态码是 101 而不是 400/404;在 Nginx 上抓包或 error_log debug 级看 Upgrade 头是否透传到了后端。常见失败形态要能对号:立即 400 是 Upgrade 头没配、连上几十秒断是 read_timeout 太短、跨节点断连是中间还有一层代理没配升级透传——多层代理每一跳都要配,漏一层断一层。
4.21 499 状态码与连接耗尽(keepalive / TIME_WAIT)【了解】
标准回答: 499 是 Nginx 特有的状态码:客户端在 Nginx 返回响应之前主动断开了连接——"客户不等了"。两个高频成因:一是后端太慢,客户端超时先撤(大量 499 往往伴随后端 504,本质是慢);二是客户端侧超时设置比 Nginx 的 proxy_read_timeout 短。看到 499 我的第一动作是统计它的 URL 分布和时段,判断是不是某几个慢接口拖的。
另一个连接层话题是 TIME_WAIT:主动关闭连接的一方进入 TIME_WAIT 等待 2MSL(约 60 秒),高 QPS 短连接场景客户端或 Nginx 会堆积大量 TIME_WAIT——这不一定是故障(是协议的自我保护),但端口耗尽时新连接建不了。治理:应用层上 keepalive 复用连接(对客户端和到后端都开),系统层调 net.ipv4.tcp_tw_reuse(客户端侧复用)——根号是减少短连接,而不是消灭 TIME_WAIT。 追问链:
- 迭问1:大量 TIME_WAIT 要不要恐慌? → 答:先看谁的 TIME_WAIT:出现在 Nginx(作为到后端的客户端,短连接模式下主动关连接)说明该给 upstream 配 keepalive 了;出现在后端应用,检查是不是应用主动关闭;出现在客户端机器上做压测是正常现象。判断要不要处理的标尺只有一个:有没有影响到新连接建立(端口耗尽、连接拒绝)。没有影响就只做容量记录,有影响就上连接复用——"看到指标异常先判断是否真有影响"是我处理这类'伪故障'的原则。
4.22 Nginx 平滑升级(热升级)【理解】
标准回答: Nginx 二进制热升级解决"升级版本不能停服"的问题,流程五步背下来:① 编译新版本(configure 参数保持一致,nginx -V 查旧参数照抄);② 备份旧二进制 mv /usr/local/nginx/sbin/nginx /usr/local/nginx/sbin/nginx.old,替换新的进 sbin;③ 给旧 master 发 USR2 信号——旧 master 把 pid 文件改名、fork 出新 master 加载新二进制,新旧 worker 并存,新 master 开始监听端口(SO_REUSEPORT 或继承 socket);④ 给旧 master 发 WINCH 优雅停掉旧 worker(处理完存量连接),观察新版本服务正常;⑤ 确认无误后给旧 master 发 QUIT 完全退出;发现问题就给新 master 发 HUP 前用旧 master 回滚——旧 worker 还在就能立刻切回。
这个流程我在课程实验里完整走过一遍;生产上我更倾向"低峰重启"这种简单方案(我们的变更窗口允许),热升级是"知道完整流程、评估后选择"的状态。 追问链:
- 迭问1:USR2 和 WINCH 各干什么? → 答:USR2 是"升级信号"——旧 master 收到后启动新二进制的新 master,产生新旧两代进程并存的过渡态,这是热升级的核心魔法;WINCH 是"优雅缩 worker"——让旧 master 的 worker 处理完当前连接后退出,把流量自然交给新 worker。两个信号的分工对应升级的两个阶段:先并行(保安全)、再收旧(保干净)。能讲清信号语义,就说明你理解 Nginx 进程模型到可以安全操作的程度——kill -HUP reload 也是同一套信号体系(1.3 的知识在这里呼应)。
4.23 Nginx 安全基线(隐藏版本 / 防盗链 / 限制方法)【了解】
标准回答: Nginx 作为入口可以做一层廉价有效的安全加固,我的基线清单五条:① 隐藏版本号 server_tokens off;——默认响应头和错误页会暴露 Nginx 版本,给攻击者送情报;② 限制 HTTP 方法:if ($request_method !~ ^(GET|POST|PUT|DELETE|HEAD)$) { return 405; },把 TRACE 这类探测方法挡在门外;③ 防盗链:valid_referers 加 if ($invalid_referer) { return 403; },保护图片和静态资源不被外站白嫖流量;④ 请求体大小限制 client_max_body_size 10m;——既防恶意大包打死后端,也顺带规范上传业务;⑤ 访问控制:管理类路径(/stats、/actuator)按 IP 白名单 allow/deny 或直接内网隔离。
再叠加 4.9 的 HTTPS 和 4.11 的限流,入口层的基本防线就齐了。这些配置在银行客户安全扫描里都是检查项,我是被扫描清单训练出来的。 追问链:
- 迭问1:server_tokens off 能防什么不能防什么? → 答:能防的是"版本信息泄露"——攻击者无法从响应头 Server: nginx/1.25.1 和 404 页面底部直接知道版本,少了一条匹配已知漏洞的路径。不能防的是漏洞本身——如果版本有已公开的漏洞,隐藏版本只是提高了攻击者批量扫描的命中成本,定向攻击照样能打。所以它是纵深防御的一层(降低暴露面),不是替代升级补丁的方案——"隐藏不等于修复"这个边界我答题时会主动说。
4.24 proxy_cache 代理缓存 【理解】
标准回答: proxy_cache 让 Nginx 把后端响应缓存到本地磁盘,重复请求直接命中本地、不打后端——适合后端慢、内容更新不频繁的接口和页面。配置三段:http 层 proxy_cache_path /data/nginx/cache levels=1:2 keys_zone=mycache:50m max_size=5g inactive=60m;(缓存目录、两级子目录、共享内存键区、容量上限、不活跃过期);location 层 proxy_cache mycache; proxy_cache_key $host$uri$is_args$args; proxy_cache_valid 200 10m;(使用哪个缓存区、键怎么算、状态码缓存多久);再配 add_header X-Cache $upstream_cache_status;(HIT/MISS 响应头,验证命中)。
进阶点:proxy_cache_bypass 和 proxy_no_cache 按条件绕过缓存(登录用户不缓存)、proxy_cache_use_stale error timeout 后端故障时用旧缓存兜底——这个"降级保命"能力是它相对应用层缓存的独特价值。
我的使用评价:动静分离场景静态由 root 直出加 expires 已经够(4.10),proxy_cache 适合"后端算得贵的半动态内容",课程实验配置验证过命中率。 追问链:
- 迭问1:怎么验证缓存生效和命中? → 答:两步:响应头加
X-Cache $upstream_cache_status——第一次请求 MISS(回源)、第二次同 URL 请求 HIT(本地返回),肉眼可见;再用ab或脚本压一轮统计命中率,同时观察后端访问日志的 QPS 是否显著下降——命中率的最终证据在后端侧。缓存排查的常见坑是"永远 MISS":多半是 cache_key 里带了变量(每请求不同的 cookie/query)或者 proxy_cache_valid 没覆盖该状态码,先检查键的定义再查有效期。
4.25 接入层组合架构:LVS + Nginx/HAProxy 分层 【了解】
标准回答: 大规模站点的接入层通常是组合架构而不是单层:最前面 LVS 做四层分发——内核态转发性能最强(百万级),把流量分给一组 Nginx/HAProxy;中间 Nginx 层做七层活——按域名路径路由、TLS 卸载、缓存、限流、静态直出;再往后是应用服务。
为什么这样拆?各取所长:LVS 抗最洪水量(DR 模式响应不经过它),七层能力交给更灵活的 Nginx;两层都可以横向扩容,LVS 自身用 Keepalived 主备保证——我项目里 Nginx 加 Keepalived 就是这套思想的小型版(四层入口 VIP 加七层能力合一在 Nginx,规模不大不需要拆开)。演变逻辑能讲清:流量小的时候一个 Nginx 全干了;流量大了先拆出独立四层;再大上云厂商 LB 托管(移动云的 CLB 就是云化的"第一层")。我对这套演进的理解是"按量分层、各层托管化"。 追问链:
- 迭问1:你们项目为什么没上 LVS? → 答:量级决定的。银行项目三台 master 的 apiserver、数币平台的流量,一个 Nginx(四层加七层)加 Keepalived 的单层入口性能余量就非常充足——加一层 LVS 只增加运维复杂度没有收益。我的架构原则:"分层是为了解决单层瓶颈,没有瓶颈就不预支复杂度"。但如果流量上量(比如七层 Nginx 需要多台做横向扩容),就需要前面的四层把流量分给它们——那时 LVS 或云 LB 才有位置。能讲清"什么时候需要"比"会用"更重要。
4.26 常用内置变量速查($host / $remote_addr / $upstream_addr)【理解】
标准回答: Nginx 变量是配置的"编程接口",我按排障和写配置的频率记六个。$remote_addr——直连客户端 IP(真实客户端或上一层代理的 IP);$http_x_forwarded_for——XFF 头的值,多级代理下的原始客户端链;$host——请求的 Host 头(域名路由和透传的核心,proxy_set_header Host $host 保持域名语义);$upstream_addr——实际命中的后端地址,日志里定位"请求打到了谁"的钥匙;$request_time——请求总耗时(含后端),配合 $upstream_response_time(纯后端耗时)对比能立刻分清"慢在 Nginx 还是后端";$proxy_add_x_forwarded_for——追加式的 XFF(在已有值后面补上 $remote_addr)。我的自定义日志格式就带 upstream_addr、request_time、upstream_response_time 三件——这三个变量是"用日志做性能归因"的原料,也是 4.13/4.14 排障方法论的实现基础。 追问链:
- 迭问1:$request_time 和 $upstream_response_time 差很多说明什么? → 答:两个值一减就把"Nginx 侧耗时"和"后端侧耗时"分开了:request_time 明显大于 upstream_response_time,说明时间耗在 Nginx 收发客户端、排队或网络传输上——常见于客户端网络慢、大响应体传输、Nginx 到后端的连接建立慢(该配 upstream keepalive,4.20);两者接近且都大,瓶颈就在后端应用本身。这个"对表归因"是我分析慢请求日志的第一刀,比拍脑袋猜"后端慢"严谨得多。
4.27 reload 的平滑性与连接排空 【理解】
标准回答: nginx -s reload 的平滑过程值得展开讲——这是"改配置不停服"的机制基础:master 收到 HUP 信号后,先做新配置的语法校验(校验失败继续用旧配置,服务不受影响——所以 reload 前的 nginx -t 是流程要求);校验通过后 master 解析新配置,启动一批加载新配置的 worker;然后给老 worker 发优雅退出信号——老 worker 停止接受新连接,但把存量连接处理完(keepalive 的连接在下次请求处理完或超时后退出)才真正退出。整个过程新旧 worker 短暂并存,正在处理的请求不丢,这就是"平滑"的确切含义。要注意的边界:reload 只重载配置,二进制升级不适用(那是 USR2 流程,4.22);如果老连接是长连接(WebSocket、推送),老 worker 可能要等连接自然结束,worker_shutdown_timeout 可以设置最长等待强制退出——配了这个参数要注意长连接业务会被切断的取舍。
我在银行项目上前端配置变更全部走 reload:改完 nginx -t 校验、reload、tail 日志确认新 worker 起来——这套三连动作是我的变更肌肉记忆。 追问链:
- 迭问1:reload 失败会怎样?怎么防? → 答:分两种失败:配置语法错误——nginx -s reload 时 master 校验失败会拒绝加载、继续用旧配置运行,服务不中断但你的变更没生效,master 会记 error 日志;这种是最安全的失败。危险的是"语法对但语义错"——比如 proxy_pass 写错后端端口,reload 成功但流量打到错误目标。防线就是流程:变更前 nginx -t(语法)、reload 后立即验证(curl 探活、tail 日志看有没有 5xx 激增)、重要变更配好回滚(保留 .conf.bak,一条 cp 加 reload 回退)。"变更三件套:校验、验证、可回滚"是我从现场带走的纪律。
4.28 常见配置错误 Top5(新手与老手都会犯)【了解】
标准回答: 我把高频配置错误整理成 Top5 清单,每条配"现象和解法"。① location 顺序错:静态 location 被前面的 / 反代规则吃掉,静态资源全 404 或打到后端——用 ^~ 锁住静态前缀(4.7);② proxy_pass 尾斜杠:带不带斜杠决定前缀剥不剥,配错则后端 404(4.9 的追问,我项目真实踩过);③ root 路径拼错:把 root 当 alias 理解导致 404——回看 4.6 的拼接规则;④ server 块缺少 default_server:IP 直接访问或未配域名落到第一个 server,出现"访问 IP 出现意外站点"——显式配一个 default_server return 444 拒绝;⑤ worker_connections 撞系统限制:配置了 10240 但 ulimit 没抬,worker 起不来或报错——limits.conf 与 worker_rlimit_nofile 要配套(4.12 和第 1 章 1.15 的联动)。这个清单的价值在于:排障时按现象对号入座,五分钟内覆盖最常见的配置类故障。
追问链:
- 迭问1:配置问题怎么系统性预防? → 答:三道防线:nginx -t 语法关(每次变更必过);code review 模板关——团队的 Nginx 变更走评审,检查清单就是这 Top5 加安全项(4.23);灰度验证关——reload 后用 curl 验证关键路径、看日志五分钟再收工。配置管理上我主张"配置进 git、变更走流程"——手工直接改线上配置文件是事故之源,我的习惯是本地改、diff、提交、再同步执行。
4.29 Nginx 自身监控:stub_status 与错误率指标化 【理解】
标准回答: 让 Nginx 自己被监控有两个抓手。第一 stub_status 状态页:编译带 --with-http_stub_status_module,配置 location /nginx_status { stub_status; allow 内网段; deny all; },输出三项核心数据——Active connections 当前活跃连接数、accepts/handled/requests 三个累计计数(总接纳/总处理/总请求数,rate 之后就是每秒新建连接和 QPS)、Reading/Writing/Waiting 三态分布(正在读头/正在返回/空闲等待的长连接)。Reading 高说明客户端发请求慢(可能被慢速攻击)、Waiting 高是 keepalive 正常形态。第二日志指标化:access_log 是现成的指标源——按 4.13 的分析命令把状态码分布、5xx 速率、$upstream_response_time 均值做成定期统计(脚本或 GoAccess),再接进告警。两个抓手配合:stub_status 给"连接层"的实时状态,日志给"请求层"的质量分布。
我们银行项目 Nginx 编译时就把 stub_status 模块带上了(4.9 的编译参数),监控入口的连通性检查就靠它。 追问链:
- 迭问1:Waiting 数量很大正常吗? → 答:正常,而且通常是好事——Waiting 是开启 keepalive 后处于空闲保持期的连接,说明 TCP 复用生效、客户端不用反复握手。异常的形态要看另两个:Reading 持续偏高(大量连接卡在收请求头,防慢速攻击的 client_header_timeout 该起作用了)、Writing 偏高(大量响应发不出去,多半是客户端网络差或响应体过大)。三态的解读口径比数值本身重要——我讲 stub_status 都按"哪个态高说明什么"来组织。
4.30 Nginx 配置的组织:include 拆分与复用 【了解】
标准回答: 配置多了以后"一个 nginx.conf 写到底"必然失控,我的组织方式是 include 分层:主 nginx.conf 只留全局配置(worker、events、http 的公共段),http 块里 include /usr/local/nginx/conf/conf.d/*.conf; 拉起各业务配置;conf.d 下一个业务(或一个 server)一个文件,命名带序号或业务名(10-web.conf、20-api.conf);公共的 upstream、日志格式、限流定义抽到单独文件(upstream.conf、log_format.conf)再被 include——同一份定义被多处引用,改一处全生效。这样组织的好处三个:变更隔离——改 A 业务不会碰坏 B 业务,回滚也按文件回;权限清晰——某业务的负责人只动自己那个文件;复用——日志格式和 upstream 定义单一来源。配套两个习惯:配置进 git(4.28 说过)、部署脚本里做"同步后 nginx -t 通过才 reload"的原子化发布。这套组织法是我从数币平台多个 location 和后续项目多站点管理里沉淀的。
追问链:
- 迭问1:include 的顺序有讲究吗? → 答:有。nginx.conf 是从上到下解析的,include 进来的内容按文件名字典序(同一 glob 模式下)生效——所以公共定义(log_format、limit_req_zone、upstream)要在使用它的 server/location 之前被加载:靠文件名序号控制顺序(00-common.conf 排最前)是惯用手法。另外同名 upstream 或 server 的 listen + server_name 冲突会直接报错,nginx -t 会拦住——这也是"公共定义只写一份"的重要原因,重复定义容易撞。
4.31 证书管理与到期监控 【理解】
标准回答: HTTPS 运维的日常一半是证书管理,我的实践四条。到期监控必须自动化:openssl x509 -in cert.crt -noout -enddate 看到期时间,配监控项在剩余 30 天、7 天两级告警——证书过期是"低级但致命"的故障,全靠日历记必漏;我们把证书到期也做成了巡检脚本的固定项。到期前换证流程:申请新证书(CA 或客户提供)→ 替换 crt/key 文件 → nginx -t 校验配置 → reload(4.27 的平滑机制保证不断线)→ curl 验证新证书链 openssl s_client -connect 域名:443 | openssl x509 -noout -dates。多域名多证书:一个 server 块一张证书,或者一张 SAN 多域名证书(Subject Alternative Names 打包多个域名)——管理上前者清晰、后者省事,按域名归属选。链完整性:服务器要发"证书 + 中间证书"的完整链,只发叶子证书会导致部分客户端(严格校验链的 Java、老安卓)报不可信——openssl s_client 验证时能发现。我经手过数币平台客户 CA 证书的部署和一次到期更换,流程跑熟了。
追问链:
- 迭问1:怎么快速确认线上证书还有多久过期、链完整不完整? → 答:两条命令给全量答案:
echo | openssl s_client -connect 域名:443 -servername 域名 2>/dev/null | openssl x509 -noout -dates -subject远程探测线上实际生效的证书(不是看文件——文件可能换了没 reload);-showcerts参数能把整条链打出来数张数。我把这两条做进了巡检脚本,对 VIP 上所有 HTTPS 入口自动出一张"域名-到期日"表——人工记忆靠不住,证书这种"不响不闹到点就炸"的东西必须工具管。
4.32 压测基础:ab 与观测口径 【了解】
标准回答: Nginx 和应用的容量要说数据就得压测,我最常用 ab(ApacheBench,装机即有)。基础用法:ab -n 10000 -c 200 https://域名/路径——n 总请求数、c 并发数;关注输出四项:Requests per second(吞吐 QPS)、Time per request(均值,注意 c 并发下的均值是"每请求全程时间"要看两个口径)、Failed requests(非零就先找原因,连接被拒或超时说明容量到顶)、Percentage line(P50/P95/P99 分位——服务质量的真话在分位数不在均值)。压测的纪律三条:压测环境别打生产(或生产低峰 + 限流保护);逐级加压(c 从 50 到 100 到 200 找拐点,而不是一上来拉满);边压边观测(Nginx 的 stub_status、后端 CPU、数据库连接数——找到"谁先到瓶颈"才有意义)。我课程实验压动静分离、评估 keepalive 效果,用的都是这套方法——压测的价值不在数字本身,在"找到第一个瓶颈点"。
追问链:
- 迭问1:QPS 上去了但 P99 也飙高说明什么? → 答:典型的"均值还行、长尾爆炸"——吞吐被牺牲了延迟质量。常见原因:连接排队(accept 队列或 upstream 连接池不够)、某类慢请求拖尾(缓存 miss 打到后端)、资源争抢(CPU throttle、IO 等待)。处理方向:看分位数对应的请求特征(是不是特定 URL 慢)、给慢路径加缓存或异步化、核对连接池和队列参数。对延迟敏感的业务,验收标准应该是"P99 小于 X 毫秒"而不是"QPS 到多少"——这是我压测时坚持同时报吞吐和分位的原因。
4.33 OpenResty 与 Lua:Nginx 的扩展形态 【了解】
标准回答: OpenResty 是"Nginx 加 LuaJIT"的发行版——在 Nginx 里嵌入了 Lua 虚拟机,能用 Lua 脚本直接处理请求,把 Nginx 从"配置驱动的代理"升级成"可编程的应用网关"。它能补齐 Nginx 原生缺失的能力,举三个典型:主动健康检查——原生 Nginx 只有被动 max_fails(4.4 的痛点),OpenResty 生态的 lua-resty-upstream-healthcheck 可以周期性主动探测后端并摘除;动态限流和鉴权——在 access 阶段跑 Lua,按业务逻辑(用户等级、活动时间)做限流和 token 校验,不用外部应用扛这些;API 聚合/裁剪——多个后端响应在网关层组装返回。生态地位:很多我们熟悉的组件就是 OpenResty 系——Kong(API 网关)、apisix(国产网关)都是它之上的产品。我的口径:OpenResty 属于"理解原理和适用场景"层——原生 Nginx 加 Keepalived 满足了我项目的需要;当需要网关层业务逻辑时,OpenResty 或 APISIX 是自然的选择方向,这个演进路径我清楚。
追问链:
- 迭问1:什么信号说明该从 Nginx 迁到 OpenResty/网关了? → 答:三个信号:请求处理需要业务逻辑了——按用户/租户限流、动态路由、请求改写复杂到配置语法写不动;需要主动健康检查这类原生没有的能力(4.4 的追问里说过);团队需要 API 治理(插件化鉴权、限流、可观测上报的统一管理)。但要克制:纯转发和静态场景继续用 Nginx——OpenResty 的灵活是以"代码化运维"为代价的(Lua 脚本要开发、测试、发布管理),没有应用层的诉求就不要把它当更高级的 Nginx 用——工具升级跟着需求走,这是我的一贯口径。
4.34 本章案例
- 【真实项目·银行多 master 高可用入口】背景:数币平台 K8s 集群最初是单 master,业务方要求控制面高可用,单点不可接受。任务:把 apiserver 入口做成无单点。行动:三台 master 各部署 Nginx(stream 监听 16443,upstream 挂三台 6443,避开与本地 apiserver 的端口冲突)加 Keepalived(priority 100/90,check_nginx.sh 检测 16443 端口监听,失败 weight -20 触发漂移);期间先踩了证书 SAN 的坑(用 VIP 连集群报 x509,按六步更新 certSANs 解决,详见 6.14)。结果:主节点关机实测 VIP 秒级漂移,kubectl 经 VIP:16443 正常返回;后来又用 haproxy 方案(roundrobin + check + 1080 stats 页面)完整复现了一遍,两个方案都沉淀进标准实施文档。这是"同一目标双方案验证"的完整故事。
- 【真实项目·数币平台 Nginx 编译部署】背景是国产化环境没有可用的 Nginx 软件包且需要 HTTPS。任务:在统信 UOS ARM 上部署生产级 Nginx。行动:源码编译 Nginx 1.25.1——
./configure --prefix=/usr/local/nginx --with-http_ssl_module --with-http_stub_status_module --with-stream && make && make install,编译前确认 ARM 依赖库;配置 80 端口 rewrite permanent 跳 443、443 挂客户 CA 证书服务前端静态页(root /opt/dcepscapi/ui 加 try_files 解决刷新 404)、/prod-api/ 反代本地 8080 后端 jar 并透传 X-Real-IP/X-Forwarded-For。结果:平台前端与 API 入口稳定运行至项目交付,这套编译参数和配置结构成为我给后续项目复用的模板。 - 【课程项目】LNMP 动静分离实验:Nginx 按后缀拆 location,静态资源直出配 expires 7d,动态请求 proxy/fastcgi 转发后端,用 ab 压测对比动静分离前后的吞吐与响应时间——数据验证了"静态走 Nginx 直出"的性能收益,也让我对 sendfile、worker_connections 这些参数有了量化的体感。
- 【经典故障】前端 history 路由刷新 404:数币平台上线初期客户反馈"登录后按 F5 刷新页面就 404,直接输入首页地址又正常"。定位:前端使用 history 路由模式,URL 路径是前端 JS 管理的虚拟路径,刷新时 Nginx 按字面路径找文件必然找不到。解决:
try_files $uri $uri/ /index.html兜底回前端路由入口,一行修复。配套收获:baseURL /prod-api 的 proxy_pass 尾斜杠剥前缀问题也在这段时间踩明白——这两个坑让我此后交付任何前端项目都条件反射地先配 try_files 和确认反代前缀规则。
4.35 章末自测清单(快问快答)
- Nginx 高并发三要素? → 多进程(master/worker)+ 事件驱动 + epoll 异步非阻塞。
- worker 数怎么定? → CPU 核数(auto),理论并发 = worker_processes × worker_connections。
- 正向/反向代理一句话? → 正向代理客户端对外,反向代理服务端对客户端。
- 四种调度策略? → 轮询(默认)、加权、ip_hash(会话保持)、least_conn(最少连接)。
- root 和 alias? → root 拼 URI,alias 替换 location 前缀。
- location 优先级? → = 精确 > ^~ > 正则(按顺序)> 普通前缀(取最长)。
- 四层和七层区别? → 四层 stream 只认 IP+端口(TCP 转发),七层 http 解析 URL/Header 可路由;K8s apiserver 用四层。
- 为什么监听 16443? → Nginx 与 master 复用机器,6443 已被本地 apiserver 占用。
- 80→443 怎么写? →
rewrite ^/(.*)$ https://$host/$1 permanent;(301)。 - 502 与 504 区别? → 502 后端死/无效响应,504 后端慢超时。
- 前端刷新 404 解法? →
try_files $uri $uri/ /index.html;。 - Keepalived 漂移触发条件? → MASTER 心跳丢失约 3 个 advert_int,或 vrrp_script 失败降权后 priority 低于备机。
- check 脚本检测什么? → 真实服务(我项目检测 16443 端口监听),不是 Keepalived 自身。
- 脑裂怎么防? → 检测真实服务 + priority 差距 + 保证 VRRP 报文畅通 + 双 VIP 告警。
- LVS 四模式? → NAT(全过调度器)、DR(改 MAC 最快、生产首选)、TUN(隧道跨机房)、FULLNAT(双向 NAT)。