mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 09:50:21 +08:00
feat(usage): 改进请求追踪与缓存预热
- 区分请求重试与故障转移:新增 has_retry 标识亲和缓存重试 - 修正候选 TTFB 计算:记录候选自身的首字节时间而非全局时间 - Streaming 状态同步 rate_multiplier,支持按 API 格式配置 - 新增缓存预热服务:启动时预热仪表盘统计、热力图、每日统计 - 优化缓存 TTL 配置:仪表盘统计 2 分钟、热力图和每日统计 10 分钟 - 自动刷新间隔从 10 秒调整为 5 秒
This commit is contained in:
51
src/services/cache/provider_cache.py
vendored
51
src/services/cache/provider_cache.py
vendored
@@ -25,6 +25,41 @@ class ProviderCacheService:
|
||||
|
||||
CACHE_TTL = CacheTTL.PROVIDER # 5 分钟
|
||||
|
||||
@staticmethod
|
||||
def compute_rate_multiplier(
|
||||
rate_multiplier: Optional[float],
|
||||
rate_multipliers: Optional[dict],
|
||||
api_format: Optional[str] = None,
|
||||
) -> float:
|
||||
"""
|
||||
计算 rate_multiplier 的纯函数(无数据库/缓存依赖)
|
||||
|
||||
优先返回指定 API 格式的倍率,如果没有则返回默认倍率。
|
||||
规则:
|
||||
- 如果指定了 api_format 且 rate_multipliers 存在:
|
||||
- 如果 rate_multipliers[api_format] 存在,返回它
|
||||
- 否则返回 1.0(rate_multipliers 存在但该格式未配置)
|
||||
- 否则返回 rate_multiplier 或 1.0
|
||||
|
||||
Args:
|
||||
rate_multiplier: 默认倍率
|
||||
rate_multipliers: 按 API 格式的倍率配置字典
|
||||
api_format: API 格式(可选),如 "CLAUDE"、"OPENAI"
|
||||
|
||||
Returns:
|
||||
计算后的 rate_multiplier
|
||||
"""
|
||||
if api_format and rate_multipliers:
|
||||
format_upper = api_format.upper()
|
||||
if format_upper in rate_multipliers:
|
||||
return float(rate_multipliers[format_upper])
|
||||
else:
|
||||
# rate_multipliers 存在但该格式未配置,使用默认值 1.0
|
||||
return 1.0
|
||||
else:
|
||||
# rate_multipliers 不存在或未指定 api_format,回退到默认倍率
|
||||
return rate_multiplier or 1.0
|
||||
|
||||
@staticmethod
|
||||
async def get_provider_api_key_rate_multiplier(
|
||||
db: Session, provider_api_key_id: str, api_format: Optional[str] = None
|
||||
@@ -64,19 +99,9 @@ class ProviderCacheService:
|
||||
|
||||
# 3. 计算倍率并写入缓存
|
||||
if provider_key:
|
||||
# 优先使用 rate_multipliers[api_format]
|
||||
# 如果 rate_multipliers 存在但未配置该格式,默认为 1.0
|
||||
# 只有当 rate_multipliers 完全不存在时,才回退到 rate_multiplier
|
||||
if api_format and provider_key.rate_multipliers:
|
||||
format_upper = api_format.upper()
|
||||
if format_upper in provider_key.rate_multipliers:
|
||||
rate_multiplier = provider_key.rate_multipliers[format_upper]
|
||||
else:
|
||||
# rate_multipliers 存在但该格式未配置,使用默认值 1.0
|
||||
rate_multiplier = 1.0
|
||||
else:
|
||||
# rate_multipliers 不存在或未指定 api_format,回退到默认倍率
|
||||
rate_multiplier = provider_key.rate_multiplier or 1.0
|
||||
rate_multiplier = ProviderCacheService.compute_rate_multiplier(
|
||||
provider_key.rate_multiplier, provider_key.rate_multipliers, api_format
|
||||
)
|
||||
|
||||
await CacheService.set(
|
||||
cache_key, rate_multiplier, ttl_seconds=ProviderCacheService.CACHE_TTL
|
||||
|
||||
@@ -9,6 +9,7 @@ from typing import List, Optional
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from src.core.batch_committer import get_batch_committer
|
||||
from src.core.logger import logger
|
||||
from src.models.database import RequestCandidate
|
||||
|
||||
|
||||
@@ -289,3 +290,40 @@ class RequestCandidateService:
|
||||
"available_count": available_count, # 新增:尚未被调度的候选数
|
||||
"failure_rate": round(failure_rate, 2),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def calculate_candidate_ttfb(
|
||||
db: Session,
|
||||
candidate_id: str,
|
||||
request_start_time: float,
|
||||
global_first_byte_time_ms: int,
|
||||
) -> int:
|
||||
"""
|
||||
计算候选自身的首字节时间 (TTFB)
|
||||
|
||||
请求链路追踪中的 TTFB 应该是"该候选自身"的首字时间,
|
||||
而不是整个请求从开始到收到首字节的时间。
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
candidate_id: 候选 ID
|
||||
request_start_time: 请求开始时间(Unix timestamp,秒)
|
||||
global_first_byte_time_ms: 全局首字节时间(相对于 request_start_time 的毫秒数)
|
||||
|
||||
Returns:
|
||||
候选自身的 TTFB(毫秒),如果计算失败则返回 global_first_byte_time_ms
|
||||
"""
|
||||
try:
|
||||
candidate = db.query(RequestCandidate).filter(RequestCandidate.id == candidate_id).first()
|
||||
if candidate and candidate.started_at:
|
||||
started_at = candidate.started_at
|
||||
if started_at.tzinfo is None:
|
||||
started_at = started_at.replace(tzinfo=timezone.utc)
|
||||
# 使用整数毫秒计算,避免浮点精度问题
|
||||
request_start_epoch_ms = round(request_start_time * 1000)
|
||||
started_at_epoch_ms = round(started_at.timestamp() * 1000)
|
||||
first_byte_epoch_ms = request_start_epoch_ms + global_first_byte_time_ms
|
||||
return max(0, int(first_byte_epoch_ms - started_at_epoch_ms))
|
||||
except Exception as e:
|
||||
logger.debug(f"计算候选 TTFB 失败: {e}")
|
||||
return global_first_byte_time_ms
|
||||
|
||||
159
src/services/system/cache_warmup.py
Normal file
159
src/services/system/cache_warmup.py
Normal file
@@ -0,0 +1,159 @@
|
||||
"""
|
||||
缓存预热服务
|
||||
|
||||
在应用启动时预热关键缓存,避免用户首次访问时的长时间等待。
|
||||
|
||||
预热的缓存包括:
|
||||
- 管理员仪表盘统计数据
|
||||
- 管理员热力图数据
|
||||
- 每日统计数据
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Dict
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from src.core.logger import logger
|
||||
from src.database import create_session
|
||||
|
||||
|
||||
@dataclass
|
||||
class WarmupContext:
|
||||
"""缓存预热专用的简化 Context"""
|
||||
|
||||
db: Session
|
||||
user: Any # User model
|
||||
audit_metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def add_audit_metadata(self, **kwargs: Any) -> None:
|
||||
"""兼容 ApiRequestContext 接口"""
|
||||
self.audit_metadata.update(kwargs)
|
||||
|
||||
|
||||
class CacheWarmupService:
|
||||
"""缓存预热服务"""
|
||||
|
||||
@classmethod
|
||||
async def warmup_all(cls, delay_seconds: float = 3.0) -> None:
|
||||
"""
|
||||
预热所有关键缓存
|
||||
|
||||
Args:
|
||||
delay_seconds: 启动后延迟执行的秒数,确保系统完全就绪
|
||||
"""
|
||||
await asyncio.sleep(delay_seconds)
|
||||
|
||||
logger.info("开始预热关键缓存...")
|
||||
start_time = time.time()
|
||||
|
||||
results = await asyncio.gather(
|
||||
cls._warmup_admin_dashboard_stats(),
|
||||
cls._warmup_admin_heatmap(),
|
||||
cls._warmup_daily_stats(),
|
||||
return_exceptions=True,
|
||||
)
|
||||
|
||||
success_count = sum(1 for r in results if r is True)
|
||||
error_count = sum(1 for r in results if isinstance(r, Exception))
|
||||
elapsed = time.time() - start_time
|
||||
|
||||
if error_count > 0:
|
||||
logger.warning(f"缓存预热完成: {success_count}/3 成功, {error_count} 失败, 耗时 {elapsed:.2f}s")
|
||||
else:
|
||||
logger.info(f"缓存预热完成: {success_count}/3 成功, 耗时 {elapsed:.2f}s")
|
||||
|
||||
@classmethod
|
||||
async def _warmup_admin_dashboard_stats(cls) -> bool:
|
||||
"""预热管理员仪表盘统计缓存"""
|
||||
db = None
|
||||
try:
|
||||
from src.api.dashboard.routes import AdminDashboardStatsAdapter
|
||||
from src.models.database import User as DBUser
|
||||
|
||||
db = create_session()
|
||||
|
||||
# 获取一个管理员用户用于构造 context
|
||||
admin_user = db.query(DBUser).filter(DBUser.role == "admin").first()
|
||||
if not admin_user:
|
||||
logger.info("缓存预热: 无管理员用户,跳过仪表盘统计预热")
|
||||
return True
|
||||
|
||||
context = WarmupContext(db=db, user=admin_user)
|
||||
adapter = AdminDashboardStatsAdapter()
|
||||
await adapter.handle(context)
|
||||
|
||||
logger.debug("缓存预热: 管理员仪表盘统计已预热")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"缓存预热失败 (仪表盘统计): {e}")
|
||||
return False
|
||||
finally:
|
||||
if db:
|
||||
db.close()
|
||||
|
||||
@classmethod
|
||||
async def _warmup_admin_heatmap(cls) -> bool:
|
||||
"""预热管理员热力图缓存"""
|
||||
db = None
|
||||
try:
|
||||
from src.services.usage.service import UsageService
|
||||
|
||||
db = create_session()
|
||||
|
||||
# 预热全局热力图(管理员视角)
|
||||
await UsageService.get_cached_heatmap(
|
||||
db=db,
|
||||
user_id=None,
|
||||
include_actual_cost=True,
|
||||
)
|
||||
|
||||
logger.debug("缓存预热: 管理员热力图已预热")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"缓存预热失败 (热力图): {e}")
|
||||
return False
|
||||
finally:
|
||||
if db:
|
||||
db.close()
|
||||
|
||||
@classmethod
|
||||
async def _warmup_daily_stats(cls) -> bool:
|
||||
"""预热每日统计缓存"""
|
||||
db = None
|
||||
try:
|
||||
from src.api.dashboard.routes import DashboardDailyStatsAdapter
|
||||
from src.models.database import User as DBUser
|
||||
|
||||
db = create_session()
|
||||
|
||||
# 获取一个管理员用户
|
||||
admin_user = db.query(DBUser).filter(DBUser.role == "admin").first()
|
||||
if not admin_user:
|
||||
logger.info("缓存预热: 无管理员用户,跳过每日统计预热")
|
||||
return True
|
||||
|
||||
context = WarmupContext(db=db, user=admin_user)
|
||||
|
||||
# 预热 7 天的每日统计
|
||||
adapter = DashboardDailyStatsAdapter(days=7)
|
||||
await adapter.handle(context)
|
||||
|
||||
logger.debug("缓存预热: 每日统计已预热")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"缓存预热失败 (每日统计): {e}")
|
||||
return False
|
||||
finally:
|
||||
if db:
|
||||
db.close()
|
||||
|
||||
|
||||
async def start_cache_warmup() -> None:
|
||||
"""启动缓存预热(作为后台任务)"""
|
||||
asyncio.create_task(CacheWarmupService.warmup_all())
|
||||
@@ -1485,6 +1485,7 @@ class UsageService:
|
||||
provider_id: Optional[str] = None,
|
||||
provider_endpoint_id: Optional[str] = None,
|
||||
provider_api_key_id: Optional[str] = None,
|
||||
api_format: Optional[str] = None,
|
||||
) -> Optional[Usage]:
|
||||
"""
|
||||
快速更新使用记录状态
|
||||
@@ -1500,6 +1501,7 @@ class UsageService:
|
||||
provider_id: Provider ID(可选,streaming 状态时更新)
|
||||
provider_endpoint_id: Endpoint ID(可选,streaming 状态时更新)
|
||||
provider_api_key_id: Provider API Key ID(可选,streaming 状态时更新)
|
||||
api_format: API 格式(可选,用于获取按格式配置的倍率)
|
||||
|
||||
Returns:
|
||||
更新后的 Usage 记录,如果未找到则返回 None
|
||||
@@ -1531,6 +1533,13 @@ class UsageService:
|
||||
usage.provider_endpoint_id = provider_endpoint_id
|
||||
if provider_api_key_id is not None:
|
||||
usage.provider_api_key_id = provider_api_key_id
|
||||
# 当设置 provider_api_key_id 时,同步获取并更新 rate_multiplier
|
||||
# 这样前端在 streaming 状态就能显示倍率
|
||||
rate_multiplier = cls._get_rate_multiplier_sync(
|
||||
db, provider_api_key_id, api_format or usage.api_format
|
||||
)
|
||||
if rate_multiplier is not None:
|
||||
usage.rate_multiplier = rate_multiplier
|
||||
|
||||
db.commit()
|
||||
|
||||
@@ -1538,6 +1547,38 @@ class UsageService:
|
||||
|
||||
return usage
|
||||
|
||||
@staticmethod
|
||||
def _get_rate_multiplier_sync(
|
||||
db: Session,
|
||||
provider_api_key_id: str,
|
||||
api_format: Optional[str] = None,
|
||||
) -> Optional[float]:
|
||||
"""
|
||||
同步获取 ProviderAPIKey 的 rate_multiplier
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
provider_api_key_id: ProviderAPIKey ID
|
||||
api_format: API 格式(可选),如 "CLAUDE"、"OPENAI"
|
||||
|
||||
Returns:
|
||||
rate_multiplier 或 None
|
||||
"""
|
||||
from src.services.cache.provider_cache import ProviderCacheService
|
||||
|
||||
provider_key = (
|
||||
db.query(ProviderAPIKey.rate_multiplier, ProviderAPIKey.rate_multipliers)
|
||||
.filter(ProviderAPIKey.id == provider_api_key_id)
|
||||
.first()
|
||||
)
|
||||
|
||||
if not provider_key:
|
||||
return None
|
||||
|
||||
return ProviderCacheService.compute_rate_multiplier(
|
||||
provider_key.rate_multiplier, provider_key.rate_multipliers, api_format
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_active_requests(
|
||||
cls,
|
||||
|
||||
@@ -487,6 +487,7 @@ class StreamUsageTracker:
|
||||
provider_id=self.provider_id,
|
||||
provider_endpoint_id=self.provider_endpoint_id,
|
||||
provider_api_key_id=self.provider_api_key_id,
|
||||
api_format=self.api_format,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"更新使用记录状态为 streaming 失败: {e}")
|
||||
@@ -948,6 +949,7 @@ class EnhancedStreamUsageTracker(StreamUsageTracker):
|
||||
provider_id=self.provider_id,
|
||||
provider_endpoint_id=self.provider_endpoint_id,
|
||||
provider_api_key_id=self.provider_api_key_id,
|
||||
api_format=self.api_format,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"更新使用记录状态为 streaming 失败: {e}")
|
||||
|
||||
Reference in New Issue
Block a user