在开发一个FastAPI应用时,我们经常会遇到性能瓶颈和调试困难的问题。最近我在重构一个电商平台的商品详情接口时,发现当并发请求量达到500QPS时,数据库查询直接占用了90%的响应时间。更糟糕的是,当线上出现异常时,我们往往只能看到"Internal Server Error"这样的模糊提示,却无法快速定位问题根源。
这就是为什么我们需要给FastAPI应用装上"缓存"和"日志"这两只翅膀。缓存可以显著减轻数据库压力,将原本需要200ms的查询缩短到5ms以内;而完善的日志系统则像飞机的黑匣子,能记录下每个请求的完整轨迹,帮助我们快速复现和解决问题。
在这个项目中,我们选择的技术栈组合是:
提示:Redis选择6.x以上版本以获得更好的TLS支持和内存优化
典型的请求处理流程如下:
python复制# 伪代码示例
async def get_product(product_id: str):
cache_key = f"product:{product_id}"
if (cached := await redis.get(cache_key)):
return JSON.parse(cached)
product = await db.query("SELECT * FROM products...")
await redis.setex(cache_key, 3600, JSON.dumps(product))
return product
缓存设计需要考虑以下几个关键点:
键名规范:采用类型:id[:子类型]的命名空间格式,例如:
product:123 商品基础信息product:123:inventory 库存信息过期时间:
序列化方式:
python复制# 带缓存的商品查询实现示例
async def get_product_with_cache(product_id: int):
cache = RedisCache()
serializer = JSONSerializer()
# 尝试从缓存获取
cache_key = f"product:{product_id}"
cached_data = await cache.get(cache_key)
if cached_data:
return serializer.deserialize(cached_data)
# 数据库查询
product = await Product.get(product_id)
if not product:
return None
# 写入缓存
await cache.setex(
cache_key,
ttl=3600,
value=serializer.serialize(product.dict())
)
return product
在实际项目中,我们需要特别注意以下缓存问题:
缓存击穿:热点key过期瞬间大量请求直达数据库
python复制lock_key = f"lock:{cache_key}"
if await redis.setnx(lock_key, 1, ex=5):
try:
# 查询数据库
data = await db.query(...)
await redis.setex(cache_key, ttl, data)
finally:
await redis.delete(lock_key)
缓存雪崩:大量key同时过期
python复制base_ttl = 3600
jitter = random.randint(-300, 300)
real_ttl = base_ttl + jitter
缓存穿透:查询不存在的数据
python复制if product is None:
await cache.setex(cache_key, 300, "NULL")
Loguru相比标准logging库提供了更人性化的API:
python复制from loguru import logger
import sys
logger.add(
"app_{time:YYYY-MM-DD}.log",
rotation="500 MB",
retention="30 days",
compression="zip",
enqueue=True,
backtrace=True,
diagnose=True,
level="INFO"
)
# 在中间件中记录请求日志
@app.middleware("http")
async def log_requests(request: Request, call_next):
start_time = time.time()
response = await call_next(request)
process_time = (time.time() - start_time) * 1000
logger.info(
"Request completed",
path=request.url.path,
method=request.method,
status=response.status_code,
latency=f"{process_time:.2f}ms"
)
return response
现代日志系统需要支持结构化数据:
python复制logger.bind(
user_id=user.id,
request_id=request.state.request_id,
client_ip=request.client.host
).info("User action recorded")
这允许我们在ELK或Loki等日志系统中执行类似SQL的查询:
code复制{app="product-service"} | json | status=500 | latency > 1000
在记录日志时必须注意数据安全:
python复制def sanitize_data(data: dict):
sensitive_fields = ["password", "token", "credit_card"]
return {
k: "***" if k in sensitive_fields else v
for k, v in data.items()
}
logger.info("User data", data=sanitize_data(user.dict()))
对于热点数据,我们可以采用预热策略:
python复制@app.on_event("startup")
async def warmup_cache():
hot_products = await Product.filter(is_hot=True)
for p in hot_products:
await cache.set(f"product:{p.id}", p.dict())
python复制@scheduler.scheduled_job("interval", minutes=30)
def refresh_hot_products():
# 更新缓存逻辑
对于超高并发场景,可以考虑多级缓存:
lru_cache缓存少量热点数据python复制from functools import lru_cache
@lru_cache(maxsize=1024)
async def get_product_name(product_id: int):
return await Product.get(product_id).name
即使有缓存,数据库查询仍需优化:
sql复制CREATE INDEX idx_product_category ON products(category_id);
python复制# 不好的写法
for order in orders:
product = await Product.get(order.product_id)
# 好的写法
product_ids = [o.product_id for o in orders]
products = await Product.filter(id__in=product_ids)
products_map = {p.id: p for p in products}
关键指标监控配置示例:
python复制from prometheus_client import Counter, Histogram
REQUEST_COUNT = Counter(
'app_requests_total',
'Total request count',
['method', 'endpoint', 'status']
)
REQUEST_LATENCY = Histogram(
'app_request_latency_seconds',
'Request latency',
['method', 'endpoint']
)
@app.middleware("http")
async def monitor_requests(request: Request, call_next):
start_time = time.time()
response = await call_next(request)
latency = time.time() - start_time
REQUEST_COUNT.labels(
method=request.method,
endpoint=request.url.path,
status=response.status_code
).inc()
REQUEST_LATENCY.labels(
method=request.method,
endpoint=request.url.path
).observe(latency)
return response
推荐监控的关键指标:
问题1:缓存更新后,客户端仍然看到旧数据
问题2:Redis内存使用率过高
redis-cli --bigkeys问题1:日志文件增长过快
问题2:生产环境日志缺失
示例Dockerfile关键配置:
dockerfile复制FROM python:3.9-slim
# 安装依赖
RUN apt-get update && apt-get install -y \
build-essential \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*
# 安装Python依赖
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 复制应用代码
COPY . .
# 日志目录
RUN mkdir /var/log/app && chown nobody /var/log/app
USER nobody
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
Kubernetes健康检查示例:
yaml复制livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 8000
initialDelaySeconds: 5
periodSeconds: 5
在实际部署中,我发现使用ConfigMap管理应用配置,结合Reloader实现配置热更新,可以显著减少重启次数。对于12因子应用来说,环境变量的管理尤为重要,特别是在Kubernetes环境中,通过PodPreset可以优雅地注入通用环境变量。