微服务架构设计实战:从单体到分布式

DNS等一等
2025-12-12 17:03
阅读 225

凌晨两点,窗外的上海还在下雨。我又一次把咖啡杯灌满——这已经是今晚第三杯了。公司楼下的便利店早就关门了,好在我租房就在附近,不然真怕自己猝死在这工位上。

我是那种典型的“手写代码党”:讨厌 IDE 自动生成的模板代码,觉得那玩意儿像泡面调料包,方便但没灵魂。虽然最近也被逼着用了一些 AI 工具辅助 review 逻辑、生成文档注释,但我始终坚信,真正的好代码,是人一行一行敲出来的,不是 prompt 喂出来的

这篇文章的起因,说来有点扎心:去年双11前一个月,我们那个“祖传”的 Django 单体应用差点崩了。数据库连接池爆满、接口响应时间飙到 5s+,运维兄弟在群里连发三条“快救火!”,产品经理在旁边幽幽地说:“要不……咱们搞微服务?”

我翻了个白眼:说得轻巧,你倒是来改啊?

但现实很骨感。领导拍板了,deadline 就在眼前,再不拆,系统就得陪我们熬通宵了。于是,在无数个深夜和周末,我和后端组两个兄弟硬是把一个 20w 行的 Python 单体应用,一点点“肢解”成了十几个微服务。

今天这篇,就结合我的实战经验,聊聊怎么从单体走向分布式,顺便吐槽下那些“纸上谈兵”的架构图,到底在生产环境里能活几天。


一、为什么非得拆?单体真有那么不堪?

先别急着骂“微服务是银弹”。我一开始也觉得:能跑就行,何必折腾?我们的老系统用的是 Django + PostgreSQL + Redis,前端是 Vue(没错,我虽然是后端,但也帮前端联调过不少次,知道他们也不容易)。

但问题来了:

  • 所有业务逻辑挤在一个 repo,改个用户头像上传,得跑完整测试套件,CI 跑半小时;
  • 某个模块内存泄漏,整个进程挂掉,全站 502;
  • 发布一次新功能,得停机 10 分钟——运营小妹直接冲进技术部:“你们又搞事情?”

最要命的是,扩展性几乎为零。比如订单服务流量暴增,但商品服务很闲,可我们只能整体扩容,浪费资源不说,成本还高。

所以,拆,不是为了赶时髦,而是被逼到墙角了。


二、拆之前,先想清楚:拆什么?怎么拆?

很多人一上来就说“按业务域拆”,听起来很专业,但实操时容易翻车。我们踩的第一个坑就是:边界划得太理想化

比如,最初我们想把“用户中心”单独拆出来,结果发现登录、权限、通知、消息全都耦合在里面,牵一发而动全身。后来我们学乖了,采用 Strangler Fig Pattern(绞杀者模式):新功能走新服务,旧功能逐步迁移,而不是一次性重写。

服务划分原则(血泪总结):

  • 高内聚低耦合:一个服务只干一件事,比如 order-service 只管下单、查单,不碰库存。
  • 数据自治:每个服务有自己的数据库,绝不共享表!我们吃过亏,早期两个服务共用一张 users 表,结果一个加字段,另一个直接报错。
  • 独立部署:能单独上线、回滚,不依赖其他服务编译。

我们最终拆出的核心服务包括:

服务名 功能 技术栈
user-service 用户注册/登录/资料 Python + FastAPI + PostgreSQL
product-service 商品管理 Python + Flask + MongoDB
order-service 订单创建/查询 Python + FastAPI + PostgreSQL
notification-service 站内信/邮件/SMS Python + Celery + RabbitMQ
gateway API 网关 Nginx + Lua (后期换 Kong)

注意:前端这边也得配合。以前是单页应用直连后端,现在得通过网关统一入口。我们让前端同学加了个 /api/v1/order 这样的代理配置,避免跨域问题。


三、技术选型:Python 微服务,真的行吗?

我知道很多人一提微服务就想到 Java + Spring Cloud,或者 Go + gRPC。但作为 Python 老兵,我偏不信邪。

Web 框架对比

框架 启动速度 异步支持 学习曲线 我们的选择
Django 有限(3.1+) ❌(太重)
Flask 无原生异步 ✅(product-service)
FastAPI 极快 原生 async/await ✅✅(主力)
Sanic 原生异步 ⚠️(社区小)

最后我们主推 FastAPI:自动生成 OpenAPI 文档、Pydantic 校验超强、异步性能吊打 Flask。而且写起来清爽,符合我“代码可读性至上”的强迫症。

举个例子,一个创建订单的接口:

# order-service/main.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import asyncio

app = FastAPI(title="Order Service")

class CreateOrderRequest(BaseModel):
    user_id: int
    product_id: int
    quantity: int

@app.post("/orders")
async def create_order(req: CreateOrderRequest):
    # 这里可以 await 调用其他服务或 DB
    if req.quantity <= 0:
        raise HTTPException(status_code=400, detail="Invalid quantity")
    
    # 模拟异步操作
    await asyncio.sleep(0.1)
    return {"order_id": "ORD123456", "status": "created"}

看,类型安全、自动校验、异步支持,这才是 Python 写微服务该有的样子


四、通信机制:REST 还是 RPC?

