微服务架构设计实战:从单体到分布式
凌晨两点,窗外的上海还在下雨。我又一次把咖啡杯灌满——这已经是今晚第三杯了。公司楼下的便利店早就关门了,好在我租房就在附近,不然真怕自己猝死在这工位上。
我是那种典型的“手写代码党”:讨厌 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 模式 + 补偿事务。
流程如下:
- order-service 创建订单(状态:pending)
- 发送
order.created消息 - inventory-service 扣库存,成功则发
inventory.success - payment-service 扣款,成功则发
payment.success - order-service 收到两个 success,更新订单为 confirmed
- 如果任一环节失败,发
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