你在浏览器里只输入了一个域名,为什么同一个网站的两次请求,最后可能落到两台完全不同的机器?更奇怪的是,其中一台机器刚好宕机,你刷新页面却什么都没感觉到。
先别急着把答案归结为“云平台很智能”。浏览器知道的通常只有 https://api.example.com,它并不知道后面有多少台应用服务器。真正站在入口处接住流量的,往往是 DNS、边缘节点、负载均衡器或反向代理。它们先决定请求该去哪个区域,再决定该进哪个服务,最后从一组健康实例中选出一台。
这里有两种很容易混淆的动作:一种是服务器告诉客户端“请换个地址再请求一次”,这叫重定向;另一种是入口组件替客户端把请求转给后端,客户端从头到尾都在访问同一个地址,这叫反向代理。负载均衡通常发生在第二种动作里,它解决的不是“把单个请求变快”,而是让许多请求能够由多台机器共同承担,并在部分机器失效时继续服务。
前面的课程已经把 HTTP 方法、状态码、缓存、Cookie、TLS 和内容协商拆开讲过。这一章要把它们重新接到一条真实的请求链路上:当中间多了代理,每一层到底看见什么、能改什么,又会引入什么代价。

假设用户请求 https://example.com/orders,入口层可以有三种处理方式。
服务器返回一个 3xx 响应,并在 Location 中给出新位置。浏览器收到后,再向新位置发起请求。因此,在开发者工具里你会看到至少两组请求与响应,新地址也可能出现在地址栏中。
GET /old-orders HTTP/1.1
Host: example.com
HTTP/1.1 308 Permanent Redirect
Location: /orders这次跳转发生在客户端。若目标换了域名,客户端还要重新做 DNS 查询、建立连接并完成 TLS 握手。重定向适合表达“资源位置变了”“提交完成后去结果页查看”这类 HTTP 语义,却不适合拿来隐藏内部服务器。
反向代理收到 /orders 后,自己选择 10.0.2.17:8080,把请求转过去,再将后端响应送回客户端。浏览器只看到它与 example.com 之间的一次交换,既不知道内网地址,也不知道下一次请求是否会换一台机器。
这里的“反向”是相对于正向代理而言的。正向代理代表客户端去访问外部服务;反向代理代表服务端集群接待客户端。它通常部署在应用服务器之前,是公网和内部服务之间的一道边界。
反向代理回答的是“谁替客户端转发”,负载均衡回答的是“这次转给哪一个后端”。两者经常由同一个软件完成,所以日常交流中常被混用,但概念上仍应分开:一个反向代理可以只转发到单个后端;一个四层负载均衡器也可以不理解 HTTP 内容。
判断眼前发生的是哪一种动作,有个很实用的方法:看客户端是否收到 3xx 并再次发起请求。收到并重发,是重定向;地址不变、入口层在内部另建连接,是代理转发。
“跳到另一个 URL”只说对了一半。假如原请求是提交订单的 POST,跳转后究竟应该继续 POST,还是改成 GET?这会直接决定请求体会不会再次发送、操作会不会重复执行。
最容易踩的坑,是把 301 和 302 理解成“必然改为 GET”。准确说法是:为了兼容早期浏览器行为,用户代理可以在原请求为 POST 时改用 GET。如果业务必须保留方法,就选 307 或 308,不要把正确性押在某个客户端的历史行为上。
303 解决的是另一类问题。用户提交表单后,服务器已经完成了写操作,随后返回:
HTTP/1.1 303 See Other
Location: /orders/8472
Cache-Control: no-store浏览器再用 GET /orders/8472 获取展示页面。此时刷新结果页只会重复读取,不会再次提交原来的 POST。这就是常说的“提交后重定向”,重点不在页面跳转效果,而在把“执行动作”和“读取结果”拆成两个资源。

