微服务架构设计实战:从单体到分布式

需求之外
2025-12-16 09:59
阅读 722

上周五晚上十一点半,我戴着 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):

  1. 识别边界:用 DDD 的限界上下文划分模块。比如“订单”包含创建、查询、状态机,“库存”独立管理。
  2. 新功能走新服务:之后所有新需求(比如“优惠券核销”)直接写在新服务里。
  3. 逐步迁移旧接口:把单体里的订单相关 API 一个个挪到 Go 服务,通过 Nginx 路由切换流量。
  4. 数据同步:初期用 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 叫醒了(他说请我喝了杯瑞幸)。


开发心得:给想转型的同学几点建议

  1. 别追求一步到位:微服务不是银弹,拆得太细反而增加复杂度。先拆出 2-3 个高价值模块试试水。
  2. 可观测性先行:日志、监控、链路追踪必须在第一天就集成,否则上线即盲跑。
  3. 自动化测试不能少:我们用 Testcontainers 做集成测试,确保 gRPC 接口兼容。
  4. 文档即契约.proto 文件就是 API 文档,前端、后端、测试都照着它干活。
  5. 心态要稳:重构期间肯定有 Bug,但只要监控告警到位,快速回滚就行。别怕犯错。

最后

写这篇文章的时候,我刚更新完简历——没错,准备跳槽了。这三年,从写 XML 布局到搞分布式事务,技术栈翻天覆地。但不变的是:解决问题的快感,和凌晨三点 fix bug 后的那罐红牛

如果你也在经历类似的架构升级,或者正纠结要不要学 Go,我的建议是:Just do it。微服务不是终点,而是让你更懂系统、更懂协作的起点。

对了,Flutter 我还在写,不过现在顺手用 Go 给 App 写了个后端 mock server,真香。

(完)

评论 0

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