fix: 5xx (503) and read-timeout now try next key instead of break/fail — key1 down but key6/key5 healthy should succeed; wide 180s per-key timeout only as safety net (no per-request deadline)

This commit is contained in:
hmo
2026-08-15 20:10:58 +08:00
parent 8de5b6ccb6
commit a7a42448d4
+14 -21
View File
@@ -48,11 +48,9 @@ UPSTREAM_COOLDOWN_SEC = 30 # 上游模型级故障(Router.Unavailable)短
USAGE_REFRESH_INTERVAL = 120 # 2 分钟
# 最大重试次数(所有 key 耗尽)
MAX_RETRIES = 6
# 单次上游请求超时(秒):翻译等小请求正常 2-5s,60s 绰绰有余;
# 从 180s 下调,避免"单次 180s × 多 key"叠加成长时间挂死(2026-08-15 事故)
UPSTREAM_REQUEST_TIMEOUT = 60
# 整个请求(含所有 key 重试)的总截止时间:3 key × 60s = 180s,保证 3 分钟内一定结束
REQUEST_DEADLINE_SEC = 180
# 单次上游请求保底超时(秒):上游处理大请求(1MB+)可能要 60-90s+
# 故设宽超时兜底(防 TCP 挂死无限等),正常靠错误分类快速换 key,不依赖此超时
UPSTREAM_REQUEST_TIMEOUT = 180
# ── 日志 ────────────────────────────────────────────────────
LOG_DIR = _GATEWAY_DIR / "logs"
@@ -548,12 +546,6 @@ class RouterHandler(BaseHTTPRequestHandler):
client_error = False # True=客户端请求错误(400/ModelError),应返回 4xx 而非 502
for attempt, k in enumerate(ranked_keys):
# 总超时保护:整个请求(含所有 key 重试)最迟在 REQUEST_DEADLINE_SEC 内结束,
# 防止"单次 60s × 多 key"叠加成 15 分钟挂死(2026-08-15 事故根因)
if time.time() - start > REQUEST_DEADLINE_SEC:
last_error = f"overall request deadline ({REQUEST_DEADLINE_SEC}s) exceeded after {attempt} key attempts"
log.warning("%s overall deadline exceeded after %d attempts", model_name, attempt)
break
kid = k["key_id"]
api_key = k.get("api_key", "")
if not api_key:
@@ -579,11 +571,12 @@ class RouterHandler(BaseHTTPRequestHandler):
else:
last_error = result.get("error", "unknown")
log.warning("%s via %s FAILED: %s", model_name, kid, last_error)
# 错误分类:只有真正的 key 级错误才冷却 key
# - 400 客户端错误:不重试、不冷却(换 key 无意义),直接返回给调用方
# - 401/403 认证失败:key 级故障 → 冷却 300s(但 ModelError=客户端传了不存在的模型,不冷却)
# 错误分类:只有 key 本身的问题才冷却;上游瞬时问题换 key 重试
# - 400/ModelError:客户端错误 → 直接返回,换 key 无意义
# - 401/403 认证失败:key 级故障 → 冷却 300s
# - 402/429 配额/限流:key 级 → 冷却 60s
# - 5xx/Router.Unavailable:上游服务问题 → 不冷却 key(误杀元凶),立即失败
# - 5xx(含 503:上游服务瞬时问题 → 不冷却,换下一个 key 重试
# - read timeout:单 key 挂起 → 不冷却,换下一个 key(保底超时兜底)
status = result.get("status", 0)
if status == -1:
# 客户端已断开:直接终止,不重试不冷却
@@ -608,15 +601,15 @@ class RouterHandler(BaseHTTPRequestHandler):
elif status in (402, 429):
mark_key_failed(kid, last_error, cooldown=60)
elif status >= 500:
# 上游服务端错误(Internal server error / Router.Unavailable):
# 不杀 key。立即失败返回,避免"全部 key 被误杀进冷却"的雪崩
log.warning("upstream %d (service-level) via %s — NOT cooling key, failing request", status, kid)
success = False
break
# 上游服务端错误(503/500/Internal/Router.Unavailable):
# 只是当前 key 的上游实例可能有问题,换下一个 key 重试(不冷却)
# 之前直接 break 导致"key1 挂了但 key6/key5 正常"时浪费了可用 key。
log.warning("upstream %d via %s (service-level) — trying next key", status, kid)
# status == 0URLError/read timeout 等)→ 继续循环换下一个 key,不冷却
except Exception as e:
last_error = str(e)
log.warning("%s via %s exception: %s", model_name, kid, e)
mark_key_failed(kid, last_error)
# 异常(如 read timeout / URLError):不冷却,换下一个 key 重试
if not success:
elapsed = time.time() - start