旧教程常说 Location 必须写绝对 URL,这已经不是现行 HTTP 语义。它的值是一个 URI 引用,可以是完整地址,也可以是相对引用。相对引用会以当前请求的目标 URI 为基准解析。
HTTP/1.1 302 Found
Location: ../signin?next=%2Forders相对写法适合同一站点内跳转,避免把协议和主机名硬编码进配置;跨域跳转则通常需要完整地址。两种形式都要注意路径解析、查询参数编码和开放重定向问题。尤其不要把用户传入的 next 参数不加校验地塞进 Location,否则攻击者可以借你的可信域名把用户带到钓鱼站点。
现实里常见这样的链条:
http://example.com
-> https://example.com
-> https://www.example.com
-> https://www.example.com/zh-CN/每一跳都要等上一跳响应回来才能继续。若还跨了主机名,DNS、连接和 TLS 的成本也可能重新发生。搜索机器人、移动网络和 API 客户端都会为这条链付出延迟。更好的做法是让任意旧入口尽可能一步到达最终规范地址。
重定向循环则更糟。常见原因不是某一条规则明显写反,而是多层组件各自做了一个“看起来正确”的判断:边缘代理已终止 TLS,却告诉后端当前连接是 HTTP;后端于是重定向到 HTTPS;入口转发后仍让后端看到 HTTP,于是循环继续。尾斜杠规则、大小写规范化、地区路径和多个代理重复改写,也会形成类似问题。
排查时不要只盯着浏览器最后的“重定向次数过多”。把每一跳的状态码、Location、主机名、协议和生成该响应的组件列出来,循环通常就会显现。

301 和 308 可以被缓存启发式复用。浏览器、共享缓存或边缘节点一旦记住旧地址到新地址的映射,后续请求可能根本到不了你的服务器。你即使改了配置,也不一定能立刻让所有客户端忘掉旧结果。
因此,重大迁移可以先用 302 或 307 验证目标地址、方法与请求体都正确,再切换到 301 或 308。如果临时响应绝对不能被保存,就明确设置合适的 Cache-Control;如果永久映射需要可控的更新窗口,也应给出明确的新鲜度,而不是完全依赖不同缓存的猜测。
这并不是说永久重定向不该用。URL 确实永久变化时,它表达得最准确。代价只是你要把它当成一次可能传播到客户端和多层缓存的发布,而不是一条随时能撤回的普通路由规则。
反向代理通常是应用前面第一个理解 HTTP 的服务端组件。它可以按域名和路径路由、终止 TLS、限制请求大小、复用上游连接、缓冲慢客户端、记录访问日志,还能在后端响应失败时选择其他实例。
能力集中到入口,应用会简单很多;反过来,入口配置错误的影响面也更大。超时设得过短,会把正常慢请求切断;缓冲设得不合适,会增加内存或磁盘压力;路由规则重叠,会让请求进入错误服务。反向代理不是一层“透明空气”,而是请求语义和安全边界的一部分。
最常见的做法是在反向代理处终止 TLS。代理持有证书,完成握手并解密 HTTP,然后把请求转到后端。这样证书轮换、协议版本和密码套件可以统一管理,后端也不必各自承担公网握手成本。
但“入口是 HTTPS”不代表整条链路都加密。代理到后端若使用明文 HTTP,就要确认这段网络是否真的受信任。跨主机、跨可用区或处在零信任网络中时,常见做法是再次使用 TLS,甚至用双向 TLS 验证代理和服务双方身份。
另一种选择是 TLS 透传:入口只根据连接信息转发加密字节,由后端终止 TLS。它保留了端到端加密边界,却让入口难以查看 URL、请求头和状态码,也就无法完成大部分七层路由、缓存和内容级安全策略。
后端与客户端不再直接连接后,会自然丢失一些外部信息:客户端 IP 是多少、用户最初使用 HTTP 还是 HTTPS、访问的主机名是什么。代理通常用 Forwarded、X-Forwarded-For、X-Forwarded-Proto、X-Forwarded-Host 等头把这些信息传下去。
问题在于,请求到达代理之前,客户端自己也能伪造同名头。如果应用无条件相信最左边的 X-Forwarded-For,攻击者就可能绕过基于 IP 的限流或审计;如果无条件相信 X-Forwarded-Proto: https,应用可能生成错误的安全链接或跳过协议升级。
正确思路是建立可信代理链:公网入口先丢弃或覆盖来自不可信连接的转发头,再追加它亲眼看到的连接信息;内部应用只信任来自已知代理地址或已知代理跳数的值。链路里多一层 CDN、云负载均衡或服务网格,信任范围和取值方向都要随之更新。

