移除一次隐藏请求:让多区域 AWS API 真正具备全局故障转移能力

2026-07-13 35 预计阅读时间: 1 分钟
来源: infoq.com AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:10 分钟

一套 API 即使部署在多个 AWS 区域,也不等于已经具备全局故障转移能力。连续发生的区域故障迫使团队重新检查请求链路,最终发现阻碍并不在主业务 API,而是藏在客户端会话初始化阶段:每次会话都会先发起一次预检发现请求,再根据返回结果访问真正的 API。

这个调用源于多年前的技术限制,当时它可能是唯一可行的接入方式。多年后,服务端已经演进,客户端却继续携带这段历史逻辑。结果是,业务请求尚未发出,客户端就可能被某个区域的发现服务卡住。

多区域部署为何仍会被单点请求拖住

旧客户端的逻辑通常可以抽象为:

Client
  |
  | 1. GET /discover
  v
Discovery service in Region A
  |
  | 2. Return regional API endpoint
  v
API in Region A or Region B

表面上,第二步返回的地址可以指向不同区域,似乎已经支持多区域。但真正决定会话能否建立的,是第一步发现请求。只要它固定依赖某个区域,系统就仍然存在一个隐藏的区域级单点。

这类设计还会引入几个容易被低估的问题:

  • 每个新会话至少多一次网络往返,增加连接建立时间。
  • 发现接口故障时,健康的业务区域也无法接收新会话。
  • 客户端重试可能放大发现服务的流量,形成重试风暴。
  • 故障转移演练如果只测试业务 API,往往无法暴露这条前置依赖。
  • DNS、身份认证、令牌签发或配置读取如果绑定原区域,移除发现调用后仍可能留下其他隐性耦合。

因此,问题不只是“减少一次 HTTP 请求”,而是重新定义客户端如何找到服务,以及故障时由哪一层做路由决策。

把区域选择移出客户端会话

更稳妥的目标是让客户端直接访问一个稳定的全局入口,由入口层把请求送往健康区域:

Client
  |
  | Single API request
  v
Global API endpoint
  |-------------------|
  v                   v
Region A API       Region B API

全局入口可以通过团队现有的 AWS 网络与路由能力实现,但具体选择需要结合协议、连接持续时间和恢复目标。短连接 HTTP API、长连接、私有网络流量以及要求固定源地址的系统,适用方案并不相同。

迁移时还要明确几个契约:

  1. 客户端是否允许显式覆盖入口地址。
  2. 认证令牌能否跨区域验证。
  3. 请求是否幂等,失败后能否切换区域重试。
  4. 各区域的数据是否已经复制到足以承接流量的程度。
  5. DNS 缓存和连接池会把旧区域保留多久。

如果这些问题没有答案,删除发现调用只会把故障点推到链路的下一层。

可以这样实践:显式端点与兼容回退

下面是一个可直接运行的 Python 示例。它使用标准库模拟两种客户端路径:新客户端优先读取稳定入口 API_ENDPOINT;只有在迁移期明确启用兼容模式时,才调用旧发现接口。

API_ENDPOINT 改成实际的全局 API 地址。迁移完成后,应删除 ALLOW_LEGACY_DISCOVERY 分支,而不是永久保留双路径。

#!/usr/bin/env python3
import json
import os
import sys
import urllib.request


def get_json(url: str, timeout: float = 3.0) -> dict:
    request = urllib.request.Request(
        url,
        headers={"Accept": "application/json"},
        method="GET",
    )
    with urllib.request.urlopen(request, timeout=timeout) as response:
        return json.load(response)


def resolve_endpoint() -> str:
    endpoint = os.getenv("API_ENDPOINT")
    if endpoint:
        return endpoint.rstrip("/")

    if os.getenv("ALLOW_LEGACY_DISCOVERY") == "1":
        discovery_url = os.environ["DISCOVERY_URL"]
        result = get_json(discovery_url)
        return result["api_endpoint"].rstrip("/")

    raise RuntimeError(
        "API_ENDPOINT is required; legacy discovery is disabled"
    )


def main() -> int:
    endpoint = resolve_endpoint()
    result = get_json(f"{endpoint}/health")
    print(json.dumps(result, indent=2))
    return 0


if __name__ == "__main__":
    try:
        raise SystemExit(main())
    except Exception as exc:
        print(f"request failed: {exc}", file=sys.stderr)
        raise SystemExit(1)

运行方式:

export API_ENDPOINT="https://api.example.com"
python3 client.py

迁移期间,旧环境可以临时使用兼容模式:

unset API_ENDPOINT
export ALLOW_LEGACY_DISCOVERY=1
export DISCOVERY_URL="https://discovery.example.com/discover"
python3 client.py

除了验证功能,还应分别测量旧路径和新路径。下面的命令会输出 DNS、TCP、TLS、首字节和总耗时,可用于确认隐藏往返是否真的消失:

curl --silent --show-error --output /dev/null \
  --write-out 'dns=%{time_namelookup}s connect=%{time_connect}s tls=%{time_appconnect}s ttfb=%{time_starttransfer}s total=%{time_total}s\n' \
  https://api.example.com/health

生产监控中还应按客户端版本、入口地址、目标区域和失败阶段拆分指标。只看 API 的整体成功率,很容易把“发现失败”误判成客户端没有发起业务请求。

真正昂贵的是发布过程

移除一行发现调用并不代表迁移完成。成本主要来自兼容性、可观测性和逐步放量:旧客户端可能长期存在;不同版本可能采用不同缓存策略;移动端或嵌入式客户端也未必能立即升级。

一个风险较低的发布顺序可以是:

  1. 为客户端增加显式全局端点,同时保留受开关控制的旧路径。
  2. 记录两条路径的调用量、延迟、错误类型和客户端版本。
  3. 让内部环境和少量流量直接访问全局入口。
  4. 扩大比例,并进行区域隔离、DNS 异常和连接复用测试。
  5. 将旧发现路径关闭,但保留短期告警和紧急回退能力。
  6. 确认旧版本占比降到可接受水平后,删除发现服务和兼容代码。

双路径阶段本身也有风险。若回退策略过于积极,客户端可能在全局入口短暂变慢时重新打向旧区域,反而恢复已经准备移除的依赖。因此,回退必须有明确期限、指标和退出条件。

上线前检查清单

多区域架构需要按完整请求链路验收,而不是按资源清单验收。上线前至少检查:

  • 新会话是否能在发现服务不可用时正常建立。
  • 全局入口是否只把流量发往真正可用且数据就绪的区域。
  • 身份认证、密钥、配置和依赖服务是否支持跨区域处理。
  • 客户端重试是否有次数上限、退避和抖动。
  • 非幂等请求是否携带幂等键,避免切换区域后重复执行。
  • 长连接、DNS 缓存和连接池是否会延迟故障转移。
  • 仪表盘能否区分入口故障、区域故障和客户端版本问题。
  • 旧发现接口是否有明确的退役日期和负责人。

这次改造揭示的核心教训是:多区域能力取决于请求抵达业务服务之前经过的每一个同步依赖。一个多年前合理的预检调用,也可能在今天成为全局恢复链路上最脆弱的一环。


相关推荐