聊到 HTTP 反向代理,很多同学脑子里会先蹦出 Nginx 的配置片段,或者是面试题里那句“反向代理隐藏后端服务器”。但真到了自己搭系统、排查线上故障的时候,才发现对反向代理的理解往往停留在表面——它到底解决的是什么问题,哪些场景非它不可,哪些配置坑是文档里不会明说的,这些才是真正值钱的经验。这篇文章我想从用途、优势到实操配置,把反向代理这件事拆开揉碎讲清楚,希望能帮你少走几步弯路。
先说一个最直观的体会:反向代理就像一个前台的接待员。访客不需要知道背后的办公区坐的是谁、工位在哪,只要把需求递给接待员,接待员会自己判断该找哪个部门、哪位同事来处理,然后把结果原样带回来。映射到技术上,客户端只跟代理服务器打交道,代理再根据规则把请求转发给内网的真实服务器,真实服务器处理完,响应又原路返回给客户端。整个过程,客户端完全感知不到后端的存在。
这个看似简单的“中间层”,恰恰是很多系统架构从“能用”走向“抗造”的分水岭。下面我按自己的理解,把反向代理的用途、优势、落地配置和踩坑经验完整过一遍。
1. 反向代理到底是什么:概念与工作模型
1.1 从一次请求看反向代理的完整链路
我们拆解一次典型请求,看反向代理在其中做了哪些事。假设用户在浏览器输入https://api.example.com,请求先到达 DNS 解析出的 IP,也就是反向代理服务器的地址。代理器收到 HTTP 请求后,第一步是解析请求头,拿到 Host、URI、Cookie 等信息,然后根据预设的路由规则决定转发目标。比如api.example.com/user/*转发到内网服务器192.168.1.10:8080,api.example.com/order/*转发到192.168.1.11:8081。
接着,代理以客户端身份向目标服务器发起一个新的连接,把原始请求头按需改写后发送出去。内网服务器处理完业务,把响应返回给代理。代理这里还有一个关键动作——它可能需要对响应做额外加工,像写入缓存、压缩响应体、修改响应头里的某些字段。处理完毕,代理再把最终响应回传给浏览器。整个过程从客户端视角看,它只跟一个“万能服务器”打交道,完全不知道内部还有路由、分流、鉴权这一堆事情。
这个模型最大的特点是:反向代理天然处于“流量咽喉”的位置,所有进出请求都经过它。这既是灵活性的来源,也是稳定性的关键点——一旦代理挂了,整个入口就断了,所以后续配置里高可用是必须考虑的问题。
1.2 反向代理与正向代理:别再把两种东西混为一谈
说到代理,最常见的混淆就是把正向代理和反向代理搞混。正代是替客户端干活,客户端主动把请求交给代理,由代理去访问目标服务器。手指指个方向就明白了:正代站在客户端一边,反代站在服务器一边。
但更本质的区别在于部署位置和感知方式。正向代理对目标服务器透明,服务器只知道请求来自代理 IP,不关心真正的客户端是谁;反向代理对客户端透明,客户端根本不知道真实服务器存在。用场景来理解最直接:正代常见于办公网络出口,用来做访问控制和内容过滤;反代则常见于业务入口,用来做负载均衡和安全防护。还有一个隐含区别——正代通常需要客户端手动配置代理地址,而反代对使用者完全无感知,用户不需要任何设置。
有一段时间,我见过一些团队把正代和反代混在一个章节里写文档,后来排查问题时就出现了认知偏差。建议所有入门同学第一件事就是把这个边界划清,后续理解很多配置选项时才不会犯迷糊。
1.3 为什么非要多加一道代理层
刚接触反向代理的人通常会有疑问:我的应用架构明明跑得好好的,为什么要在前面硬塞一个代理层,这不是多了一个故障点吗?这个问题的答案,其实就是反向代理存在价值的核心。
没有代理层时,所有请求直接打向业务服务器,业务的每一个变化都牵扯到域名解析、对外IP、甚至客户端的感知。比如你想给数据库读写分离、想增加一台应用服务器、想临时下掉某台故障机器,都得改 DNS 或者让客户端感知节点变化。有了代理层之后,对外只暴露一个稳定的入口,内部的任何调整都被隔离在代理后面,客户端无感。其次,统一入口意味着可以在一个地方集中做 TLS 终止、限流、缓存、鉴权、日志采样,而不必在每个业务服务里重复造轮子。这本质上是一种“关注点分离”架构思想——把通用的基础设施能力抽离到独立的代理层,让业务系统专注于自身的业务逻辑。
我实际经历的项目里,有一个早期架构把所有功能都塞进单体应用,每当要上线新模块,都得在入口代码里加路由判断,后来拆分为多个微服务后,反向代理几乎是自然引入的第一件事。没有它,服务的注册发现、路由转发、灰度策略全都无处安放。
2. 反向代理的核心用途:六大典型场景全拆解
2.1 负载均衡:把流量“摊平”到多台服务器
负载均衡是最经典的反向代理用途。当单台服务器处理能力达到上限,最直接的办法就是再加服务器,但加完之后要把流量分配到不同机器上,否则新加的机器就成了摆设。反向代理可以基于多种策略做分发:轮询、加权轮询、最少连接数、IP哈希、URI哈希等。
轮询适合后端机器配置相同的场景,每台机器轮流处理请求,简单但无法应对机器的性能差异。加权轮询则给每台机器配置权重,比如高性能机器权重为3,普通机器权重为1,代理按权重比例分配请求,这个在实际应用中比纯轮询更常见。最少连接数策略适合请求处理时长差异大的场景,如果某些请求耗时很长而另一些很快,连接数比轮询更能反映真实负载。IP哈希则是根据客户端 IP 计算哈希值映射到固定机器,优势在于同一客户端的请求总会落到同一台机器,适合有本地缓存的场景,但缺点也很明显——某台机器故障时,落在它上面的客户端会受影响。
实操中,我见过不少团队一开始只配了最简单的轮询,结果每到高峰就有一两台机器 CPU 打满而其他机器空闲。后来把策略改为 least_conn,再配合健康检查,流量才均匀起来。这里的关键经验是:没有一种负载均衡策略是万能的,必须结合业务请求的耗时分布和后端机器的性能差异来选。
2.2 安全防护:核心业务不直接暴露
把业务服务器放在反向代理后面,本身就有安全收益。首先,后端服务器可以只分配内网 IP,不暴露公网,所有外部流量只能到达代理层。攻击者要渗透内网,必须先攻破代理层,而代理层是一个经过高度精简的基础设施组件,攻击面比一个带着各种业务逻辑的应用服务器小得多。
其次,代理层可以做很多安全策略的集中落地。比如通过配置允许和拒绝的 IP 列表,封锁恶意来源;通过限制单 IP 的请求频率,缓解暴力破解和 CC 攻击;通过过滤请求头和 URL 里的可疑模式,拦截 SQL 注入和 XSS 攻击的简单变种。虽然这些不能替代专业的安全产品,但作为第一道防线,性价比极高。
还有一个容易被忽略的点:对外隐藏技术栈。HTTP 响应头里的 Server 字段会暴露服务器类型和版本,通过反向代理可以统一改写成泛化名称,减少被针对性攻击的风险。我实践中会把上游应用返回的版本号相关的响应头全部剥离掉,这一条虽然简单,但对安全扫描结果有立竿见影的改善。
2.3 缓存加速:让热点请求直接止步于代理层
反向代理可以缓存后端返回的响应。当客户端请求的同一个资源再次到达时,代理直接返回缓存副本,不再把请求转发给后端。对静态资源来说,这种加速效果非常明显,原本需要经过应用服务器、数据库的全链路查询,被缩短为代理层一次内存读取。
但很多人对缓存的理解停留在“配置一把就要拿着”的层面,实际上中间有很多讲究。缓存的关键是:哪些请求可以缓存、缓存多久、怎么保证后端数据更新后缓存不失效。按照 HTTP 协议,代理缓存主要依赖响应头里的 Cache-Control、Expires、Etag、Last-Modified 等字段。实操中,静态资源通常用 Cache-Control 的 max-age 来设置过期时间,动态接口则很少缓存或只做极短时间的缓存。反向代理可以利用 Etag 做条件请求——请求带上 If-None-Match,如果资源没变化,后端返回 304,代理再把缓存的响应体发给客户端,省掉了响应体的重复传输。
缓存策略要谨慎的是:宁可少缓存、短缓存,也不要缓存错数据。尤其是用户隐私类信息,凡是带 Cookie 的响应默认就不要缓存,避免出现 A 用户看到 B 用户数据的严重事故。这一点我会在问题章节再展开。
2.4 SSL 终止与统一证书管理
HTTPS 已经是标配,但证书的部署和管理在服务器数量多了之后会变成很大的负担。每台服务器都需要配置证书、定期更新,稍有不慎就出现证书过期的事故。反向代理可以将 TLS 终止统一收敛到一个位置——客户端与代理之间走 HTTPS,代理与后端服务器之间走 HTTP(或内部 HTTPS)。
这样做的好处很明显:证书只需要在代理层管理,后端服务器不需要处理加解密,节省了 CPU 开销,也让证书统一续期变得容易。不少团队全站用了自动化的证书管理方案,配合反向代理,几乎实现了证书无限期自动续期,再也不用记过期时间。
关于 SSL 终止,有一个必须提醒的点:代理和后端之间使用明文 HTTP 时,要确保这段链路只存在于内网或可信网络。如果网络环境不可控,还是应该在后端也启用 TLS,代理层与后端之间走自定义的内部 HTTPS。另外,正确配置 SSL 协议版本和加密套件也很关键,比如禁用 TLSv1.0/1.1,只开启 TLSv1.2/1.3,这会让安全扫描的结果变得好看很多。
2.5 灰度发布与多版本流量调度
在微服务和持续交付的语境下,反向代理成了灰度发布的关键工具。我们希望新版本代码先只让一小部分流量验证,没问题后再逐步扩大比例。基于反向代理,可以通过权重调整控制新旧版本的流量比例,比如先让 5% 的请求到新版,观察错误率和延迟指标正常后,再调整到 50%,最后全量切换。
还有一种更精确的灰度方式是基于请求特征做路由。比如某个内部测试账号组的请求固定转发到新版,外部用户的流量仍然走旧版;或者按特定 Header、Cookie 来分流。Nginx 的配置灵活性足以支撑这类场景。更复杂的场景甚至可以叠加多个代理层规则,把流量分为按比例、按用户特征等多维度组合。灰度发布这件事,代理层不是唯一的实现方案,服务网格和注册中心也能做,但在没有引入网格的团队里,用反向代理实现几乎零成本。
2.6 复杂路由与微服务网关能力
随着业务拆分,后端服务数量从一两个增长到几十个,客户端不可能记住每个服务的地址,也不应该暴露给外部。反向代理天然承担了 API 网关的角色——客户端只请求一个域名,代理按路径前缀转发到不同服务。
比如 /api/user/ 转发到用户服务,/api/order/ 转发到订单服务,/api/pay/ 转发到支付服务。这样的路由规则让前端可以统一接口调用方式,后端服务之间保持独立演进。网关能力的进一步延展,包括请求体大小限制、接口鉴权、请求ID注入、流量染色、超时控制、熔断等,这些虽然不一定都写在反向代理的默认功能里,但通过扩展模块或者配合脚本语言都可以实现。实际项目中,网关层往往还承担统一的 CORS 跨域处理、参数校验和访问日志持久化,让各业务线不用重复实现这些公共逻辑。
3. 反向代理的优势剖析:为什么它不是可有可无的中间层
3.1 对客户端透明:架构调整无感知
反向代理最容易被忽略的优势是——它对客户端完全不可见。用户在浏览器的地址栏里输入的还是同一个域名,DNS 解析出的 IP 可能从 A 变成了 B,甚至变成了代理集群的多个 IP,但用户不会感觉到任何变化。这种透明性带来的操作空间极大:后端服务器可以换操作系统、换语言框架、扩缩容,前端客户端完全不需要升级适配。
线上踩过的一个具体场景:某次需要把后端服务从机房迁移到云上,IP 全部变化,但因为架构里已经有反向代理,我们只需要把代理配置里的 upstream 指向新地址,客户端零改动地完成了迁移。如果是直连后端,光改 DNS 解析的生效时间和缓存问题就够折腾一阵子。
3.2 集中式管控:一处配置,全局生效
没有代理层时,安全策略、访问控制、日志策略分散在每个应用里,维护成本极高。有了反向代理,公共策略可以集中发布。比如公司要求所有对外接口增加统一的响应头,用来标识服务状态或安全合规信息,直接在代理层加一个 add_header 配置就能全局生效。类似的还有跨域头设置、字符集设置、请求超时时间等。
这种集中式管控还体现在故障排查上。当线上出现问题,第一步看代理层的访问日志,能快速判断是入口流量异常,还是后端响应慢。集中的日志、集中的监控指标采集点,让排查链路清晰很多。如果每个服务各自记录五花八门的格式,出了问题光对齐日志格式就要浪费不少时间。
3.3 横向扩展能力:加机器就能扛住增长
没有代理层时,系统扩容的直接做法是加机器,但加了机器还得考虑流量怎么分发,很多时候就要靠 DNS 轮询或者硬改架构。有了反向代理,扩容动作可以做到非常平滑——在上游配置里加入新机器,或者调整集群的权重,代理会自动把流量引到新节点。
这里要提到健康检查机制。代理会定期探测后端服务的可用性,比如通过 TCP 端口探测或者 HTTP 主动检查一个健康检查路径。一旦某台机器探测失败,代理自动把它摘除出可用列表,不再转发流量给它;恢复后自动重新加入。这个机制保障了后端单点故障不会影响整体可用性。对于高峰期来说,临时扩容也异常方便——现开两台机器,配置加进去,reload 一下,流量立刻分担过去。整个过程业务代码零改动。
3.4 安全纵深:防御面收敛到单一入口
信息安全的思路历来强调“减少攻击面”。在反向代理架构下,后端服务器不直接对外,攻击者不能直接探测后端的操作系统、端口、中间件版本。暴露的公网端口只剩 80/443,攻击路径被大幅收窄。
代理层还可以实现连接级别的防护。比如限制单 IP 并发连接数、限制每个连接请求速率、自动识别低频慢速攻击。这类防护如果放在应用层实现,每个服务都要做一遍,很难统一口径。集中在代理层之后,一来可以直接下线有风险的旧协议接口,二来可以对敏感路径做差异化的访问控制。当然,安全不是只靠一层,代理给了你第一道防线,后面的纵深防御照样不能少。
3.5 协议与流量治理的灵活性
反向代理提供了 HTTP 协议层之外的流量治理能力。比如你可以在代理层同时接收 HTTP/1.1 和 HTTP/2 流量,转给后端时统一使用 HTTP/1.1,避免后端技术栈对 HTTP/2 支持不完整的问题。TLS 终止也给了你调整加密策略的便利,可以统一提升客户端侧的 TLS 版本要求,而不需要动所有后端应用。
流量治理方面,可以通过限流、熔断、超时控制等手段保护后端系统。代理面前是海量请求,代理后端的资源有限,不加治理的话,任何流量尖峰都可能打垮后端。代理层的限流可以做得很细,比如按 IP 限流、按 URL 限流、按全局限流。超时控制则可以避免某个后端服务变慢时,客户端连接长时间挂着不放,拖垮整个代理。
4. 实操:用 Nginx 构建一个生产可用的反向代理
4.1 基础安装与最简配置
Nginx 是搭建反向代理最常用的选择,轻量、稳定、配置灵活。安装方式各系统都有包管理器可用,不再赘述。安装完成后,先看最简配置长什么样:
server { listen 80; server_name api.example.com; location / { proxy_pass http://192.168.1.10:8080; 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; } }这段配置的核心是 proxy_pass 指令,它定义了请求转发到哪个上游地址。这里直接把请求全部转发给单台服务器,算是反向代理的最小可用形态。三个 proxy_set_header 分别干了什么?Host 头用来让后端虚拟主机正确识别域名,X-Real-IP 和 X-Forwarded-For 用来传递客户端真实 IP,X-Forwarded-Proto 用来标识客户端请求是 HTTP 还是 HTTPS。这些 Header 不设置的话,后端的访问日志和业务逻辑拿到的都是代理的 IP,而不是真实客户端 IP。我见过不少线上系统因为漏配这几行,导致风控模块永远拿不到用户真实 IP,整个拦截逻辑全部失效。
4.2 负载均衡配置与权重调整
接下来组一个简单的 upstream 集群:
upstream backend { server 192.168.1.10:8080 weight=3 max_fails=3 fail_timeout=30s; server 192.168.1.11:8080 weight=1 max_fails=3 fail_timeout=30s; server 192.168.1.12:8080 backup; keepalive 32; } server { listen 80; server_name api.example.com; location / { proxy_pass http://backend; proxy_http_version 1.1; proxy_set_header Connection ""; include proxy_params; } }weight 参数控制权重,weight=3 的机器接收的请求是 weight=1 的三倍。max_fails 和 fail_timeout 组合定义了健康检查规则——在 fail_timeout 时间内失败 max_fails 次,该服务器被标记为不可用,剔除出负载池。backup 参数标记备份服务器,平时不参与流量,只有当其他所有服务器都不可用时才会上场。keepalive 32 表示与后端建立空闲长连接的数量上限,这对提升吞吐很有帮助。加一行 proxy_http_version 1.1 和清空 Connection 头,是为了让后端复用连接,避免每次请求都重新 TCP 握手。
实际配置时,权重不是拍脑袋定的。先压测出每台机器的实际吞吐能力,比如机器 A 能抗 2000 QPS,机器 B 只能抗 1000 QPS,权重设为 2:1 才是合理的。我曾经草率地把权重设为 5:1,结果机器 A 的 CPU 先扛不住了,因为业务接口里的一些热点查询在流量倾斜后放大了对数据库的压力。这个教训是:先压测,再定权重。
4.3 缓存与静态资源加速配置
给代理加一层缓存,对读多写少的接口非常有效。Nginx 的缓存机制主要依赖 proxy_cache 相关指令:
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=api_cache:10m max_size=1g inactive=60m; server { listen 80; server_name api.example.com; location /api/static/ { proxy_pass http://backend; proxy_cache api_cache; proxy_cache_valid 200 10m; proxy_cache_key "$scheme$request_method$host$request_uri"; add_header X-Cache-Status $upstream_cache_status; } }proxy_cache_path 定义了缓存存储路径和共享内存区大小。levels=1:2 是缓存文件的目录层级,10m 是元数据内存缓存大小,max_size 是磁盘缓存上限,inactive 表示 60 分钟内未被访问的缓存会被清理。proxy_cache_valid 200 10m 表示对于 200 状态码的响应缓存 10 分钟。最后的 add_header X-Cache-Status 很实用,响应头会显示 HIT、MISS、EXPIRED 等缓存状态,排障时一眼就能看出缓存有没有生效。
注意一点:透传Set-Cookie的响应不要配置缓存,否则用户隐私数据可能缓存在代理层,酿成严重事故。对于登录态接口,要么完全不缓存,要么按用户维度区分缓存键,但这样做缓存命中率会很低。我的实践是:动态接口默认不缓存,静态资源使用较长缓存时间,接口层面只有确保无用户差异的数据才考虑加缓存。
4.4 安全相关报头与访问控制
代理层可以做的基础安全加固包括:限制请求方法、限制请求体大小、屏蔽敏感 URL、统一安全响应头:
server { listen 443 ssl; server_name api.example.com; ssl_certificate /etc/nginx/ssl/api.example.com.pem; ssl_certificate_key /etc/nginx/ssl/api.example.com.key; ssl_protocols TLSv1.2 TLSv1.3; # 安全响应头 add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "SAMEORIGIN" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; # 限制请求体大小 client_max_body_size 10m; # 限制单个IP的并发连接数 limit_conn_zone $binary_remote_addr zone=perip:10m; limit_conn perip 20; # 限制请求速率 limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s; limit_req zone=api_limit burst=20 nodelay; location / { deny 1.2.3.4; # 拒绝特定IP allow 192.168.0.0/16; # 允许内网网段 deny all; # 其余拒绝 proxy_pass http://backend; } }limit_conn 和 limit_req 是两类不同的限流。limit_conn 限制的是同时连接数,适合防止单 IP 占满连接池;limit_req 限制的是请求速率,rate=10r/s 表示每秒 10 个请求,burst=20 表示允许突发到 20 个请求的瞬时排队。配置 limit_req 时要根据业务实际流量来设定,过小会误伤正常用户,过大起不到保护作用。我一般建议先放开记录日志观察一周的 QPS 分布,再定阈值,而不是上来就拍一个数。
4.5 一个完整的生产级入口配置模板
把上面的配置组合起来,整理一个相对完整的入口模板,覆盖 HTTP/HTTPS 双入口、跳转、负载均衡、缓存、安全头、访问日志:
upstream backend { server 192.168.1.10:8080 max_fails=3 fail_timeout=30s; server 192.168.1.11:8080 max_fails=3 fail_timeout=30s; keepalive 16; } log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for" ' 'rt=$request_time uct=$upstream_connect_time uht=$upstream_header_time urt=$upstream_response_time'; server { listen 80; server_name api.example.com; return 301 https://$host$request_uri; } server { listen 443 ssl; http2 on; server_name api.example.com; ssl_certificate /etc/nginx/ssl/fullchain.pem; ssl_certificate_key /etc/nginx/ssl/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; access_log /var/log/nginx/api_access.log main; # 统一安全头 add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "SAMEORIGIN" always; # 服务端关闭等安全相关处理 server_tokens off; location / { proxy_pass http://backend; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header Connection ""; 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; proxy_connect_timeout 5s; proxy_read_timeout 10s; proxy_send_timeout 10s; } location /healthz { return 200 "ok"; add_header Content-Type text/plain; } }server_tokens off 很重要,它隐藏了 Nginx 的版本号。超时时间的配置也值得细说:proxy_connect_timeout 建议设短一些,比如 5 秒,避免后端不可达时代理层大量堆积连接;proxy_read_timeout 是根据业务接口的最长响应时间设置的,太短会出现正常慢接口被误杀的问题。访问日志格式里加上了 $upstream_response_time 和 $request_time,这两个值在排查慢接口时价值很大——如果 request_time 很大而 upstream_response_time 很小,说明瓶颈在代理层或网络上;如果两者都很大,问题在后端。
5. 常见问题与排查实录
5.1 502 与 504 Bad Gateway 的逐层排查
最经典的问题就是突然出现一批 502 或者 504。502 表示代理成功连接到了上游,但上游返回了非法响应或连接被重置;504 表示代理连接上游超时。排查思路分三层:
第一层,确认上游是否活着。在代理服务器上直接用 curl 访问上游地址,看是否有正常响应。如果 curl 都连不上,问题出在上游服务本身,可能是进程崩了、端口没监听、防火墙拦截。第二层,查看代理错误日志,Nginx 的 error.log 通常会有具体原因,比如 connect() failed (111: Connection refused) 是连接被拒绝,而 connect() timed out 则是网络不通或防火墙丢包。第三层,看是不是健康检查把节点摘除后配置里还有请求在路由。曾经遇到过一次情况,上游服务正常,但代理配置里的 upstream 指向了一个旧的备用 IP,流量打到一台已经关机的机器上,排查绕了很久,最后是看 proxy_pass 写的是域名还是 IP 才发现的。
如果是 504,优先看超时时间配置。上游接口确实需要 12 秒才能返回,但 proxy_read_timeout 设的是 10 秒,那必然会出现 504。我遇到过一个情况:某导出接口在处理大文件时需要 30 秒以上,原配置的超时时间是 15 秒,导致该接口在高峰期被大量报错。调整后不仅问题消失,还顺手把慢接口的日志单独拉出来做了监控。
5.2 客户端真实 IP 丢失问题
很多业务需要拿到客户端真实 IP,比如登录风控、访问频率限制、操作审计。如果反代没有正确传递 IP 头,后端看到的全是代理服务器的 IP,业务逻辑会乱套。
正确做法是在代理层设置 X-Forwarded-For 头,把这个头设置为原有值加上$remote_addr。Nginx 的$proxy_add_x_forwarded_for会自动处理追加逻辑——如果请求头里没有 XFF,它就等于$remote_addr;如果有,就在原值后面追加当前连接 IP。但要注意一个问题:如果客户端可以自己伪造 XFF 头,那这个头就是不可信的。比如有用户直接请求绕过了代理,或者代理配置允许透传客户端传入的 XFF,那么后端的风控系统拿到的 IP 就是用户伪造的。生产环境更稳妥的做法是,在代理入口处用proxy_set_header X-Forwarded-For ""清空外部传入的 XFF,然后重新设置,或者直接改用专门的标准头X-Real-IP,由代理保证其可信。另外,如果架构里有多层代理,需要配置real_ip_recursive on并设置set_real_ip_from信任代理网段,才能正确解析到最真实的客户端 IP。
5.3 WebSocket 与长连接被中断
反向代理默认转发的是 HTTP 短连接,遇到 WebSocket 应用就会出现连接被反复断开的问题。因为 WebSocket 升级握手需要在转发时保留 HTTP 头里的 Upgrade 和 Connection 字段,而默认配置会把 Connection 头清掉。
WebSocket 场景下需要在 location 里显式设置:
location /ws/ { proxy_pass http://backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_read_timeout 3600s; }proxy_read_timeout 要调大,因为 WebSocket 连接是长时间的,如果保持默认的 10 秒,连接会在 10 秒内被强制断开。这里有个容易踩的坑:必须确保代理层和后端都支持 HTTP/1.1 长连接,如果后端框架默认关闭了 keep-alive,WebSocket 还是会异常断开。排查思路是抓包看 TCP 连接是被谁断开的,断开方向不同问题位置就完全不同。
5.4 缓存问题:缓存雪崩、缓存穿透与数据不一致
加了代理缓存后,最容易出问题的是缓存和数据不一致。比如商品详情接口缓存了 30 分钟,但后台改了价格后客户端仍然看到旧价格。解决方案就是主动清理缓存,或者在后台修改数据时调用缓存清理接口。
更隐蔽的问题是缓存雪崩——如果所有缓存的过期时间都设置在同一时刻,到期后所有请求瞬间穿透到后端,后端压力瞬间翻倍,甚至被打挂。解决方案是给缓存过期时间加入随机偏移量,设定为比如 10 分钟加上随机 0-300 秒。这一点在代理缓存层和后端应用缓存层都适用。
还有缓存穿透问题——当请求的 key 在数据库里根本不存在时,缓存层永远无法命中,每次都打到后端。代理层本身很难识别这类请求的业务语义,通常需要后端配合处理,比如对空结果也做短期缓存,或者使用布隆过滤器拦截非法 key。代理层的职责是确保缓存配置正确,业务层的穿透防护还是要靠应用自己兜底。
5.5 配置修改与平滑重载
Nginx 修改配置后,很多人习惯直接 restart,但这会引发连接中断,正在进行的请求会被打断。正确做法是使用nginx -t先校验配置语法,确认无误后执行nginx -s reload平滑重载。
reload 的本质是:master 进程重新读取配置,创建新的 worker 进程,然后优雅地关闭旧的 worker 进程,让旧的 worker 处理完正在进行的请求后再退出。这才是生产环境该有的操作方式。此外,上线新配置前最好留一个可回滚的副本,万一新配置引入了错误,可以快速找回上一版。我在一个小团队时有一次直接改线上配置没测试,结果 reload 后立即出现 502,后来才知道 upstream 地址写错了。从那以后养成了习惯:配置改完先在预发布环境完整验证一遍,再上生产,并且保留cp nginx.conf nginx.conf.bak.日期的备份。
5.6 排错手段速查表
这里把日常排障用得最多的几个手段整理成一张表,便于对照:
| 现象 | 关键排查点 | 常用命令/思路 |
|---|---|---|
| 502 | 上游不可达、无监听 | curl 上游地址、netstat 查端口 |
| 504 | 上游响应慢、超时配置过短 | 查 upstream_response_time、调整 proxy_read_timeout |
| 499 | 客户端提前断开 | 查客户端网络、超时设置、后端响应时间 |
| 缓存不生效 | 未命中、缓存键不匹配 | 看 X-Cache-Status 头、对比 proxy_cache_key |
| 真实IP错误 | Header 未传递、被伪造 | 检查 X-Forwarded-For、set_real_ip_from |
| 连接被断开 | WebSocket 超时、keepalive 配置不当 | 抓包看 FIN 方向、调大 proxy_read_timeout |
这份速查表是我每次排障时的第一反应清单。比起在网上搜各种零散的文章,自己总结一张表效率高得多。建议每个团队都根据自己的业务场景建立这样的排障手册,遇到问题先按表排查,再深入分析。
写在最后的几点心得
在多个项目的架构迭代中,我越来越意识到反向代理不是可选项,而是分布式系统的基本盘。它解决的是“入口统一、后端隐藏、流量可控”这三个最基础的问题。没有这个中间层,很多问题会散落到业务代码里,每一个都够你喝一壶。
选型方面,Nginx 是使用最广的成熟方案,HAProxy 在纯负载均衡场景表现同样出色,Caddy 则以配置简单和自动 HTTPS 见长。没有“最好”的代理,只有最适合当前团队维护能力和业务场景的代理。但不管选哪种,原理一致——代理层必须稳定、透明、可观测。稳定性靠高可用架构,透明性靠正确配置,可观测性靠日志和监控指标。这三者缺一不可。
个人最大的体会是:不要等到出故障才研究反向代理。在流量还小的时候就把代理层配置规范起来,压力测试做起来,日志格式定下来。等到流量真正起来,你才有余力去专注业务本身,而不是和网关纠缠。反过来说,代理层又是最值得花时间打磨的——它是所有流量的必经之路,一次配置优化带来的收益,可能超过几十次业务代码微调。