下面的配置把 HTTP 永久升级到 HTTPS,在入口终止 TLS,再把 /api/ 请求交给两个后端。它展示的是关键关系,证书路径、DNS、超时和失败条件仍要按真实系统调整。
upstream api_backend {
least_conn;
keepalive 64;
server 10.0.2.17:8080 weight=2 max_fails=3 fail_timeout=10s;
server 10.0.2.18:8080 weight=1 max_fails=3 fail_timeout=10s;
}
server {
listen 80;
server_name api.example.com;
return 308 https://$host$request_uri;
}
这里的失败重试要格外小心。连接失败发生在请求尚未交付时,换一台后端通常合理;后端已经执行写操作、只是响应在途中断开时,自动重试 POST 可能制造两笔订单。代理能否重试,必须结合方法是否幂等、是否已经发送请求体或响应字节,以及业务有没有幂等键,不能只看“上一台返回了 502”。
“负载均衡器”并不天然等于 HTTP 代理。它可能工作在传输层,也可能工作在应用层。
一个容易被忽略的现实是:四层均衡通常按连接分配,而不是按请求分配。HTTP/2 或 gRPC 可以在一条长连接上承载大量并发请求。哪怕连接数平均,实际请求量也可能严重倾斜。七层代理看得见每个流,能做更细的均衡,但也要正确处理流式响应、WebSocket、超时和背压。
很多生产架构会把两者叠起来:外层四层组件提供稳定虚拟 IP 和高吞吐,内层七层代理负责 HTTP 路由与策略。多一层可以分工,也意味着多一层连接、监控和故障判断,不能因为架构图更“标准”就机械添加。

假设有 A、B、C 三台服务器。“依次各发一个请求”听起来很公平,但请求成本可能相差几百倍:一个请求读取缓存只要 5 毫秒,另一个要导出报表 20 秒。算法分配的是它能看见的指标,不是业务世界里抽象的“工作量”。
轮询按 A、B、C、A、B、C 的顺序选择。它不需要复杂状态,后端容量接近、请求耗时相近且数量足够多时,效果通常不错。
代价也很直接:它不知道 B 正在处理慢查询,也不知道 C 的 CPU 已经逼近上限。短时间内请求量很少时,分布也未必刚好相等。因此,轮询的优势是简单和可预测,不是对所有负载都最公平。
如果 A 的容量约是 B 的两倍,可以给 A 更高权重。权重既能配合轮询,也能配合最少连接等策略。它适合混合机型、逐步引入新版本或让备用区域只接少量流量。
权重不是 CPU 核数的机械换算。应用可能受数据库连接、内存、锁竞争或外部 API 限制。更稳妥的做法是先用压测和生产指标估计单实例可承载量,再逐步调整权重,并观察尾延迟和错误率,而不是只追求请求数比例漂亮。
最少连接把新请求交给当前活动连接较少的后端。对 WebSocket、gRPC 流、下载和其他长连接,它通常比轮询更接近真实占用情况。
不过,连接数依然不等于工作量。一条空闲 WebSocket 和一条持续推送大流量的连接都只计作一条;连接刚结束的慢服务器也可能因为计数下降而再次被选中。更复杂的实现会结合权重、活动请求或响应时间,但复杂度和反馈抖动也会增加。
按观测延迟选择看起来最聪明:谁最近响应快,就把请求给谁。它能避开明显变慢的实例,但必须处理样本窗口、偶发尖峰和“新实例没有历史数据”这些问题。
如果所有流量突然涌向当前最快的服务器,它很快又会变成最慢的。成熟实现通常会把延迟与活动请求数结合,并进行平滑处理。这里的权衡是响应更灵敏,同时也更容易被短期噪声误导。
普通哈希可以用用户 ID、租户 ID、缓存键或请求路径选择后端,让相同键大概率落到同一位置。但直接用“哈希值对服务器数量取模”有个麻烦:服务器数量一变,大量键都会换位置。
一致性哈希把键和服务器映射到同一个环形空间。加入或移除节点时,通常只有邻近区间的键需要重新映射。它很适合分布式缓存、需要局部性的工作集和按租户路由。
它并不会自动解决热点。某个超级租户的流量远高于其他租户时,即使键分布均匀,承载它的节点仍可能过载;节点太少或虚拟节点设置不合理,也会产生倾斜。哈希键应稳定、基数足够,并避免直接使用攻击者可以集中控制的值。

