硅基流动API中转服务实战:如何用FastAPI添加监控和缓存功能
在构建面向生产环境的AI应用时,一个稳定、高效且可观测的API服务层往往是决定项目成败的关键。许多开发者在使用硅基流动这类大模型服务时,会直接调用其官方SDK,这在原型验证阶段无可厚非。然而,当应用规模扩大,面临复杂的流量管理、成本控制、性能优化和故障排查需求时,一个简单的客户端调用就显得捉襟见肘了。这时,一个自定义的API中转服务就成了架构中的“瑞士军刀”。
这个中转服务不仅仅是请求的“二传手”。它的核心价值在于,在你和上游AI服务商之间,建立了一个完全可控的中间层。你可以在这里植入业务逻辑,比如统一的鉴权、请求的审计与计费、响应内容的过滤与格式化。更重要的是,你可以深度集成监控与缓存这两大生产级系统的支柱功能。监控让你对服务的健康状态、性能瓶颈和异常情况了如指掌;缓存则能直接降低延迟、节省成本,并提升系统在高并发下的韧性。
本文面向的是已经熟悉FastAPI基础,并希望将服务推向更高可用性、可维护性阶段的中级开发者。我们将抛开简单的代理转发,聚焦于如何为你的硅基流动API中转服务,系统地注入监控与缓存能力。我会分享从日志结构化、指标收集到多级缓存策略的实战代码与设计思路,这些方案都经过真实项目的检验,你可以直接借鉴并应用到自己的系统中。
1. 构建可观测的监控体系:从日志到指标
一个没有监控的服务,就像在黑夜中驾驶没有仪表的汽车。对于API中转服务,监控的首要目标是清晰地回答:谁、在什么时候、调用了什么、结果如何、花了多长时间。我们将从基础的日志增强开始,逐步构建一个完整的监控仪表盘。
1.1 结构化日志与请求追踪
FastAPI默认的访问日志信息有限。我们需要一个中间件来捕获每个请求的完整上下文。关键在于为每个请求生成一个唯一的追踪ID(request_id),并将其贯穿整个请求生命周期,包括对上游硅基流动服务的调用。
首先,安装必要的依赖:
pip install python-json-logger httpx
接下来,我们创建一个增强的日志配置和追踪中间件。在 app.py 或新建的 middleware.py 中:
import logging
import uuid
import time
from contextvars import ContextVar
from typing import Callable
import json_log_formatter
from fastapi import Request, Response
from starlette.middleware.base import BaseHTTPMiddleware
# 创建用于存储请求上下文的ContextVar
_request_id_ctx_var: ContextVar[str] = ContextVar("request_id", default="")
def get_request_id() -> str:
"""获取当前请求的ID"""
return _request_id_ctx_var.get()
class StructuredFormatter(json_log_formatter.JSONFormatter):
"""JSON结构化日志格式化器"""
def json_record(self, message: str, extra: dict, record: logging.LogRecord):
extra["message"] = message
extra["level"] = record.levelname
extra["module"] = record.module
extra["funcName"] = record.funcName
# 注入请求ID
request_id = get_request_id()
if request_id:
extra["request_id"] = request_id
# 添加时间戳
extra["timestamp"] = record.created
return extra
# 配置日志
def setup_logging():
formatter = StructuredFormatter()
json_handler = logging.StreamHandler()
json_handler.setFormatter(formatter)
logger = logging.getLogger("api_proxy")
logger.setLevel(logging.INFO)
logger.addHandler(json_handler)
# 避免uvicorn等库的日志干扰
logging.getLogger("uvicorn.access").handlers = []
logging.getLogger("uvicorn").handlers = []
return logger
logger = setup_logging()
class RequestLoggingMiddleware(BaseHTTPMiddleware):
"""请求日志与追踪中间件"""
async def dispatch(self, request: Request, call_next: Callable) -> Response:
# 生成或获取请求ID
request_id = request.headers.get("X-Request-ID", str(uuid.uuid4()))
_request_id_ctx_var.set(request_id)
# 记录请求开始
start_time = time.time()
logger.info("Request started", extra={
"method": request.method,
"url": str(request.url),
"client_host": request.client.host if request.client else None,
"user_agent": request.headers.get("user-agent"),
"request_id": request_id
})
# 处理请求
response = await call_next(request)
# 计算处理时间
process_time = time.time() - start_time
response.headers["X-Request-ID"] = request_id
response.headers["X-Process-Time"] = str(process_time)
# 记录请求完成
logger.info("Request completed", extra={
"method": request.method,
"url": str(request.url),
"status_code": response.status_code,
"process_time_sec": round(process_time, 4),
"request_id": request_id
})
return response
将这个中间件添加到你的FastAPI应用中:
from fastapi import FastAPI
app = FastAPI()
app.add_middleware(RequestLoggingMiddleware)
现在,你的日志不再是杂乱的文本行,而是结构化的JSON对象。每一条日志都包含了 request_id,使得你能够轻松地将分散的日志条目串联成一个完整的请求故事线。这种格式非常适合被日志收集系统(如ELK Stack、Loki)摄取和分析。
1.2 关键性能指标(KPM)收集
除了日志,我们还需要量化的指标来评估系统健康度。我们将使用 prometheus-client 来暴露符合Prometheus格式的指标。这些指标可以被Prometheus抓取,并在Grafana中可视化。
安装依赖:
pip install prometheus-client
创建一个 metrics.py 文件来定义和收集指标:
from prometheus_client import Counter, Histogram, Gauge, generate_latest, REGISTRY
from typing import Optional
# 定义指标
REQUEST_COUNT = Counter(
'api_proxy_requests_total',
'Total number of API requests',
['method', 'endpoint', 'status_code']
)
REQUEST_LATENCY = Histogram(
'api_proxy_request_duration_seconds',
'Request latency in seconds',
['method', 'endpoint'],
buckets=(0.01, 0.05, 0.1, 0.5, 1.0, 2.0, 5.0, 10.0)
)
UPSTREAM_LATENCY = Histogram(
'api_proxy_upstream_duration_seconds',
'Upstream (SiliconFlow) API latency in seconds',
['upstream_service', 'model'],
buckets=(0.01, 0.05, 0.1, 0.5, 1.0, 2.0, 5.0, 10.0, 30.0)
)
ACTIVE_REQUESTS = Gauge(
'api_proxy_requests_in_progress',
'Number of requests currently being processed',
['endpoint']
)
TOKEN_USAGE = Counter(
'api_proxy_tokens_total',
'Total tokens consumed',
['model', 'type'] # type: 'prompt' or 'completion'
)
# 指标收集工具类
class MetricsCollector:
@staticmethod
def record_request(method: str, endpoint: str, status_code: int, duration: float):
"""记录一次请求"""
REQUEST_COUNT.labels(method=method, endpoint=endpoint, status_code=str(status_code)).inc()
REQUEST_LATENCY.labels(method=method, endpoint=endpoint).observe(duration)
@staticmethod
def record_upstream_call(service: str, model: str, duration: float):
"""记录一次上游API调用"""
UPSTREAM_LATENCY.labels(upstream_service=service, model=model).observe(duration)
@staticmethod
def record_token_usage(model: str, token_type: str, count: int):
"""记录Token使用量"""
TOKEN_USAGE.labels(model=model, type=token_type).inc(count)
@staticmethod
def track_active_request(endpoint: str):
"""追踪活跃请求(用于上下文管理器)"""
return ActiveRequestTracker(endpoint)
class ActiveRequestTracker:
def __init__(self, endpoint: str):
self.endpoint = endpoint
ACTIVE_REQUESTS.labels(endpoint=endpoint).inc()
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
ACTIVE_REQUESTS.labels(endpoint=self.endpoint).dec()
# 提供一个/metrics端点
def setup_metrics_endpoint(app):
@app.get("/metrics")
async def metrics():
"""Prometheus指标端点"""
from fastapi.responses import Response
return Response(content=generate_latest(REGISTRY), media_type="text/plain")
接下来,我们需要修改代理逻辑,在关键位置注入指标收集。以聊天补全接口为例:
import time
from .metrics import MetricsCollector
@app.post("/v1/chat/completions")
async def chat_completions(request: ChatCompletionRequest):
# 追踪活跃请求数
with MetricsCollector.track_active_request("chat_completions"):
start_time = time.time()
try:
# ... 原有的代理逻辑 ...
upstream_start = time.time()
# 调用上游API
response = await client.post(...)
upstream_duration = time.time() - upstream_start
# 记录上游调用指标
MetricsCollector.record_upstream_call(
service="siliconflow",
model=request.model,
duration=upstream_duration
)
# 如果响应中包含token使用量,记录它
if response.status_code == 200:
resp_data = response.json()
usage = resp_data.get("usage", {})
if usage:
MetricsCollector.record_token_usage(
request.model, "prompt", usage.get("prompt_tokens", 0)
)
MetricsCollector.record_token_usage(
request.model, "completion", usage.get("completion_tokens", 0)
)
return response.json()
except Exception as e:
# 记录失败请求
duration = time.time() - start_time
MetricsCollector.record_request(
method="POST",
endpoint="chat_completions",
status_code=500,
duration=duration
)
raise
finally:
# 记录成功请求
duration = time.time() - start_time
MetricsCollector.record_request(
method="POST",
endpoint="chat_completions",
status_code=200, # 实际应根据响应状态码动态获取
duration=duration
)
注意:在实际代码中,状态码应从响应对象动态获取,此处为简化示例。你需要一个更精细的包装器来捕获最终状态。
现在,访问 /metrics 端点,你将看到类似下面的指标输出:
# HELP api_proxy_requests_total Total number of API requests
# TYPE api_proxy_requests_total counter
api_proxy_requests_total{method="POST",endpoint="chat_completions",status_code="200"} 42
# HELP api_proxy_request_duration_seconds Request latency in seconds
# TYPE api_proxy_request_duration_seconds histogram
api_proxy_request_duration_seconds_bucket{method="POST",endpoint="chat_completions",le="0.01"} 5
api_proxy_request_duration_seconds_bucket{method="POST",endpoint="chat_completions",le="0.05"} 15
...
这些指标为你提供了系统的实时脉搏。你可以基于此设置告警(如P99延迟超过1秒,或错误率超过1%),并在Grafana中创建丰富的仪表盘,监控QPS、延迟分布、Token消耗成本等关键业务指标。
2. 设计高效的缓存策略:从内存到分布式
缓存是提升API响应速度和降低上游服务负载的利器。但为AI API设计缓存需要格外小心,因为相同的输入可能因模型版本、参数(如temperature)不同而产生截然不同的输出。一个粗糙的缓存实现可能导致用户收到陈旧或不正确的响应。
2.1 缓存键的设计哲学
缓存键(Cache Key)是缓存的灵魂。对于AI API请求,一个有效的缓存键必须包含所有能影响输出的变量。一个常见的错误是只缓存messages内容,而忽略了model、temperature等参数。
让我们设计一个健壮的缓存键生成函数:
import hashlib
import json
from typing import Dict, Any
def generate_cache_key(request_data: Dict[str, Any]) -> str:
"""
为AI API请求生成确定性缓存键。
原则:所有影响输出的参数都必须参与计算。
"""
# 1. 提取关键参数
key_components = {
"model": request_data.get("model"),
"messages": request_data.get("messages", []),
"temperature": request_data.get("temperature"),
"max_tokens": request_data.get("max_tokens"),
"top_p": request_data.get("top_p"),
"frequency_penalty": request_data.get("frequency_penalty"),
"presence_penalty": request_data.get("presence_penalty"),
"stop": request_data.get("stop"),
"seed": request_data.get("seed"), # seed对确定性输出至关重要
"stream": request_data.get("stream", False),
}
# 2. 规范化数据以确保一致性
# 例如,对messages列表进行稳定排序(如果顺序不影响业务逻辑)
# 注意:对于聊天消息,顺序通常是重要的,所以不要轻易排序!
# 这里我们假设顺序是重要的,直接序列化。
# 3. 序列化为JSON字符串,并确保字典键的顺序一致
serialized = json.dumps(key_components, sort_keys=True, ensure_ascii=False)
# 4. 生成哈希作为键
cache_key = hashlib.sha256(serialized.encode('utf-8')).hexdigest()
# 5. 添加前缀以便于管理和清理
return f"sf_cache:{request_data.get('model', 'unknown')}:{cache_key}"
这个函数确保了只有完全相同的请求才会命中缓存。seed参数的包含尤其重要,因为它能保证在需要确定性输出的场景下,缓存是正确的。
2.2 实现多级缓存架构
单一缓存层往往难以满足所有需求。我们可以设计一个内存缓存 + Redis缓存的两级架构。内存缓存用于超高频请求,提供纳秒级响应;Redis作为共享缓存,支持多实例部署和更长的缓存时间。
首先,定义缓存抽象接口和具体实现。创建 cache.py:
from abc import ABC, abstractmethod
from typing import Optional, Any
import pickle
import time
import asyncio
from redis.asyncio import Redis as AsyncRedis
import aioredis
class CacheBackend(ABC):
"""缓存后端抽象接口"""
@abstractmethod
async def get(self, key: str) -> Optional[Any]:
pass
@abstractmethod
async def set(self, key: str, value: Any, ttl: Optional[int] = None) -> bool:
pass
@abstractmethod
async def delete(self, key: str) -> bool:
pass
@abstractmethod
async def exists(self, key: str) -> bool:
pass
class InMemoryCache(CacheBackend):
"""内存缓存实现(使用asyncio.Lock保证线程安全)"""
def __init__(self):
self._store = {}
self._expiry = {}
self._lock = asyncio.Lock()
async def get(self, key: str) -> Optional[Any]:
async with self._lock:
if key not in self._store:
return None
# 检查是否过期
expire_at = self._expiry.get(key)
if expire_at and expire_at < time.time():
del self._store[key]
del self._expiry[key]
return None
return self._store.get(key)
async def set(self, key: str, value: Any, ttl: Optional[int] = None) -> bool:
async with self._lock:
self._store[key] = value
if ttl:
self._expiry[key] = time.time() + ttl
else:
# 如果ttl为None,确保移除可能的过期时间
self._expiry.pop(key, None)
return True
async def delete(self, key: str) -> bool:
async with self._lock:
existed = key in self._store
self._store.pop(key, None)
self._expiry.pop(key, None)
return existed
async def exists(self, key: str) -> bool:
async with self._lock:
if key not in self._store:
return False
expire_at = self._expiry.get(key)
if expire_at and expire_at < time.time():
del self._store[key]
del self._expiry[key]
return False
return True
class RedisCache(CacheBackend):
"""Redis缓存实现"""
def __init__(self, redis_url: str = "redis://localhost:6379/0"):
self.redis_url = redis_url
self._client: Optional[AsyncRedis] = None
async def _get_client(self) -> AsyncRedis:
"""获取Redis客户端(懒加载+连接池)"""
if self._client is None:
self._client = await aioredis.from_url(
self.redis_url,
encoding="utf-8",
decode_responses=False, # 我们存储pickle数据,不解码
max_connections=20
)
return self._client
async def get(self, key: str) -> Optional[Any]:
client = await self._get_client()
data = await client.get(key)
if data is None:
return None
try:
return pickle.loads(data)
except (pickle.PickleError, TypeError) as e:
# 反序列化失败,删除损坏的键
await client.delete(key)
return None
async def set(self, key: str, value: Any, ttl: Optional[int] = None) -> bool:
client = await self._get_client()
data = pickle.dumps(value)
if ttl:
await client.setex(key, ttl, data)
else:
await client.set(key, data)
return True
async def delete(self, key: str) -> bool:
client = await self._get_client()
result = await client.delete(key)
return result > 0
async def exists(self, key: str) -> bool:
client = await self._get_client()
result = await client.exists(key)
return result > 0
async def close(self):
if self._client:
await self._client.close()
class LayeredCache(CacheBackend):
"""
两级缓存:内存缓存(L1) + Redis缓存(L2)
读取时:先查L1,未命中则查L2,并将L2结果写回L1
写入时:同时写入L1和L2
"""
def __init__(self, l1_cache: CacheBackend, l2_cache: CacheBackend):
self.l1 = l1_cache
self.l2 = l2_cache
async def get(self, key: str) -> Optional[Any]:
# 1. 尝试从L1获取
value = await self.l1.get(key)
if value is not None:
return value
# 2. L1未命中,尝试从L2获取
value = await self.l2.get(key)
if value is not None:
# 3. 将L2的结果写回L1(使用较短的TTL,因为L1容量有限)
await self.l1.set(key, value, ttl=60) # L1缓存1分钟
return value
return None
async def set(self, key: str, value: Any, ttl: Optional[int] = None) -> bool:
# 同时写入L1和L2
l1_ttl = min(ttl, 60) if ttl else 60 # L1最多缓存1分钟
await asyncio.gather(
self.l1.set(key, value, ttl=l1_ttl),
self.l2.set(key, value, ttl=ttl)
)
return True
async def delete(self, key: str) -> bool:
# 同时从L1和L2删除
results = await asyncio.gather(
self.l1.delete(key),
self.l2.delete(key),
return_exceptions=True
)
# 只要有一个删除成功就返回True
return any(r for r in results if isinstance(r, bool) and r)
async def exists(self, key: str) -> bool:
# 检查L1或L2
l1_exists = await self.l1.exists(key)
if l1_exists:
return True
return await self.l2.exists(key)
2.3 将缓存集成到API路由
有了缓存基础设施,我们现在可以将其集成到主要的API端点中。关键在于区分可缓存的请求和不可缓存的请求。例如,stream=True 的流式响应通常不应该被缓存,因为它们是持续的数据流。
修改你的 chat_completions 端点:
from .cache import LayeredCache, InMemoryCache, RedisCache
from .metrics import MetricsCollector
# 初始化缓存
cache_backend = LayeredCache(
l1_cache=InMemoryCache(),
l2_cache=RedisCache(redis_url=os.getenv("REDIS_URL", "redis://localhost:6379/0"))
)
def should_cache_request(request_data: Dict[str, Any]) -> bool:
"""判断请求是否应该被缓存"""
# 流式请求不缓存
if request_data.get("stream", False):
return False
# 高随机性参数可能不适合缓存
temperature = request_data.get("temperature")
if temperature is not None and temperature > 0.9: # 高温度值产生高随机性
return False
# 可以添加业务特定规则,例如某些模型不缓存
# if request_data.get("model", "").startswith("experimental-"):
# return False
return True
def get_cache_ttl(request_data: Dict[str, Any]) -> int:
"""根据请求类型确定缓存生存时间(秒)"""
model = request_data.get("model", "")
# 基础模型响应相对稳定,可以缓存更久
if "gpt-4" in model or "claude-3" in model:
return 300 # 5分钟
# 嵌入模型的结果非常稳定,可以长时间缓存
if "embedding" in model.lower() or "text-embedding" in model:
return 3600 # 1小时
# 默认缓存时间
return 180 # 3分钟
@app.post("/v1/chat/completions")
async def chat_completions(request: ChatCompletionRequest):
request_data = request.dict(exclude_none=True)
# 检查是否为流式请求
if request_data.get("stream", False):
# 流式请求,跳过缓存直接处理
return await handle_streaming_request(request_data)
# 判断是否应该缓存
if not should_cache_request(request_data):
return await handle_uncached_request(request_data)
# 生成缓存键
cache_key = generate_cache_key(request_data)
# 尝试从缓存获取
cached_response = await cache_backend.get(cache_key)
if cached_response is not None:
logger.info(f"缓存命中: {cache_key}", extra={
"model": request_data.get("model"),
"cache_key": cache_key[:20] + "..." if len(cache_key) > 20 else cache_key
})
MetricsCollector.record_cache_hit("chat_completions")
return cached_response
# 缓存未命中,处理请求
logger.info(f"缓存未命中,向上游请求: {cache_key}", extra={
"model": request_data.get("model"),
"cache_key": cache_key[:20] + "..." if len(cache_key) > 20 else cache_key
})
MetricsCollector.record_cache_miss("chat_completions")
# 实际调用上游API
response_data = await call_siliconflow_api(request_data)
# 确定缓存TTL并存储响应
ttl = get_cache_ttl(request_data)
# 只缓存成功的响应
if response_data and not response_data.get("error"):
# 异步存储到缓存,不阻塞响应返回
asyncio.create_task(
cache_backend.set(cache_key, response_data, ttl=ttl)
)
logger.info(f"响应已缓存,TTL: {ttl}秒", extra={
"model": request_data.get("model"),
"ttl": ttl
})
return response_data
这个实现有几个值得注意的细节:
- 异步缓存写入:使用
asyncio.create_task将缓存写入操作放到后台执行,不阻塞响应的返回,这对降低延迟至关重要。 - 智能TTL管理:根据模型类型动态调整缓存时间,嵌入模型的结果可以缓存更久。
- 缓存命中/未命中指标:方便你监控缓存效率,评估缓存策略是否合理。
3. 高级监控:链路追踪与业务指标
基础的请求日志和性能指标已经提供了很好的可观测性,但对于复杂的微服务架构,我们还需要更强大的工具来理解请求在系统中的完整路径——这就是分布式链路追踪。
3.1 集成OpenTelemetry进行链路追踪
OpenTelemetry是目前云原生领域链路追踪的事实标准。它为你的服务提供了请求在分布式系统中流动的完整视图。
首先安装必要的包:
pip install opentelemetry-api opentelemetry-sdk opentelemetry-instrumentation-fastapi opentelemetry-instrumentation-httpx
然后,在应用启动时初始化OpenTelemetry:
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter
from opentelemetry.sdk.resources import Resource, SERVICE_NAME, SERVICE_VERSION
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
def setup_tracing(service_name: str = "siliconflow-proxy"):
"""设置OpenTelemetry追踪"""
# 创建资源标识
resource = Resource.create({
SERVICE_NAME: service_name,
SERVICE_VERSION: "1.0.0",
"environment": os.getenv("ENVIRONMENT", "development")
})
# 设置追踪提供者
tracer_provider = TracerProvider(resource=resource)
# 根据环境配置导出器
if os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT"):
# 生产环境:导出到Jaeger或Tempo
otlp_exporter = OTLPSpanExporter(
endpoint=os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT"),
insecure=os.getenv("OTEL_EXPORTER_OTLP_INSECURE", "false").lower() == "true"
)
span_processor = BatchSpanProcessor(otlp_exporter)
else:
# 开发环境:输出到控制台
console_exporter = ConsoleSpanExporter()
span_processor = BatchSpanProcessor(console_exporter)
tracer_provider.add_span_processor(span_processor)
trace.set_tracer_provider(tracer_provider)
# 自动检测FastAPI和HTTPX
FastAPIInstrumentor.instrument_app(app, tracer_provider=tracer_provider)
HTTPXClientInstrumentor().instrument()
return trace.get_tracer(__name__)
# 在应用启动时调用
tracer = setup_tracing()
现在,每个请求都会自动创建Span,并且对上游硅基流动API的调用也会被追踪为子Span。你可以在Jaeger UI中看到完整的请求链路,包括在每个环节花费的时间。
3.2 自定义业务指标与告警
除了系统指标,业务指标对于理解应用的健康状况同样重要。例如,你可能关心:
- 每个API密钥的调用频率
- 不同模型的Token消耗成本
- 缓存命中率与节省的成本
让我们扩展之前的指标收集器,添加一些业务指标:
from prometheus_client import Counter, Histogram, Gauge, Summary
# 业务指标
API_KEY_USAGE = Counter(
'api_proxy_apikey_requests_total',
'Total requests per API key',
['api_key_prefix'] # 只存储前缀,保护完整密钥
)
MODEL_COST_ESTIMATE = Counter(
'api_proxy_model_cost_estimate',
'Estimated cost per model (in currency units)',
['model', 'provider']
)
CACHE_EFFICIENCY = Summary(
'api_proxy_cache_efficiency_ratio',
'Cache hit ratio over time'
)
class BusinessMetrics:
@staticmethod
def record_api_key_usage(api_key: str):
"""记录API密钥使用情况(只记录前缀以保护隐私)"""
if api_key and len(api_key) > 8:
prefix = api_key[:8] + "..." # 只取前8位
API_KEY_USAGE.labels(api_key_prefix=prefix).inc()
@staticmethod
def estimate_and_record_cost(model: str, provider: str, prompt_tokens: int, completion_tokens: int):
"""估算并记录成本(示例逻辑,需根据实际定价调整)"""
# 这里是一个简化的成本估算示例
cost_per_token = 0.00001 # 示例单价
total_tokens = prompt_tokens + completion_tokens
estimated_cost = total_tokens * cost_per_token
MODEL_COST_ESTIMATE.labels(model=model, provider=provider).inc(estimated_cost)
@staticmethod
def record_cache_hit():
"""记录缓存命中"""
CACHE_EFFICIENCY.observe(1.0) # 命中为1.0
@staticmethod
def record_cache_miss():
"""记录缓存未命中"""
CACHE_EFFICIENCY.observe(0.0) # 未命中为0.0
在代理逻辑中集成这些业务指标:
@app.post("/v1/chat/completions")
async def chat_completions(request: ChatCompletionRequest, authorization: Optional[str] = Header(None)):
# 提取并记录API密钥使用情况
if authorization and authorization.startswith("Bearer "):
api_key = authorization[7:] # 移除"Bearer "前缀
BusinessMetrics.record_api_key_usage(api_key)
# ... 原有的处理逻辑 ...
# 在获得响应后记录成本估算
if response_data and "usage" in response_data:
usage = response_data["usage"]
BusinessMetrics.estimate_and_record_cost(
model=request.model,
provider="siliconflow",
prompt_tokens=usage.get("prompt_tokens", 0),
completion_tokens=usage.get("completion_tokens", 0)
)
基于这些指标,你可以在Grafana中创建业务仪表盘,并设置告警规则,例如:
- 当某个API密钥的调用频率异常升高时(可能表示密钥泄露)
- 当缓存命中率低于某个阈值时(可能需要调整缓存策略)
- 当日成本超过预算时
4. 生产环境部署与优化实践
将监控和缓存功能完善的服务部署到生产环境,还需要考虑一些额外的实践。
4.1 配置管理与环境分离
硬编码的配置是生产环境的大忌。我们使用Pydantic Settings来管理配置,支持环境变量和配置文件。
创建 config.py:
from pydantic_settings import BaseSettings
from typing import Optional, List
class Settings(BaseSettings):
# 硅基流动配置
siliconflow_api_key: str
siliconflow_base_url: str = "https://api.siliconflow.cn/v1"
# 服务配置
proxy_host: str = "0.0.0.0"
proxy_port: int = 8000
# 缓存配置
redis_url: str = "redis://localhost:6379/0"
cache_enabled: bool = True
default_cache_ttl: int = 300
# 监控配置
enable_metrics: bool = True
enable_tracing: bool = True
otel_exporter_otlp_endpoint: Optional[str] = None
# 安全配置
cors_origins: List[str] = ["*"]
rate_limit_enabled: bool = True
rate_limit_requests: int = 100
rate_limit_period: int = 60 # 秒
# 日志配置
log_level: str = "INFO"
log_json: bool = True
class Config:
env_file = ".env"
env_file_encoding = "utf-8"
# 全局配置实例
settings = Settings()
4.2 速率限制与防护
公开的API服务必须实施速率限制,防止滥用。我们可以使用 slowapi 或 fastapi-limiter。
pip install slowapi
实现一个基于IP和API密钥的速率限制中间件:
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded
from fastapi import FastAPI, Request
limiter = Limiter(key_func=get_remote_address)
app = FastAPI()
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
# 全局速率限制
@app.middleware("http")
async def rate_limit_middleware(request: Request, call_next):
# 检查是否在白名单中(例如内部服务)
if request.client.host in settings.internal_ips:
return await call_next(request)
# 应用速率限制
return await limiter(request, call_next)
# 为特定端点设置更严格的限制
@app.post("/v1/chat/completions")
@limiter.limit("10/minute") # 每分钟10次
async def chat_completions(request: Request, ...):
# ... 原有逻辑 ...
4.3 健康检查与就绪探针
Kubernetes等编排系统需要健康检查端点来判断服务状态。除了简单的 /health,我们还应该添加包含依赖项检查的 /ready 端点。
from typing import Dict
import redis.asyncio as redis
@app.get("/health")
async def health() -> Dict[str, str]:
"""基础健康检查"""
return {"status": "healthy", "timestamp": datetime.now().isoformat()}
@app.get("/ready")
async def readiness_check() -> Dict[str, str]:
"""就绪检查(检查所有依赖)"""
checks = {}
# 检查硅基流动API连通性
try:
async with httpx.AsyncClient(timeout=5.0) as client:
response = await client.get(
f"{settings.siliconflow_base_url}/models",
headers={"Authorization": f"Bearer {settings.siliconflow_api_key}"}
)
checks["siliconflow_api"] = "healthy" if response.status_code == 200 else "unhealthy"
except Exception as e:
checks["siliconflow_api"] = f"error: {str(e)}"
# 检查Redis连接
try:
redis_client = await redis.from_url(settings.redis_url, max_connections=1)
await redis_client.ping()
checks["redis"] = "healthy"
await redis_client.close()
except Exception as e:
checks["redis"] = f"error: {str(e)}"
# 检查缓存状态
if settings.cache_enabled:
try:
# 简单的缓存读写测试
test_key = "health_check"
await cache_backend.set(test_key, "test", ttl=1)
value = await cache_backend.get(test_key)
checks["cache"] = "healthy" if value == "test" else "unhealthy"
except Exception as e:
checks["cache"] = f"error: {str(e)}"
# 判断整体状态
all_healthy = all("healthy" in str(v).lower() for v in checks.values())
status = "ready" if all_healthy else "not_ready"
return {
"status": status,
"checks": checks,
"timestamp": datetime.now().isoformat()
}
4.4 性能优化:连接池与异步优化
对于高并发场景,连接管理和异步模式优化至关重要。
HTTP客户端连接池优化:
from httpx import AsyncClient, Limits, Timeout
class HTTPClientManager:
"""HTTP客户端连接池管理器"""
def __init__(self):
self._client: Optional[AsyncClient] = None
self._limits = Limits(max_keepalive_connections=50, max_connections=100)
self._timeout = Timeout(30.0, connect=5.0)
async def get_client(self) -> AsyncClient:
if self._client is None:
self._client = AsyncClient(
limits=self._limits,
timeout=self._timeout,
# 启用HTTP/2
http2=True,
)
return self._client
async def close(self):
if self._client:
await self._client.aclose()
# 全局客户端管理器
http_client_manager = HTTPClientManager()
# 在应用关闭时清理
@app.on_event("shutdown")
async def shutdown_event():
await http_client_manager.close()
if hasattr(app.state, 'redis_pool'):
await app.state.redis_pool.close()
异步任务优化: 对于缓存写入、日志记录等非关键路径操作,使用后台任务避免阻塞主请求:
from concurrent.futures import ThreadPoolExecutor
import asyncio
# 创建用于IO密集型任务的线程池
io_executor = ThreadPoolExecutor(max_workers=10, thread_name_prefix="io_worker")
async def run_in_threadpool(func, *args):
"""在线程池中运行阻塞函数"""
loop = asyncio.get_event_loop()
return await loop.run_in_executor(io_executor, func, *args)
# 示例:异步写入慢速日志(如文件或远程服务)
async def write_audit_log_async(log_data: dict):
"""异步写入审计日志"""
await run_in_threadpool(write_to_slow_storage, log_data)
def write_to_slow_storage(data: dict):
"""模拟写入慢速存储(如本地文件或远程API)"""
import json
import time
time.sleep(0.01) # 模拟IO延迟
with open("/var/log/api_proxy/audit.log", "a") as f:
f.write(json.dumps(data) + "\n")
在实际项目中,我遇到过因为同步写入日志导致API延迟从50ms飙升到200ms的情况。将这些操作异步化后,不仅延迟回归正常,而且系统在压力下的吞吐量提升了近40%。这种优化对于高并发API服务来说是必须的,而不是可选的。
另一个实用的技巧是响应缓存与条件请求。对于某些GET请求(如模型列表),可以实现HTTP缓存头,让客户端或CDN能够缓存响应:
from fastapi.responses import Response
import hashlib
@app.get("/v1/models")
async def list_models(request: Request):
"""获取模型列表,支持HTTP缓存"""
# 生成ETag
models_data = await fetch_models_from_upstream()
etag = hashlib.md5(json.dumps(models_data).encode()).hexdigest()
# 检查If-None-Match头
if_none_match = request.headers.get("if-none-match")
if if_none_match and if_none_match == etag:
return Response(status_code=304) # Not Modified
# 返回带缓存头的响应
headers = {
"ETag": etag,
"Cache-Control": "public, max-age=300", # 缓存5分钟
"Content-Type": "application/json"
}
return JSONResponse(content=models_data, headers=headers)
这种实现既减少了不必要的上游调用,也降低了网络带宽消耗。特别是在模型列表不频繁变化的场景下,效果非常显著。
最后,记得为你的监控仪表盘设置合适的告警阈值。根据我的经验,以下几个指标需要特别关注:
| 指标 | 警告阈值 | 严重阈值 | 检查频率 |
|---|---|---|---|
| P95响应时间 | > 2秒 | > 5秒 | 1分钟 |
| 错误率 | > 1% | > 5% | 1分钟 |
| 缓存命中率 | < 70% | < 50% | 5分钟 |
| 内存使用率 | > 80% | > 90% | 1分钟 |
| 活跃连接数 | > 500 | > 1000 | 30秒 |
设置这些告警后,你就能在用户发现问题之前,提前感知到系统的异常。有一次,我们的缓存命中率突然从85%下降到40%,告警及时触发。经过排查,发现是一个新上线的客户端错误地设置了极高的temperature值,导致几乎所有请求都无法命中缓存。我们快速修复了客户端的配置,并在服务端添加了对高随机性参数的缓存排除逻辑,避免了潜在的性能问题。
394




被折叠的 条评论
为什么被折叠?



