mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 17:30:23 +08:00
- 删除全部 Python 源码 (src/) 及 Alembic 迁移脚本,归档至 _deprecated_py_src/ - 重构 Rust gateway ai_pipeline: 拆分 planner/finalize 模块,新增 contracts/adaptation 层 - 重组 handlers 模块为 admin/public/proxy/internal/shared 子模块结构 - 新增 executor 模块,引入 Rust 原生数据库迁移 (aether-data/migrations) - 简化 CI/Docker 构建流程,移除 base image 二级构建,统一为单一 app image - 移除 Python 相关基础设施文件 (entrypoint.sh, gunicorn_conf.py, Dockerfile.base)
758 lines
30 KiB
Python
758 lines
30 KiB
Python
"""
|
||
缓存亲和性管理器 (Cache Affinity Manager) - 支持 Redis 或内存存储
|
||
|
||
职责:
|
||
1. 跟踪请求API Key的Provider+Key缓存状态
|
||
2. 管理缓存有效期
|
||
3. 提供缓存统计和分析
|
||
4. 自动失效不支持缓存的Provider
|
||
|
||
设计原理:
|
||
- 每个API Key使用某个Provider的Key后,在缓存TTL期内,应该继续使用同一个Key
|
||
- 这样可以最大化利用提供商的Prompt Caching机制
|
||
- 当Key故障时,自动失效该Key的缓存亲和性
|
||
- 当Provider关闭缓存支持时,自动失效所有相关亲和性
|
||
|
||
注意:
|
||
- affinity_key 参数通常为请求使用的 API Key ID(api_key_id)
|
||
- 这样可以支持"独立余额Key"场景,每个Key有自己的缓存亲和性
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import asyncio
|
||
import json
|
||
import os
|
||
import time
|
||
from contextlib import asynccontextmanager
|
||
from typing import Any, NamedTuple
|
||
|
||
from src.config.constants import CacheTTL
|
||
from src.core.logger import logger
|
||
from src.core.redis_utils import delete_redis_keys, scan_delete_pattern
|
||
|
||
|
||
class CacheAffinity(NamedTuple):
|
||
"""缓存亲和性信息"""
|
||
|
||
provider_id: str
|
||
endpoint_id: str
|
||
key_id: str
|
||
api_format: str # API格式 (claude/openai)
|
||
model_name: str # 模型名称
|
||
created_at: float # 创建时间戳
|
||
expire_at: float # 过期时间戳
|
||
request_count: int # 使用次数
|
||
|
||
|
||
class CacheAffinityManager:
|
||
"""
|
||
缓存亲和性管理器(支持 Redis 或内存存储)
|
||
|
||
存储结构:
|
||
----------------------
|
||
Key格式: cache_affinity:{affinity_key}:{api_format}:{model_name}
|
||
- affinity_key: 通常为请求使用的 API Key ID(支持独立余额Key场景)
|
||
- api_format: API格式 (claude/openai)
|
||
- model_name: 模型名称(区分不同模型的缓存亲和性)
|
||
Value格式: JSON/Dict
|
||
{
|
||
"provider_id": "xxx",
|
||
"endpoint_id": "yyy",
|
||
"key_id": "zzz",
|
||
"model_name": "claude-3-5-sonnet-20241022",
|
||
"created_at": 1234567890.123,
|
||
"expire_at": 1234567890.123,
|
||
"request_count": 5
|
||
}
|
||
TTL: 自动过期
|
||
|
||
设计改进:
|
||
- 每个API Key可以对多个API格式和模型分别维护缓存亲和性
|
||
- 不同模型请求使用独立的缓存亲和性,避免模型切换导致的缓存失效
|
||
- 某个端点故障切换不会影响其他端点的亲和性
|
||
- 更精确的缓存命中率统计
|
||
- 支持"独立余额Key"场景,每个Key有独立的缓存亲和性
|
||
"""
|
||
|
||
# 默认缓存TTL(秒)- 使用统一常量
|
||
DEFAULT_CACHE_TTL = CacheTTL.CACHE_AFFINITY
|
||
REDIS_SCAN_BATCH_SIZE = 200
|
||
REDIS_DELETE_BATCH_SIZE = 500
|
||
|
||
def __init__(
|
||
self, redis_client: Any | None = None, default_ttl: int = DEFAULT_CACHE_TTL
|
||
) -> None:
|
||
"""
|
||
初始化缓存亲和性管理器
|
||
|
||
Args:
|
||
redis_client: Redis客户端(可选)
|
||
default_ttl: 默认缓存TTL(秒)
|
||
"""
|
||
self.redis = redis_client
|
||
self.default_ttl = default_ttl
|
||
self._memory_store: dict[str, dict[str, Any]] = {}
|
||
self._memory_store_max_size: int = 5000 # 内存模式最大条目数
|
||
self._memory_lock: asyncio.Lock | None = None
|
||
|
||
# L1 缓存(即使使用 Redis 也启用,减少网络往返)
|
||
# 注意:L1 是本地进程内缓存,多实例部署时存在短暂不一致窗口(TTL 秒级)。
|
||
# 当前 TTL 默认 3 秒,对于亲和性路由来说可接受:最坏情况是短暂路由到
|
||
# 旧 provider,下次请求即可自动修正。如果需要严格一致性,将 TTL 设为 0
|
||
# 以禁用 L1 缓存,或通过 CacheSyncService 接收 pub/sub 主动失效。
|
||
self._l1_cache_ttl = int(os.getenv("CACHE_AFFINITY_L1_TTL", str(CacheTTL.L1_LOCAL)))
|
||
self._l1_cache: dict[str, tuple[float, dict[str, Any]]] = {}
|
||
self._l1_lock = asyncio.Lock()
|
||
self._l1_max_size = int(os.getenv("CACHE_AFFINITY_L1_MAX_SIZE", "1000")) # 最大缓存条目数
|
||
self._l1_last_cleanup = time.time()
|
||
|
||
# 请求级别锁,避免同一用户+端点同时更新造成抖动
|
||
self._request_locks: dict[str, asyncio.Lock] = {}
|
||
self._request_locks_max_size: int = 500 # 锁字典上限,防止无界增长
|
||
|
||
# 统计信息
|
||
self._stats = {
|
||
"total_affinities": 0,
|
||
"cache_hits": 0,
|
||
"cache_misses": 0,
|
||
"cache_invalidations": 0,
|
||
"provider_switches": 0,
|
||
"key_switches": 0,
|
||
}
|
||
|
||
if self.redis:
|
||
logger.debug("CacheAffinityManager: 使用Redis存储")
|
||
else:
|
||
logger.debug(
|
||
"CacheAffinityManager: Redis不可用,回退到内存存储(仅适用于单实例/开发环境)"
|
||
)
|
||
|
||
def _is_memory_backend(self) -> bool:
|
||
"""是否处于内存模式"""
|
||
return self.redis is None
|
||
|
||
def _get_memory_lock(self) -> asyncio.Lock:
|
||
"""懒初始化内存锁"""
|
||
if self._memory_lock is None:
|
||
self._memory_lock = asyncio.Lock()
|
||
return self._memory_lock
|
||
|
||
def _get_cache_key(self, affinity_key: str, api_format: str, model_name: str) -> str:
|
||
"""
|
||
生成Redis Key
|
||
|
||
Args:
|
||
affinity_key: 亲和性标识符(通常为API Key ID)
|
||
api_format: API格式 (claude/openai)
|
||
model_name: 模型名称
|
||
|
||
Returns:
|
||
格式化的缓存键: cache_affinity:{affinity_key}:{api_format}:{model_name}
|
||
"""
|
||
return f"cache_affinity:{affinity_key}:{api_format}:{model_name}"
|
||
|
||
async def _get_l1_entry(self, cache_key: str) -> dict[str, Any] | None:
|
||
async with self._l1_lock:
|
||
record = self._l1_cache.get(cache_key)
|
||
if not record:
|
||
return None
|
||
expire_at, payload = record
|
||
if time.time() > expire_at:
|
||
self._l1_cache.pop(cache_key, None)
|
||
return None
|
||
return dict(payload)
|
||
|
||
async def _set_l1_entry(self, cache_key: str, payload: dict[str, Any] | None) -> None:
|
||
async with self._l1_lock:
|
||
if not payload:
|
||
self._l1_cache.pop(cache_key, None)
|
||
return
|
||
expire_at = time.time() + max(1, self._l1_cache_ttl)
|
||
self._l1_cache[cache_key] = (expire_at, dict(payload))
|
||
|
||
# 定期清理过期条目(每 60 秒最多一次)
|
||
current_time = time.time()
|
||
if current_time - self._l1_last_cleanup > 60:
|
||
self._cleanup_l1_cache_unlocked(current_time)
|
||
self._l1_last_cleanup = current_time
|
||
|
||
def _cleanup_l1_cache_unlocked(self, current_time: float) -> int:
|
||
"""清理过期的 L1 缓存条目(需要在持有锁的情况下调用)
|
||
|
||
Returns:
|
||
清理的条目数量
|
||
"""
|
||
expired_keys = [
|
||
key for key, (expire_at, _) in self._l1_cache.items() if current_time > expire_at
|
||
]
|
||
for key in expired_keys:
|
||
self._l1_cache.pop(key, None)
|
||
|
||
# 如果缓存仍然过大,按过期时间排序移除最旧的条目
|
||
if len(self._l1_cache) > self._l1_max_size:
|
||
sorted_items = sorted(
|
||
self._l1_cache.items(), key=lambda x: x[1][0] # 按 expire_at 排序
|
||
)
|
||
# 移除最旧的 20% 条目
|
||
remove_count = len(self._l1_cache) - int(self._l1_max_size * 0.8)
|
||
for key, _ in sorted_items[:remove_count]:
|
||
self._l1_cache.pop(key, None)
|
||
expired_keys.extend([k for k, _ in sorted_items[:remove_count]])
|
||
|
||
if expired_keys:
|
||
logger.debug(
|
||
f"L1 缓存清理: 移除 {len(expired_keys)} 个条目,当前 {len(self._l1_cache)} 个"
|
||
)
|
||
|
||
return len(expired_keys)
|
||
|
||
@asynccontextmanager
|
||
async def _acquire_request_lock(self, cache_key: str) -> None:
|
||
lock = self._request_locks.get(cache_key)
|
||
if lock is None:
|
||
# 超出上限时淘汰未被持有的锁,防止无界增长
|
||
if len(self._request_locks) >= self._request_locks_max_size:
|
||
free_keys = [k for k, lk in self._request_locks.items() if not lk.locked()]
|
||
for k in free_keys[: len(free_keys) // 2 or 1]: # 清理一半空闲锁
|
||
del self._request_locks[k]
|
||
lock = asyncio.Lock()
|
||
self._request_locks[cache_key] = lock
|
||
await lock.acquire()
|
||
try:
|
||
yield
|
||
finally:
|
||
lock.release()
|
||
|
||
async def _load_affinity_dict(self, cache_key: str) -> dict[str, Any] | None:
|
||
"""读取缓存亲和性字典"""
|
||
# 先尝试L1缓存
|
||
l1_value = await self._get_l1_entry(cache_key)
|
||
if l1_value is not None:
|
||
return l1_value
|
||
|
||
if not self._is_memory_backend():
|
||
data = await self.redis.get(cache_key)
|
||
if not data:
|
||
return None
|
||
value = json.loads(data)
|
||
await self._set_l1_entry(cache_key, value)
|
||
return value
|
||
|
||
lock = self._get_memory_lock()
|
||
async with lock:
|
||
record = self._memory_store.get(cache_key)
|
||
if record:
|
||
await self._set_l1_entry(cache_key, record)
|
||
return dict(record) if record else None
|
||
|
||
async def _save_affinity_dict(
|
||
self, cache_key: str, ttl: int, affinity_dict: dict[str, Any]
|
||
) -> None:
|
||
"""存储缓存亲和性字典"""
|
||
if not self._is_memory_backend():
|
||
await self.redis.setex(cache_key, ttl, json.dumps(affinity_dict))
|
||
await self._set_l1_entry(cache_key, affinity_dict)
|
||
return
|
||
|
||
lock = self._get_memory_lock()
|
||
async with lock:
|
||
self._memory_store[cache_key] = dict(affinity_dict)
|
||
# 超出上限时清理过期条目
|
||
if len(self._memory_store) > self._memory_store_max_size:
|
||
now = time.time()
|
||
expired = [k for k, v in self._memory_store.items() if now > v.get("expire_at", 0)]
|
||
for k in expired:
|
||
del self._memory_store[k]
|
||
await self._set_l1_entry(cache_key, affinity_dict)
|
||
|
||
async def _delete_affinity_key(self, cache_key: str) -> None:
|
||
"""删除缓存亲和性"""
|
||
if not self._is_memory_backend():
|
||
await self.redis.delete(cache_key)
|
||
else:
|
||
lock = self._get_memory_lock()
|
||
async with lock:
|
||
self._memory_store.pop(cache_key, None)
|
||
|
||
await self._set_l1_entry(cache_key, None)
|
||
|
||
async def _delete_redis_keys(self, keys: list[str]) -> int:
|
||
if self._is_memory_backend() or not keys:
|
||
return 0
|
||
return await delete_redis_keys(self.redis, keys)
|
||
|
||
async def _scan_delete_pattern(self, pattern: str) -> int:
|
||
if self._is_memory_backend():
|
||
return 0
|
||
return await scan_delete_pattern(
|
||
self.redis,
|
||
pattern,
|
||
scan_batch_size=self.REDIS_SCAN_BATCH_SIZE,
|
||
delete_batch_size=self.REDIS_DELETE_BATCH_SIZE,
|
||
)
|
||
|
||
async def _clear_l1_entries_by_prefix(self, prefix: str) -> int:
|
||
"""清理匹配前缀的 L1 本地缓存。"""
|
||
async with self._l1_lock:
|
||
keys_to_remove = [key for key in self._l1_cache if key.startswith(prefix)]
|
||
for key in keys_to_remove:
|
||
self._l1_cache.pop(key, None)
|
||
return len(keys_to_remove)
|
||
|
||
async def _snapshot_memory_items(self) -> dict[str, dict[str, Any]]:
|
||
"""复制内存存储内容(仅内存模式使用)"""
|
||
lock = self._get_memory_lock()
|
||
async with lock:
|
||
return {k: dict(v) for k, v in self._memory_store.items()}
|
||
|
||
async def get_affinity(
|
||
self, affinity_key: str, api_format: str, model_name: str
|
||
) -> CacheAffinity | None:
|
||
"""
|
||
获取指定亲和性标识符对特定API格式和模型的缓存亲和性
|
||
|
||
Args:
|
||
affinity_key: 亲和性标识符(通常为API Key ID)
|
||
api_format: API格式 (claude/openai)
|
||
model_name: 模型名称
|
||
|
||
Returns:
|
||
CacheAffinity对象,如果不存在或已过期则返回None
|
||
"""
|
||
try:
|
||
cache_key = self._get_cache_key(affinity_key, api_format, model_name)
|
||
async with self._acquire_request_lock(cache_key):
|
||
affinity_dict = await self._load_affinity_dict(cache_key)
|
||
|
||
if not affinity_dict:
|
||
self._stats["cache_misses"] += 1
|
||
return None
|
||
|
||
# 检查是否过期(双重检查,防止TTL未及时清理)
|
||
current_time = time.time()
|
||
if current_time > affinity_dict["expire_at"]:
|
||
await self._delete_affinity_key(cache_key)
|
||
self._stats["cache_misses"] += 1
|
||
return None
|
||
|
||
self._stats["cache_hits"] += 1
|
||
|
||
return CacheAffinity(
|
||
provider_id=affinity_dict["provider_id"],
|
||
endpoint_id=affinity_dict["endpoint_id"],
|
||
key_id=affinity_dict["key_id"],
|
||
api_format=affinity_dict.get("api_format", api_format),
|
||
model_name=affinity_dict.get("model_name", model_name),
|
||
created_at=affinity_dict["created_at"],
|
||
expire_at=affinity_dict["expire_at"],
|
||
request_count=affinity_dict["request_count"],
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.exception(f"获取缓存亲和性失败: {e}")
|
||
self._stats["cache_misses"] += 1
|
||
return None
|
||
|
||
async def set_affinity(
|
||
self,
|
||
affinity_key: str,
|
||
provider_id: str,
|
||
endpoint_id: str,
|
||
key_id: str,
|
||
api_format: str,
|
||
model_name: str,
|
||
supports_caching: bool = True,
|
||
ttl: int | None = None,
|
||
) -> None:
|
||
"""
|
||
设置指定亲和性标识符对特定API格式和模型的缓存亲和性
|
||
|
||
Args:
|
||
affinity_key: 亲和性标识符(通常为API Key ID)
|
||
provider_id: Provider ID
|
||
endpoint_id: Endpoint ID
|
||
key_id: Key ID
|
||
api_format: API格式 (claude/openai)
|
||
model_name: 模型名称
|
||
supports_caching: 该Provider是否支持缓存
|
||
ttl: 缓存有效期(秒),如果不提供则使用默认值
|
||
|
||
注意:每次调用都会刷新过期时间(滑动窗口机制),以保持对同一个Provider/Endpoint/Key的亲和性
|
||
"""
|
||
if not supports_caching:
|
||
# 不支持缓存的Provider不记录亲和性
|
||
logger.debug(f"Provider {provider_id[:8]}... 不支持缓存,跳过亲和性记录")
|
||
return
|
||
|
||
ttl = ttl or self.default_ttl
|
||
current_time = time.time()
|
||
expire_at = current_time + ttl # 每次都刷新过期时间
|
||
cache_key = self._get_cache_key(affinity_key, api_format, model_name)
|
||
|
||
try:
|
||
async with self._acquire_request_lock(cache_key):
|
||
existing_dict = await self._load_affinity_dict(cache_key)
|
||
existing_affinity: CacheAffinity | None = None
|
||
if existing_dict and current_time <= existing_dict.get("expire_at", 0):
|
||
existing_affinity = CacheAffinity(
|
||
provider_id=existing_dict["provider_id"],
|
||
endpoint_id=existing_dict["endpoint_id"],
|
||
key_id=existing_dict["key_id"],
|
||
api_format=existing_dict.get("api_format", api_format),
|
||
model_name=existing_dict.get("model_name", model_name),
|
||
created_at=existing_dict["created_at"],
|
||
expire_at=existing_dict["expire_at"],
|
||
request_count=existing_dict.get("request_count", 0),
|
||
)
|
||
|
||
if existing_affinity:
|
||
created_at = existing_affinity.created_at
|
||
request_count = existing_affinity.request_count + 1
|
||
|
||
# 检查是否切换了 Provider/Endpoint/Key
|
||
if (
|
||
existing_affinity.provider_id != provider_id
|
||
or existing_affinity.endpoint_id != endpoint_id
|
||
or existing_affinity.key_id != key_id
|
||
):
|
||
self._stats["key_switches"] += 1
|
||
logger.debug(
|
||
f"Key {affinity_key[:8]}... 在 {api_format} 格式下切换后端: "
|
||
f"[{existing_affinity.provider_id[:8]}.../{existing_affinity.endpoint_id[:8]}.../"
|
||
f"{existing_affinity.key_id[:8]}...] → "
|
||
f"[{provider_id[:8]}.../{endpoint_id[:8]}.../{key_id[:8]}...], 重置计数器"
|
||
)
|
||
created_at = current_time
|
||
request_count = 1
|
||
else:
|
||
logger.debug(
|
||
f"刷新缓存亲和性: key={affinity_key[:8]}..., api_format={api_format}, "
|
||
f"provider={provider_id[:8]}..., endpoint={endpoint_id[:8]}..., "
|
||
f"provider_key={key_id[:8]}..., ttl+={ttl}s"
|
||
)
|
||
else:
|
||
created_at = current_time
|
||
request_count = 1
|
||
self._stats["total_affinities"] += 1
|
||
|
||
affinity_dict = {
|
||
"provider_id": provider_id,
|
||
"endpoint_id": endpoint_id,
|
||
"key_id": key_id,
|
||
"api_format": api_format,
|
||
"model_name": model_name,
|
||
"created_at": created_at,
|
||
"expire_at": expire_at,
|
||
"request_count": request_count,
|
||
}
|
||
|
||
await self._save_affinity_dict(cache_key, ttl, affinity_dict)
|
||
|
||
logger.debug(
|
||
f"设置缓存亲和性: key={affinity_key[:8]}..., api_format={api_format}, "
|
||
f"model={model_name}, provider={provider_id[:8]}..., endpoint={endpoint_id[:8]}..., "
|
||
f"provider_key={key_id[:8]}..., ttl={ttl}s"
|
||
)
|
||
except Exception as e:
|
||
logger.exception(f"设置缓存亲和性失败: {e}")
|
||
|
||
async def invalidate_affinity(
|
||
self,
|
||
affinity_key: str,
|
||
api_format: str,
|
||
model_name: str,
|
||
key_id: str | None = None,
|
||
provider_id: str | None = None,
|
||
endpoint_id: str | None = None,
|
||
) -> None:
|
||
"""
|
||
失效指定亲和性标识符对特定API格式和模型的缓存亲和性
|
||
|
||
Args:
|
||
affinity_key: 亲和性标识符(通常为API Key ID)
|
||
api_format: API格式 (claude/openai)
|
||
model_name: 模型名称
|
||
key_id: Provider Key ID(可选,如果提供则只在Key匹配时失效)
|
||
provider_id: Provider ID(可选,如果提供则只在Provider匹配时失效)
|
||
endpoint_id: Endpoint ID(可选,如果提供则只在Endpoint匹配时失效)
|
||
"""
|
||
existing_affinity = await self.get_affinity(affinity_key, api_format, model_name)
|
||
|
||
if not existing_affinity:
|
||
return
|
||
|
||
# 检查是否匹配过滤条件
|
||
should_invalidate = True
|
||
|
||
if key_id and existing_affinity.key_id != key_id:
|
||
should_invalidate = False
|
||
|
||
if provider_id and existing_affinity.provider_id != provider_id:
|
||
should_invalidate = False
|
||
|
||
if endpoint_id and existing_affinity.endpoint_id != endpoint_id:
|
||
should_invalidate = False
|
||
|
||
if not should_invalidate:
|
||
logger.debug(
|
||
f"跳过失效: affinity_key={affinity_key[:8]}..., api_format={api_format}, "
|
||
f"model={model_name}, 过滤条件不匹配 (key={key_id}, provider={provider_id}, endpoint={endpoint_id})"
|
||
)
|
||
return
|
||
|
||
try:
|
||
cache_key = self._get_cache_key(affinity_key, api_format, model_name)
|
||
async with self._acquire_request_lock(cache_key):
|
||
await self._delete_affinity_key(cache_key)
|
||
|
||
self._stats["cache_invalidations"] += 1
|
||
|
||
logger.debug(
|
||
f"失效缓存亲和性: affinity_key={affinity_key[:8]}..., api_format={api_format}, "
|
||
f"model={model_name}, provider={existing_affinity.provider_id[:8]}..., "
|
||
f"endpoint={existing_affinity.endpoint_id[:8]}..., "
|
||
f"provider_key={existing_affinity.key_id[:8]}..."
|
||
)
|
||
except Exception as e:
|
||
logger.exception(f"删除缓存亲和性失败: {e}")
|
||
|
||
async def invalidate_all_for_provider(self, provider_id: str) -> int:
|
||
"""
|
||
失效所有与指定Provider相关的缓存亲和性
|
||
|
||
用途:当Provider关闭缓存支持时调用
|
||
|
||
Args:
|
||
provider_id: Provider ID
|
||
|
||
Returns:
|
||
失效的亲和性数量
|
||
"""
|
||
try:
|
||
invalidated_count = 0
|
||
|
||
if not self._is_memory_backend():
|
||
cursor: int | str = 0
|
||
while True:
|
||
cursor, scan_keys = await self.redis.scan(
|
||
cursor=cursor,
|
||
match="cache_affinity:*",
|
||
count=self.REDIS_SCAN_BATCH_SIZE,
|
||
)
|
||
|
||
if scan_keys:
|
||
# Pipeline batch GET to reduce round-trips.
|
||
values = await self.redis.mget(scan_keys)
|
||
keys_to_delete: list[str] = []
|
||
for key, raw in zip(scan_keys, values):
|
||
if not raw:
|
||
continue
|
||
try:
|
||
data = json.loads(raw)
|
||
except Exception:
|
||
continue
|
||
if data.get("provider_id") == provider_id:
|
||
keys_to_delete.append(key)
|
||
|
||
if keys_to_delete:
|
||
deleted = await self._delete_redis_keys(keys_to_delete)
|
||
invalidated_count += deleted
|
||
self._stats["cache_invalidations"] += deleted
|
||
# Clear L1 for deleted keys.
|
||
for key in keys_to_delete:
|
||
await self._set_l1_entry(key, None)
|
||
|
||
if int(cursor) == 0:
|
||
break
|
||
else:
|
||
keys = list((await self._snapshot_memory_items()).keys())
|
||
for key in keys:
|
||
affinity_dict = await self._load_affinity_dict(key)
|
||
if not affinity_dict:
|
||
continue
|
||
|
||
if affinity_dict.get("provider_id") == provider_id:
|
||
await self._delete_affinity_key(key)
|
||
invalidated_count += 1
|
||
self._stats["cache_invalidations"] += 1
|
||
|
||
if invalidated_count > 0:
|
||
logger.debug(
|
||
f"批量失效Provider缓存亲和性: provider={provider_id[:8]}..., "
|
||
f"失效数量={invalidated_count}"
|
||
)
|
||
|
||
return invalidated_count
|
||
|
||
except Exception as e:
|
||
logger.exception(f"批量失效Provider缓存亲和性失败: {e}")
|
||
return 0
|
||
|
||
async def clear_all(self) -> int:
|
||
"""
|
||
清除所有缓存亲和性(管理功能)
|
||
|
||
Returns:
|
||
清除的数量
|
||
"""
|
||
try:
|
||
if not self._is_memory_backend():
|
||
count = await self._scan_delete_pattern("cache_affinity:*")
|
||
await self._clear_l1_entries_by_prefix("cache_affinity:")
|
||
if count:
|
||
logger.debug(f"清除所有Redis缓存亲和性: {count} 个")
|
||
return count
|
||
|
||
lock = self._get_memory_lock()
|
||
async with lock:
|
||
count = len(self._memory_store)
|
||
self._memory_store.clear()
|
||
await self._clear_l1_entries_by_prefix("cache_affinity:")
|
||
if count:
|
||
logger.debug(f"清除所有内存缓存亲和性: {count} 个")
|
||
return count
|
||
except Exception as e:
|
||
logger.exception(f"清除缓存亲和性失败: {e}")
|
||
return 0
|
||
|
||
def get_stats(self) -> dict[str, Any]:
|
||
"""获取统计信息"""
|
||
cache_hit_rate = 0.0
|
||
total_requests = self._stats["cache_hits"] + self._stats["cache_misses"]
|
||
|
||
if total_requests > 0:
|
||
cache_hit_rate = self._stats["cache_hits"] / total_requests
|
||
|
||
storage_type = "redis" if not self._is_memory_backend() else "memory"
|
||
|
||
return {
|
||
"storage_type": storage_type,
|
||
"total_affinities": self._stats["total_affinities"],
|
||
"cache_hits": self._stats["cache_hits"],
|
||
"cache_misses": self._stats["cache_misses"],
|
||
"cache_hit_rate": cache_hit_rate,
|
||
"cache_invalidations": self._stats["cache_invalidations"],
|
||
"provider_switches": self._stats["provider_switches"],
|
||
"key_switches": self._stats["key_switches"],
|
||
"config": {
|
||
"default_ttl": self.default_ttl,
|
||
},
|
||
}
|
||
|
||
async def list_affinities(self) -> list[dict[str, Any]]:
|
||
"""获取所有缓存亲和性列表
|
||
|
||
返回的每条记录包含:
|
||
- affinity_key: 亲和性标识符(通常是 API Key ID)
|
||
- provider_id, endpoint_id, key_id: Provider 相关信息
|
||
- api_format, model_name: API 格式和模型名称
|
||
- created_at, expire_at, request_count: 缓存元数据
|
||
"""
|
||
results: list[dict[str, Any]] = []
|
||
|
||
try:
|
||
pattern = "cache_affinity:*"
|
||
cursor = 0
|
||
|
||
if not self._is_memory_backend():
|
||
while True:
|
||
cursor, keys = await self.redis.scan(cursor=cursor, match=pattern, count=200)
|
||
|
||
if keys:
|
||
values = await self.redis.mget(*keys)
|
||
for cache_key, data in zip(keys, values):
|
||
if not data:
|
||
continue
|
||
|
||
try:
|
||
affinity = json.loads(data)
|
||
# 解析 cache_affinity:{affinity_key}:{api_format}:{model_name}
|
||
parts = cache_key.split(":")
|
||
affinity_key_value = parts[1] if len(parts) > 1 else cache_key
|
||
api_format = (
|
||
parts[2]
|
||
if len(parts) > 2
|
||
else affinity.get("api_format", "unknown")
|
||
)
|
||
model_name = (
|
||
parts[3]
|
||
if len(parts) > 3
|
||
else affinity.get("model_name", "unknown")
|
||
)
|
||
|
||
affinity["affinity_key"] = affinity_key_value
|
||
if "api_format" not in affinity:
|
||
affinity["api_format"] = api_format
|
||
if "model_name" not in affinity:
|
||
affinity["model_name"] = model_name
|
||
results.append(affinity)
|
||
except json.JSONDecodeError as e:
|
||
logger.exception(f"解析缓存亲和性记录失败: {cache_key} - {e}")
|
||
|
||
if cursor == 0:
|
||
break
|
||
else:
|
||
snapshot = await self._snapshot_memory_items()
|
||
expired_keys: list[str] = []
|
||
current_time = time.time()
|
||
|
||
for cache_key, affinity in snapshot.items():
|
||
if current_time > affinity["expire_at"]:
|
||
expired_keys.append(cache_key)
|
||
continue
|
||
|
||
# 解析 cache_affinity:{affinity_key}:{api_format}:{model_name}
|
||
parts = cache_key.split(":")
|
||
affinity_key_value = parts[1] if len(parts) > 1 else cache_key
|
||
api_format = (
|
||
parts[2] if len(parts) > 2 else affinity.get("api_format", "unknown")
|
||
)
|
||
model_name = (
|
||
parts[3] if len(parts) > 3 else affinity.get("model_name", "unknown")
|
||
)
|
||
|
||
affinity_with_key = dict(affinity)
|
||
affinity_with_key["affinity_key"] = affinity_key_value
|
||
if "api_format" not in affinity_with_key:
|
||
affinity_with_key["api_format"] = api_format
|
||
if "model_name" not in affinity_with_key:
|
||
affinity_with_key["model_name"] = model_name
|
||
results.append(affinity_with_key)
|
||
|
||
# 清理过期的键
|
||
if expired_keys:
|
||
async with self._get_memory_lock():
|
||
for key in expired_keys:
|
||
self._memory_store.pop(key, None)
|
||
|
||
except Exception as e:
|
||
logger.exception(f"获取缓存亲和性列表失败: {e}")
|
||
|
||
return results
|
||
|
||
|
||
# 全局单例
|
||
_affinity_manager: CacheAffinityManager | None = None
|
||
|
||
|
||
async def get_affinity_manager(redis_client: Any | None = None) -> CacheAffinityManager:
|
||
"""
|
||
获取全局CacheAffinityManager实例(若Redis不可用则降级为内存模式)
|
||
|
||
Args:
|
||
redis_client: Redis客户端(可选)
|
||
|
||
Returns:
|
||
CacheAffinityManager实例
|
||
"""
|
||
global _affinity_manager
|
||
|
||
if _affinity_manager is None:
|
||
_affinity_manager = CacheAffinityManager(redis_client)
|
||
elif redis_client and _affinity_manager.redis is None:
|
||
# 当最初使用内存后 Redis 可用时,升级为 Redis 存储
|
||
_affinity_manager = CacheAffinityManager(redis_client)
|
||
|
||
return _affinity_manager
|