Skip to content

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) ​

当时的架构 ​

python
# 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 进程
  • 调试方便,不用跨服务
  • 没有分布式问题

什么时候该重构 ​

三个信号出现任何一个就该考虑重构了:

  1. 代码文件超过 1000 行:修改一个功能要在文件里上下翻找
  2. 多人协作冲突频繁:Git merge 经常打架
  3. 功能耦合导致测试困难:改存储逻辑怕影响生成逻辑

我们是在 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 | 日志 | 配置            │   │
│  └──────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────┘

分层规范 ​

python
# 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)

关键改进 ​

改进点之前之后
数据库SQLitePostgreSQL(并发支持好)
进程模型单进程 uvicornGunicorn 多 worker
配置管理硬编码环境变量 + .env
日志printlogging 结构化日志
测试无pytest + 单元测试

模块化单体的价值 ​

这一步非常关键——不要跳过模块化单体直接上微服务。原因:

  1. 模块化单体帮你理清业务边界,这是微服务拆分的前提
  2. 单体内重构成极低,改个接口签名不用协调其他服务
  3. 如果模块化单体就够用,根本不需要微服务

很多团队直接从单体脚本跳到微服务,结果拆分边界乱七八糟,服务间调用像蜘蛛网。

阶段三:微服务拆分 ​

触发条件 ​

三个硬需求叠加,才决定拆微服务:

  1. LLM 推理服务需要独立扩缩容:内容生成是 CPU/GPU 密集型,其他服务是 IO 密集型,混在一起扩容浪费资源
  2. 团队规模到 5 人:3 人以下模块化单体够用,5 人以上单仓库协作开始吃力
  3. 需要独立部署节奏:生成服务要频繁迭代 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 密集 → 拆开
团队边界不同人负责 → 拆开
数据一致性强一致需求 → 不要拆,放一起
调用频率高频互调 → 不要拆,网络开销大

服务间通信 ​

python
# 同步调用:用户鉴权(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 编排 ​

yaml
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 模式 + 补偿事务。

python
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 贯穿全链路。

python
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+人)
             │
             ├── 需要独立扩缩容? → 是 → 微服务
             └── 团队需要独立迭代? → 是 → 微服务
                                         否 → 维持模块化单体

三个关键认知:

  1. 单体不是坏架构:中小项目用单体是最务实的选择,微服务是规模到了才需要
  2. 模块化单体是必经阶段:它帮你理清业务边界,为微服务拆分做准备
  3. 微服务拆的是业务边界不是技术层:按业务域拆,每个服务能独立部署、独立扩缩容

最后一句:架构演进没有终点,只有「当前阶段最合适的架构」。不要为了微服务而微服务,也不要因为「以后可能要拆」就提前拆。业务驱动架构,不是架构驱动业务。

📖本文阅读--次|📊全站访问--次|👥访客--人