fix: 治理 Prometheus 指标基数爆炸和内存缓存无界增长

- 移除 token/latency Prometheus 指标的 model 标签,避免 provider x model 笛卡尔积
- HealthMonitor 滑动窗口从 DB JSON 迁移至进程内存,减少写放大
- ModelCostService 三层缓存增加 500 条上限,超限时清空
- StickyPriority 粘性缓存和健康状态字典增加容量淘汰
- AffinityManager 请求锁字典增加 500 条上限,淘汰空闲锁
- 配额刷新/探测查询使用 defer/load_only 避免加载大 JSON 列
- Alembic 迁移清理 DB 中遗留的 request_results_window 数据
- 同步更新测试适配 batch_get_cooldowns 返回值和批量删除异步化
This commit is contained in:
fawney19
2026-03-10 10:40:50 +08:00
parent c9f0685b40
commit 7c580e843f
20 changed files with 241 additions and 85 deletions

View File

@@ -78,10 +78,12 @@ class StickyPriorityStrategy(LoadBalancerStrategy):
"total_failures": 0,
}
)
self._max_provider_health_entries: int = 200 # 上限,防止无界增长
# 当前粘性提供商缓存 {cache_key: provider_id}
# cache_key 可以是 api_key_id 或者其他标识
self._sticky_providers: dict[str, str] = {}
self._max_sticky_entries: int = 2000 # 上限,防止无界增长
# 统计信息
self._stats = {
@@ -260,6 +262,12 @@ class StickyPriorityStrategy(LoadBalancerStrategy):
# 没有缓存或缓存失效,选择权重最大的
sticky_candidate = max(candidates, key=lambda c: c.weight)
# 超出上限时清理不在当前候选列表中的旧条目
if len(self._sticky_providers) >= self._max_sticky_entries:
active_ids = {str(c.provider.id) for c in candidates}
stale = [k for k, v in self._sticky_providers.items() if v not in active_ids]
for k in stale[: len(stale) // 2 or len(stale)]:
del self._sticky_providers[k]
self._sticky_providers[cache_key] = str(sticky_candidate.provider.id)
logger.info(f"Set new sticky provider {sticky_candidate.provider.name}")
@@ -358,6 +366,17 @@ class StickyPriorityStrategy(LoadBalancerStrategy):
error: 错误信息(如果失败)
"""
provider_id = str(provider.id)
# 超出上限时淘汰健康且请求量最少的条目
if (
provider_id not in self._provider_health
and len(self._provider_health) >= self._max_provider_health_entries
):
healthy_keys = [
k for k, v in self._provider_health.items() if v["is_healthy"] and k != provider_id
]
if healthy_keys:
victim = min(healthy_keys, key=lambda k: self._provider_health[k]["total_requests"])
del self._provider_health[victim]
health_info = self._provider_health[provider_id]
health_info["total_requests"] += 1

View File

@@ -209,7 +209,7 @@ class MonitorPlugin(BasePlugin):
def record_token_usage(
self,
provider: str,
model: str,
model: str, # noqa: ARG002 - 保留签名兼容性,不再用于 Prometheus 标签
input_tokens: int,
output_tokens: int,
cost: float | None = None,
@@ -219,12 +219,12 @@ class MonitorPlugin(BasePlugin):
Args:
provider: 提供商名称
model: 模型名称
model: 模型名称(保留签名兼容性,不再作为 Prometheus 标签)
input_tokens: 输入token数
output_tokens: 输出token数
cost: 费用
"""
labels = {"provider": provider, "model": model}
labels = {"provider": provider}
import asyncio

View File

@@ -74,21 +74,19 @@ class PrometheusPlugin(MonitorPlugin):
http_label_names,
)
# Token使用指标
# Token使用指标(仅按 provider 维度聚合,避免 provider x model 笛卡尔积导致基数爆炸)
self._metrics["tokens_input_total"] = Counter(
"tokens_input_total", "Total input tokens", ["provider", "model"]
"tokens_input_total", "Total input tokens", ["provider"]
)
self._metrics["tokens_output_total"] = Counter(
"tokens_output_total", "Total output tokens", ["provider", "model"]
"tokens_output_total", "Total output tokens", ["provider"]
)
self._metrics["tokens_total"] = Counter(
"tokens_total", "Total tokens", ["provider", "model"]
)
self._metrics["tokens_total"] = Counter("tokens_total", "Total tokens", ["provider"])
self._metrics["usage_cost_total"] = Counter(
"usage_cost_total", "Total usage cost in USD", ["provider", "model"]
"usage_cost_total", "Total usage cost in USD", ["provider"]
)
# 系统指标
@@ -112,7 +110,7 @@ class PrometheusPlugin(MonitorPlugin):
self._metrics["provider_latency_seconds"] = Histogram(
"provider_latency_seconds",
"Provider response latency in seconds",
["provider", "model"],
["provider"],
buckets=(0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30),
)