反向代理与负载均衡

本页目录

反向代理代表服务接收请求,再向后端发起交换;负载均衡决定把连接或请求分配给哪个可用目标。两者常在一个组件中实现,但不是同一概念:只有一个后端也可以需要反向代理,一台四层均衡器也可以不解析HTTP。

以 https://api.example.com/orders 为例,用户请求可能先经过边缘入口,再到区域代理和订单实例。每一跳都有独立的连接、超时和身份边界;客户端看到的一个502,可能对应后端已提交、只是响应途中失败。

想解决的问题应先明确
按 Host 或路径路由哪一层能解密和解析 HTTP
后端看到错误域名或协议原始 Host、上游TLS名称和转发元信息
实例请求数很均匀却负载不均分配单位和每次请求成本
续流卡住或 SSE 成批出现缓冲、刷新和空闲超时
自动重试使故障加重尝试预算、并发和幂等契约
下线后仍有旧流量连接排空、池化及长连接状态

两条连接,三个容易混淆的身份

一条业务请求,通常跨越两条独立的传输连接 客户端 HTTPS / HTTP/2 或 HTTP/3 反向代理 终止、路由、池化、策略 后端实例 HTTP/1.1 或其他上游协议 下游身份、上游身份、业务身份分别核验 前端协商 h3,不代表代理到后端也使用 h3 L4 透传、NAT、直接路由等架构另有连接边界;本图表示终止 HTTP 的七层反向代理。

七层代理通常终止下游TLS,并建立独立上游连接。客户端验证代理提供的公开服务证书,代理若用HTTPS访问后端,还需验证后端身份;后端看到的TLS客户端可能是代理,而不是最终用户。

还要分别记录:客户端原始请求的authority,代理连接上游时的TLS ServerName,以及应用用于生成绝对URL的外部服务名。它们可能相同,也可能不同。把Host一律改成127.0.0.1:3000会让某些应用生成错误跳转;保留外部Host又可能不符合托管上游的虚拟主机要求。

正向代理代表客户端选择目标,反向代理代表服务提供入口;“能隐藏地址”是可能结果,不是定义。代理也可能主动传递原始地址,所以不能把两者简单定义为谁被匿名化。

L4 / L7:选择单位比速度标签更重要

方式通常按什么分配可见信息与局限
L4连接转发新建TCP连接等通常不理解HTTP路径;已有连接固定到目标
TLS透传并读取可见握手信息连接及适用的SNI等不解密HTTP;ECH等会改变可见信息
L7 HTTP代理请求、流及实现的池化策略可按方法、Host、路径或认证结果路由

HTTP/2一条连接内可有很多请求。L4把这条连接选给一个后端,不代表其中每个请求会分别均衡;L7则可以按请求选择上游,但仍受连接池、流并发及亲和策略约束。

最少连接不是L7专属,例如NGINX stream upstream就支持least_conn。反过来,连接少也不一定业务负载低:一个连接可能承载大量HTTP/2流或一个昂贵长任务。NGINX stream upstream

L4不解析HTTP通常减少部分处理工作,但不能脱离TLS、拷贝、缓存命中和硬件路径,宣布所有L4部署都比L7快。WebSocket也不要求只能走L4;支持握手和隧道转发的HTTP代理同样可以承载它。

均衡算法:先问均衡的是次数、连接还是成本

算法适用理解典型偏差
轮询 / 加权轮询调度选择次数按序或权重分配请求成本不同,CPU与字节未必同比例
最少连接 / 最少活动请求选择观测负载较小的目标指标口径、长短任务与多路复用影响效果
随机 / 两次随机选择降低集中决策成本结果受样本和本地视图影响
IP或会话亲和同键尽量落在同一目标NAT大客户热点、地址变化和故障迁移
一致性哈希 / HRW成员变化时减少键迁移不保证每次恰好迁移1/N,也不消除热点

例如A分到100个2ms请求,B分到100个200ms请求,调度次数完全均匀,串行服务工作量却相差100倍。权重3:1也只在其具体选择模型下约束选择份额,不保证三倍字节或三倍CPU使用。

若健康实例平均处理能力合计为1000请求/秒,而入口持续到达1200,换一种均衡算法无法消除持续增长的队列。需要排队上限、背压、限流或扩容,且给重试留出预算。NGINX HTTP upstream

客户端地址与转发头:可信来源先于字段值

Forwarded、X-Forwarded-For、X-Forwarded-Proto等可以表达上一跳观察到的信息,但外部客户端也能构造同名字段。后端不能无条件取最左边地址用于审计、限流或访问控制。

应定义哪些直接连接来源是受信代理,以及每层清理、追加、覆盖的规则。多层链可从已知可信末端向前解析,遇到不可信边界停止;私有网段并不天然等于受控代理。Forwarded本身也不提供密码学签名。RFC 7239

PROXY protocol是在连接前部传递元信息的另一种机制,不是HTTP头,也不会自动认证发送者;必须在限定入口上由双方匹配启用。把它发到期待普通TLS或HTTP的监听器,会导致解析失败。

最小 Caddy 示例:保留默认转发规则

以下是供离线理解的配置:在本机8080入口按路径转给两个本地实例,保留/api/前缀。没有公网证书申请,也不依赖真实域名。

