feat: 软踢预警线提前至 1.95M 用量墙与 10万余额墙
- should_soft_evict: usage ≥ min(effective_limit, 1.95M) 或 (balance_at_sync < 10万 且 usage>0)——usage>0 防小额初始池(B0<10万) 模型误杀永久死锁 - should_restore_model 恢复对称: usage < min(额度,1.95M) 且 (余额≥10万 或 usage==0),否则踢了立即被同步恢复成抖动 - _check_request_block 对已软踢条目(*-limit-reached)恒 429, 防踢后 1.95M~2M 区间小请求放行→路由原名缺失→400 - startup_audit / restore_blocked_models / restore_model_if_healthy 统一接入判定; SOFT_EVICT_USAGE_LIMIT/SOFT_EVICT_BALANCE_LIMIT env 可调 - docs §7.6 新增软踢预警线小节(两墙条件/恢复对称/前置联动/2.5% 采集代价) - 数字用例 9/9 验证(踢/不踢/死锁防护/防抖动/动态墙取 min)
This commit is contained in:
+100
-21
@@ -173,6 +173,19 @@ HARD_DAILY_LIMITS = {
|
||||
"__DEFAULT__": 2_000_000,
|
||||
}
|
||||
|
||||
# ==========================================
|
||||
# 🚨 Soft Evict Thresholds(软踢预警线)
|
||||
# ==========================================
|
||||
# 提前于硬限额/动态墙踢出模型的预警线,留出缓冲:
|
||||
# 1. 用量墙:usage ≥ SOFT_EVICT_USAGE_LIMIT(默认 1.95M) 即踢——避免贴 2M 边时
|
||||
# 大请求 429/小请求放行的灰色地带(reserve 低估 / 并发击穿余量)
|
||||
# 2. 余额墙:balance_at_sync < SOFT_EVICT_BALANCE_LIMIT(10万) 且 usage > 0 即踢——
|
||||
# usage>0 限定防误杀小额初始池模型(B0<10 万未使用时被永久踢出死锁)
|
||||
# 恢复条件对称(should_restore_model):usage < min(effective_limit, 用量墙)
|
||||
# 且 (余额 ≥ 10万 或 usage == 0),否则踢了立即被恢复成踢/恢复抖动
|
||||
SOFT_EVICT_USAGE_LIMIT = float(os.getenv("SOFT_EVICT_USAGE_LIMIT", "1950000"))
|
||||
SOFT_EVICT_BALANCE_LIMIT = float(os.getenv("SOFT_EVICT_BALANCE_LIMIT", "100000"))
|
||||
|
||||
# ==========================================
|
||||
# 🔌 Redis Connection
|
||||
# ==========================================
|
||||
@@ -477,6 +490,67 @@ def get_effective_limit(clean_model, api_base, current_usage=None):
|
||||
|
||||
# ==========================================
|
||||
# 👻 Handler Logic (DailyLimitHandler)
|
||||
# ==========================================
|
||||
# 🚨 Soft Evict / Restore Decision(软踢与恢复对称判定)
|
||||
# ==========================================
|
||||
def soft_evict_wall(effective_limit):
|
||||
"""软踢用量墙 = min(有效限额, SOFT_EVICT_USAGE_LIMIT);None 表示无限额。"""
|
||||
if effective_limit is None:
|
||||
return None
|
||||
return min(float(effective_limit), SOFT_EVICT_USAGE_LIMIT)
|
||||
|
||||
|
||||
def should_soft_evict(clean_model, api_base, current_usage, effective_limit):
|
||||
"""
|
||||
软踢判定:任一满足即触发
|
||||
- 用量墙:current_usage >= min(effective_limit, SOFT_EVICT_USAGE_LIMIT)
|
||||
- 余额墙:balance_at_sync < SOFT_EVICT_BALANCE_LIMIT 且 current_usage > 0
|
||||
(balance_at_sync 缺失视为充足;usage>0 防小额初始池模型误杀死锁)
|
||||
返回 (triggered, reason)。
|
||||
"""
|
||||
wall = soft_evict_wall(effective_limit)
|
||||
if wall is not None and float(current_usage) >= wall:
|
||||
return True, f"usage {int(float(current_usage)):,} >= soft wall {int(wall):,}"
|
||||
try:
|
||||
bal_key = f"limit:dynamic:volcengine:balance_at_sync:{clean_model}"
|
||||
bal_val = redis_client.get(bal_key) if redis_client else None
|
||||
if (
|
||||
bal_val is not None
|
||||
and float(bal_val) < SOFT_EVICT_BALANCE_LIMIT
|
||||
and float(current_usage) > 0
|
||||
):
|
||||
return (
|
||||
True,
|
||||
f"balance {int(float(bal_val)):,} < {SOFT_EVICT_BALANCE_LIMIT:.0f} (usage>0)",
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
return False, ""
|
||||
|
||||
|
||||
def should_restore_model(clean_model, api_base, current_usage, effective_limit):
|
||||
"""
|
||||
恢复判定(与 should_soft_evict 对称,防踢/恢复抖动):
|
||||
usage < min(effective_limit, SOFT_EVICT_USAGE_LIMIT) 且 (余额 ≥ 10万 或 usage == 0)
|
||||
effective_limit None 时仅按余额条件判定(原逻辑 None → 可恢复)。
|
||||
"""
|
||||
wall = soft_evict_wall(effective_limit)
|
||||
if wall is not None and float(current_usage) >= wall:
|
||||
return False
|
||||
try:
|
||||
bal_key = f"limit:dynamic:volcengine:balance_at_sync:{clean_model}"
|
||||
bal_val = redis_client.get(bal_key) if redis_client else None
|
||||
if (
|
||||
bal_val is not None
|
||||
and float(bal_val) < SOFT_EVICT_BALANCE_LIMIT
|
||||
and float(current_usage) > 0
|
||||
):
|
||||
return False
|
||||
except Exception:
|
||||
pass
|
||||
return True
|
||||
|
||||
|
||||
# ==========================================
|
||||
def soft_evict_from_router(target_model, target_api_base):
|
||||
"""
|
||||
@@ -596,14 +670,11 @@ def restore_blocked_models():
|
||||
# get_effective_limit handles fetching usage if None passed.
|
||||
effective_limit = get_effective_limit(clean_model, original_api_base)
|
||||
|
||||
should_restore = False
|
||||
if effective_limit is None:
|
||||
should_restore = True
|
||||
else:
|
||||
usage_key = get_usage_key(clean_model, original_api_base)
|
||||
current_val = float(redis_client.get(usage_key) or 0)
|
||||
if current_val < float(effective_limit):
|
||||
should_restore = True
|
||||
usage_key = get_usage_key(clean_model, original_api_base)
|
||||
current_val = float(redis_client.get(usage_key) or 0)
|
||||
should_restore = should_restore_model(
|
||||
clean_model, original_api_base, current_val, effective_limit
|
||||
)
|
||||
|
||||
if should_restore:
|
||||
dprint(f"♻️ [RESTORE] Restoring {backup['model_name']} (Quota OK)")
|
||||
@@ -655,14 +726,11 @@ def restore_model_if_healthy(clean_model, allowed_hashes=None):
|
||||
continue
|
||||
|
||||
effective_limit = get_effective_limit(clean_model, m_base)
|
||||
should_restore = False
|
||||
if effective_limit is None:
|
||||
should_restore = True
|
||||
else:
|
||||
usage_key = get_usage_key(clean_model, m_base)
|
||||
current_val = float(redis_client.get(usage_key) or 0)
|
||||
if current_val < float(effective_limit):
|
||||
should_restore = True
|
||||
usage_key = get_usage_key(clean_model, m_base)
|
||||
current_val = float(redis_client.get(usage_key) or 0)
|
||||
should_restore = should_restore_model(
|
||||
clean_model, m_base, current_val, effective_limit
|
||||
)
|
||||
|
||||
if should_restore:
|
||||
dprint(f"♻️ [RESTORE] Restoring {backup['model_name']} (Quota OK)")
|
||||
@@ -732,11 +800,14 @@ def startup_audit():
|
||||
continue
|
||||
|
||||
usage_key = get_usage_key(clean_model, api_base)
|
||||
current_val = redis_client.get(usage_key)
|
||||
current_val = float(redis_client.get(usage_key) or 0)
|
||||
|
||||
if current_val and float(current_val) >= float(effective_limit):
|
||||
evict, evict_reason = should_soft_evict(
|
||||
clean_model, api_base, current_val, effective_limit
|
||||
)
|
||||
if evict:
|
||||
dprint(
|
||||
f"🚫 [STARTUP BLOCK] Found {raw_model} over limit ({int(float(current_val))}/{int(effective_limit)}). Evicting now."
|
||||
f"🚫 [STARTUP BLOCK] {raw_model} soft-evict: {evict_reason}. Evicting now."
|
||||
)
|
||||
soft_evict_from_router(raw_model, api_base)
|
||||
blocked_count += 1
|
||||
@@ -798,6 +869,11 @@ def _check_request_block(model_name, reserve=0.0):
|
||||
val = redis_client.get(usage_key)
|
||||
usage = float(val or 0)
|
||||
|
||||
# 已被软踢的条目恒 429(无论 reserve),避免踢后灰色区间(如 1.95M~2M)
|
||||
# 小请求被放行 → 路由原名不存在 → 400 路由错误
|
||||
if entry_name == f"{model_name}-limit-reached" and "_daily_limit_backup" in entry:
|
||||
return (clean, usage, effective_limit)
|
||||
|
||||
if usage + float(reserve) >= float(effective_limit):
|
||||
return (clean, usage, effective_limit)
|
||||
|
||||
@@ -964,9 +1040,12 @@ class DailyLimitHandler(CustomLogger):
|
||||
level="INFO",
|
||||
)
|
||||
|
||||
if effective_limit is not None and float(current_total) >= float(effective_limit):
|
||||
evict, evict_reason = should_soft_evict(
|
||||
clean_model, api_base, current_total, effective_limit
|
||||
)
|
||||
if evict:
|
||||
log(
|
||||
f"🚫 [BLOCK] {clean_model} Limit Hit: {current_total}/{effective_limit}",
|
||||
f"🚫 [BLOCK] {clean_model} soft-evict: {evict_reason} (limit: {effective_limit})",
|
||||
level="WARNING",
|
||||
)
|
||||
soft_evict_from_router(raw_model, api_base)
|
||||
|
||||
Reference in New Issue
Block a user