微服务架构设计实战:从单体到分布式
上周五晚上十一点半,我戴着 AirPods 听着 Lo-fi Beats 写 Flutter 页面的时候,突然收到运维老王的微信:“线上订单服务又崩了,CPU 100%,快看看是不是你改的接口有问题?”
我当时手一抖差点把 MacBook 掉地上——这特么又是“背锅侠”的日常。
我是三年 Android 开发出身,去年转岗做跨平台,主力写 Flutter。但最近公司搞微服务重构,领导拍板说:“你不是学过分布式系统嘛?来搭新服务。” 我心里嘀咕:那只是研究生课上摸过几眼 Raft 和 CAP 啊!但嘴上只能回一句:“好的,我今晚就开干。”
今天这篇技术分享,就是记录我们团队从去年双11前启动的“微服务拆分”项目,怎么把一个跑在 Tomcat 上、20万行 Java 的单体应用,硬生生拆成十几组 Go 写的微服务。过程酸爽,踩坑无数,但也收获满满。
背景:为什么非要拆?
我们的产品是一个电商后台系统,早期为了快速上线,所有模块(用户、商品、订单、支付、库存)全塞在一个 Spring Boot 应用里。本地开发时还好,一到生产环境——特别是大促期间——简直灾难。
- 部署慢:改一行日志都要全量发布,CI/CD 流程动辄 15 分钟
- 故障扩散:某次库存扣减逻辑死循环,直接拖垮整个系统
- 扩展性差:订单模块压力大需要扩容,结果连用户模块也一起扩,浪费资源
- 语言绑定:想用 Go 写高性能计价服务?不行,必须 Java!
产品经理小李还天天催:“能不能把下单流程优化到 300ms 内?” 我心想:您这需求倒是简单,可这坨代码现在连单元测试都跑不起来,咋优化?
于是,CTO 在一次复盘会上一锤定音:“拆!全部拆成微服务!”
技术选型:为什么是 Go?
说实话,我一开始提议用 Kotlin + Ktor,毕竟我 Java 系出身。但后端大佬老张直接泼冷水:“Kotlin 还是 JVM,GC 停顿扛不住高并发。而且你看看社区生态——Go 的微服务框架多成熟。”
最终我们定了技术栈:
- 语言:Go(1.20+)
- 框架:Gin + gRPC
- 注册中心:Consul
- 配置中心:Apollo
- 链路追踪:Jaeger
- 数据库:MySQL(分库分表)+ Redis(缓存)
选 Go 不光因为性能,更因为它的简洁和并发模型。写一个 HTTP 服务,10 行代码搞定;goroutine 处理并发,比 Java 线程池清爽太多。而且编译成二进制,部署无依赖——运维老王第一次看到 ./order-service 直接跑起来的时候,眼睛都亮了。
拆分策略:别一口吃成胖子
我们没敢直接全拆,而是采用“绞杀者模式”(Strangler Fig Pattern):
- 识别边界:用 DDD 的限界上下文划分模块。比如“订单”包含创建、查询、状态机,“库存”独立管理。
- 新功能走新服务:之后所有新需求(比如“优惠券核销”)直接写在新服务里。
- 逐步迁移旧接口:把单体里的订单相关 API 一个个挪到 Go 服务,通过 Nginx 路由切换流量。
- 数据同步:初期用 binlog + Canal 同步 MySQL 数据,后期逐步切断依赖。
最痛苦的是数据一致性。比如下单要扣库存,以前在一个事务里搞定,现在跨服务了怎么办?我们试过两阶段提交(2PC),结果发现性能太差。最后用了本地消息表 + 定时补偿:
// 创建订单时,同时写入消息表
tx := db.Begin()
tx.Create(&Order{...})
tx.Create(&OutboxMessage{
Type: "DECREASE_STOCK",
Payload: `{"sku_id": "123", "count": 1}`,
Status: "pending",
})
tx.Commit()
// 异步 worker 轮询 outbox 表,发送消息到库存服务
// 如果失败,标记为 retry,后续重试
虽然不够“优雅”,但在业务容忍范围内,且稳定可靠。毕竟,能跑的狗屎代码 > 完美但挂掉的架构。
关键代码:一个 gRPC 服务长啥样?
我们内部服务间通信全用 gRPC,性能比 REST 高不少。定义 .proto 文件:
syntax = "proto3";
service OrderService {
rpc CreateOrder(CreateOrderRequest) returns (CreateOrderResponse);
}
message CreateOrderRequest {
string user_id = 1;
repeated OrderItem items = 2;
}
message CreateOrderResponse {
string order_id = 1;
int32 code = 2;
string message = 3;
}
Go 实现:
type orderServer struct {
db *gorm.DB
stockClient stockpb.StockServiceClient // gRPC client
}
func (s *orderServer) CreateOrder(ctx context.Context, req *pb.CreateOrderRequest) (*pb.CreateOrderResponse, error) {
// 1. 校验参数
if len(req.Items) == 0 {
return nil, status.Error(codes.InvalidArgument, "items empty")
}
// 2. 创建订单(省略 DB 操作)
orderID := uuid.New().String()
// 3. 发送扣库存请求(带超时)
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
_, err := s.stockClient.DecreaseStock(ctx, &stockpb.DecreaseStockRequest{
SkuId: req.Items[0].SkuId,
Count: req.Items[0].Count,
})
if err != nil {
// 记录失败,后续补偿
log.Errorf("DecreaseStock failed: %v", err)
// 这里会写入 outbox 表,由补偿任务处理
}
return &pb.CreateOrderResponse{OrderId: orderID, Code: 200}, nil
}
注意点:
- 所有 gRPC 调用必须带
context.WithTimeout - 错误要用
status.Error包装,方便客户端识别 - 敏感操作(如扣库存)必须异步解耦,避免级联失败
运维血泪史:上线不是终点
代码写完只是开始。第一次灰度发布,就遇到经典问题:
“服务注册了,但调用方找不到!”
查了半天,发现 Consul 的健康检查配置错了:
# 错误配置
check {
http = "http://localhost:8080/health"
interval = "10s"
}
# 正确:必须用容器内 IP 或 0.0.0.0
check {
http = "http://0.0.0.0:8080/health"
interval = "10s"
}
还有一次,Jaeger 链路追踪没生效,排查发现是 context 透传漏了。在 Gin 中间件里必须手动注入:
func TracingMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
ctx, span := tracer.Start(c.Request.Context(), c.FullPath())
defer span.End()
// 关键:把 span context 注入到 gin 的 context
c.Request = c.Request.WithContext(ctx)
c.Next()
}
}
最惊险的是数据库连接池耗尽。Go 默认 sql.DB 连接池无上限,高并发下直接打爆 MySQL。后来加了限制:
db.SetMaxOpenConns(50)
db.SetMaxIdleConns(10)
db.SetConnMaxLifetime(30 * time.Minute)
效果对比:值不值得?
三个月后,我们拆出了 6 个核心微服务。效果如下:
| 指标 | 单体架构 | 微服务架构 |
|---|---|---|
| 平均响应时间 | 850ms | 220ms |
| 部署时间 | 12min | <1min |
| 故障隔离 | ❌ 全站挂 | ✅ 仅影响单模块 |
| 资源利用率 | CPU 峰谷差 70% | 各服务按需扩缩容 |
| 新人上手 | 需理解全系统 | 只需关注 1-2 个服务 |
双11当天,订单服务 QPS 达到 5k,CPU 稳稳 40%,而库存服务独立扛住 3k QPS。运维老王终于不用半夜被 PagerDuty 叫醒了(他说请我喝了杯瑞幸)。
开发心得:给想转型的同学几点建议
- 别追求一步到位:微服务不是银弹,拆得太细反而增加复杂度。先拆出 2-3 个高价值模块试试水。
- 可观测性先行:日志、监控、链路追踪必须在第一天就集成,否则上线即盲跑。
- 自动化测试不能少:我们用 Testcontainers 做集成测试,确保 gRPC 接口兼容。
- 文档即契约:
.proto文件就是 API 文档,前端、后端、测试都照着它干活。 - 心态要稳:重构期间肯定有 Bug,但只要监控告警到位,快速回滚就行。别怕犯错。
最后
写这篇文章的时候,我刚更新完简历——没错,准备跳槽了。这三年,从写 XML 布局到搞分布式事务,技术栈翻天覆地。但不变的是:解决问题的快感,和凌晨三点 fix bug 后的那罐红牛。
如果你也在经历类似的架构升级,或者正纠结要不要学 Go,我的建议是:Just do it。微服务不是终点,而是让你更懂系统、更懂协作的起点。
对了,Flutter 我还在写,不过现在顺手用 Go 给 App 写了个后端 mock server,真香。
(完)

评论 0