http://127.0.0.1:8080 {
    handle /api/* {
        reverse_proxy 127.0.0.1:9001 127.0.0.1:9002 {
            lb_policy round_robin
            health_uri /readyz
            health_interval 10s
            health_timeout 2s
            health_fails 2
            health_passes 2
            transport http {
                dial_timeout 2s
                response_header_timeout 5s
            }
        }
    }
    handle {
        respond "not found" 404
    }
}

handle不剥离匹配前缀;若改为handle_path,需确认后端期望的URI随之变化。/api/*也不自动包含恰好/api的路径,入口契约应明确。

Caddy默认处理X-Forwarded-For/Proto/Host,并忽略不可信入口传入的相应值;需要额外配置的是可信代理边界,而非无条件覆盖所有转发字段。这里使用HTTP上游;HTTPS上游的Host行为有版本差异,Caddy 2.11.0起可自动匹配上游Host,旧版本应按实际名称显式配置。Caddy reverse_proxy

本示例已用Caddy 2.10.2离线适配检查;配置语法通过不代表后端/readyz实现正确,也不代表已做网络流量验证。自动HTTPS、TLS信任与生产暴露地址应结合部署单独配置。

超时与重试:用同一条业务 deadline 约束所有尝试

一次请求的时间预算不能在每次重试时重新从零计时 已耗时 140 ms 入口排队与处理 首次尝试 220 ms 建连 / 等待 / 失败 剩余最多 390 ms 850−140−220−100 总预算 850 ms;预留响应返回 100 ms 再次尝试还需满足方法、请求体和执行状态条件 数字为教学预算;连接、首部、空闲读取和端到端 deadline 不是同一种超时。

建连超时限制连接建立,响应首部超时限制等待首部,读取空闲超时限制两次进展间隔,端到端deadline限制业务总耗时。它们不能互相替代。例如NGINX proxy_read_timeout约束连续读取之间的等待,不是整个响应最长只能持续这么久。NGINX proxy timeout

第一次尝试已耗220ms后,第二次不能再把850ms完整预算重置一次。还要考虑代理排队、返回响应和客户端剩余等待。超时后后端任务是否取消,要看取消信号是否继续传到应用及下游。

可重试需同时满足:请求体可重放,失败阶段允许,业务执行不确定性可接受,剩余预算足够。向客户端已发送部分响应后,通常也无法透明换一个后端重演整个响应。POST可能依赖明确的幂等键;“收到502”不自动证明可安全重复执行。

budget, elapsed, first_attempt, reserve = 850, 140, 220, 100
remaining = max(0, budget - elapsed - first_attempt - reserve)
assert remaining == 390
# 教学模型:每次尝试独立失败概率 p,最多尝试三次。
p = 0.2
expected_attempts = 1 + p + p**2
assert abs(expected_attempts - 1.24) < 1e-12
assert abs(1000 * expected_attempts - 1240) < 1e-9
# 三层各最多三次,最坏可放大到27次下游尝试。
assert 3**3 == 27
print('剩余毫秒 / 每秒期望尝试 / 嵌套最坏尝试:', remaining, 1000 * expected_attempts, 3**3)

上面的独立失败只是教学假设;真实故障常相关,重试可能进一步压垮依赖。限制尝试数、总时长与额外重试流量,并使用适当退避和抖动,才能避免层层重试放大。

每次重试重新给满超时,会多等多久?

时间条共用同一个起点和截止线,首次失败不会使它们平移。这里的“可用预算”只回答还来不来得及,不回答能不能安全重复业务操作;这两项判断都通过才应重试。

正在呈现知识画面
每次重试重新给满超时,会多等多久?

重试预算是总deadline的余额,不是重新开始一个完整时窗。余额为零时,再多的重试次数配置也不会产生时间。

缓冲、流式与背压

响应缓冲让代理更快读完上游、暂存内容后慢慢发给客户端,减少上游被慢读者长期占用;代价是内存/磁盘预算和可能的首字节或分块延迟。请求缓冲则影响慢上传如何占用后端,以及请求体能否在失败后重放。

SSE、模型流式输出与大下载要按交付需求配置刷新。仅在应用调用flush不保证每层代理和压缩器都立即输出;关闭所有缓冲也不是免费优化,慢客户端可能把背压传回上游并长期占用资源。NGINX proxy buffering

连接池复用减少握手,但要协调上游空闲连接寿命。后端已经关闭而代理尚未察觉的池连接,会造成复用时失败;能否重试仍回到请求语义和发送阶段,不能无限重试掩盖池配置问题。

健康检查与发布排空

主动检查定期发探针,被动检查观察真实请求失败;两者回答的问题不同。/readyz返回200可以表示此实例准备接收新工作,但不应无条件把所有共享依赖瞬时抖动扩散成整个实例池同时摘除。

检测周期、失败阈值和探针超时共同决定发现延迟。每10秒探测、连续两次失败才摘除,并不意味着故障发生后恰好20秒切走,还受发生相位、探针耗时及调度影响。恢复时可逐步增加流量,并预热缓存或连接。

下线先停止接收新工作,再等待已接收请求结束,最后在截止时间关闭残余连接。HTTP/2和HTTP/3可使用相应GOAWAY,WebSocket/SSE等长连接则需明确最长排空时间和客户端重连行为。只把实例从发现列表删除,不能让所有已有连接立即消失。

蓝绿或按权重灰度还需看会话亲和、请求成本和样本分布。1%的请求不一定覆盖1%的用户,也可能遗漏大租户或特定路径;指标应同时按版本、路由和错误类型观察。

观测:让一次失败能对应到具体尝试

访问日志至少应能关联请求标识、路由、所选上游、尝试次数、状态与关键耗时。把客户端总延迟拆成排队、建连、等待首部和响应传输,可区分“上游没回头”“客户端读得慢”和“代理池等待”。

缓存、限流和授权也要标明由哪一层执行;若缓存命中绕过后端,后端日志没有请求是正常现象。不要把用户令牌、原始证书私钥或敏感内容混入调试日志;诊断应依赖结构化结果及必要标识。

继续查阅