从单体到分布式:一个应届生的微服务拆分血泪史

代码小镇
2025-12-25 07:49
阅读 288

去年秋招拿到深圳某腾讯系公司的 offer 后,我本以为可以躺平半年等入职。结果刚签完三方,就被导师拉进了一个“历史遗留系统重构”项目——美其名曰“提前熟悉业务”,实则是团队缺人手,让我这个还没毕业的菜鸟顶上。

那个系统,用 Go 写的单体应用,跑了快五年,代码量将近 30 万行。每次发版都像在雷区跳舞,CI/CD 流水线跑一次要 40 分钟,测试同学一看到 PR 就叹气。更离谱的是,数据库居然是 MySQL 单点 + 手动主从切换,双 11 前运维大哥天天念经:“别崩、别崩、千万别崩……”

直到去年 12 月,系统在促销高峰直接 OOM,整个用户下单链路瘫了 27 分钟。老板震怒,CTO 亲自下场拍板:“必须拆!三个月内完成微服务化!”——于是,我和另外两个实习生,加上一位刚转管理的前架构师,组成了“微服务敢死队”。


为什么非拆不可?

一开始我也觉得小题大做。不就是加点机器、优化下 SQL 吗?但当我真正去读那坨“意大利面条式”的代码时,我悟了。

比如一个看似简单的“创建订单”接口:

func CreateOrder(w http.ResponseWriter, r *http.Request) {
    // 1. 校验用户登录状态(调用 Auth 模块)
    // 2. 查询商品库存(查 Product 表)
    // 3. 计算优惠券(Coupon 模块)
    // 4. 冻结库存(Inventory 模块)
    // 5. 生成支付链接(Payment 模块)
    // 6. 发送短信通知(SMS 模块)
    // 7. 记录操作日志(Log 模块)
    // 8. 更新用户积分(User 模块)
}

所有逻辑全塞在一个函数里,事务横跨 8 张表。更可怕的是,每个模块之间高度耦合:改个优惠券规则,可能影响库存冻结;发个短信失败,整个订单回滚。这哪是单体,简直是“单体炸弹”。

而且技术栈混乱得离谱:核心下单用 Go,但营销活动用 Python Flask,报表系统又是 Django。三个语言混跑,部署脚本写了上千行 Shell,连 Dockerfile 都有五个版本。

“这系统不是不能跑,是不敢动。” —— 我们组长在周会上苦笑。


拆!但怎么拆?

我们没搞什么高大上的 DDD(领域驱动设计),毕竟 deadline 在追命。而是采用 “垂直切片 + 能力下沉” 的土办法:

  1. 按业务域拆:用户、商品、订单、支付、营销五大模块
  2. 优先拆高频、高风险路径:下单 → 支付 → 库存
  3. 老系统保留只读接口,新功能全部走新服务

有意思的是,团队对语言选型吵了一架。后端老哥坚持全 Go,但营销团队那边全是 Python 熟手。最后折中:核心交易链路用 Go(性能+并发强),营销、BI 类服务用 Python(开发快、生态好)

于是架构图变成了这样:

[Client]
   │
   ├── [API Gateway] (Go)
   │
   ├── [User Service]      (Go)
   ├── [Product Service]   (Go)
   ├── [Order Service]     (Go)
   ├── [Payment Service]   (Go)
   └── [Promotion Service] (Python)

API Gateway 用 Go 写,轻量又快;而促销服务用 Python + FastAPI,三天就搭出 MVP,产品经理直呼“神速”。


通信协议:gRPC vs REST?我们都要!

一开始想统一用 gRPC,毕竟 Go 生态支持好。但 Python 团队抱怨 Protobuf 学习成本高,而且他们还要对接第三方 BI 工具,人家只认 JSON。

最后我们定了规矩:

  • 内部服务间通信:Go ↔ Go 用 gRPC(高性能、强类型)
  • 对外或异构系统:统一 RESTful + JSON(兼容性好)

举个例子,Order Service 调用 Payment Service:

// Go 客户端(gRPC)
paymentClient := pb.NewPaymentServiceClient(conn)
resp, err := paymentClient.CreatePayment(ctx, &pb.CreatePaymentRequest{
    OrderId: "123",
    Amount:  99.9,
})

而 Promotion Service(Python)需要查用户等级:

# Python 客户端(REST)
resp = requests.get("http://user-service/api/v1/users/123")
user_level = resp.json()["level"]

虽然混合架构有点“缝合怪”,但务实比纯洁更重要。上线后压测发现,gRPC 路径延迟 < 15ms,REST 路径 < 50ms,都在可接受范围。


数据库:终于告别“一张表打天下”