如果登录状态只保存在 A 的内存里,请求下一次落到 B 就会“掉登录”。最直接的补丁是会话粘性:按客户端 IP 哈希,或由入口设置 Cookie,让同一个用户尽量回到同一实例。
它确实能快速兼容有状态应用,但代价不小:
更可扩展的方向,是把会话放到共享存储,或使用可由任意实例验证的签名凭证,让应用实例尽量无状态。粘性不是绝对错误,它可以是迁移期方案,也可用于确实需要连接局部性的场景;只是要清楚,它换来的便利是更差的均衡和故障恢复能力。
一台机器进程还在,不代表它能正确服务。线程池可能耗尽,数据库连接池可能卡死,磁盘也可能只读。负载均衡器如果只检查“端口能连上”,就会继续把真实用户送进故障实例。
主动健康检查由负载均衡器定期发起,可以是 TCP 建连,也可以请求 /readyz 并检查状态码。它不用等用户先踩坑,但检查之间存在时间窗口,频率过高还会给系统制造额外压力。
被动检查观察真实请求中的连接失败、超时和特定错误。它能发现探针没有覆盖的问题,却意味着至少有真实流量先经历失败。低流量服务上,样本积累也会更慢。
两者结合时,常用连续失败阈值把节点摘除,再用连续成功阈值让它恢复。这种“降级需要几次失败、恢复需要几次成功”的滞回设计,可以避免实例在健康与不健康之间频繁抖动。
检查内容也要拿捏分寸。只返回进程存活,可能放过依赖故障;每秒深查所有下游数据库和第三方接口,又可能把健康检查本身变成放大器。通常会把“进程是否需要重启”的存活检查和“是否应该接收新流量”的就绪检查分开。依赖短暂异常时,先停止接新流量往往比立刻重启整个进程更稳妥。
假设集群有 10 台机器,每台稳定承担 8% 的容量。一台被摘除后,剩余流量还能轻松接住。可如果平时每台已经跑到 95%,任何摘除都会把压力推给其余实例,引发更多超时,再触发更多摘除,这就是典型的级联故障。
因此,健康检查必须和容量余量一起设计。系统要能承受计划内发布、单机故障,必要时还要承受整个可用区退出。具体留多少余量取决于扩容速度、流量波动、故障目标和成本,但“所有机器平时刚好跑满”绝不是高可用。
当健康实例比例低到某个程度,系统还要做一个艰难选择:继续只向少数健康实例发流量,可能把它们压垮;重新尝试部分被判不健康的实例,也许能让少量请求成功,却可能扩大故障;直接快速失败则保护上游,但用户会立刻看到错误。没有统一答案,关键是明确服务在过载时更适合“尽量做成一点”还是“尽快拒绝,保住恢复空间”。
负载均衡器发现主后端失败后,可以转给备用实例或备用区域。但对单个请求而言,“没收到响应”不等于“后端没执行”。支付、下单、发消息一类非幂等操作若被自动重试,可能产生重复结果。
安全的故障转移通常依赖几层配合:只自动重试明确幂等的请求;限制重试次数和总时间预算;让写接口接收幂等键;在开始返回响应体后不再换上游;用熔断和重试预算防止故障时请求量成倍增长。重试可以隐藏瞬时故障,也可以把一次故障放大成流量风暴,区别就在这些边界条件。

