从单体到微服务:一个前测试工程师的血泪实战

半夜部署日记
2026-01-03 05:39
阅读 639

去年冬天,上海冷得离谱。我裹着羽绒服坐在公司楼下的全家便利店啃饭团,一边刷着手机里刚收到的紧急邮件——“系统扛不住双11流量,老板点名要拆微服务”。那一刻,我真的想把饭团塞进邮箱里。

我是谁?一个从测试转开发三年的“半路出家”选手,现在在上海一家中型电商公司做后端。以前天天拿着Postman怼接口、写自动化脚本,现在却要亲手把那个祖传单体应用切成几十个微服务。说实话,刚开始接到这个任务时,我心里直打鼓:分布式事务、服务发现、链路追踪……这些词听起来都像天书。但没办法,程序员嘛,不是被需求推着走,就是被 deadline 赶着跑。

今天这篇技术分享,不讲大道理,就聊聊我们是怎么一步步把那个又胖又慢的单体系统拆成微服务的,中间踩了哪些坑,又怎么爬出来的。顺便提一句,虽然标题里有“区块链”,但别误会——我们没上链,只是在设计某些数据不可篡改场景时参考了它的思路(后面会细说)。


单体架构的“甜蜜陷阱”

我们的老系统是典型的 Django + PostgreSQL 单体应用,代码量超过 20 万行。功能模块全挤在一起:用户管理、订单处理、库存、支付、物流……所有业务逻辑在一个代码库里跑。本地开发倒是爽——python manage.py runserver,一切搞定。

但上线后问题就来了:

  • 部署慢:改一行日志,全量构建要 8 分钟;
  • 扩展难:促销期间订单模块爆了,但库存模块闲得发慌,资源没法按需分配;
  • 故障扩散:有一次 Redis 连不上,整个系统直接瘫痪,连登录页都打不开;
  • 团队协作撕逼:前端、后端、测试全挤在一个 Git 分支上,PR 合并能吵三天。

最夸张的是去年双11前夜,订单创建接口 QPS 突破 5000,数据库连接池直接打满。运维兄弟在群里吼:“再不优化就准备写事故报告吧!” 那天晚上,我和两个同事在公司通宵,泡面吃到想吐,最后靠加缓存+SQL 优化勉强扛过去。但我们都清楚:这只是缓兵之计。


拆!但怎么拆?

老板一句话:“参考阿里、Netflix,搞微服务。”
我内心 OS:人家有几百人的中间件团队,我们后端加我就五个人……

冷静下来后,我们定了几个原则:

  1. 按业务边界拆分,不是按技术栈;
  2. 先拆高频、高风险模块(比如订单、支付);
  3. 保留单体作为“遗留系统”并行运行,逐步迁移;
  4. 用 Python,不盲目追新——团队熟悉 Django/Flask,没必要为了微服务硬上 Go。

于是我们画了第一版服务划分图:

服务名 职责 技术栈
user-service 用户注册、登录、鉴权 Flask + JWT
order-service 创建订单、状态流转 Django REST
inventory-svc 库存扣减、回滚 FastAPI
payment-svc 支付回调、对账 Flask

注意:我们故意把 orderinventory 拆开——因为业务上它们解耦了(下单成功不一定立刻扣库存,比如预售)。但这也带来了分布式事务的大坑。


分布式事务:比产品经理的需求还难搞

想象这个场景:用户下单 → 扣库存 → 创建支付单。
如果扣库存成功,但支付服务挂了,怎么办?总不能让用户付了钱却没货。

我们试过几种方案:

方案一:两阶段提交(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-serviceinventory-svc 的 P99 延迟高达 2.3 秒。点进 Jaeger,发现是库存服务的数据库慢查询——原来是某运营误删了索引。10 分钟定位问题,运维秒加索引,搞定。这种体验,比当年在单体里 grep 日志幸福一万倍。


接口设计:别让前端同事骂你祖宗

微服务拆完,前后端联调成了新战场。前端小哥甩过来一句:“你们这 API 返回字段一会儿 snake_case 一会儿 camelCase,能不能统一?”

反思后,我们定了三条接口规范:

  1. 全部用 snake_case(Python 习惯,前端自己转);
  2. 错误码全局统一,比如 1001=参数错误,2001=库存不足;
  3. 版本号放 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

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