在开发一个FastAPI应用时,单纯实现基础CRUD功能只是完成了最基础的部分。当用户量增长到一定规模,你会发现两个明显的瓶颈:数据库查询开始变慢,问题排查变得困难。这就是我们需要引入缓存和日志系统的根本原因。
我在去年开发一个电商平台API时就深有体会。促销活动期间,商品详情接口的QPS从平时的50激增到3000,直接导致PostgreSQL数据库CPU飙升至90%。紧急添加Redis缓存层后,数据库负载立即降至20%以下。同样,当用户报障"订单支付失败"时,如果没有完善的日志系统,你可能需要花几个小时才能定位到是某个第三方支付接口的超时问题。
FastAPI的异步特性使其特别适合I/O密集型应用。根据我的实测数据,在同等硬件条件下,FastAPI处理并发请求的能力比Flask高出3-5倍。而PostgreSQL作为关系型数据库,不仅支持JSONB等NoSQL特性,其MVCC机制在高并发写入场景下表现尤为出色。
我最终选择Redis主要基于以下几点考量:
一个完整的日志系统需要包含:
首先安装依赖:
bash复制pip install redis fastapi-redis-cache
然后在FastAPI中初始化Redis连接:
python复制from fastapi_redis_cache import FastApiRedisCache
@app.on_event("startup")
def startup():
redis_cache = FastApiRedisCache()
redis_cache.init(
host_url="redis://localhost:6379",
prefix="myapi-cache",
response_header="X-MyAPI-Cache"
)
为商品查询接口添加缓存:
python复制from fastapi_cache.decorator import cache
@app.get("/products/{id}")
@cache(expire=300) # 缓存5分钟
async def get_product(id: int):
# 数据库查询逻辑
重要提示:缓存时间需要根据业务特点调整。对于价格等敏感信息,建议设置较短的过期时间(如30秒),而商品描述等静态内容可以缓存更久。
创建日志存储表:
sql复制CREATE TABLE app_logs (
id SERIAL PRIMARY KEY,
timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(),
level VARCHAR(10) NOT NULL,
service VARCHAR(50) NOT NULL,
message TEXT NOT NULL,
context JSONB,
trace_id VARCHAR(36)
);
这个设计有几个关键点:
创建一个核心日志中间件:
python复制import logging
from contextvars import ContextVar
import uuid
request_id = ContextVar('request_id')
class PostgreSQLHandler(logging.Handler):
def __init__(self, db_conn):
self.db_conn = db_conn
def emit(self, record):
log_entry = {
"level": record.levelname,
"service": "product_service",
"message": record.getMessage(),
"context": record.__dict__.get("extra", {}),
"trace_id": request_id.get(str(uuid.uuid4()))
}
# 异步写入PostgreSQL
await self.db_conn.execute("""
INSERT INTO app_logs (level, service, message, context, trace_id)
VALUES (:level, :service, :message, :context, :trace_id)
""", log_entry)
@app.middleware("http")
async def log_requests(request: Request, call_next):
request_id.set(str(uuid.uuid4()))
start_time = time.time()
response = await call_next(request)
process_time = (time.time() - start_time) * 1000
logger.info(
"Request completed",
extra={
"path": request.url.path,
"method": request.method,
"status": response.status_code,
"latency": f"{process_time}ms"
}
)
return response
当热点key过期时,大量请求直接打到数据库会导致雪崩。我的解决方案是:
python复制from redis.lock import Lock
async def get_product_with_lock(id: int):
cache_key = f"product:{id}"
data = await redis.get(cache_key)
if not data:
lock = Lock(redis, f"lock:{cache_key}", timeout=10)
if await lock.acquire(blocking=False):
try:
# 只有拿到锁的请求才查询数据库
data = await db.query_product(id)
await redis.set(cache_key, data, ex=300)
finally:
await lock.release()
else:
# 其他请求等待100ms后重试缓存
await asyncio.sleep(0.1)
return await get_product_with_lock(id)
return data
随着日志量增长,直接查询PostgreSQL会变慢。我采用的优化策略:
sql复制CREATE INDEX idx_logs_service_time ON app_logs (service, timestamp);
CREATE INDEX idx_logs_trace_id ON app_logs (trace_id);
sql复制CREATE INDEX idx_logs_context ON app_logs USING GIN (context);
在生产环境我推荐使用Redis Sentinel模式:
yaml复制# redis.conf
port 6379
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 60000
使用Linux logrotate工具管理日志文件:
code复制/var/log/myapp/*.log {
daily
rotate 7
compress
delaycompress
missingok
notifempty
create 644 root root
postrotate
kill -USR1 `cat /var/run/myapp.pid`
endscript
}
建议监控这些关键指标:
缓存一致性问题:更新商品信息后,记得同时清除相关缓存。我曾在生产环境因为忘记清除缓存,导致用户看到旧价格持续了5分钟。
日志级别滥用:不要将所有日志都设为ERROR级别,否则真正的问题会被淹没。我的经验法则是:
连接池配置:Redis和PostgreSQL连接池大小不是越大越好。通常设置为:(核心数 * 2) + 有效磁盘数。比如4核服务器带SSD,建议设置10-12个连接。
日志异步写入:直接同步写日志到数据库会导致性能下降。我现在的做法是用内存队列缓冲,由后台线程批量写入:
python复制from concurrent.futures import ThreadPoolExecutor
import queue
log_queue = queue.Queue(maxsize=1000)
executor = ThreadPoolExecutor(max_workers=2)
def log_worker():
while True:
batch = []
for _ in range(100):
try:
batch.append(log_queue.get_nowait())
except queue.Empty:
break
if batch:
db.execute_many("INSERT INTO logs (...) VALUES (...)")
这个方案将日志写入对主线程的影响降到了最低,实测接口延迟仅增加约2ms。