refactor: 移除 Python 后端源码,全面迁移至 Rust gateway 架构

- 删除全部 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)
This commit is contained in:
fawney19
2026-04-03 16:26:16 +08:00
parent 8f26e1a31f
commit 1d9c77522a
868 changed files with 1735 additions and 2433 deletions

View File

@@ -0,0 +1,757 @@
"""
缓存亲和性管理器 (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 IDapi_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