我们内部吵过很久。Java 组力推 gRPC,说性能高、强类型。但考虑到团队熟悉度和前端对接成本,我们最终选择了 REST over HTTP/JSON,理由很现实:

  • 前端同学只会 fetch 和 axios;
  • 调试方便,Postman 直接测;
  • 日志、监控工具对 HTTP 支持更成熟。

不过,对于内部高频调用(比如 order → inventory),我们用了 RabbitMQ + 异步消息,避免同步阻塞。

比如用户下单后,order-service 发一条消息到 order.created 队列,inventory-service 监听并扣减库存:

# order-service/publish.py
import pika

def publish_order_created(order_data):
    connection = pika.BlockingConnection(pika.ConnectionParameters('rabbitmq'))
    channel = connection.channel()
    channel.queue_declare(queue='order.created')
    channel.basic_publish(
        exchange='',
        routing_key='order.created',
        body=json.dumps(order_data)
    )
    connection.close()

这样即使 inventory 服务挂了,消息也不会丢(前提是配置了持久化)。

吐槽一句:运维兄弟第一次配 RabbitMQ 集群,忘了开持久化,结果测试环境重启后消息全没了。我当时真的想砸键盘……


五、数据一致性:分布式事务怎么搞?

这是最头疼的问题。单体时代,一个 @transaction.atomic 就搞定;现在,user 扣钱、order 创建、inventory 减库存,三个服务,怎么保证要么全成功,要么全失败?

我们没上 Seata 那种重型方案(太重,Python 支持差),而是用了 Saga 模式 + 补偿事务

流程如下:

  1. order-service 创建订单(状态:pending)
  2. 发送 order.created 消息
  3. inventory-service 扣库存,成功则发 inventory.success
  4. payment-service 扣款,成功则发 payment.success
  5. order-service 收到两个 success,更新订单为 confirmed
  6. 如果任一环节失败,发 compensate 消息,逆向操作(比如加回库存)

关键点:所有操作必须幂等!我们给每个事件加了唯一 ID,重复消费直接忽略。

# inventory-service/consumer.py
processed_events = set()

def handle_order_created(event_id, data):
    if event_id in processed_events:
        return  # 幂等处理
    try:
        deduct_stock(data['product_id'], data['quantity'])
        processed_events.add(event_id)
        publish('inventory.success', event_id)
    except Exception as e:
        publish('inventory.failed', event_id, error=str(e))

虽然 Saga 不能保证强一致性,但对我们电商场景来说,最终一致性 + 人工兜底足够了。毕竟,真出问题,客服还能手动补单嘛(笑)。


六、网关与认证:别让每个服务都写 login 逻辑

初期我们犯了个低级错误:每个服务都复制一套 JWT 验证代码。后来实在受不了,统一在 API Gateway 做认证。

我们先用 Nginx + Lua 写了个简单网关,验证 token 后把 user_id 注入 header:

location /api/ {
    access_by_lua_block {
        local jwt = require "resty.jwt"
        local token = ngx.req.get_headers()["Authorization"]
        if not token then
            ngx.exit(401)
        end
        local verified = jwt:verify("secret_key", token)
        if not verified.verified then
            ngx.exit(401)
        end
        ngx.req.set_header("X-User-ID", verified.payload.user_id)
    }
    proxy_pass http://backend;
}

后来流量大了,换成 Kong,插件生态更丰富,还能限流、日志聚合。

真实事故:有一次 secret_key 泄露,测试环境 token 被伪造,差点删库。从此我们上了 Vault 动态管理密钥——安全这东西,不吃亏不知道痛


七、监控与日志:没有可观测性,等于裸奔

微服务最大的噩梦不是写代码,是线上出问题找不到锅

我们搭了 ELK(Elasticsearch + Logstash + Kibana)收集日志,每个服务启动时注入 trace_id:

# middleware.py
import uuid
from fastapi import Request

@app.middleware("http")
async def add_trace_id(request: Request, call_next):
    trace_id = request.headers.get("X-Trace-ID") or str(uuid.uuid4())
    request.state.trace_id = trace_id
    response = await call_next(request)
    response.headers["X-Trace-ID"] = trace_id
    return response

这样,从前端发起的一个请求,所有服务的日志都能通过 trace_id 串起来。

再加上 Prometheus + Grafana 监控 QPS、延迟、错误率,终于敢睡觉了。


八、写在最后:代码人生,哪有银弹?

从去年双11到现在,系统稳如老狗。订单峰值 QPS 从 200 提升到 2000+,发布频率从月更变成天更。最重要的是,我不用再因为一个 bug 通宵了

但我也清醒得很:微服务不是万能药。它带来了复杂度、运维成本、调试难度。如果你的业务还没到那个量级,别为了“架构”而架构

我依然坚持手写每一行代码,哪怕用 AI 辅助,也只是让它帮我生成 docstring 或单元测试模板。因为我知道,代码人生,终究是人的修行

前端同学上周还调侃我:“你这微服务拆得,连泡面都得分碗吃。”
我回他:“总比一群人抢一碗强。”

好了,天快亮了,我去睡了。希望这篇能帮到正在挣扎的你。

—— 一个住在公司隔壁、靠咖啡续命的 Python 手艺人

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