两个 API 中转站同时变慢或报错?上游依赖与故障归因排查
两个入口同时出现 5xx 或超时,不一定是本地网络故障。按 DNS、TLS、HTTP 和业务终态分层,比较响应指纹与 request ID,用低成本探针判断共享上游、单站故障还是客户端问题,避免重复重试和扣费。
两个 API 中转站同时变慢或报错,并不一定说明本地网络坏了。两个服务商可能共用同一个上游模型、出口网关、DNS 服务或区域线路;也可能只是客户端把一次失败重复放大。要做出可复核的判断,不能只看“都失败了”,而要把 DNS、TLS、HTTP、路由和业务终态分开记录。
先定义“同时故障”
先给每次尝试生成一个本地 probe_id,并记录开始时间、目标主机、模型名、是否使用流式、客户端版本和超时预算。一次用户点击可能产生多次网络尝试,所以不要把“最终成功”直接当成服务稳定。
把故障分成三层:
- 本地或入口层:DNS 解析失败、TCP 无法建立、TLS 主机名不匹配、代理连接超时。
- 中转层:HTTP 401/403/404、429、网关 5xx、响应头中的服务商 request ID 缺失或格式变化。
- 上游与业务层:两个入口都返回相似的 5xx/超时,或者 HTTP 200 但模型路由、工具调用或异步任务终态异常。
HTTP 状态码只描述服务器对当前请求的处理结果,不能单独证明哪一层发生故障。可将状态码语义与 RFC 9110 的状态码定义 对照;例如 429 表示当前资源受到限流,503 通常表示服务暂时无法处理请求,但还需要结合响应头和时间线。
用无凭证探针先隔离本地问题
不要为了“测试一下”反复发送真实提示词。先对两个入口分别做 DNS、TCP/TLS 和一个不会产生模型费用的请求。服务商若没有公开健康端点,就只发一个明确不会执行模型的 OPTIONS 或错误路径请求,并把它标为诊断流量;不要猜测某个路径一定免费。
在同一台机器、同一代理设置下,保存以下结果:
| 阶段 | 记录 | 可能结论 |
|---|---|---|
| DNS | 地址、解析耗时、IPv4/IPv6 | 只有一个入口解析失败,优先查域名或本地解析;两者一起失败,查网络或公共 DNS |
| TCP/TLS | 建连耗时、证书主机名、协议版本 | 证书或握手失败发生在 HTTP 之前,不能归因于模型上游 |
| HTTP | 状态码、响应头、首字节耗时 | 两个入口状态和时间相似,才有共享依赖的线索 |
| 业务 | request ID、模型路由、终态 | 200 也可能只是请求被受理,不能当成生成完成 |
Node.js 的 dns 和 tls 文档分别说明了解析与安全连接的可观察事件;在诊断脚本中只打印耗时、错误代码和证书主机名,不打印 Authorization。PowerShell、curl 或 Python 也可以采集同样字段,关键是两个入口使用同一组条件。
比较响应“指纹”,不要只比较错误文字
给每个响应保存一个脱敏指纹:状态码、content-type、retry-after、服务商 request ID 的格式、错误对象的顶层键、首字节耗时区间和连接是否在正文前断开。错误正文可能包含订单号、内部地址或用户输入,保存前应截断并脱敏。
如果 A、B 两个入口在相同时间段返回同样的状态码和错误键,但 request ID 前缀、Server 头或 TLS 证书不同,说明它们可能在不同入口共享更上游的依赖。若只有 A 的证书、DNS 或 429 发生变化,优先判定为 A 自身问题。若两者都能建立连接,却只有某个模型失败,则比较模型路由、分组权限和上游能力,而不是立即更换网络。
记录四个时间点:请求发出、收到首字节、收到终态、账单明细出现。终态缺失时不要把超时当成“没有扣费”;等账单或服务商请求查询结果确认后,再决定是否重试。有关 202 异步任务和终态查询,可参考 API 返回 202 的轮询排查;重试统计的口径见 API 稳定性测试怎么统计。
三个最小对照实验
实验 A:同一入口换模型
固定入口、Key、代理和短提示,只替换一个已确认有权限的模型。若所有模型都失败,入口或账户层更可疑;若只有一个模型失败,先查模型名称、分组和上游映射。不要因为列表里出现模型名就认为调用权限一定存在。
实验 B:同一模型换入口
固定模型、请求体和超时预算,先对 A、B 各做一次低成本请求,再等待一个相同的观察窗口。若 A 成功、B 在 TLS 前失败,是入口问题;若两者都能建立连接并在业务层同时返回相似 503,才把“共享上游或区域线路”列为假设。一次对照不足以证明共享依赖,至少要在两个时间段重复,并保留原始时间线。
实验 C:同一入口换网络
在不改变 Key 和请求体的前提下,用另一条合法网络或受控代理做一次探针。若只有某个网络失败,先查代理、NO_PROXY、IPv6 和出口策略。不要同时更换入口、模型和网络,否则没有可归因的单变量。
用离线样例计算相关性
下面的脚本不访问真实 API,只对已经保存的探针记录计算相似错误窗口。它把五分钟划为一个窗口,要求两个入口都至少有一次记录,避免把缺测误算成一致失败。
const samples = [
{ at: '2026-11-13T01:00:04Z', provider: 'A', phase: 'http', status: 503, errorKey: 'upstream_unavailable' },
{ at: '2026-11-13T01:01:12Z', provider: 'B', phase: 'http', status: 503, errorKey: 'upstream_unavailable' },
{ at: '2026-11-13T01:08:10Z', provider: 'A', phase: 'tls', status: null, errorKey: 'handshake_timeout' }
]
function windowKey(iso) {
const ms = Date.parse(iso)
if (!Number.isFinite(ms)) throw new Error('invalid timestamp')
return Math.floor(ms / 300_000)
}
function sharedWindows(rows) {
const buckets = new Map()
for (const row of rows) {
if (!['A', 'B'].includes(row.provider)) continue
const key = windowKey(row.at)
const list = buckets.get(key) ?? []
list.push(row)
buckets.set(key, list)
}
return [...buckets].flatMap(([window, list]) => {
const byProvider = new Map(list.map(row => [row.provider, row]))
const a = byProvider.get('A')
const b = byProvider.get('B')
if (!a || !b) return []
const sameFingerprint = a.phase === b.phase && a.status === b.status && a.errorKey === b.errorKey
return [{ window, sameFingerprint, providers: { A: a, B: b } }]
})
}
console.log(sharedWindows(samples))
脚本只表示“同一时间窗出现相同指纹”,不表示两个服务商确实共用某个上游。生产记录还应保存采样数量、缺测窗口和网络条件;不要用三条样本写成可用率结论。运行前可把 samples 替换成脱敏后的本地 JSON,避免任何真实付费请求。
根据证据决定切换还是等待
可以按下面的顺序处理:
- DNS、TLS 或代理层失败:修正本机解析、证书信任或代理设置,暂不重复充值,也不要关闭证书验证。
- 只有一个入口在 HTTP 层失败:暂时切到已验证的备用线路,并给该入口保留 request ID、时间和状态码。
- 两个入口在业务层同时失败:把共享上游或区域线路作为待确认假设,降低重试频率,等待服务商公告或状态查询;不要把相似错误当成某个品牌的真假结论。
- 两个入口都返回成功但结果不一致:比较模型路由、参数、流式终止和账单,而不是只看 HTTP 200。可参考 中转站服务条款与退出条件核验 保存服务商应答和迁移证据。
切换前先把当前请求标记为“结果未知”,为每个任务设置幂等键或客户端去重键。对于已经收到终态的请求不要盲目重放;对于流式中断的请求,先查询账单或任务状态,再决定是否重新提交。这样既能减少重复扣费,也能让“两个服务商一起坏了”的判断留下可以复查的依据。