本文记录一个AI写作辅助产品从Day 1到日活10万的架构演进过程。三次大重构,每次都是被业务逼着走的。不是最佳实践,是真实经历。
阶段一:单体应用(0-1000用户)
架构
最初就是一台服务器跑所有东西:
用户 → Nginx → FastAPI(单进程) → OpenAI API
│
└─ SQLite(存对话记录)
代码结构也很简单:
python
# app.py - 一个文件搞定
from fastapi import FastAPI
from openai import OpenAI
app = FastAPI()
client = OpenAI(api_key="your-key")
@app.post("/chat")
async def chat(prompt: str):
response = await client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}]
)
return {"text": response.choices[0].message.content}
这个阶段的特点
- 没有缓存:每次请求都调API,相同问题也重新生成
- 没有队列:请求同步处理,用户等着
- 没有监控:出问题靠用户投诉
- 数据库用SQLite:单文件,够用
暴露的问题
用户量到500左右时,开始出现两个问题:
问题1:API成本失控
很多用户问的是相似问题("帮我润色这段话""改写得更正式"),每次都调GPT-4o,月成本8000元。
问题2:高峰期响应慢
晚上8-10点是高峰,请求堆积,单进程处理不过来,响应时间从2秒飙到15秒。
阶段二:加缓存和队列(1000-1万用户)
架构变更
用户 → Nginx → FastAPI(多worker) → Redis缓存 → 命中则直接返回
│ ↓ 未命中
├─ Celery队列 → OpenAI API
│
└─ PostgreSQL(换掉SQLite)
三个关键改动:
改动1:语义缓存
不是简单的key-value缓存,而是语义缓存------意思相近的请求复用结果:
python
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity
class SemanticCache:
"""基于向量相似度的缓存"""
def __init__(self, redis_client, embedder):
self.redis = redis_client
self.embedder = embedder # 用轻量模型做embedding
async def get(self, prompt: str, threshold=0.92):
"""查找语义相似的缓存"""
query_vec = await self.embedder.embed(prompt)
# 从redis拿所有缓存的向量
keys = self.redis.keys("cache:*")
for key in keys:
cached = self.redis.hgetall(key)
cached_vec = np.frombuffer(cached["vector"], dtype=np.float32)
similarity = cosine_similarity([query_vec], [cached_vec])[0][0]
if similarity > threshold:
return cached["response"] # 命中
return None
async def set(self, prompt: str, response: str):
"""写入缓存"""
vec = await self.embedder.embed(prompt)
key = f"cache:{hash(prompt)}"
self.redis.hset(key, mapping={
"prompt": prompt,
"response": response,
"vector": vec.tobytes(),
"created_at": time.time()
})
self.redis.expire(key, 3600) # 1小时过期
效果:缓存命中率35%,API成本降了30%。
改动2:异步队列
长文本生成走队列,用户先拿到任务ID,完成后回调通知:
python
from celery import Celery
celery = Celery("tasks", broker="redis://localhost:6379")
@app.post("/chat/async")
async def chat_async(prompt: str):
# 先查缓存
cached = await cache.get(prompt)
if cached:
return {"status": "done", "text": cached}
# 进队列
task = celery.send_task("generate", args=[prompt])
return {"status": "processing", "task_id": task.id}
@celery.task
def generate(prompt):
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}]
)
text = response.choices[0].message.content
# 写缓存
cache.set(prompt, text)
return text
效果:高峰期不再堆积,用户体验从"等15秒"变成"立即返回processing,30秒后拿结果"。
改动3:PostgreSQL + 连接池
SQLite扛不住并发写。换PostgreSQL,加连接池:
python
from sqlalchemy.ext.asyncio import create_async_engine
engine = create_async_engine(
"postgresql+asyncpg://user:pass@localhost/db",
pool_size=20,
max_overflow=10,
pool_pre_ping=True
)
这个阶段踩的坑
坑:Celery worker内存泄漏
Celery默认不复用连接,每个任务都新建OpenAI客户端,长时间运行后内存暴涨。解决方案:
python
# celery配置:每个worker处理100个任务后重启
celery.conf.worker_max_tasks_per_child = 100
阶段三:微服务化(1万-10万用户)
架构变更
┌─ API网关(认证/限流/路由)
│
用户 → Nginx ───────┤
├─ 写作服务(核心对话)
├─ 摘要服务(长文本摘要)
├─ 审核服务(内容安全)
├─ 计费服务(用量统计)
└─ 监控服务(指标采集)
│
├─ Redis集群
├─ PostgreSQL主从
└─ 消息队列(Kafka)
为什么拆服务
单体应用在1万用户时遇到了三个瓶颈:
- 部署耦合:改一个功能要重启整个应用,影响所有用户
- 资源竞争:文本生成是CPU密集型,审核是IO密集型,混在一起互相影响
- 团队协作:3个开发改同一个代码库,冲突频繁
拆分原则
不是按"技术层"拆(前端/后端/数据库),而是按业务能力拆:
| 服务 | 职责 | 技术栈 |
|---|---|---|
| 写作服务 | 核心对话生成 | Python + FastAPI |
| 摘要服务 | 长文本压缩 | Python + Celery |
| 审核服务 | 内容安全检查 | Go(高性能) |
| 计费服务 | Token统计+账单 | Java + Spring |
服务间通信
同步调用(gRPC):写作服务调用审核服务,需要实时结果
python
# 写作服务 → 审核服务
import grpc
async def check_content(text):
async with grpc.aio.insecure_channel('review:50051') as channel:
stub = ReviewStub(channel)
result = await stub.Check(ContentRequest(text=text))
return result.safe
异步消息(Kafka):写作服务发事件给计费服务,不需要等待
python
# 写作服务发事件
from kafka import KafkaProducer
producer = KafkaProducer(bootstrap_servers='kafka:9092')
def emit_usage_event(user_id, model, tokens):
producer.send('usage_events', json.dumps({
"user_id": user_id,
"model": model,
"tokens": tokens,
"timestamp": time.time()
}).encode())
API网关设计
网关承担认证、限流、路由三个职责:
python
class APIGateway:
"""统一网关入口"""
def __init__(self):
self.routes = {
"/chat": "writing-service:8001",
"/summary": "summary-service:8002",
"/usage": "billing-service:8003",
}
self.rate_limiter = SlidingWindowLimiter(
window=60, max_requests=30
)
async def handle(self, request):
# 1. 认证
token = request.headers.get("Authorization")
user = await self.authenticate(token)
if not user:
return Response(401, "未授权")
# 2. 限流
if not self.rate_limiter.allow(user.id):
return Response(429, "请求过于频繁")
# 3. 路由
service = self.routes.get(request.path)
if not service:
return Response(404, "路径不存在")
# 4. 转发
response = await self.forward(service, request, user)
# 5. 记录
self.log_request(user, request, response)
return response
这个阶段踩的坑
坑1:分布式事务
用户调用写作服务扣了Token,但计费服务没收到事件(Kafka消费失败),导致账目不平。
解决方案:最终一致性 + 对账机制。每天凌晨跑对账任务,比对写作服务日志和计费服务记录,修复差异。
坑2:服务雪崩
审核服务挂了,写作服务等待超时,线程池耗尽,整个系统瘫痪。
解决方案:熔断+降级。审核服务超时时,降级为关键词过滤:
python
async def safe_generate(text):
try:
# 正常流程:调用审核服务
safe = await asyncio.wait_for(
review_service.check(text),
timeout=2
)
if not safe:
return "内容不合规"
except asyncio.TimeoutError:
# 降级:用本地关键词过滤
if contains_unsafe_keywords(text):
return "内容不合规"
# 审核通过,继续生成
return await writing_service.generate(text)
坑3:日志分散
每个服务有自己的日志,排查问题时要翻5个服务的日志。
解决方案:统一链路追踪。每个请求分配trace_id,所有服务日志带这个ID:
python
import uuid
from contextvars import ContextVar
trace_id_var: ContextVar[str] = ContextVar("trace_id")
@app.middleware("http")
async def add_trace_id(request, call_next):
trace_id = request.headers.get("X-Trace-Id") or str(uuid.uuid4())
trace_id_var.set(trace_id)
response = await call_next(request)
response.headers["X-Trace-Id"] = trace_id
return response
# 日志格式统一带trace_id
logging.basicConfig(format='%(asctime)s [%(trace_id)s] %(message)s')
阶段四:当前的架构(10万+用户)
完整架构
┌─ CDN(静态资源)
│
用户 → 负载均衡 ─────────┤
├─ API网关集群(3节点)
│
┌────┴────┐
│ │
写作服务集群 摘要服务集群
(5节点) (3节点)
│
┌─────┼─────┐
│ │ │
Redis PG主从 Kafka
集群 (1主2从) (3节点)
│
┌─────┴─────┐
│ │
审核服务 计费服务
(Go,2节点) (Java,2节点)
│
┌─────┴─────┐
│ │
监控服务 对账服务
(定时任务) (每日凌晨)
关键设计决策
1. 写作服务用多模型路由
不同质量要求的请求走不同模型:
python
def select_model(user_tier, prompt):
"""按用户等级和请求复杂度选模型"""
if user_tier == "free":
return "gpt-4o-mini"
if user_tier == "pro":
if len(prompt) > 2000:
return "gpt-4o" # 长文本用强模型
return "gpt-4o-mini" # 短文本用便宜模型
return "gpt-4o" # 企业版默认强模型
2. 预生成+队列消费
对高频场景(如"润色"),预生成一批结果放队列,用户请求时直接消费:
python
# 后台预生成任务
@celery.task
def pre_generate_common_requests():
common_prompts = get_trending_prompts() # 从日志挖高频请求
for prompt in common_prompts:
result = generate(prompt)
cache.set(prompt, result, ttl=3600)
3. 多级缓存
L1: 进程内缓存(LRU,100条)→ 命中延迟0ms
L2: Redis集群 → 命中延迟1ms
L3: 语义缓存 → 命中延迟5ms
L4: API调用 → 延迟500-2000ms
python
class MultiLevelCache:
async def get(self, prompt):
# L1
if prompt in self.l1_cache:
return self.l1_cache[prompt]
# L2
result = await self.redis.get(prompt)
if result:
self.l1_cache[prompt] = result # 回填L1
return result
# L3 语义缓存
result = await self.semantic_cache.get(prompt)
if result:
await self.redis.set(prompt, result) # 回填L2
self.l1_cache[prompt] = result # 回填L1
return result
return None # 未命中,走API
架构演进的核心教训
教训1:不要过早优化
阶段一的SQLite和单进程,在1000用户以内完全够用。过早引入Redis、Celery、微服务,只会增加运维负担,拖慢业务迭代。
教训2:监控先行
每次架构升级前,先上监控。没有监控的架构升级是盲飞------你不知道新架构到底比旧的好不好。
python
# 最小监控集
metrics = {
"request_count": Counter("请求总数"),
"request_latency": Histogram("请求延迟"),
"error_count": Counter("错误数"),
"cache_hit_rate": Gauge("缓存命中率"),
"active_users": Gauge("活跃用户数"),
}
教训3:拆服务要按业务能力
不要按技术层拆("把数据库操作拆出去"),要按业务能力拆("写作"和"审核"是两个独立能力)。技术层拆分只会制造耦合,业务能力拆分才能独立部署。
教训4:降级比高可用更重要
追求100%可用性成本极高。更实际的做法是:每个核心功能都有降级方案。审核服务挂了用关键词过滤,缓存挂了直连API,队列挂了同步处理。用户能接受偶尔变慢,不能接受完全不可用。
总结
三年三次大重构,每次都是被业务增长逼着走的。架构没有好坏,只有合不合适。1000用户时的"烂架构"可能比10万用户时的"好架构"更合理------因为前者能快速迭代,后者虽然稳定但迭代慢。
选架构的唯一标准:当前阶段最简单的能扛住的方案。