很多事故并不是机器坏了,而是“刚恢复的机器立刻被当成满血机器”。新实例虽然已经监听端口,运行时还在预热,连接池尚未建立,本地缓存也是空的。若马上按完整权重灌入流量,它会在最脆弱的时候收到最多工作。
慢启动会在一段时间内逐步提高新实例的有效权重。它让代码缓存、数据库连接和数据缓存有时间变热,也让监控有机会在满负载前发现异常。
代价是扩容容量不会瞬间全部可用。如果突发流量要求十秒内翻倍,而实例预热需要五分钟,慢启动只能让失败更温和,不能凭空创造容量。此时要结合预扩容、更快的启动路径、缓存预热或排队限流。
缩容或滚动发布时,一个实用顺序是:
如果先杀进程再摘流量,传播窗口里的新请求就会撞上连接拒绝。如果标记不就绪后立即退出,外部负载均衡器可能还没来得及完成下一次探测。优雅下线的等待时间要覆盖配置传播、健康检查间隔和最长可接受请求时间,也要设置上限,否则一条永不结束的连接会让发布永久卡住。
只看总 QPS 和平均响应时间,很难判断问题出在客户端连接、代理排队还是后端处理。入口层至少要能区分:
502、503、504 与后端原样返回的错误;指标要按区域、路由、后端池和实例拆分。整体平均值正常时,可能正有一台实例持续失败,只是被其他实例掩盖。告警也不应只盯单个瞬时值,把错误率、尾延迟、队列增长和健康实例比例放在一起,才能更接近用户真实感受。
负载均衡器自身也不能成为单点。多实例入口需要共享或一致的配置来源,并通过虚拟 IP、DNS、Anycast 或上一级负载均衡提供可达性。配置发布要有校验、版本和回滚,数据面在控制面短暂不可用时也应尽量继续使用最后一份有效配置。

当用户遍布不同地区,系统还要先决定进入哪个数据中心。常见入口包括基于 DNS 的调度、Anycast、边缘网络和 HTTP 重定向。
DNS 可以根据客户端大致位置和区域健康状态返回不同地址,部署简单、覆盖面广,但响应会被递归 DNS 和客户端缓存。某个区域故障后,旧记录在 TTL 到期前仍可能被使用。缩短 TTL 能加快变化传播,却会增加查询量,而且客户端不一定严格按你期待的时间刷新。
Anycast 让多个地点宣告同一个 IP,由网络路由把连接送到拓扑上合适的入口。它能减少 DNS 切换依赖,但“网络上最近”不等于“应用延迟最低”,路由变化还可能让长连接重新建立。边缘网络通常把这些能力与就近缓存、TLS 和七层路由组合起来。
HTTP 重定向也能把用户带到地区域名,例如从 example.com 跳到 cn.example.com。它直观且可由应用做业务判断,但会增加一次往返,并把地区选择暴露在 URL 与客户端缓存中。对于 API,重定向还必须重新考虑请求方法、凭证是否能跨域发送和目标域名证书。
地理调度还有三个经常被低估的约束:
上一章讲过语言偏好,这里还要补一句:语言不等于地理位置。Accept-Language: zh-CN 不能证明用户身在中国,IP 位置也不能替代用户主动选择的语言。内容选择、数据驻留和流量就近可以互相参考,但不该混成同一条规则。
现在再看开头的问题:用户只访问一个域名,请求却落到了不同机器,是因为稳定域名后面还有多级选择。DNS 或边缘入口先选区域,四层组件可能先选代理连接,七层反向代理再按主机名和路径选服务,负载均衡算法最后从健康实例中选一个后端。
一台机器宕机而用户没感觉,是因为健康检查或真实错误让入口暂停选择它,剩余实例又有足够容量接住流量。这里没有魔法,只有一组必须同时成立的条件:故障能被及时发现、错误请求可以安全处理、流量有地方转移、会话不被锁死在故障机器上,入口自身也不是单点。
这也解释了为什么后端架构总是在做权衡:重定向把决定交给客户端,语义清楚却增加往返;反向代理隐藏内部拓扑,却扩大入口配置的责任;会话粘性能快速兼容有状态应用,却削弱均衡和故障转移;主动健康检查更早发现问题,也可能误判或制造额外压力;跨地域就近降低网络延迟,却把数据复制和一致性问题推到台前。
学完这一章,你不必先记住某个产品的全部配置项。更重要的是遇到 301、502、流量倾斜或发布抖动时,能沿着同一条链路追问:客户端有没有重新发请求?TLS 在哪一层结束?代理信任了谁提供的地址?算法依据什么选择?后端为何被摘除?剩余容量能不能接住?把这些问题问清楚,复杂架构就不再只是一张画满方框和箭头的图。