从单体到微服务:一个前测试工程师的血泪实战
去年冬天,上海冷得离谱。我裹着羽绒服坐在公司楼下的全家便利店啃饭团,一边刷着手机里刚收到的紧急邮件——“系统扛不住双11流量,老板点名要拆微服务”。那一刻,我真的想把饭团塞进邮箱里。
我是谁?一个从测试转开发三年的“半路出家”选手,现在在上海一家中型电商公司做后端。以前天天拿着Postman怼接口、写自动化脚本,现在却要亲手把那个祖传单体应用切成几十个微服务。说实话,刚开始接到这个任务时,我心里直打鼓:分布式事务、服务发现、链路追踪……这些词听起来都像天书。但没办法,程序员嘛,不是被需求推着走,就是被 deadline 赶着跑。
今天这篇技术分享,不讲大道理,就聊聊我们是怎么一步步把那个又胖又慢的单体系统拆成微服务的,中间踩了哪些坑,又怎么爬出来的。顺便提一句,虽然标题里有“区块链”,但别误会——我们没上链,只是在设计某些数据不可篡改场景时参考了它的思路(后面会细说)。
单体架构的“甜蜜陷阱”
我们的老系统是典型的 Django + PostgreSQL 单体应用,代码量超过 20 万行。功能模块全挤在一起:用户管理、订单处理、库存、支付、物流……所有业务逻辑在一个代码库里跑。本地开发倒是爽——python manage.py runserver,一切搞定。
但上线后问题就来了:
- 部署慢:改一行日志,全量构建要 8 分钟;
- 扩展难:促销期间订单模块爆了,但库存模块闲得发慌,资源没法按需分配;
- 故障扩散:有一次 Redis 连不上,整个系统直接瘫痪,连登录页都打不开;
- 团队协作撕逼:前端、后端、测试全挤在一个 Git 分支上,PR 合并能吵三天。
最夸张的是去年双11前夜,订单创建接口 QPS 突破 5000,数据库连接池直接打满。运维兄弟在群里吼:“再不优化就准备写事故报告吧!” 那天晚上,我和两个同事在公司通宵,泡面吃到想吐,最后靠加缓存+SQL 优化勉强扛过去。但我们都清楚:这只是缓兵之计。
拆!但怎么拆?
老板一句话:“参考阿里、Netflix,搞微服务。”
我内心 OS:人家有几百人的中间件团队,我们后端加我就五个人……
冷静下来后,我们定了几个原则:
- 按业务边界拆分,不是按技术栈;
- 先拆高频、高风险模块(比如订单、支付);
- 保留单体作为“遗留系统”并行运行,逐步迁移;
- 用 Python,不盲目追新——团队熟悉 Django/Flask,没必要为了微服务硬上 Go。
于是我们画了第一版服务划分图:
| 服务名 | 职责 | 技术栈 |
|---|---|---|
| user-service | 用户注册、登录、鉴权 | Flask + JWT |
| order-service | 创建订单、状态流转 | Django REST |
| inventory-svc | 库存扣减、回滚 | FastAPI |
| payment-svc | 支付回调、对账 | Flask |
注意:我们故意把 order 和 inventory 拆开——因为业务上它们解耦了(下单成功不一定立刻扣库存,比如预售)。但这也带来了分布式事务的大坑。
分布式事务:比产品经理的需求还难搞
想象这个场景:用户下单 → 扣库存 → 创建支付单。
如果扣库存成功,但支付服务挂了,怎么办?总不能让用户付了钱却没货。
我们试过几种方案:
方案一:两阶段提交(2PC)
理论上完美,但实测性能差到哭。一次下单要等 3 个服务来回确认,TPS 直接掉到 50。放弃。
方案二:消息队列 + 最终一致性
用 RabbitMQ 发“订单创建成功”消息,库存服务消费后扣减。但如果消息丢了?或者消费者处理失败?
这时候我想起了区块链里的“不可篡改日志”思想——虽然我们不用区块链,但可以借鉴其“事件溯源”(Event Sourcing)模式。
最终我们搞了个 本地消息表 + 定时补偿 的混合方案:
# order-service 中
from django.db import transaction
def create_order(user_id, items):
with transaction.atomic():
# 1. 创建订单(状态为 PENDING)
order = Order.objects.create(user_id=user_id, status="PENDING")
# 2. 插入本地消息表(确保和订单在同一个事务)
MessageLog.objects.create(
event_type="ORDER_CREATED",
payload={"order_id": order.id, "items": items},
status="PENDING"
)
return order.id
然后有个后台任务每 30 秒扫描 MessageLog,把状态为 PENDING 的消息发给 RabbitMQ。如果发送成功,更新状态为 SENT;如果失败,重试 3 次后告警。
库存服务收到消息后,同样用本地事务处理:
# inventory-service 中
def handle_order_created(payload):
order_id = payload["order_id"]
items = payload["items"]
with transaction.atomic():
# 扣库存
for item in items:
Stock.objects.select_for_update().get(sku=item["sku"])
# ...扣减逻辑
# 记录已处理
ProcessedEvent.objects.create(event_id=payload["event_id"])
这样即使 MQ 挂了,重启后也能通过补偿任务恢复。线上跑了半年,没丢过单。虽然不如强一致性干净,但在电商场景下,“最终一致”完全够用。
服务治理:那些 VSCode 插件救不了的坑
拆完服务,新问题来了:怎么知道哪个服务调用了哪个?延迟高不高?错误率多少?
我那堆 VSCode 插件(Python Docstring Generator、Rainbow Brackets、GitLens)这时候帮不上忙了,得上正经工具链。
我们选型时对比了几套方案:
| 工具 | 优点 | 缺点 |
|---|---|---|
| Zipkin | 轻量,集成简单 | UI 老旧,功能弱 |
| Jaeger | CNCF 项目,支持采样 | 部署稍复杂 |
| SkyWalking | APM 功能强,中文文档好 | Java 亲儿子,Python 支持一般 |
最后折中用了 Jaeger + Prometheus + Grafana 组合:
- Jaeger 做链路追踪(每个请求带 trace_id);
- Prometheus 抓取各服务的 metrics(QPS、延迟、错误数);
- Grafana 做可视化大盘。
关键是在每个服务入口加 middleware:
# Flask 示例
from jaeger_client import Config
def init_tracer(service_name):
config = Config(
config={
'sampler': {'type': 'const', 'param': 1},
'logging': True,
},
service_name=service_name,
)
return config.initialize_tracer()
@app.before_request
def before_request():
span = tracer.start_span('HTTP ' + request.method)
g.span = span
@app.after_request
def after_request(response):
g.span.finish()
return response
效果立竿见影。上周五晚上,用户反馈“下单慢”,我打开 Grafana 一看:order-service 到 inventory-svc 的 P99 延迟高达 2.3 秒。点进 Jaeger,发现是库存服务的数据库慢查询——原来是某运营误删了索引。10 分钟定位问题,运维秒加索引,搞定。这种体验,比当年在单体里 grep 日志幸福一万倍。
接口设计:别让前端同事骂你祖宗
微服务拆完,前后端联调成了新战场。前端小哥甩过来一句:“你们这 API 返回字段一会儿 snake_case 一会儿 camelCase,能不能统一?”
反思后,我们定了三条接口规范:
- 全部用 snake_case(Python 习惯,前端自己转);
- 错误码全局统一,比如 1001=参数错误,2001=库存不足;
- 版本号放 header,不污染 URL(
X-API-Version: v2)。
还用 Pydantic 做了强校验:
from pydantic import BaseModel, validator
class CreateOrderRequest(BaseModel):
user_id: int
items: List[Item]
@validator('items')
def check_items_not_empty(cls, v):
if not v:
raise ValueError('items cannot be empty')
return v
这样非法请求直接 400,不用进业务逻辑。前端终于不再半夜微信轰炸我:“你这接口为啥返回 500 啊?”
生产环境:运维不是敌人
以前当测试时,总觉得运维是“拦路虎”——不让随便改配置、非要走工单。现在自己负责服务上线,才理解他们的苦。
我们和运维共建了几条铁律:
- 每个服务必须有健康检查接口(
/healthz),K8s 用它做探活; - 日志必须结构化(JSON 格式),方便 ELK 收集;
- 配置外置,用 Consul 或 K8s ConfigMap,禁止 hardcode。
举个血泪教训:有次我把数据库密码写死在代码里,测试环境没事,上线后连接拒绝。运维大哥黑着脸说:“你这是要把 DB 密码 commit 到 Git 吗?” 从此我再也不敢了。
回头看:值不值得?
从单体到微服务,我们花了 6 个月。中间熬过无数个加班夜,被线上报警吵醒过三次,也因为服务超时被客诉过。
但结果是值得的:
- 订单服务独立部署后,发布速度从 8 分钟降到 45 秒;
- 双11 当天,系统平稳扛住 12000 QPS,零重大事故;
- 新人入职,只需看自己负责的服务,学习成本大降。
当然,微服务不是银弹。如果你的系统日活就几百,真没必要折腾。但对我们这种中等规模、业务复杂的系统,拆分带来的弹性、可观测性、团队自治,远超初期投入。
至于区块链?其实只是启发我们重视“数据可追溯性”。真正的分布式系统,靠的不是炫技,而是扎实的工程实践——哪怕你是个从测试转来的“野生”开发。
最后几句真心话
写这篇文章时,我正坐在出租屋里,窗外是上海阴沉的天空。桌上摆着半杯冷掉的咖啡,VSCode 里开着三个服务的代码。三年前,我还在纠结 Selenium 怎么定位元素;现在,我居然在设计分布式事务方案。
技术这条路,没有白走的路。测试经历让我更关注边界 case 和可观测性;Python 的简洁让我能快速验证架构想法;而上海这座城市的快节奏,逼着我不断进化。
如果你也在从单体走向分布式,别怕。踩坑是常态,搞砸是过程。只要每次上线后还能笑着吃泡面,你就走在正确的路上。
共勉。

评论 0