From 85ef30f89696bd2c50dbda51b9edba89cfed4949 Mon Sep 17 00:00:00 2001 From: mohe Date: Fri, 14 Aug 2026 10:08:51 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20ocg=5Frouter=20key=E9=99=8D=E7=BA=A7?= =?UTF-8?q?=E6=94=B9=E6=8C=87=E6=95=B0=E9=80=80=E9=81=BF=E2=80=94=E2=80=94?= =?UTF-8?q?=E6=8C=89key=E7=8B=AC=E7=AB=8B=E7=BB=9F=E8=AE=A1=E5=A4=B1?= =?UTF-8?q?=E8=B4=A5=E6=AC=A1=E6=95=B0,=E5=A4=B1=E8=B4=A5=E5=86=B7?= =?UTF-8?q?=E5=8D=B4base2s=C3=972^count=E6=8C=87=E6=95=B0=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0(=E4=B8=8A=E9=99=90600s/10=E5=88=86=E9=92=9F),?= =?UTF-8?q?=E6=88=90=E5=8A=9F=E5=90=8E=E9=87=8D=E7=BD=AE=E8=AE=A1=E6=95=B0?= =?UTF-8?q?;=20=E7=A9=BA=E8=BE=93=E5=87=BA/=E6=8A=A5=E9=94=99=E7=AB=8B?= =?UTF-8?q?=E5=8D=B3=E6=8D=A2key+=E8=AF=A5key=E8=BF=9B=E5=85=A5=E6=8C=87?= =?UTF-8?q?=E6=95=B0=E9=80=80=E9=81=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gateway/scripts/ocg_router.py | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/gateway/scripts/ocg_router.py b/gateway/scripts/ocg_router.py index 29a5e50..dbea9e0 100644 --- a/gateway/scripts/ocg_router.py +++ b/gateway/scripts/ocg_router.py @@ -67,6 +67,7 @@ _keys: list[dict] = [] # [{key_id, label, api_key, workspace_id}] _key_usage: dict[str, dict] = {} # key_id → {rolling_pct, weekly_pct, monthly_pct} _key_failures: dict[str, float] = {} # key_id → fail_until_timestamp _key_failure_reason: dict[str, str] = {} # key_id → 最近失败原因(冷却跳过时用于报错) +_key_fail_count: dict[str, int] = {} # key_id → 连续失败次数(指数退避基数,2026-08-14 老莫) _route_stats: dict[str, int] = {} # key_id → hit_count _total_hits = 0 _current_key_id = "" @@ -233,24 +234,44 @@ def pick_key_round_robin(): return _keys[0] -def mark_key_failed(kid, reason="", cooldown=FAIL_COOLDOWN_SEC): - """标记 key 故障,进入冷却期。记录失败原因供冷却期报错。""" - until = time.time() + cooldown +def mark_key_failed(kid, reason="", cooldown=None): + """标记 key 故障,进入指数退避冷却期(按 key 独立统计失败次数)。 + 2026-08-14 老莫设计:失败冷却时长指数增加(base × 2^count),上限 600s(10分钟)。 + 空输出/报错 → 立即换下一个 key,同时该 key 进入指数退避冷却。""" + now = time.time() with _state_lock: + # 失败计数 +1(独立统计) + _key_fail_count[kid] = _key_fail_count.get(kid, 0) + 1 + count = _key_fail_count[kid] + # 指数退避:base=2s, 每次失败 ×2,上限 600s(10分钟) + base = 2 + cool = min(base * (2 ** (count - 1)), 600) + until = now + cool _key_failures[kid] = until if reason: _key_failure_reason[kid] = reason - log.warning("key %s marked failed until %s (%ds): %s", kid, - datetime.fromtimestamp(until).strftime("%H:%M:%S"), cooldown, reason) + log.warning("key %s marked failed (count=%d, cooldown=%ds): %s", kid, count, cool, reason) + + +def reset_key_fail(kid): + """key 成功 → 重置失败计数(清除指数退避)""" + with _state_lock: + if kid in _key_fail_count: + del _key_fail_count[kid] + if kid in _key_failures: + del _key_failures[kid] + if kid in _key_failure_reason: + del _key_failure_reason[kid] def record_route(kid): - """记录一次成功路由。""" + """记录一次成功路由 + 重置失败计数(指数退避清除)。""" global _total_hits, _current_key_id with _state_lock: _route_stats[kid] = _route_stats.get(kid, 0) + 1 _total_hits += 1 _current_key_id = kid + reset_key_fail(kid) # 成功重置失败计数 # ═══════════════════════════════════════════════════════════════