原系统所有业务共用一个 MySQL 实例,orders 表居然有 87 个字段!拆分时我们做了三件事:

  1. 按服务拆库:每个微服务拥有自己的数据库(甚至可以不同引擎)
  2. 读写分离:写主库,读从库(Go 用 gorm 自带支持)
  3. 关键数据冗余:比如 Order 服务本地缓存用户昵称,避免频繁调 User 服务

这里踩了个大坑:分布式事务

比如“下单成功后扣减库存”,如果 Order 成功但 Inventory 失败,数据就乱了。我们试过 Saga 模式,但流程太复杂。最后用了 “本地消息表 + 定时补偿” 的土办法:

// Order Service 中
tx := db.Begin()
tx.Create(&Order{...})
tx.Create(&LocalMessage{
    Target: "inventory-service",
    Action: "decrease_stock",
    Payload: `{"sku_id": "A1", "qty": 1}`,
    Status: "pending",
})
tx.Commit()

// 后台 goroutine 轮询 pending 消息,调 Inventory 服务
// 成功则标记为 done,失败则重试(最多 3 次)

虽然不够优雅,但简单可靠,而且和现有系统兼容。Python 的 Promotion 服务也照搬这套逻辑,用 Celery 做异步任务,效果不错。


配置与部署:别再手动改 YAML 了!

以前改个配置要登录五台服务器,scp 文件,重启进程。现在我们用:

  • 配置中心:Apollo(公司自研,类似 Spring Cloud Config)
  • 服务注册发现:Consul
  • 容器化:Docker + K8s(测试环境用 Minikube)

每个服务启动时自动从 Apollo 拉配置:

// Go 服务
config := apollo.GetConfig("order-service")
dbHost := config.GetString("db.host")
# Python 服务
from apollo.client import Client
client = Client(app_id="promotion-service")
sms_key = client.get_value("sms.api_key")

部署流程也标准化了:

# 开发提交代码 → GitLab CI → 构建镜像 → 推送到 Harbor → K8s 自动滚动更新

最爽的是,再也不用求运维大哥帮忙上线了!自己一条 kubectl apply -f deployment.yaml,服务就跑起来了。


监控告警:没有监控的微服务等于裸奔

拆完之后,问题从“整个系统挂了”变成“哪个服务慢了?”。我们立刻接入公司统一监控体系:

组件 技术栈 作用
日志 ELK 全链路日志追踪
指标 Prometheus QPS、延迟、错误率
链路追踪 Jaeger 跨服务调用链可视化
告警 AlertManager 企业微信机器人通知

特别有用的是 Jaeger。有一次用户反馈“下单慢”,我们直接搜 TraceID,发现卡在 Python 的 Promotion 服务里——原来他们用 Pandas 处理实时优惠计算,大数据量时 CPU 打满。立马优化算法,QPS 从 50 提升到 800


性能对比:值不值得折腾?

我们拿“创建订单”接口做了压测(4核8G 机器,模拟 1000 并发):

指标 单体架构 微服务架构 提升
平均延迟 (ms) 320 85 ↓ 73%
错误率 (%) 4.2 0.3 ↓ 93%
发布耗时 (分钟) 40 8 ↓ 80%
故障隔离能力 ❌ 全挂 ✅ 局部降级 ✔️

最直观的感受是:现在改促销逻辑,再也不用提心吊胆怕影响下单了。上周五晚上,产品经理临时要加一个“分享得红包”功能,Python 同学两小时搞定,Go 这边完全无感。


给后来者的几点建议

作为一个刚入行就被扔进火坑的应届生,我想说:

  1. 不要为了微服务而微服务:如果你的系统 QPS 不到 100,单体完全够用。我们拆,是因为真的扛不住了。
  2. 语言不是重点,接口契约才是:Go 和 Python 能共存,前提是 API 文档清晰(我们用 Swagger + Protobuf 双维护)。
  3. 先解决“能跑”,再追求“跑得漂亮”:本地消息表虽土,但救了我们三次线上事故。
  4. 监控必须前置:没监控的微服务,就像闭眼开车。

最后吐槽一句:微服务最大的成本不是技术,是沟通。现在每天站会要对齐五个服务的状态,产品经理已经学会说“你们那个 gRPC 接口啥时候 ready?”了(笑)。


现在我坐在深圳南山的工位上,看着 K8s Dashboard 里十几个 Pod 稳稳运行,突然觉得——被逼着啃源码、画架构图、通宵 debug 的日子,好像也没那么糟

毕竟,谁不想在简历上写一句:“主导完成核心系统微服务化,支撑日订单量 50W+” 呢?

(P.S. 下周正式入职,希望别让我维护自己写的代码……)

评论 0

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