请求在哪一跳分流,响应就走哪条路吗
DNS 改了记录,客户端还在连接旧地址;入口改成 TCP 代理,后端看到的仍是代理地址。选后端的位置,会改变故障切换和身份传递的边界。
讨论负载均衡时,先画出请求和响应的双向路径,再讨论轮询或最少连接。客户端重新请求、解析地址、代理转发和内核转发,涉及不同的协议参与者。
本文用 Nginx 1.30.0、HAProxy 3.4.2、CoreDNS 1.14.7 和 CPython 3.14.8,在无宿主机端口映射的 Docker 网络里验证可见行为。NAT、直接路由、跨机器带宽和真实故障切换只作示意,没有运行 LVS 或网络性能测试。
一、把「选择」和「返回」分别画出来
DNS 返回的是供客户端使用的地址,后续应用响应通常不经过 DNS 服务。HTTP 重定向则要求客户端处理另一个 URL。反向代理和 TCP 代理会建立通向后端的连接,响应仍需经过代理。
| 方式 | 后端选择发生处 | 响应经过分流组件吗 | 主要约束 |
|---|---|---|---|
| HTTP 重定向 | 收到 HTTP 请求后返回新位置 | 首次重定向响应经过;第二次请求的业务响应可直回客户端 | 客户端跟随策略、方法语义、目标证书与鉴权 |
| DNS | 地址解析阶段 | 业务响应通常不经过 DNS | 缓存、解析器行为、已有连接 |
| 七层反向代理 | 理解 HTTP 后选 upstream | 通常经过 | HTTP 语义、缓冲、重试、可信转发头 |
| TCP 代理 | 建立连接时选后端 | 经过 | 连接级分配、代理连接的源地址 |
| 内核 NAT 转发 | 按网络与连接规则选目的地 | 常见 NAT 拓扑要求回程匹配转换路径 | 路由、连接跟踪与地址转换规则 |
| 直接路由(DR) | 入口转发到选中的后端 | 设计上业务响应可由后端直回 | VIP、ARP、二层可达性与回程路由 |
后两行是拓扑示意,不是对任意四层产品的默认行为承诺。TCP 代理也不能代表所有四层转发:全代理、NAT 与 DR 的连接和源地址处理各不相同。
连接复用会延长某次选择的影响。DNS 更新之后,客户端仍可能复用原来的连接;连接级代理也未必对同一连接里的每个 HTTP 请求重新选后端。集群内的具体路径可对照一次请求在集群里的经过。
二、302 不适合承担「原样重放 POST」的保证
实验入口把 POST 重定向到同源的另一路径,输入只是 demo=1。使用 CPython 3.14.8 的 urllib.request 自动跟随后:
302:目标收到 GET,请求体为空
307:客户端没有自动跟随 POST,向调用者返回 307第二行值得单独看:307 的协议语义要求自动重定向时保持方法,但不强迫每个客户端替调用者重放请求。应用仍需处理库的限制、请求体能否重放、跨目标鉴权和重定向次数。
RFC 9110允许客户端在跟随 302 时把 POST 改为 GET;需要保留方法的临时重定向应考虑 307,永久场景对应 308。这里的具体自动跟随结果只代表该 Python 客户端,没有冒充浏览器实验。重定向到另一个域名时,还要检查 Cookie、认证头和 HTTPS 证书;不要把原请求的全部凭据直接交给新地址。
三、DNS TTL 没有检查后端是否健康
实验由一个本地权威应答器返回 A 记录和 5 秒 TTL,CoreDNS 只负责转发与缓存,成功缓存的上限设为 5 秒、最小 TTL 设为 0。先查询缓存,再修改权威答案:
| 查询时间与对象 | 地址 |
|---|---|
| 修改前,查询缓存 | 203.0.113.10 |
| 修改后,直接查询权威端 | 203.0.113.20 |
| 修改后立即查询缓存 | 203.0.113.10 |
| 缓存过期后重新查询 | 203.0.113.20 |
地址属于文档示例网段,实验没有连接它们。这验证的是旧答案在 TTL 内仍可被缓存返回,没有模拟地址对应节点宕机。CoreDNS 的缓存配置见插件文档。
健康检查修改了权威记录,不会主动清掉所有缓存。降低 TTL 也要等旧 TTL 结束,且应用自己的缓存和连接池还会增加下一层影响。没有新的解析请求,就没有获取新地址的机会。DNS 与 Java 客户端缓存进一步区分了这些层次。
四、后端看到的地址由连接和信任链决定
同一个实验客户端依次直连后端、经过 Nginx、经过 HAProxy TCP 模式。后端分别记录 socket 的对端地址和 X-Forwarded-For:
| 路径 | socket 对端 | X-Forwarded-For |
|---|---|---|
| 直连 | 客户端容器 | 无 |
| Nginx HTTP 代理 | Nginx 容器 | 实验入口写入的客户端容器地址 |
| HAProxy TCP 代理 | HAProxy 容器 | 无 |
Nginx 配置在本实验只有一个可信入口:
proxy_set_header X-Forwarded-For $remote_addr;这行代码覆盖客户端提交的同名头,只传递该入口实际看到的对端地址。真实的多层代理需要明确哪些上游可信、如何解析地址链;从任意请求头直接读取「客户端 IP」不能作为身份或权限依据。
HAProxy 在实验中按 TCP 全代理转发字节,没有解析或补充 HTTP 头,也没有启用透明代理。若用 PROXY protocol 传递连接信息,两端必须同时配置并限制可信发送者;不能只打开一端。本文没有验证这种配置。这里的差异来自代理模式,与选择轮询还是最少连接无关;选择策略的故障边界另见慢实例与快速失败实例。
五、代理允许重试,会放大哪种不确定性
实验让首选后端完整读取 POST 的请求体后返回 503,备用后端返回 200。两个路径都配置了 http_503 和最多两次尝试,只有第二个增加 non_idempotent:
proxy_next_upstream http_503 non_idempotent;
proxy_next_upstream_tries 2;结果为:
| 配置 | 首选后端收到 POST | 备用后端收到 POST | 客户端结果 |
|---|---|---|---|
没有 non_idempotent | 1 次 | 0 次 | 503 |
有 non_idempotent | 1 次 | 1 次,包含相同请求体 | 200 |
这是对实验配置的验证,没有写入订单或扣款。首选后端已经接收请求,503 并不能证明业务没有发生;启用重试需要业务级幂等或可查询的执行结果。Nginx 文档还要求能向客户端继续返回结果,已经发出响应的中途错误不能靠换 upstream 修复;能否重放请求体也受缓冲与转发进度影响。proxy_next_upstream 文档列出了这些条件。
确定入口方案时,验收对象应包括客户端实际使用的重定向库、DNS 缓存、连接池、请求体重放和来源地址链。重试预算与幂等约束见超时预算与重试放大,不能仅以「最后返回 200」判断请求只执行过一次。
配套实验
- codesphere-labs/distributed/traffic-response-paths:Nginx 1.30.0、HAProxy 3.4.2、CoreDNS 1.14.7、CPython 3.14.8:本机容器网络中的重定向、DNS TTL、两种代理的对端地址和 POST 重试(验证记录)
参考资料