AI 应用从单体到微服务的架构演进复盘
一个 AI 内容生成平台,从最初 300 行的 Python 脚本,到模块化单体,最终演进出 6 个微服务。用了八个月,重构了三次。这篇复盘每次架构变更的触发条件、决策过程和踩过的坑。
演进时间线
时间轴:
2025.12 2026.03 2026.06 2026.08
│ │ │ │
▼ ▼ ▼ ▼
┌──────┐ ┌────────┐ ┌──────────┐ ┌──────────┐
│ 阶段一 │ │ 阶段二 │ │ 阶段三 │ │ 现状 │
│ 单体 │ ───→ │ 模块化 │ ───→ │ 微服务 │ ───→ │ 6 服务 │
│ 脚本 │ │ 单体 │ │ 拆分 │ │ 稳定运行 │
└──────┘ └────────┘ └──────────┘ └──────────┘
300 行脚本 5000 行工程 6 个微服务 持续迭代
FastAPI 分层架构 独立部署 K8s 编排
SQLite PostgreSQL Redis + PG 消息队列
单进程 Gunicorn Docker Compose K8s阶段一:单体脚本(300 行搞定 MVP)
当时的架构
# app.py — 全部逻辑在一个文件里
from fastapi import FastAPI
import openai
app = FastAPI()
openai.api_key = "sk-xxx"
@app.post("/generate")
def generate(topic: str, style: str = "professional"):
# 1. 构建 Prompt
prompt = f"请用{style}的风格写一篇关于{topic}的文章"
# 2. 调用大模型
response = openai.ChatCompletion.create(
model="gpt-4",
messages=[{"role": "user", "content": prompt}]
)
content = response.choices[0].message.content
# 3. 存 SQLite
import sqlite3
conn = sqlite3.connect("data.db")
conn.execute("INSERT INTO articles VALUES (?, ?, ?)",
(topic, style, content))
conn.commit()
return {"content": content}单体架构图
┌─────────────────────────────────────┐
│ app.py (300 行) │
│ │
│ ┌─────────┐ ┌──────┐ ┌────────┐ │
│ │ FastAPI │→ │ LLM │→ │ SQLite │ │
│ │ 路由 │ │ 调用 │ │ 存储 │ │
│ └─────────┘ └──────┘ └────────┘ │
│ │
│ 全部逻辑挤在一个文件里 │
└─────────────────────────────────────┘为什么从单体开始
这个阶段的原则是快速验证。不要一上来就微服务,那是过度设计。
单体的好处:
- 开发快,一个文件改完就跑
- 部署简单,一个 Python 进程
- 调试方便,不用跨服务
- 没有分布式问题
什么时候该重构
三个信号出现任何一个就该考虑重构了:
- 代码文件超过 1000 行:修改一个功能要在文件里上下翻找
- 多人协作冲突频繁:Git merge 经常打架
- 功能耦合导致测试困难:改存储逻辑怕影响生成逻辑
我们是在 5000 行、3 人协作时触发的重构。
阶段二:模块化单体
重构后的架构
┌─────────────────────────────────────────────────────┐
│ app/ (模块化单体) │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ API 层 (routers/) │ │
│ │ generate.py | article.py | user.py │ │
│ └────────────────────┬─────────────────────────┘ │
│ ▼ │
│ ┌──────────────────────────────────────────────┐ │
│ │ Service 层 (services/) │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │ │
│ │ │Generate │ │Article │ │User │ │ │
│ │ │Service │ │Service │ │Service │ │ │
│ │ └────┬─────┘ └────┬─────┘ └──────┬───────┘ │ │
│ └───────┼────────────┼──────────────┼─────────┘ │
│ ▼ ▼ ▼ │
│ ┌──────────────────────────────────────────────┐ │
│ │ Data 层 (models/ + db/) │ │
│ │ PostgreSQL (替代 SQLite) │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ 基础设施 (config/ + utils/) │ │
│ │ LLM Client | Redis | 日志 | 配置 │ │
│ └──────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘分层规范
# routers/generate.py — 只做请求接收和响应
from fastapi import APIRouter, Depends
from services.generate_service import GenerateService
router = APIRouter()
@router.post("/generate")
def generate(req: GenerateRequest, svc: GenerateService = Depends()):
return svc.generate(req.topic, req.style)
# services/generate_service.py — 业务逻辑
class GenerateService:
def __init__(self, llm_client: LlmClient, article_repo: ArticleRepo):
self.llm = llm_client
self.repo = article_repo
def generate(self, topic: str, style: str) -> dict:
prompt = self._build_prompt(topic, style)
content = self.llm.chat(prompt)
article = self.repo.save(topic, style, content)
return {"id": article.id, "content": content}
# models/article.py — 数据模型
from sqlalchemy import Column, Integer, String, Text
class Article(Base):
__tablename__ = "articles"
id = Column(Integer, primary_key=True)
topic = Column(String(200))
style = Column(String(50))
content = Column(Text)关键改进
| 改进点 | 之前 | 之后 |
|---|---|---|
| 数据库 | SQLite | PostgreSQL(并发支持好) |
| 进程模型 | 单进程 uvicorn | Gunicorn 多 worker |
| 配置管理 | 硬编码 | 环境变量 + .env |
| 日志 | logging 结构化日志 | |
| 测试 | 无 | pytest + 单元测试 |
模块化单体的价值
这一步非常关键——不要跳过模块化单体直接上微服务。原因:
- 模块化单体帮你理清业务边界,这是微服务拆分的前提
- 单体内重构成极低,改个接口签名不用协调其他服务
- 如果模块化单体就够用,根本不需要微服务
很多团队直接从单体脚本跳到微服务,结果拆分边界乱七八糟,服务间调用像蜘蛛网。
阶段三:微服务拆分
触发条件
三个硬需求叠加,才决定拆微服务:
- LLM 推理服务需要独立扩缩容:内容生成是 CPU/GPU 密集型,其他服务是 IO 密集型,混在一起扩容浪费资源
- 团队规模到 5 人:3 人以下模块化单体够用,5 人以上单仓库协作开始吃力
- 需要独立部署节奏:生成服务要频繁迭代 Prompt 逻辑,用户服务要稳定不动
微服务架构图
┌─────────────────┐
│ API Gateway │
│ (Nginx/Kong) │
└────────┬────────┘
│
┌──────────────────┼──────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ User Service │ │Article Service│ │Generate Service│
│ (Python) │ │ (Python) │ │ (Python) │
│ │ │ │ │ │
│ - 注册登录 │ │ - 文章CRUD │ │ - Prompt管理 │
│ - 权限管理 │ │ - 列表分页 │ │ - LLM调用 │
│ - 用户信息 │ │ - 搜索 │ │ - 流式输出 │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
│ ┌──────┴───────┐ │
│ │ │ │
▼ ▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐
│PostgreSQL│ │PostgreSQL│ │ Redis │ │ LLM Service │
│(用户库) │ │(文章库) │ │ (缓存) │ │ (vLLM 推理) │
└──────────┘ └──────────┘ └──────────┘ └──────┬───────┘
│
┌──────┴───────┐
│ GPU Server │
│ (A10 24G) │
└──────────────┘
服务间通信:
- 同步调用:HTTP (用户鉴权、文章查询)
- 异步消息:Redis/RabbitMQ (生成任务、通知)拆分边界怎么定
这是微服务最难的一步。我们的判断方法:
拆分原则:按「变化频率」和「资源需求」拆,不按「技术层」拆
✅ 正确拆法: ❌ 错误拆法:
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│Generate │ │Article │ │API层服务 │ │DB层服务 │
│(频繁迭代) │ │(稳定) │ │(所有路由) │ │(所有数据) │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
按业务域拆,每个服务独立变化 按技术层拆,每个请求跨两次网络拆分判断清单:
| 判断维度 | 说明 |
|---|---|
| 变化频率 | 频繁迭代 vs 稳定不动 → 拆开 |
| 资源需求 | CPU 密集 vs IO 密集 → 拆开 |
| 团队边界 | 不同人负责 → 拆开 |
| 数据一致性 | 强一致需求 → 不要拆,放一起 |
| 调用频率 | 高频互调 → 不要拆,网络开销大 |
服务间通信
# 同步调用:用户鉴权(Generate Service → User Service)
import httpx
async def verify_user(token: str) -> dict:
async with httpx.AsyncClient() as client:
resp = await client.get(
"http://user-service:8001/verify",
headers={"Authorization": token},
timeout=5.0 # 必须设超时
)
if resp.status_code != 200:
raise AuthError("Invalid token")
return resp.json()
# 异步消息:生成任务入队(Article Service → Generate Service)
import redis
import json
r = redis.Redis()
def submit_generate_task(article_id: int, topic: str, style: str):
r.lpush("queue:generate", json.dumps({
"article_id": article_id,
"topic": topic,
"style": style
}))
# Generate Service 消费队列
def consume_generate_tasks():
while True:
_, data = r.brpop("queue:generate", timeout=30)
task = json.loads(data)
generate_article(task)Docker Compose 编排
version: '3.8'
services:
gateway:
image: nginx:alpine
ports: ["80:80"]
volumes: ["./nginx.conf:/etc/nginx/nginx.conf:ro"]
depends_on: [user-service, article-service, generate-service]
user-service:
build: ./services/user
ports: ["8001:8001"]
environment:
- DATABASE_URL=postgresql://user:pass@user-db:5432/users
depends_on: [user-db]
article-service:
build: ./services/article
ports: ["8002:8002"]
environment:
- DATABASE_URL=postgresql://user:pass@article-db:5432/articles
- REDIS_URL=redis://redis:6379
depends_on: [article-db, redis]
generate-service:
build: ./services/generate
ports: ["8003:8003"]
environment:
- LLM_SERVICE_URL=http://llm-service:8000/v1
- REDIS_URL=redis://redis:6379
depends_on: [redis, llm-service]
llm-service:
image: vllm/vllm-openai:latest
deploy:
resources:
reservations:
devices:
- driver: nvidia
capabilities: [gpu]
command: ["--model", "/models/Qwen2-7B-Chat", "--port", "8000"]
user-db:
image: postgres:16-alpine
environment:
POSTGRES_DB: users
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
article-db:
image: postgres:16-alpine
environment:
POSTGRES_DB: articles
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
redis:
image: redis:7-alpine三阶段架构对比
| 维度 | 单体脚本 | 模块化单体 | 微服务 |
|---|---|---|---|
| 代码量 | 300 行 | 5000 行 | 6 服务 × ~1000 行 |
| 部署复杂度 | 极低 | 低 | 中(Docker Compose) |
| 开发效率 | 极高 | 高 | 中(跨服务调试) |
| 扩展性 | 无 | 有限 | 好(独立扩缩容) |
| 团队协作 | 1 人 | 2-3 人 | 4+ 人 |
| 运维成本 | 极低 | 低 | 中高 |
| 适用阶段 | MVP | 产品验证期 | 增长期 |
微服务踩的坑
坑 1:拆太细了,一个请求转三次
最初把「Prompt 构建」和「LLM 调用」拆成两个服务,结果一个生成请求要跨三个服务:Article → Prompt Builder → LLM。延迟翻倍,调试地狱。
解决:Prompt 构建和 LLM 调用合回 Generate Service。微服务不是越细越好,能独立部署和扩缩容的最小业务单元才是合理的边界。
坑 2:分布式事务没考虑,数据不一致
用户扣积分 → 生成文章。扣了积分但生成失败,积分没回滚。
解决:用 Saga 模式 + 补偿事务。
def generate_with_payment(user_id, topic):
# Step 1: 扣积分
try:
user_service.deduct_credits(user_id, 10)
except:
raise PaymentError("积分不足")
# Step 2: 生成文章
try:
article = generate_service.generate(topic)
except:
# 补偿:退还积分
user_service.refund_credits(user_id, 10)
raise GenerateError("生成失败,积分已退还")
return article坑 3:服务发现用硬编码 IP,扩容就崩
最初在配置文件里写死 http://10.0.0.5:8001,容器重启 IP 变了全挂。
解决:用 Docker Compose 的服务名做 DNS,或上 Consul/K8s 做服务发现。
坑 4:没有链路追踪,排查问题靠猜
一个请求跨三个服务,报错了不知道是哪个环节的问题。
解决:上 OpenTelemetry,每个请求带 trace_id 贯穿全链路。
from opentelemetry import trace
tracer = trace.get_tracer(__name__)
@router.post("/generate")
async def generate(req: GenerateRequest):
with tracer.start_as_current_span("generate_api") as span:
span.set_attribute("topic", req.topic)
# 自动传递 trace_id 到下游服务
result = await generate_service.generate(req)
span.set_attribute("result_length", len(result.content))
return result复盘总结
架构演进的核心原则:不要提前设计,按需演进。
架构选择决策树:
项目阶段?
│
├── MVP / 验证期 (1-2人)
│ └── 单体脚本,越简单越好
│
├── 产品成长期 (3-5人)
│ └── 模块化单体,分层清晰
│
└── 规模化期 (5+人)
│
├── 需要独立扩缩容? → 是 → 微服务
└── 团队需要独立迭代? → 是 → 微服务
否 → 维持模块化单体三个关键认知:
- 单体不是坏架构:中小项目用单体是最务实的选择,微服务是规模到了才需要
- 模块化单体是必经阶段:它帮你理清业务边界,为微服务拆分做准备
- 微服务拆的是业务边界不是技术层:按业务域拆,每个服务能独立部署、独立扩缩容
最后一句:架构演进没有终点,只有「当前阶段最合适的架构」。不要为了微服务而微服务,也不要因为「以后可能要拆」就提前拆。业务驱动架构,不是架构驱动业务。