03-反向代理与负载均衡
03-反向代理与负载均衡
1. 阶段定位
第三阶段是 Nginx 从“能写配置”进入“能作为稳定入口代理”的关键阶段。前两阶段已经解决了 Nginx 如何部署、配置如何组织、请求如何匹配 server 和 location。本阶段重点解决下面这些问题:
1 | 一个请求进入 Nginx 后: |
反向代理与负载均衡是 Nginx 在生产环境中最常见、最核心的使用方式。很多公司把 Nginx 放在应用服务、微服务网关、前端静态站点、对象存储、容器入口、Kubernetes Ingress 或 API Gateway 前面,用它承担统一入口、流量转发、负载分摊、故障隔离、Header 标准化和基础日志观测能力。
完成本阶段后,应该具备下面的能力:
- 能独立配置 Nginx 代理 Spring Boot、Tomcat、Node.js、Go、Python、PHP-FPM、静态前端服务等后端。
- 能清晰解释
proxy_pass带 URI 和不带 URI 的转发差异。 - 能使用
upstream定义后端集群,并配置轮询、权重、IP 哈希、最少连接和一致性哈希类策略。 - 能理解
max_fails、fail_timeout、proxy_next_upstream的真实行为和局限。 - 能合理设置连接超时、发送超时、读取超时,避免误判 502、503、504。
- 能正确传递
Host、客户端真实 IP、协议、端口、链路追踪 Header。 - 能配置 upstream keepalive、WebSocket 代理和基本 gRPC 代理。
- 能根据 access log、error log、upstream 响应时间和后端日志定位代理链路问题。
2. 学习边界
2.1 本阶段重点
本阶段重点关注 Nginx 作为 HTTP 反向代理和七层负载均衡器时的核心配置与生产实践:
proxy_pass的基本用法和 URI 拼接规则。proxy_set_header的 Header 传递规则。upstream后端集群定义。- 负载均衡算法与适用场景。
- 后端故障判断、失败计数和重试策略。
- 连接、发送、读取等代理超时。
- 客户端真实 IP 与代理链路 Header。
- upstream keepalive 连接复用。
- WebSocket、SSE、长轮询等长连接代理。
- gRPC 与 HTTP/2 代理基础。
- 502、503、504 常见原因和排查路径。
- 面向生产的反向代理配置模板。
2.2 本阶段暂不深入
下面内容会在后续阶段系统展开,本阶段只做必要铺垫:
- HTTPS、证书、TLS 协议、安全套件和 HTTP/2 入口治理。
- 限流、访问控制、防盗链、安全 Header 和 WAF 类加固。
- proxy cache、静态资源缓存、gzip、brotli 和性能优化。
- Prometheus、Grafana、OpenTelemetry、ELK、Loki 等完整观测体系。
- Nginx 高可用、Keepalived、灰度发布、蓝绿发布和热升级。
- Nginx upstream 模块、phase handler、filter 链源码实现。
- OpenResty、Lua、Ingress Controller、Envoy、HAProxy 等相邻技术细节。
本阶段的目标不是背完整指令手册,而是建立稳定的代理链路思维:
1 | 客户端 |
3. 反向代理基础认知
3.1 什么是正向代理
正向代理是代理客户端访问外部服务。客户端知道自己在使用代理,目标服务通常不知道真实客户端是谁,只看到代理服务器。
典型场景:
1 | 客户端 -> 正向代理 -> 外部网站 |
常见用途:
- 内网客户端通过代理访问外网。
- 开发环境配置 HTTP 代理。
- 企业网关统一出口。
- 爬虫或测试流量通过代理出口。
3.2 什么是反向代理
反向代理是代理后端服务对外提供访问入口。客户端通常不知道真实后端服务地址,只知道访问 Nginx 入口。
典型场景:
1 | 客户端 -> Nginx -> 后端应用 |
客户端访问的是:
1 | http://www.example.com/api/users |
Nginx 内部可能转发到:
1 | http://10.0.0.11:8080/users |
反向代理的核心价值:
- 隐藏后端真实地址。
- 统一入口域名和端口。
- 对多个后端做负载均衡。
- 在入口层统一处理 Header、日志、安全和超时。
- 隔离客户端与后端应用的直接连接。
- 降低后端服务暴露面。
3.3 Nginx 在代理链路中的位置
生产中常见链路如下:
1 | 用户浏览器 / App |
也可能是简化版:
1 | 用户浏览器 |
或者前后端分离场景:
1 | 用户浏览器 |
Nginx 在这些场景中通常承担:
- 入口路由。
- 反向代理。
- 负载均衡。
- 静态资源服务。
- Header 标准化。
- 日志采集。
- 基础故障隔离。
4. 最小反向代理配置
4.1 最小配置示例
假设后端应用监听在本机 8080 端口:
1 | server { |
访问:
1 | curl http://app.example.com/ |
Nginx 会把请求转发到:
1 | http://127.0.0.1:8080/ |
4.2 代理远程后端
后端不在本机时,可以写远程 IP:
1 | server { |
如果后端使用域名:
1 | server { |
注意:如果 proxy_pass 后面使用变量或依赖运行时 DNS 解析,后续还要理解 resolver。普通固定域名通常在配置加载阶段解析。
4.3 代理到不同路径
例如:
1 | server { |
含义:
1 | /api/ -> 8080 后端 |
但这里最容易踩坑的是 proxy_pass 后面是否带 /,下一节重点解释。
5. proxy_pass 核心规则
5.1 proxy_pass 基础语法
常见语法:
1 | proxy_pass http://127.0.0.1:8080; |
proxy_pass 最常见的位置是 location。它决定当前请求被转发到哪个上游服务,以及转发时 URI 如何变化。
理解 proxy_pass 时一定要分清两个问题:
1 | 后端地址是谁 |
很多代理问题不是后端不可用,而是 URI 被 Nginx 改成了后端不认识的路径。
5.2 不带 URI 的 proxy_pass
示例:
1 | location /api/ { |
请求:
1 | GET /api/users?id=1 |
后端收到:
1 | GET /api/users?id=1 |
也就是说,proxy_pass 不带 URI 时,Nginx 通常保留当前请求 URI,/api/ 前缀仍然传给后端。
适用场景:
- 后端应用本身就有
/api上下文路径。 - Nginx 只做透明代理,不负责改路径。
- Spring Boot、Tomcat、网关服务本身已经配置了
/api前缀。
5.3 带 URI 的 proxy_pass
示例:
1 | location /api/ { |
请求:
1 | GET /api/users?id=1 |
后端收到:
1 | GET /users?id=1 |
也就是说,proxy_pass 带 URI / 时,Nginx 会用 proxy_pass 中的 URI 替换匹配到的 location 前缀。
适用场景:
- 对外暴露
/api/,后端接口实际没有/api/前缀。 - 前端统一访问
/api/users,后端实际接口是/users。 - 入口层希望隐藏后端真实上下文路径。
5.4 URI 替换示例表
假设请求是:
1 | /api/users/list?status=1 |
| location | proxy_pass | 后端收到的 URI |
|---|---|---|
location /api/ |
http://backend |
/api/users/list?status=1 |
location /api/ |
http://backend/ |
/users/list?status=1 |
location /api/ |
http://backend/v1/ |
/v1/users/list?status=1 |
location /api |
http://backend |
/api/users/list?status=1 |
location /api |
http://backend/ |
可能形成 //users/list?status=1,容易出现双斜杠问题 |
推荐写法:
1 | location /api/ { |
不推荐在需要目录前缀语义时写成:
1 | location /api { |
原因是 /api 前缀可能匹配 /api、/api/xxx,甚至容易误伤类似 /apixxx 的路径。更清晰的做法是用 /api/ 表示目录型前缀。
5.5 proxy_pass 与正则 location
正则 location 中使用 proxy_pass 要非常谨慎。
示例:
1 | location ~ ^/api/(.*)$ { |
这类配置看起来灵活,但容易引入问题:
- URI 编码处理不一致。
- 查询参数拼接不清晰。
- 捕获组为空导致路径异常。
- 双斜杠问题。
- 后续维护者很难一眼判断后端实际收到什么路径。
能用普通前缀匹配解决的场景,不建议优先使用正则 location。
更推荐:
1 | location /api/ { |
5.6 proxy_pass 与 rewrite
有些配置会先 rewrite 再 proxy:
1 | location /api/ { |
这类写法可以工作,但可读性通常不如:
1 | location /api/ { |
选择建议:
- 只是去掉固定前缀,优先使用
proxy_pass http://backend/。 - 需要复杂路径改写,再考虑
rewrite。 - rewrite 与 proxy 混用时必须写清楚
last、break的影响。 - 生产配置中要用日志或后端回显接口验证实际 URI。
6. upstream 基础
6.1 upstream 是什么
upstream 用于定义一组后端服务器,供 proxy_pass 引用。
1 | upstream app_backend { |
请求进入 Nginx 后,会从 app_backend 中选择一个后端节点转发。
6.2 upstream 所在上下文
upstream 定义在 http 上下文中,不能写在 server 或 location 里面。
正确:
1 | http { |
错误:
1 | server { |
配置检查会报类似错误:
1 | "upstream" directive is not allowed here |
6.3 upstream 命名建议
命名要表达业务含义,不要只写 backend、server1。
推荐:
1 | upstream user_service { |
不推荐:
1 | upstream backend { |
原因:
- 多个系统共用 Nginx 时容易混淆。
- access log 中记录 upstream 名称时不利于排查。
- 配置拆分后不容易判断用途。
6.4 upstream server 常见参数
示例:
1 | upstream app_backend { |
常见参数:
| 参数 | 含义 |
|---|---|
weight |
权重,权重越高分配到的请求越多 |
max_fails |
在 fail_timeout 时间内允许失败的次数 |
fail_timeout |
失败统计窗口,也是临时不可用时间 |
backup |
备用节点,主节点不可用时才使用 |
down |
标记节点不可用,常用于临时下线 |
max_conns |
限制到该后端的最大连接数,需注意版本和模块支持 |
6.5 upstream 与 server/location 的关系
可以理解为:
1 | server/location 负责匹配入口请求 |
示例:
1 | upstream user_service { |
这种写法可以把不同路径分发到不同后端服务。
7. 负载均衡算法
7.1 round-robin 轮询
默认算法就是轮询,不需要额外配置。
1 | upstream app_backend { |
请求大致按顺序分配到不同节点。
适用场景:
- 后端机器配置接近。
- 请求处理耗时相对均匀。
- 无强会话粘性要求。
- 大多数普通 HTTP API 服务。
注意:轮询不是严格每个节点完全相等。连接状态、失败重试、keepalive、请求耗时都会影响实际分布。
7.2 weight 权重
权重用于控制不同节点接收请求的比例。
1 | upstream app_backend { |
大致含义:
1 | 10.0.0.11 接收约 3 份请求 |
适用场景:
- 新旧机器配置不同。
- 容器实例资源不同。
- 灰度导流一小部分流量。
- 临时降低某个节点流量。
灰度示例:
1 | upstream app_backend { |
表示大部分流量仍然到旧节点,少量流量到新节点。但这只是非常基础的权重灰度,不能替代完整的灰度发布系统。
7.3 ip_hash
ip_hash 根据客户端 IP 做哈希,同一个客户端 IP 尽量转发到同一个后端。
1 | upstream app_backend { |
适用场景:
- 后端使用本地 Session,不能跨节点共享。
- 老系统暂时无法改造 Session 存储。
- 希望同一客户端尽量访问同一实例。
局限:
- 多个用户经过同一个 NAT 出口时,可能集中到同一个后端。
- 客户端 IP 变化会导致后端节点变化。
- 后端节点上下线会影响哈希分布。
- 不适合大规模动态扩缩容场景。
更推荐的会话方案:
1 | 应用无状态化 |
7.4 least_conn 最少连接
least_conn 会优先选择当前活动连接数较少的后端。
1 | upstream app_backend { |
适用场景:
- 请求耗时差异较大。
- 有长连接或慢请求。
- 后端服务处理时间不均匀。
- 轮询容易让某些节点积压连接。
注意:最少连接不是最少负载。连接数少不代表 CPU、内存、数据库压力一定小。
7.5 hash
hash 可以根据指定 key 做哈希。
1 | upstream app_backend { |
也可以使用一致性哈希:
1 | upstream app_backend { |
适用场景:
- 需要相同 URI 尽量落到同一后端。
- 后端有本地缓存,希望提升命中率。
- 需要按某个请求变量固定路由。
常见 key:
1 | hash $request_uri consistent; |
选择 key 时要注意:
- key 分布是否均匀。
- key 是否可能为空。
- key 是否容易被客户端伪造。
- 后端扩缩容时是否能接受重新分布。
7.6 算法选择建议
| 场景 | 推荐算法 |
|---|---|
| 普通无状态 API | 默认轮询 |
| 节点配置不同 | weight |
| 老系统本地 Session | ip_hash,但建议尽快改造 |
| 请求耗时差异大 | least_conn |
| 需要按 URI 或用户固定路由 | hash ... consistent |
| 简单灰度少量导流 | weight |
| 大规模灰度发布 | 结合网关、发布系统或服务治理平台 |
8. Header 传递
8.1 为什么要设置 Header
Nginx 代理后,后端看到的直接客户端通常是 Nginx,而不是真实用户。
如果不传递必要 Header,后端可能出现:
- 获取到的客户端 IP 是 Nginx IP。
- 生成回调地址时 Host 不正确。
- 判断 HTTP/HTTPS 协议错误。
- 登录、OAuth、SSO 回调地址错误。
- 后端审计日志缺少真实来源。
- 链路追踪信息丢失。
因此生产代理配置通常要显式设置代理 Header。
8.2 Host
常见配置:
1 | proxy_set_header Host $host; |
含义:把客户端请求的 Host 传给后端。
也有人使用:
1 | proxy_set_header Host $http_host; |
两者区别:
| 变量 | 含义 |
|---|---|
$host |
Nginx 规范化后的主机名,可能来自 Host、server_name 或请求行 |
$http_host |
原始 Host 请求头,可能包含端口 |
一般建议:
1 | proxy_set_header Host $host; |
如果后端必须感知原始端口,可以根据场景使用:
1 | proxy_set_header Host $http_host; |
8.3 X-Real-IP
配置:
1 | proxy_set_header X-Real-IP $remote_addr; |
含义:把 Nginx 看到的客户端 IP 放到 X-Real-IP。
如果 Nginx 前面没有 CDN、SLB、其他代理,那么 $remote_addr 通常就是真实客户端 IP。
如果 Nginx 前面还有代理,$remote_addr 可能是上一层代理的 IP,需要配合 real_ip_header 和 set_real_ip_from 使用。
8.4 X-Forwarded-For
配置:
1 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; |
X-Forwarded-For 用于记录代理链路上的客户端 IP。
假设原始请求头:
1 | X-Forwarded-For: 1.1.1.1 |
Nginx 的 $remote_addr 是:
1 | 2.2.2.2 |
使用 $proxy_add_x_forwarded_for 后传给后端:
1 | X-Forwarded-For: 1.1.1.1, 2.2.2.2 |
注意:X-Forwarded-For 是普通请求头,客户端可以伪造。生产中不能无条件信任最左侧 IP,必须结合可信代理列表处理。
8.5 X-Forwarded-Proto
配置:
1 | proxy_set_header X-Forwarded-Proto $scheme; |
含义:告诉后端客户端访问 Nginx 时使用的协议:
1 | http |
典型问题:
1 | 用户通过 HTTPS 访问 Nginx |
此时需要传递:
1 | proxy_set_header X-Forwarded-Proto $scheme; |
后端框架也要正确开启 forwarded header 识别。
8.6 X-Forwarded-Host 与 X-Forwarded-Port
常见配置:
1 | proxy_set_header X-Forwarded-Host $host; |
用于让后端知道原始访问域名和端口。
如果 Nginx 在 HTTPS 入口监听 443,转发到后端 8080,后端默认只能看到 8080。某些框架生成绝对 URL 时会依赖这些 Header。
8.7 推荐代理 Header 模板
常见 HTTP API 代理模板:
1 | proxy_set_header Host $host; |
如果有链路追踪,可以继续传:
1 | proxy_set_header X-Request-ID $request_id; |
9. 真实客户端 IP
9.1 单层 Nginx 场景
链路:
1 | 客户端 -> Nginx -> 后端 |
此时 Nginx 看到的 $remote_addr 通常就是客户端真实 IP。
配置:
1 | proxy_set_header X-Real-IP $remote_addr; |
后端可读取:
1 | X-Real-IP |
9.2 多层代理场景
链路:
1 | 客户端 -> CDN -> SLB -> Nginx -> 后端 |
此时 Nginx 看到的 $remote_addr 可能是 SLB、CDN 回源节点或上一层代理的 IP,而不是真实客户端。
可以配置:
1 | set_real_ip_from 10.0.0.0/8; |
含义:
set_real_ip_from指定可信代理 IP 段。real_ip_header指定从哪个 Header 取真实 IP。real_ip_recursive on在多级代理中递归寻找第一个非可信代理 IP。
不要写成:
1 | set_real_ip_from 0.0.0.0/0; |
这样等于信任任意客户端伪造的 X-Forwarded-For,有明显安全风险。
9.3 后端框架读取真实 IP
不同后端框架处理方式不同:
| 后端 | 常见处理 |
|---|---|
| Spring Boot | 配置 server.forward-headers-strategy 或使用容器相关 Forwarded Header 支持 |
| Tomcat | 配置 RemoteIpValve |
| Node.js Express | 配置 trust proxy |
| Go | 手动读取并校验 Header,或使用可信代理中间件 |
| Django | 配置 SECURE_PROXY_SSL_HEADER 等相关项 |
核心原则:
1 | 只有来自可信代理的 X-Forwarded-For 才能被后端当作真实 IP |
9.4 真实 IP 排查方法
临时加一个调试 location:
1 | location = /debug-ip { |
访问:
1 | curl -H "X-Forwarded-For: 1.1.1.1" http://example.com/debug-ip |
观察返回值,判断 Nginx 是否信任了正确代理层。
10. 代理超时
10.1 超时为什么重要
反向代理链路中至少有三段关键时间:
1 | Nginx 连接后端的时间 |
如果超时设置不合理,会出现:
- 后端只是慢,Nginx 却提前返回 504。
- 后端已经挂了,Nginx 却长时间占用连接等待。
- 大请求上传时被误断开。
- 长连接接口被当成普通短请求切断。
- 用户体验、后端资源和 Nginx 连接资源全部受影响。
10.2 proxy_connect_timeout
配置:
1 | proxy_connect_timeout 3s; |
含义:Nginx 与后端建立 TCP 连接的超时时间。
发生阶段:
1 | Nginx -> backend TCP connect |
常见原因:
- 后端端口未监听。
- 后端机器宕机。
- 防火墙阻断。
- 网络路由异常。
- 后端连接队列满。
配置建议:
1 | proxy_connect_timeout 2s; |
或:
1 | proxy_connect_timeout 3s; |
一般不建议设置太长。连接后端都连接不上,通常应该快速失败或尝试其他节点。
10.3 proxy_send_timeout
配置:
1 | proxy_send_timeout 30s; |
含义:Nginx 向后端发送请求的超时时间。更准确地说,它控制两次成功写操作之间的超时,不是整个请求体发送完成的总耗时。
常见影响场景:
- 大文件上传。
- 客户端请求体很大。
- 后端读取请求体很慢。
- 后端网络拥塞。
普通 API 可以设置:
1 | proxy_send_timeout 30s; |
上传接口可能需要更长:
1 | proxy_send_timeout 300s; |
10.4 proxy_read_timeout
配置:
1 | proxy_read_timeout 60s; |
含义:Nginx 等待后端响应数据的超时时间。它也是两次成功读操作之间的超时,不是整个响应下载完成的总时间。
常见影响场景:
- 后端接口处理慢。
- 数据库查询慢。
- 后端调用第三方接口慢。
- WebSocket、SSE、长轮询连接。
- 大文件下载或流式响应。
普通 API 示例:
1 | proxy_read_timeout 60s; |
长轮询或 WebSocket 示例:
1 | proxy_read_timeout 3600s; |
10.5 超时参数对比表
| 指令 | 发生阶段 | 常见错误 | 调整思路 |
|---|---|---|---|
proxy_connect_timeout |
连接后端 | 502/504,具体看日志 | 一般较短,2-5 秒 |
proxy_send_timeout |
向后端发送请求 | 连接关闭或请求发送失败 | 上传场景适当变长 |
proxy_read_timeout |
等待后端响应 | 504 | 慢接口、长连接场景变长 |
10.6 不要盲目调大超时
很多人遇到 504 的第一反应是:
1 | proxy_read_timeout 600s; |
这可能掩盖真正问题:
- SQL 慢查询。
- 后端线程池耗尽。
- RPC 调用超时过长。
- 下游服务不可用。
- 数据库连接池不足。
- 接口设计不适合同步等待。
正确思路:
1 | 先确认接口是否真的应该执行这么久 |
10.7 常见超时模板
普通 API:
1 | proxy_connect_timeout 3s; |
上传接口:
1 | client_max_body_size 200m; |
WebSocket:
1 | proxy_connect_timeout 3s; |
后台导出接口:
1 | proxy_connect_timeout 3s; |
生产上要根据业务 SLA、后端耗时分布和调用链超时统一设计,不要让 Nginx 超时明显大于所有下游超时。
11. 故障摘除与失败判断
11.1 max_fails 与 fail_timeout
示例:
1 | upstream app_backend { |
含义不是“失败 2 次后永久下线”。更准确地说:
1 | 如果某个后端在 10 秒内失败达到 2 次,Nginx 会在接下来的 10 秒内暂时不选它。 |
fail_timeout 有两个作用:
- 失败统计窗口。
- 节点被认为不可用后的临时屏蔽时间。
11.2 什么算失败
失败通常与以下情况有关:
- 连接后端失败。
- 向后端发送请求失败。
- 读取后端响应失败。
- 后端响应超时。
- 根据
proxy_next_upstream配置把特定状态码视为可重试失败。
不是所有 HTTP 500 都必然导致节点摘除,具体行为要结合 proxy_next_upstream。
11.3 max_fails=0
配置:
1 | server 10.0.0.11:8080 max_fails=0; |
含义:关闭该节点的失败计数。
不建议普通生产配置随意关闭失败判断,否则故障节点可能持续接收请求。
11.4 单节点 upstream 的特殊性
如果 upstream 只有一个节点:
1 | upstream app_backend { |
即使它失败,也没有其他节点可选。用户仍然会看到错误。
因此高可用至少需要:
1 | 两个以上后端节点 |
开源版 Nginx 的主动健康检查能力有限,更多依赖被动失败判断。主动健康检查通常需要 Nginx Plus、第三方模块、OpenResty、服务发现系统或上层负载均衡器支持。
11.5 backup 备用节点
配置:
1 | upstream app_backend { |
含义:正常情况下使用前两个主节点,主节点都不可用时才使用 backup 节点。
适用场景:
- 临时灾备节点。
- 降级服务节点。
- 只读兜底服务。
注意:备用节点不是灰度节点,不适合用来承接少量常规流量。
11.6 down 临时下线
配置:
1 | upstream app_backend { |
含义:10.0.0.12 暂时不参与负载均衡。
适用场景:
- 临时摘除故障节点。
- 发布前手动下线节点。
- 保留配置但暂不启用。
修改后需要:
1 | nginx -t && nginx -s reload |
或使用 systemd:
1 | nginx -t && systemctl reload nginx |
12. 重试策略 proxy_next_upstream
12.1 proxy_next_upstream 是什么
proxy_next_upstream 控制在什么情况下,Nginx 可以把请求转发给下一个 upstream 节点。
示例:
1 | proxy_next_upstream error timeout http_502 http_503 http_504; |
含义:当当前后端出现连接错误、超时、502、503、504 时,尝试下一个后端。
12.2 常见可选条件
| 条件 | 含义 |
|---|---|
error |
与后端建立连接、发送请求、读取响应头时发生错误 |
timeout |
与后端连接、发送、读取时超时 |
invalid_header |
后端返回无效响应头 |
http_500 |
后端返回 500 |
http_502 |
后端返回 502 |
http_503 |
后端返回 503 |
http_504 |
后端返回 504 |
http_403 |
后端返回 403 |
http_404 |
后端返回 404 |
off |
关闭重试 |
12.3 限制重试次数
示例:
1 | proxy_next_upstream error timeout http_502 http_503 http_504; |
含义:最多尝试 2 个后端节点。
也可以限制重试总时间:
1 | proxy_next_upstream_timeout 10s; |
生产建议:
1 | proxy_next_upstream error timeout http_502 http_503 http_504; |
12.4 幂等风险
重试不是总是安全的。
GET 通常相对安全:
1 | GET /products/1 |
但 POST、PUT、PATCH、DELETE 可能有副作用:
1 | POST /orders |
如果 Nginx 把一个已经被后端处理但响应中断的 POST 请求重试到另一个节点,可能导致:
- 重复下单。
- 重复扣款。
- 重复创建数据。
- 状态不一致。
所以对于非幂等请求:
- 应用层必须设计幂等键。
- 支付、订单、库存类接口要谨慎重试。
- 不要简单把所有 500 都加入重试。
- 不要无限重试。
12.5 请求体已发送后的重试限制
如果 Nginx 已经把请求体发送给后端,再发生错误,通常无法安全切换到下一个后端,尤其是非幂等请求。
可以理解为:
1 | 还没把请求完整交给后端之前,重试相对可控 |
因此重试策略必须结合业务语义,不是越多越好。
12.6 推荐重试模板
普通查询 API:
1 | proxy_next_upstream error timeout http_502 http_503 http_504; |
核心交易 API:
1 | proxy_next_upstream error timeout; |
或者直接关闭:
1 | proxy_next_upstream off; |
是否关闭要看业务系统是否具备幂等能力。
13. upstream keepalive 连接复用
13.1 为什么需要 keepalive
没有 upstream keepalive 时,Nginx 到后端可能频繁建立和关闭 TCP 连接。
代价包括:
- TCP 三次握手开销。
- TLS 后端连接时还有额外握手开销。
- 后端 accept 压力增加。
- TIME_WAIT 增多。
- 高并发下端口和连接资源消耗明显。
upstream keepalive 允许 Nginx worker 复用到后端的空闲连接,减少重复建连成本。
13.2 基础配置
示例:
1 | upstream app_backend { |
关键点:
1 | proxy_http_version 1.1; |
否则后端连接复用可能无法按预期工作。
13.3 keepalive 数值含义
1 | keepalive 64; |
含义不是总连接数最大为 64,而是每个 worker 进程保留的 upstream 空闲 keepalive 连接数量上限。
如果有 4 个 worker:
1 | 最多可能保留约 4 * 64 个空闲 upstream keepalive 连接 |
注意:这不是到后端的最大活动连接数限制。活动连接和空闲连接要分开理解。
13.4 keepalive 适用场景
适合:
- 高 QPS HTTP API。
- 后端支持 HTTP/1.1 keep-alive。
- Nginx 与后端网络稳定。
- 后端连接建立成本较高。
不适合或要谨慎:
- 后端连接数非常有限。
- 后端对长时间空闲连接处理不好。
- 中间网络设备会悄悄断开空闲连接。
- 请求量很低,收益不明显。
13.5 keepalive 常见误区
误区一:以为 keepalive 64 是最大并发连接数。
实际它控制的是空闲连接池数量,不是活动连接上限。
误区二:只在 upstream 配置 keepalive,但忘了:
1 | proxy_http_version 1.1; |
误区三:把 keepalive 设置得极大,导致后端维持大量空闲连接。
建议从较小值开始,例如:
1 | keepalive 32; |
或:
1 | keepalive 64; |
再结合后端连接数、Nginx worker 数量和压测结果调整。
14. 请求体与缓冲
14.1 client_max_body_size
上传文件时经常遇到:
1 | 413 Request Entity Too Large |
原因通常是请求体超过 client_max_body_size。
配置示例:
1 | client_max_body_size 100m; |
可以放在 http、server 或 location 中。
上传接口单独放大:
1 | location /upload/ { |
不要全局无限放大,否则会增加入口层被大请求拖垮的风险。
14.2 proxy_request_buffering
默认情况下,Nginx 通常会先接收客户端请求体,再转发给后端。
关闭请求缓冲:
1 | proxy_request_buffering off; |
含义:Nginx 边接收客户端请求体,边转发给后端。
适用场景:
- 大文件上传。
- 流式上传。
- 希望减少 Nginx 本地磁盘临时文件。
风险:
- 后端会直接承受慢客户端影响。
- Nginx 更难在请求体未完整接收前做重试。
- 后端连接占用时间可能变长。
14.3 proxy_buffering
proxy_buffering 控制响应缓冲。
默认通常是开启:
1 | proxy_buffering on; |
Nginx 会从后端尽快读取响应,缓存在内存或临时文件中,再发给客户端。
优点:
- 后端可以更快释放连接。
- 慢客户端不直接拖慢后端。
- 有利于保护后端。
关闭:
1 | proxy_buffering off; |
适用场景:
- SSE。
- 流式响应。
- 实时输出日志。
- 长时间分块响应。
14.4 proxy_buffers
示例:
1 | proxy_buffer_size 16k; |
常见场景:
- 后端响应头较大。
- Set-Cookie 很多。
- 网关追加了大量 Header。
- 出现
upstream sent too big header。
错误日志可能出现:
1 | upstream sent too big header while reading response header from upstream |
可以适当调大:
1 | proxy_buffer_size 32k; |
但要先确认为什么响应头这么大,例如 Cookie 是否过多、Token 是否过长。
14.5 缓冲参数选择建议
| 场景 | 建议 |
|---|---|
| 普通 API | 保持默认或小幅调整 |
| 大文件上传 | 调整 client_max_body_size,谨慎关闭 proxy_request_buffering |
| SSE/流式响应 | 关闭 proxy_buffering |
| 大响应头 | 调整 proxy_buffer_size、proxy_buffers |
| 慢客户端很多 | 保持响应缓冲,保护后端 |
15. WebSocket 代理
15.1 WebSocket 与普通 HTTP 的区别
WebSocket 初始握手是 HTTP 请求,但随后会升级为长连接。
关键 Header:
1 | Upgrade: websocket |
如果 Nginx 没有正确透传这些 Header,WebSocket 连接会失败。
15.2 最小 WebSocket 配置
1 | server { |
15.3 更通用的 Connection 写法
在 http 中定义:
1 | map $http_upgrade $connection_upgrade { |
在 location 中使用:
1 | location /ws/ { |
这种写法比固定 Connection "Upgrade" 更通用。
15.4 WebSocket upstream 示例
1 | upstream ws_backend { |
15.5 WebSocket 常见问题
常见错误:
- 忘记
proxy_http_version 1.1。 - 忘记传
Upgrade。 - 忘记传
Connection。 proxy_read_timeout太短,连接被断开。- 中间层 SLB、CDN 不支持或限制 WebSocket。
- 后端服务没有正确处理路径前缀。
排查命令:
1 | curl -i \ |
正常握手通常返回:
1 | HTTP/1.1 101 Switching Protocols |
16. SSE 与长轮询代理
16.1 SSE 特点
SSE 即 Server-Sent Events,服务端通过 HTTP 长连接持续向客户端推送事件。
典型响应头:
1 | Content-Type: text/event-stream |
SSE 不是 WebSocket,但同样依赖长连接和流式响应。
16.2 SSE 推荐配置
1 | location /events/ { |
关键点:
1 | proxy_buffering off; |
否则事件可能被 Nginx 缓冲,客户端不能及时收到。
16.3 长轮询配置
长轮询通常是后端保持请求一段时间,有消息就返回,没有消息超时返回。
示例:
1 | location /polling/ { |
注意 Nginx 超时要略大于后端长轮询等待时间。
例如后端最长等待 60 秒,Nginx 可以设置:
1 | proxy_read_timeout 75s; |
17. gRPC 与 HTTP/2 代理基础
17.1 gRPC 代理特点
gRPC 基于 HTTP/2。Nginx 代理 gRPC 时通常使用 grpc_pass,不是 proxy_pass。
简单链路:
1 | gRPC client -> Nginx -> gRPC server |
17.2 明文 gRPC 示例
1 | upstream grpc_backend { |
17.3 TLS gRPC 示例
如果后端 gRPC 使用 TLS:
1 | location / { |
生产中更常见的是客户端到 Nginx 使用 TLS,Nginx 到后端使用明文内网 gRPC 或双向 TLS,具体取决于安全要求。
17.4 gRPC 超时
常见配置:
1 | grpc_connect_timeout 3s; |
与 HTTP 代理类似,也要区分连接、发送和读取阶段。
17.5 gRPC 本阶段掌握程度
本阶段只要求理解:
- gRPC 不是普通 HTTP/1.1 代理。
- gRPC 通常用
grpc_pass。 - gRPC 依赖 HTTP/2。
- 超时、Header、负载均衡仍然有类似问题。
- 如果生产中大量使用 gRPC,需要继续深入 HTTP/2、连接复用、流、错误码和客户端负载均衡。
18. HTTPS 后端代理
18.1 代理到 HTTPS 后端
Nginx 可以代理到 HTTPS 后端:
1 | location / { |
如果使用 upstream:
1 | upstream https_backend { |
18.2 SNI
当后端 HTTPS 依赖 SNI 时,需要:
1 | proxy_ssl_server_name on; |
完整示例:
1 | location / { |
如果不配置,可能出现后端证书不匹配或握手失败。
18.3 是否校验证书
可以开启后端证书校验:
1 | proxy_ssl_verify on; |
内网 HTTPS 后端如果使用自签 CA,应该把内部 CA 证书配置给 Nginx。
不要为了省事长期使用不校验证书的方式,尤其是在跨机房、跨云或零信任网络场景中。
18.4 入口 HTTPS 与后端 HTTPS 的区别
常见模式:
1 | 客户端 --HTTPS--> Nginx --HTTP--> 后端 |
或:
1 | 客户端 --HTTPS--> Nginx --HTTPS--> 后端 |
前者简单、性能开销较小,适合可信内网。
后者链路更安全,适合高安全要求、跨网络边界、服务网格或零信任场景。
HTTPS 入口配置会在第四阶段详细展开。
19. 日志与 upstream 观测
19.1 为什么默认日志不够
默认 access log 通常只能看到:
- 客户端 IP。
- 请求方法和 URI。
- 响应状态码。
- 响应大小。
- User-Agent。
但反向代理排障还需要知道:
- 转发到了哪个 upstream 地址。
- upstream 返回了什么状态码。
- 连接后端花了多久。
- 后端响应头等待了多久。
- 整个请求耗时多久。
- 是否发生了重试。
19.2 推荐 upstream 日志格式
在 http 中定义:
1 | log_format upstream_main '$remote_addr - $remote_user [$time_local] ' |
使用:
1 | access_log /var/log/nginx/app_access.log upstream_main; |
19.3 upstream 变量解释
| 变量 | 含义 |
|---|---|
$upstream_addr |
实际连接的 upstream 地址,发生重试时可能有多个 |
$upstream_status |
upstream 返回的状态码,重试时可能有多个 |
$upstream_connect_time |
与 upstream 建立连接耗时 |
$upstream_header_time |
从 upstream 接收响应头耗时 |
$upstream_response_time |
upstream 完整响应耗时 |
$request_time |
Nginx 处理整个请求的总耗时 |
示例:
1 | upstream_addr="10.0.0.11:8080, 10.0.0.12:8080" |
表示第一次请求 10.0.0.11 返回 502,随后重试 10.0.0.12 成功。
19.4 通过日志判断瓶颈
情况一:
1 | request_time=5.001 upstream_response_time=5.000 |
说明主要耗时在后端。
情况二:
1 | request_time=10.000 upstream_connect_time=3.000 upstream_status=502 |
说明连接后端阶段异常,可能是网络、端口、后端连接队列或防火墙问题。
情况三:
1 | request_time=60.001 upstream_status=504 upstream_response_time=60.000 |
说明 Nginx 等后端响应超时,重点查后端慢接口或下游依赖。
情况四:
1 | request_time=120.000 upstream_response_time=0.010 |
说明后端很快返回,但客户端接收慢,或者响应传输阶段耗时长。
19.5 error_log 关键字
常见错误日志关键字:
1 | connect() failed |
排查时不要只看 HTTP 状态码,要同时看:
1 | access.log + error.log + 后端应用日志 + 系统网络状态 |
20. 502、503、504 排查
20.1 502 Bad Gateway
502 表示 Nginx 作为网关或代理,从上游拿到了异常响应,或者无法正确完成上游通信。
常见原因:
- 后端服务未启动。
- 后端端口未监听。
- 后端进程崩溃。
- 后端主动关闭连接。
- 后端返回了非法 HTTP 响应。
- Nginx 到后端网络不通。
- 后端响应头过大。
- HTTPS 后端握手失败。
常见 error_log:
1 | connect() failed (111: Connection refused) while connecting to upstream |
排查命令:
1 | curl -v http://10.0.0.11:8080/health |
如果是容器:
1 | docker ps |
20.2 503 Service Unavailable
503 表示服务不可用。
在 Nginx upstream 场景中常见原因:
- upstream 没有可用节点。
- 所有后端都被临时判定失败。
- 配置了限流或并发限制并返回 503。
- 后端主动返回 503。
- 维护页或降级配置返回 503。
常见 error_log:
1 | no live upstreams while connecting to upstream |
排查重点:
1 | upstream 节点是否都挂了 |
20.3 504 Gateway Timeout
504 表示 Nginx 等待 upstream 超时。
常见原因:
- 后端接口处理时间超过
proxy_read_timeout。 - 后端连接建立超时。
- 后端线程池或连接池耗尽。
- 数据库慢查询。
- RPC/第三方接口无响应。
- 网络丢包或拥塞。
常见 error_log:
1 | upstream timed out (110: Connection timed out) while reading response header from upstream |
排查重点:
1 | proxy_read_timeout 设置 |
20.4 502/503/504 对比表
| 状态码 | 直观含义 | 常见方向 |
|---|---|---|
| 502 | 后端响应异常或连接异常 | 后端挂了、端口不通、协议错误、响应头异常 |
| 503 | 当前没有可用服务 | upstream 全不可用、限流、维护、后端主动 503 |
| 504 | 等后端超时 | 慢接口、下游卡住、超时配置过短 |
20.5 标准排查流程
建议按下面顺序排查:
1 | 1. 看 access.log 的 status、upstream_status、upstream_addr、upstream_response_time |
不要在没有定位原因前直接把所有超时调大。
21. 生产代理配置模板
21.1 普通 HTTP API 模板
1 | upstream app_backend { |
21.2 前后端分离模板
1 | upstream api_backend { |
注意这里:
1 | location /api/ { |
表示对外 /api/users 转发给后端 /users。
如果后端也需要 /api/users,应改为:
1 | location /api/ { |
21.3 多服务路由模板
1 | upstream user_service { |
可以把通用 Header 抽成:
1 | # conf.d/proxy_headers.conf |
这样可以减少重复配置。
21.4 WebSocket 与 API 共站点模板
1 | upstream api_backend { |
22. 配置拆分建议
22.1 为什么要拆分
反向代理配置一多,很容易出现:
- Header 重复写几十遍。
- 超时配置不一致。
- 日志格式不统一。
- upstream 与 server 混在一个大文件里。
- 修改一个服务配置时误伤其他服务。
因此建议按职责拆分。
22.2 推荐目录结构
示例:
1 | /etc/nginx/ |
22.3 nginx.conf 示例
1 | http { |
注意 include 顺序:
1 | 先定义 log_format |
否则 server 中引用 upstream 或 log_format 时可能找不到。
22.4 proxy_headers.conf
1 | proxy_http_version 1.1; |
22.5 proxy_timeout.conf
1 | proxy_connect_timeout 3s; |
在 location 中引用:
1 | location /api/ { |
22.6 拆分注意事项
- 不要把
upstream写进只会被server内 include 的文件。 - 不要在多个文件中重复定义同名 upstream。
- 不要让 include 顺序依赖文件系统随机排序,尽量用明确前缀。
- 通用片段只放通用指令,不要混入业务服务地址。
- 修改配置后必须执行
nginx -t。
23. 后端应用接入要点
23.1 Spring Boot
常见 Nginx 配置:
1 | location /api/ { |
如果 Spring Boot 需要识别代理 Header,可配置:
1 | server.forward-headers-strategy=framework |
或根据 Spring Boot、容器版本使用对应配置。
常见问题:
- 后端生成跳转地址变成
http。 - 获取客户端 IP 是 Nginx IP。
- context-path 与
proxy_passURI 拼接不一致。 - 上传接口被 Nginx 的
client_max_body_size拦截。
23.2 Tomcat
Tomcat 常见问题:
request.getRemoteAddr()获取的是 Nginx IP。request.getScheme()判断为http。- 应用生成绝对 URL 错误。
可在 Tomcat 层配置 RemoteIpValve,或由应用框架处理 forwarded headers。
Nginx 侧仍然需要传:
1 | proxy_set_header X-Real-IP $remote_addr; |
23.3 Node.js / Express
Nginx:
1 | location / { |
Express 中通常需要:
1 | app.set('trust proxy', true) |
但生产中更建议配置可信代理范围,而不是无脑信任所有代理 Header。
23.4 Go HTTP 服务
Go 后端通常手动读取 Header:
1 | X-Real-IP |
注意不要直接信任客户端伪造的 X-Forwarded-For。应在 Nginx 或网关层先做可信代理处理,再传给应用。
23.5 静态前端 + API
前端构建产物由 Nginx 提供:
1 | location / { |
API 转发:
1 | location /api/ { |
重点确认:
- 前端请求 API 的 baseURL 是
/api还是完整域名。 - 后端是否需要
/api前缀。 - 浏览器跨域问题是否通过同域代理解决。
- 前端 history 路由是否正确回退到
index.html。
24. DNS 与动态 upstream
24.1 固定 IP upstream
最简单稳定:
1 | upstream app_backend { |
优点:
- 行为明确。
- 排查简单。
- 不依赖 DNS 运行时解析。
缺点:
- 后端 IP 变化需要改配置并 reload。
- 容器动态扩缩容不够灵活。
24.2 域名 upstream
1 | upstream app_backend { |
注意:开源 Nginx 对 upstream 中域名解析的动态刷新能力有限,通常不是每次请求都重新解析。后端 IP 动态变化时,要谨慎验证。
24.3 proxy_pass 使用变量
示例:
1 | resolver 10.0.0.2 valid=30s; |
这种方式会涉及运行时 DNS 解析。
风险:
- 配置更复杂。
- DNS 故障会影响请求。
- upstream keepalive、负载均衡能力可能受限。
- 排查成本上升。
普通生产环境不建议为了“看起来动态”而滥用变量 proxy_pass。服务发现、动态 upstream 和容器编排应结合具体架构设计。
24.4 容器和 Kubernetes 场景
在 Kubernetes 中,Nginx 可能代理到:
1 | Service ClusterIP |
如果是普通 Nginx 部署在集群外,建议优先代理到稳定 Service 或上层负载均衡地址,而不是直接写 Pod IP。
Kubernetes Ingress Controller 属于另一套配置模型,本阶段只需要理解 Nginx 反向代理基础,后续再学习 Ingress 注解和控制器行为会更容易。
25. 实验一:两个后端轮询转发
25.1 实验目标
通过两个简单后端服务验证 Nginx upstream 默认轮询。
目标链路:
1 | curl -> Nginx:8088 -> backend1:9001 / backend2:9002 |
这个实验重点不是后端服务本身,而是观察 Nginx 如何在多个 upstream 节点之间分配请求。
25.2 启动两个后端
可以用 Python 快速模拟。
后端 1:
1 | mkdir -p /tmp/backend1 |
另一个终端启动后端 2:
1 | mkdir -p /tmp/backend2 |
如果是 Windows,可以用 PowerShell 或任意本地 HTTP 服务启动两个不同端口,核心是让两个端口返回不同内容。
25.3 配置 Nginx
1 | upstream demo_backend { |
检查并加载:
1 | nginx -t |
25.4 验证
1 | for i in {1..10}; do curl -s http://127.0.0.1:8088/; done |
预期看到类似:
1 | backend1 |
实际分布可能受连接复用、失败状态和请求时序影响,但大体会在两个后端之间分配。
25.5 观察 access log
如果使用 upstream 日志格式,可以重点看:
1 | upstream_addr |
例如:
1 | upstream_addr="127.0.0.1:9001" upstream_status="200" |
这比只看返回内容更适合生产排查。
26. 实验二:权重分配
26.1 实验目标
验证 weight 对请求分布的影响。
26.2 修改 upstream
1 | upstream demo_backend { |
重新加载:
1 | nginx -t && nginx -s reload |
26.3 验证分布
1 | for i in {1..40}; do curl -s http://127.0.0.1:8088/; done | sort | uniq -c |
预期大致:
1 | 30 backend1 |
注意这不是严格数学等分。请求数量越大,比例越接近权重。
26.4 实验结论
weight 适合简单权重分流:
- 机器配置不同,强机器分配更多流量。
- 新版本灰度,只给少量流量。
- 某个节点压力大,临时降低权重。
但它不是完整发布系统,不能替代自动健康检查、灰度策略、回滚控制和监控告警。
27. 实验三:后端故障与摘除
27.1 实验目标
停止一个后端,观察 Nginx 行为和错误日志。
27.2 配置失败参数
1 | upstream demo_backend { |
重新加载:
1 | nginx -t && nginx -s reload |
27.3 停止 backend1
停止监听 9001 的 Python 服务。
验证后端已经不可用:
1 | curl -v http://127.0.0.1:9001/ |
应连接失败。
27.4 访问 Nginx
1 | for i in {1..10}; do curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8088/; done |
观察 error log:
1 | tail -f /var/log/nginx/error.log |
可能看到:
1 | connect() failed (111: Connection refused) while connecting to upstream |
如果另一个节点正常,请求会逐渐转向正常节点。
27.5 恢复 backend1
重新启动 9001 后端,等待 fail_timeout 后再次访问,观察它是否重新接收请求。
27.6 实验结论
这个实验要理解三点:
max_fails和fail_timeout是被动失败判断,不是主动健康检查。- 节点被临时摘除后,过了
fail_timeout还会被再次尝试。 - 如果所有节点都不可用,Nginx 无法凭空恢复服务,只能返回错误。
28. 实验四:proxy_pass URI 差异
28.1 实验目标
验证:
1 | proxy_pass http://backend; |
对后端 URI 的影响。
28.2 准备回显后端
用 Node.js、Go、Python Flask 都可以。下面是 Python Flask 示例:
1 | from flask import Flask, request |
启动:
1 | python3 app.py |
28.3 不带 URI 配置
1 | location /api/ { |
访问:
1 | curl http://127.0.0.1:8088/api/users?id=1 |
后端看到:
1 | /api/users |
28.4 带 URI 配置
1 | location /api/ { |
访问:
1 | curl http://127.0.0.1:8088/api/users?id=1 |
后端看到:
1 | /users |
28.5 实验结论
如果后端接口实际路径是:
1 | /users |
则使用:
1 | location /api/ { |
如果后端接口实际路径是:
1 | /api/users |
则使用:
1 | location /api/ { |
上线前必须确认后端实际收到的路径,而不是只凭感觉判断。
29. 实验五:真实 IP 与 Header
29.1 实验目标
让后端打印 Header,验证 Nginx 是否正确传递真实 IP、Host 和协议。
这个实验用于确认:
1 | 客户端请求 Header |
29.2 Nginx 配置
1 | location /headers/ { |
29.3 访问验证
1 | curl -H "X-Forwarded-For: 1.1.1.1" http://127.0.0.1:8088/headers/ |
后端应能看到类似:
1 | X-Real-IP: 127.0.0.1 |
29.4 实验结论
X-Real-IP通常记录 Nginx 识别出的直接客户端 IP。X-Forwarded-For会追加代理链路。X-Forwarded-Proto告诉后端入口层协议。- 后端不能无条件信任客户端传来的伪造 Header。
30. 实验六:WebSocket 代理
30.1 实验目标
通过 Nginx 代理 WebSocket,并验证 101 Switching Protocols。
30.2 准备 WebSocket 后端
可以使用任意 WebSocket 服务。Node.js 示例:
1 | const WebSocket = require('ws'); |
启动:
1 | node ws-server.js |
30.3 Nginx 配置
1 | map $http_upgrade $connection_upgrade { |
30.4 curl 验证握手
1 | curl -i \ |
看到:
1 | HTTP/1.1 101 Switching Protocols |
说明升级成功。
30.5 常见失败现象
如果返回 400、426、502 或连接很快断开,优先检查:
- WebSocket 后端是否可直接连接。
- 是否设置
proxy_http_version 1.1。 - 是否透传
Upgrade和Connection。 location路径是否与后端 WebSocket 路径一致。- 中间层是否支持 WebSocket。
31. 实验七:upstream keepalive
31.1 实验目标
开启 upstream keepalive,观察 Nginx 到后端连接复用。
31.2 配置 keepalive
1 | upstream keepalive_backend { |
31.3 观察连接
压测:
1 | wrk -t2 -c50 -d30s http://127.0.0.1:8090/ |
观察连接:
1 | ss -ant | grep ':9010' |
对比关闭 keepalive 前后的连接建立数量、TIME_WAIT 数量和后端 accept 压力。
31.4 实验结论
- keepalive 能减少 Nginx 到后端的连接建立开销。
- keepalive 数值不是最大并发连接数。
- 后端也要支持并合理配置 keep-alive。
- keepalive 收益需要结合 QPS、后端连接上限和压测结果判断。
32. 实验八:复现 504
32.1 实验目标
通过设置较短 proxy_read_timeout 复现 504。
32.2 准备慢接口
Python Flask 示例:
1 | from flask import Flask |
32.3 Nginx 配置
1 | server { |
32.4 访问验证
1 | curl -i http://127.0.0.1:8091/slow |
预期:
1 | HTTP/1.1 504 Gateway Time-out |
error_log 中可能看到:
1 | upstream timed out (110: Connection timed out) while reading response header from upstream |
32.5 修复
把超时改为大于后端处理时间:
1 | proxy_read_timeout 10s; |
重新加载:
1 | nginx -t && nginx -s reload |
再次访问应成功。
32.6 实验结论
504 不是简单的“Nginx 坏了”,而是 Nginx 等待 upstream 超时。真实原因可能在:
- Nginx
proxy_read_timeout设置过短。 - 后端接口本身耗时过长。
- 后端依赖数据库、缓存、RPC 或第三方接口阻塞。
- 后端线程池、连接池或系统资源耗尽。
33. 常用排查命令
33.1 检查 Nginx 配置
1 | nginx -t |
nginx -T 可以打印完整合并后的配置,适合确认 include 后最终生效内容。
33.2 检查端口监听
1 | ss -lntp |
或:
1 | netstat -lntp |
33.3 直接访问后端
1 | curl -v http://10.0.0.11:8080/health |
从 Nginx 所在机器访问后端,比从自己电脑访问更有意义。
33.4 查看 access log
1 | tail -f /var/log/nginx/access.log |
筛选 5xx:
1 | awk '$9 ~ /^5/ {print}' /var/log/nginx/access.log | tail -50 |
如果使用自定义日志格式,可以筛选 upstream 状态和耗时。
33.5 查看 error log
1 | tail -f /var/log/nginx/error.log |
搜索 upstream 错误:
1 | grep -i "upstream" /var/log/nginx/error.log | tail -50 |
33.6 查看连接状态
1 | ss -ant | awk '{print $1}' | sort | uniq -c |
查看到某个后端端口的连接:
1 | ss -ant | grep ':8080' |
查看 TIME_WAIT:
1 | ss -ant state time-wait | wc -l |
33.7 抓包
1 | tcpdump -i any host 10.0.0.11 and port 8080 -nn |
保存抓包:
1 | tcpdump -i any host 10.0.0.11 and port 8080 -nn -w upstream.pcap |
抓包用于确认:
- Nginx 是否真的向后端发起连接。
- TCP 是否握手成功。
- 后端是否返回数据。
- 是否存在重传、RST、超时。
34. 常见误区
34.1 混淆 proxy_pass URI 规则
错误理解:
1 | proxy_pass 后面有没有 / 都一样 |
实际差异很大:
1 | proxy_pass http://backend; |
一个通常保留当前 URI,一个会用 proxy_pass 中的 URI 替换匹配到的 location 前缀。
判断标准不是“配置看起来对不对”,而是后端实际收到的 URI 是否符合预期。
34.2 以为 max_fails 是主动健康检查
max_fails 和 fail_timeout 是被动失败判断,不是定时探测后端健康。
如果没有请求打到某个节点,Nginx 不会主动知道它是否恢复。
需要主动健康检查时,应考虑:
- Nginx Plus。
- 第三方健康检查模块。
- OpenResty 实现健康检查。
- 上层 SLB / 服务发现 / 注册中心。
- 应用发布系统配合上下线。
34.3 盲目开启重试
把所有状态码都加入重试:
1 | proxy_next_upstream error timeout http_500 http_502 http_503 http_504 http_404; |
可能导致:
- 业务重复处理。
- 故障扩散。
- 错误被掩盖。
- 排查困难。
尤其是订单、支付、库存、优惠券、账户余额等接口,必须优先考虑幂等设计。
34.4 后端拿不到真实 IP 就只怪 Nginx
真实 IP 需要完整链路配合:
1 | CDN / SLB 正确传 Header |
任一环节错误都会导致 IP 不准。
34.5 所有 502 都怪 Nginx
502 常常是后端问题暴露在 Nginx 上:
- 后端拒绝连接。
- 后端崩溃。
- 后端返回非法响应。
- 后端响应头过大。
- 后端 HTTPS 握手异常。
- Nginx 到后端网络不通。
Nginx 是报错点,不一定是根因。
34.6 keepalive 设置越大越好
keepalive 太大可能导致后端维持大量空闲连接,反而浪费资源。
应结合:
1 | worker 数量 |
34.7 用 ip_hash 解决所有 Session 问题
ip_hash 只是临时缓解方案,不是根本方案。
更好的方案是应用无状态化:
1 | Session / 登录态 / 用户上下文 |
34.8 只改 Nginx 不看后端
反向代理问题往往跨越多层:
1 | 客户端 -> Nginx -> 后端应用 -> 数据库 / RPC / 缓存 / 第三方服务 |
只看 Nginx 配置,很难定位慢接口、线程池耗尽、数据库阻塞、RPC 雪崩等问题。
34.9 滥用变量 proxy_pass
变量 proxy_pass 看起来灵活:
1 | set $backend "app.example.com"; |
但会引入运行时 DNS、连接复用、负载均衡和排查复杂度问题。
除非确实需要动态代理,否则普通业务更推荐固定 upstream。
35. 阶段练习清单
35.1 必做练习
- 使用两个后端服务完成 Nginx 轮询负载均衡。
- 配置
weight并统计请求分布。 - 对比
proxy_pass http://backend和proxy_pass http://backend/的 URI 差异。 - 配置
Host、X-Real-IP、X-Forwarded-For、X-Forwarded-Proto并在后端打印验证。 - 停止一个后端节点,观察 error log 和 upstream 行为。
- 配置
proxy_connect_timeout、proxy_send_timeout、proxy_read_timeout并复现超时。 - 配置
proxy_next_upstream并观察重试后的$upstream_addr和$upstream_status。 - 配置 upstream keepalive 并观察连接复用。
- 配置 WebSocket 代理并验证 101 协议升级。
- 自定义 upstream access log 格式,记录 upstream 地址、状态码和耗时。
35.2 进阶练习
- 使用
least_conn对慢接口做负载均衡测试。 - 使用
hash $request_uri consistent验证相同 URI 固定路由。 - 配置一个前后端分离站点,静态资源走本地,API 走 upstream。
- 配置一个多服务路由站点,
/users/、/orders/、/files/分别转发到不同后端。 - 对上传接口单独设置
client_max_body_size和较长超时。 - 对 SSE 接口关闭
proxy_buffering并验证实时输出。 - 代理 HTTPS 后端并配置
proxy_ssl_server_name。 - 使用
tcpdump抓取 Nginx 到后端的 TCP 连接。
35.3 生产思考题
- 如果后端接口平均 50ms,但偶尔 30s,应该调大 Nginx 超时还是优化后端?
- 如果订单创建接口发生 502,是否应该自动重试?为什么?
- 如果所有用户经过同一个公司 NAT 出口,使用
ip_hash会有什么问题? - 如果后端扩容后请求分布不均,应该从哪些维度排查?
- 如果
$upstream_response_time很短但$request_time很长,可能是什么原因? - 如果后端拿到的协议总是 HTTP,但用户访问的是 HTTPS,应该检查哪些 Header 和框架配置?
- 如果 upstream keepalive 开启后后端连接数很高,应该如何评估是否合理?
36. 过关标准
完成本阶段后,应能做到:
- 能独立编写一个稳定的 HTTP 反向代理配置。
- 能独立配置一个多节点 upstream,并解释负载均衡分配逻辑。
- 能解释
proxy_pass带 URI 和不带 URI 的转发差异。 - 能根据业务选择轮询、权重、
ip_hash、least_conn、hash等策略。 - 能正确传递 Host、真实 IP、协议和代理链路 Header。
- 能说明
proxy_connect_timeout、proxy_send_timeout、proxy_read_timeout分别对应哪个阶段。 - 能解释
max_fails、fail_timeout的被动失败判断机制。 - 能谨慎配置
proxy_next_upstream,理解幂等风险。 - 能配置 upstream keepalive,并说明它不是最大并发连接数。
- 能配置 WebSocket、SSE 等长连接代理。
- 能通过 access log 和 error log 初步定位 502、503、504。
- 能把 Nginx 接入常见后端应用,并处理真实 IP、协议、路径前缀等问题。
37. 本阶段推荐配置骨架
下面是一份可作为学习和小型生产场景起点的配置骨架:
1 | http { |
使用前必须根据实际情况调整:
server_name。- upstream 后端 IP 和端口。
/api/是否需要去掉前缀。- 超时时间。
- 是否需要 WebSocket。
- 是否需要 HTTPS。
- 日志路径。
- 后端是否支持 keepalive。
38. 本阶段总结
第三阶段的核心不是记住某几个配置项,而是建立完整代理链路模型:
1 | 入口匹配 |
学习反向代理时,最重要的几个判断是:
- 请求是否进入了正确的
server和location。 - 后端实际收到的 URI 是否符合预期。
- 后端实际收到的 Host、IP、协议 Header 是否符合预期。
- Nginx 选中了哪个 upstream 节点。
- 失败发生在连接、发送、读取还是后端处理阶段。
- 发生重试时是否存在业务幂等风险。
- 超时是 Nginx 设置问题,还是后端和下游链路问题。
掌握本阶段后,就可以把 Nginx 作为比较稳定的应用入口代理使用。下一阶段建议进入 HTTPS、安全与访问治理,继续补齐入口层安全能力。
