fix: 闸门 fail-closed 堵重启/Redis 故障窗口漏放
- _check_request_block: Redis 不可用/无限额/读取异常时监控条目恒 429(标记 -1,-1),非监控渠道不受影响;原实现任何异常 fail-open - async_pre_call_hook: 识别 fail-closed 标记返回 quota unavailable 429 - FALLBACK_LIMIT 900k→200k(免费池 2M 的 10%),收窄 0 余额模型兜底泄露 - docker-compose: redis 开启 AOF(--appendonly yes everysec),防重启丢快照/用量 - active_monitor_loop: 首轮跳过 CHECK_INTERVAL,额度未同步空窗 12-20s 缩至 2-5s
This commit is contained in:
+3
-1
@@ -50,7 +50,9 @@ services:
|
||||
# --maxmemory-policy allkeys-lru : 内存满了之后,自动删除最久未使用的缓存(无论是否过期)
|
||||
# --save "" : 如果你不需要持久化缓存到硬盘(重启后清空),可以把 save 设为空,减少磁盘IO
|
||||
# command: redis-server --save 60 1 --maxmemory 128mb --maxmemory-policy allkeys-lru --loglevel warning
|
||||
command: redis-server --maxmemory 32mb --maxmemory-policy allkeys-lru --save 300 1 --loglevel warning
|
||||
# --appendonly yes: 开启 AOF,防止 Redis 崩溃/重启丢失快照与用量 key →
|
||||
# 避免"额度未同步"fallback 窗口(首次同步前)被大请求烧穿免费额度
|
||||
command: redis-server --maxmemory 32mb --maxmemory-policy allkeys-lru --save 300 1 --appendonly yes --appendfsync everysec --loglevel warning
|
||||
environment:
|
||||
TZ: Asia/Shanghai
|
||||
# 配置详解:
|
||||
|
||||
@@ -196,7 +196,7 @@ HARD_DAILY_LIMITS → 统一 2M(采集上限;doubao-seed-code-preview-251028
|
||||
- 消耗任务 23:20 烧(usage 计入):池不变 → 滞后窗口内限额偏松 ≤ BUFFER ✓
|
||||
- 余额耗尽:池 − 30 万 ≤ 0 → 限额 0 → 全拦,扣费结构性杜绝 ✓
|
||||
|
||||
**fallback(stable_limit 缺失窗口)**:免费模型余额常态 < 2M(昨日采多少返多少),若 dynamic 缺失(首次同步前/新增模型/TTL 过期)时裸奔到静态 2M,余额不足的模型会被烧穿扣费。故 fallback 用保守值 `FALLBACK_LIMIT = min(static, 90万)`(env 可调),宁可短时收紧不可裸奔。
|
||||
**fallback(stable_limit 缺失窗口)**:免费模型余额常态 < 2M(昨日采多少返多少),若 dynamic 缺失(首次同步前/新增模型/TTL 过期)时裸奔到静态 2M,余额不足的模型会被烧穿扣费。故 fallback 用保守值 `FALLBACK_LIMIT = min(static, 20万)`(env 可调;2M 免费池的 10%,0 余额模型最大泄露收窄到 20 万),宁可短时收紧不可裸奔。另:Redis 不可用/读取异常时请求闸门 fail-closed(直接 429),不再放行。
|
||||
|
||||
**与消耗任务同构**:限额放行条件 = `balance − BUFFER ≥ 0`;消耗任务停止条件 = `余额快照 − 已烧 ≤ 0`——同一把尺子。
|
||||
|
||||
|
||||
+50
-10
@@ -109,10 +109,12 @@ PREBLOCK_RESERVE_TOKENS = int(os.getenv("PREBLOCK_RESERVE_TOKENS", "50000"))
|
||||
# max(声明值, 本下限) 兜住思考开销;2M 限额下 4096 偏差仅 0.2%。
|
||||
THINKING_RESERVE_FLOOR = int(os.getenv("THINKING_RESERVE_FLOOR", "4096"))
|
||||
|
||||
# stable_limit 缺失时(首次同步前/新增模型/TTL 过期)的保守兜底限额。
|
||||
# 免费模型余额常态 < 2M(昨日采多少返多少),若 fallback 裸奔到采集上限 2M,
|
||||
# 余额不足的模型会被烧穿扣费。宁可短时收紧,不可裸奔。
|
||||
FALLBACK_LIMIT = int(os.getenv("FALLBACK_LIMIT", "900000"))
|
||||
# stable_limit 缺失时(首次同步前/新增模型/TTL 过期/Redis 数据丢失)的保守兜底限额。
|
||||
# 免费池常态 ~2M(首开赠送/昨日采多少返多少),若 fallback 裸奔到采集上限 2M,
|
||||
# 余额不足或已耗尽的模型会被烧穿扣费。900k 对 0 余额模型仍是 9 倍优惠,
|
||||
# 收紧到 20 万(2M 的 10%,单次消耗语料 5~11 万不受影响):宁可短时错杀大请求,
|
||||
# 不可漏放大烧;快照由守护线程恢复后立即回到真实限额。
|
||||
FALLBACK_LIMIT = int(os.getenv("FALLBACK_LIMIT", "200000"))
|
||||
|
||||
# Redis Config
|
||||
REDIS_HOST = os.getenv(
|
||||
@@ -857,9 +859,11 @@ def _check_request_block(model_name, reserve=0.0):
|
||||
只拦截 MONITORED_MODEL_PAIRS 内的火山监控条目;newapi 等非监控渠道天然放行。
|
||||
reserve: 本次请求的预估消耗(max_tokens 或常量兜底),usage + reserve >= limit 即拦截,
|
||||
避免并发请求在闸门检查后集体撞线(限额是低频快照,用量是实时累计)。
|
||||
返回 (clean_model, usage, limit) 表示超限,None 表示放行。任何异常 fail-open。
|
||||
返回 (clean_model, usage, limit) 表示超限,None 表示放行。
|
||||
fail-closed:Redis 不可用 / 限额获取失败 / 读取异常时,对监控条目返回
|
||||
(clean_model, -1, -1) 标记拒绝(宁可错杀,不可漏放);非监控条目不受影响。
|
||||
"""
|
||||
if not redis_client or not LITELLM_AVAILABLE:
|
||||
if not LITELLM_AVAILABLE:
|
||||
return None
|
||||
try:
|
||||
from litellm.proxy.proxy_server import llm_router
|
||||
@@ -889,9 +893,24 @@ def _check_request_block(model_name, reserve=0.0):
|
||||
if MONITORED_MODEL_PAIRS and (clean, h) not in MONITORED_MODEL_PAIRS:
|
||||
continue
|
||||
|
||||
# ── 监控条目 fail-closed:Redis 不可用直接拒绝 (usage=-1, limit=-1) ──
|
||||
# 快照缺失但 Redis 可达时走 get_effective_limit 内部 fallback(收紧限额),
|
||||
# 不在此拒绝——那是"尚未同步"的正常启动态,守护线程几分钟内补上。
|
||||
if not redis_client:
|
||||
log(
|
||||
f"🚫 Redis unavailable, fail-closed 429 for monitored {clean}",
|
||||
level="WARNING",
|
||||
)
|
||||
return (clean, -1, -1)
|
||||
|
||||
effective_limit = get_effective_limit(clean, api_base)
|
||||
if effective_limit is None:
|
||||
continue
|
||||
# 监控条目取不到限额:拒绝而非放行(fail-closed)
|
||||
log(
|
||||
f"🚫 No effective limit for monitored {clean}, fail-closed 429",
|
||||
level="WARNING",
|
||||
)
|
||||
return (clean, -1, -1)
|
||||
|
||||
usage_key = get_usage_key(clean, api_base)
|
||||
val = redis_client.get(usage_key)
|
||||
@@ -907,8 +926,11 @@ def _check_request_block(model_name, reserve=0.0):
|
||||
|
||||
return None
|
||||
except Exception as e:
|
||||
log(f"❌ Pre-block check failed: {e}", level="ERROR")
|
||||
return None
|
||||
# fail-closed:无法确认额度状态即拒绝(调用方 429)。
|
||||
# 到达此处时必已命中监控条目(非监控条目在循环内 continue 跳过,
|
||||
# 不触发 Redis 调用),不会误伤 newapi 等非监控渠道。
|
||||
log(f"❌ Pre-block check failed (fail-closed 429): {e}", level="ERROR")
|
||||
return (model_name, -1, -1)
|
||||
|
||||
|
||||
def quota_interceptor(kwargs, user_api_key_dict=None):
|
||||
@@ -1000,6 +1022,18 @@ class DailyLimitHandler(CustomLogger):
|
||||
return None
|
||||
|
||||
clean, usage, limit = blocked
|
||||
if usage < 0 or limit < 0:
|
||||
# fail-closed 标记:Redis 不可用/限额状态未知,拒绝防漏放
|
||||
log(
|
||||
f"🚫 [PRE-BLOCK] {data['model']} denied: quota state unavailable (fail-closed)",
|
||||
level="WARNING",
|
||||
)
|
||||
raise RateLimitError(
|
||||
message=f"Quota state unavailable for model '{data['model']}', request denied to prevent overspend",
|
||||
llm_provider="volcengine",
|
||||
model=data["model"],
|
||||
)
|
||||
|
||||
log(
|
||||
f"🚫 [PRE-BLOCK] {data['model']} daily limit reached ({int(usage):,}+{int(reserve):,}/{int(limit):,})",
|
||||
level="WARNING",
|
||||
@@ -1464,9 +1498,15 @@ def active_monitor_loop():
|
||||
)
|
||||
print(f"SmallBuffer / 缓冲: {SMALL_BUFFER}", flush=True)
|
||||
|
||||
first_iteration = True
|
||||
while True:
|
||||
try:
|
||||
time.sleep(CHECK_INTERVAL)
|
||||
if first_iteration:
|
||||
# 首轮跳过 CHECK_INTERVAL,紧接 startup_audit(sleep 2s) 后立即同步,
|
||||
# 把"额度未同步"空窗从 ~12-20s 缩到 ~2-5s(仅剩拉包 API 往返)
|
||||
first_iteration = False
|
||||
else:
|
||||
time.sleep(CHECK_INTERVAL)
|
||||
_maybe_trigger_daily_consume()
|
||||
now = time.time()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user