微服务架构设计实战:从单体到分布式——一个县城远程打工人的真实踩坑记录

开源搬砖工
2025-12-19 04:14
阅读 1871

凌晨一点半,窗外只有几盏路灯还亮着。我所在的这个小县城,大多数人早已入睡,而我的 MacBook 依然嗡嗡作响,终端里不断滚动着日志。入职新公司两个月了,从一开始接手那个“祖传”的 Java 单体应用,到现在在 Go 里摸爬滚打搞微服务拆分,说不累是假的,但每次看到系统跑稳了、QPS 上去了,心里又莫名有点小骄傲。

说起来也挺魔幻的:我在北京读完本科,回老家县城远程办公,白天遛狗、晚上写代码,偶尔被产品经理的“紧急需求”拉回现实。上周五晚上,leader 在群里@我:“老张那边反馈订单超时严重,双11前必须搞定。”我看了看日历——离大促只剩三周。得,又是个修罗场。

这篇文章,就记录下我们团队如何把一个臃肿不堪的 Java 单体应用,硬生生拆成一套基于 Go + Java 混合技术栈的微服务体系。不是什么高大上的理论,就是实打实的血泪教训和半夜 debug 的成果。


起因:那个重达 200MB 的 JAR 包

一切的起点,是我们那个“史诗级”单体应用——legacy-order-service。它用 Spring Boot 写的,集成了用户、商品、库存、支付、消息通知……所有模块全塞在一个项目里。每次部署都要打包 200MB+ 的 fat jar,CI/CD 流水线跑一次要 8 分钟。更可怕的是,任何一个模块出问题,整个系统就瘫痪。

去年双11,因为一个优惠券逻辑的小 bug,导致支付回调阻塞,订单积压上万条。运维大哥半夜打电话吼我:“你这代码是不是没测过?!”我只能苦笑——测是测了,但单体架构下,模块间耦合太深,改一行代码都可能引发雪崩。

领导终于坐不住了,在 Q3 架构评审会上拍板:“拆!必须拆成微服务。”

于是,我和另一个同事(人称“Go 大神”,其实也就比我早学三个月)被拉进“微服务攻坚小组”。目标很明确:把订单核心流程拆出来,用高性能语言重构,其他非核心功能保留 Java 原有体系,逐步迁移。


技术选型:为什么是 Go + Java 混搭?

说实话,我一开始是抗拒的。我在学校刷 LeetCode 都是用 Java,工作后也是 Java 全栈。但现实很骨感:

  • 订单创建、库存扣减这些核心链路,对并发和延迟要求极高
  • 现有 Java 服务 GC 停顿频繁,高峰期 Full GC 能卡 2 秒+
  • Go 的 goroutine 和低内存开销,在同类场景已有成功案例

团队讨论后决定:核心交易链路用 Go 重写,周边服务(如用户中心、消息推送)继续用 Java 维护。既保证性能,又避免全量重写的风险。

🤔 有人问:“为什么不全用 Go?”
因为……我们组里还有三个老哥只会 Java 啊!而且用户中心那套权限模型,改起来比生孩子还难。


拆分策略:先解耦,再拆服

微服务不是简单地把代码复制粘贴到新项目。我们采用“绞杀者模式”(Strangler Fig Pattern)——逐步替换旧功能,而不是一次性推倒重来。

第一步:定义服务边界

我们画了一张超简陋的领域模型图(其实是白板上手绘拍照发群里的),确定首批拆出的服务:

服务名 职责 技术栈 是否暴露 HTTP 接口
order-core 创建订单、状态流转 Go
inventory 库存查询与扣减 Go 否(仅 gRPC)
user-service 用户信息、地址管理 Java (Spring Boot)
notification 短信/邮件通知 Java 否(MQ 消费)

注意:inventory 服务不直接对外提供 HTTP 接口,只通过 gRPC 被 order-core 调用。这样可以减少攻击面,也便于内部协议优化。

第二步:接口契约先行

我们用 Protocol Buffers 定义所有跨服务通信的接口。比如库存扣减:

// inventory.proto
syntax = "proto3";

package inventory;

service InventoryService {
  rpc DeductStock(DeductRequest) returns (DeductResponse);
}

message DeductRequest {
  string sku_id = 1;
  int32 quantity = 2;
  string order_id = 3;
}

message DeductResponse {
  bool success = 1;
  string message = 2;
}

生成 Go 和 Java 的 stub 代码后,两边开发可以并行进行。契约驱动开发(CDD)真的救了我们一命——再也不用互相等对方联调了。


关键实现:Go 服务的高性能设计

1. 使用 Gin + gRPC Gateway

order-core 既要对外提供 RESTful API(给前端和外部系统),又要高效调用 inventory 的 gRPC 服务。我们用了 grpc-gateway 自动生成 HTTP 到 gRPC 的代理层。

// main.go
func main() {
    // 启动 gRPC 服务
    lis, _ := net.Listen("tcp", ":50051")
    s := grpc.NewServer()
    pb.RegisterOrderServiceServer(s, &server{})
    
    // 启动 HTTP 网关
    conn, _ := grpc.DialContext(context.Background(), "localhost:50051", grpc.WithInsecure())
    gwmux := runtime.NewServeMux()
    pb.RegisterOrderServiceHandler(context.Background(), gwmux, conn)

    // 混合路由
    ginEngine := gin.Default()
    ginEngine.Any("/v1/*any", gwmux)
    ginEngine.Run(":8080")
}

这样,前端调 /v1/orders/create,底层自动转成 gRPC 请求,省去手动写适配器的麻烦。

2. 数据库连接池调优

Go 用 database/sql + pgx 驱动连 PostgreSQL。一开始用默认配置,压测时发现大量 connection timeout。查了文档才知道,默认 MaxOpenConns 居然是无限制的!

赶紧加上:

db.SetMaxOpenConns(50)
db.SetMaxIdleConns(10)
db.SetConnMaxLifetime(30 * time.Minute)

配合 Prometheus 监控连接数,终于稳住了。

3. 异步解耦:用 Kafka 削峰

订单创建成功后,要发通知、更新用户积分、记录操作日志……这些非核心操作不能阻塞主流程。我们引入 Kafka:

// 成功创建订单后
msg := &kafka.Message{
    Topic: "order-created",
    Value: []byte(orderJSON),
}
producer.Send(msg)

Java 写的 notification 服务消费这个 topic,失败自动重试。削峰填谷,主链路响应时间从 800ms 降到 120ms


跨语言通信:Go 调 Java 服务的坑

user-service 还是 Java 写的,order-core 需要查用户地址。本来想直接 HTTP 调,但测试发现:

  • Java 服务启动慢(Spring Boot 冷启动 15s+)
  • JSON 序列化反序列化开销大
  • 网络抖动时容易超时

后来我们做了两件事:

  1. 给 Java 服务加 gRPC 接口(用 Spring Cloud Stream 不太合适,太重)
  2. 在 Go 侧加熔断和缓存
// 使用 hystrix-go 实现熔断
hystrix.ConfigureCommand("getUser", hystrix.CommandConfig{
    Timeout:               500,
    MaxConcurrentRequests: 100,
    ErrorPercentThreshold: 25,
})

result := hystrix.DoC(ctx, "getUser", func(ctx context.Context) error {
    user, err := userClient.GetUser(ctx, &pb.UserId{Id: userId})
    if err != nil {
        return err
    }
    // 缓存到 Redis
    cache.Set(fmt.Sprintf("user:%d", userId), user, 5*time.Minute)
    return nil
}, func(err error) error {
    // fallback: 从缓存读 or 返回默认地址
    return nil
})

上线后,即使 user-service 挂了,订单也能正常创建(用缓存地址),用户体验不受影响。


数据一致性:分布式事务怎么搞?

最头疼的就是下单扣库存。本地事务好办,分布式环境下怎么保证要么都成功,要么都失败?

我们没上 Seata(太重,且 Go 支持不完善),而是用 Saga 模式 + 补偿事务

  1. order-core 创建订单(状态=processing)
  2. 调 inventory 扣库存
    • 成功:更新订单状态=confirmed
    • 失败:发送 cancel_order 事件
  3. inventory 收到 cancel_order,回滚库存

关键点:所有操作必须幂等

比如扣库存接口:

func (s *InventoryService) DeductStock(ctx context.Context, req *pb.DeductRequest) (*pb.DeductResponse, error) {
    // 先查是否已处理过该订单
    if s.isOrderProcessed(req.OrderId) {
        return &pb.DeductResponse{Success: true}, nil // 幂等返回
    }

    // 扣库存逻辑
    err := s.db.Exec("UPDATE stock SET quantity = quantity - $1 WHERE sku_id = $2 AND quantity >= $1", 
        req.Quantity, req.SkuId)
    if err != nil {
        return &pb.DeductResponse{Success: false, Message: "insufficient stock"}, nil
    }

    // 记录已处理
    s.markOrderProcessed(req.OrderId)
    return &pb.DeductResponse{Success: true}, nil
}

虽然 Saga 不能保证强一致性,但在电商场景下,“最终一致”完全可接受。毕竟用户更关心能不能下单,而不是库存数字是否绝对精确。


生产环境踩过的雷

雷区 1:Go 服务 OOM

上线第一天,凌晨 3 点报警:order-core 内存飙升到 2GB,被 Kubernetes kill 了。

排查发现:goroutine 泄漏!我们在每个请求里起了 goroutine 发 Kafka,但没做上下文取消:

// 错误示范 ❌
go func() {
    producer.Send(msg) // 可能永远阻塞
}()

改成带超时的 context:

// 正确做法 ✅
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
producer.SendWithContext(ctx, msg)

雷区 2:Java 服务慢查询拖垮整个链路

user-service 的 MySQL 没加索引,查用户地址用了全表扫描。Go 服务等 2s 超时,触发熔断,结果订单创建失败率飙升。

教训:微服务不是银弹,每个环节都得压测! 我们现在要求所有服务上线前必须提供:

  • 接口响应 P99 < 200ms
  • DB 慢查询日志开启
  • 核心接口有单元测试 + 压测报告

效果对比:值不值得折腾?

拆分完成后,我们做了压力测试(4核8G 机器,模拟双11流量):

指标 单体架构 (Java) 微服务架构 (Go+Java)
订单创建 QPS 320 1850
平均响应时间 780ms 110ms
99分位延迟 2100ms 280ms
部署时间 8分钟 Go服务 45秒 / Java服务 3分钟
故障隔离 无(全挂) 库存服务挂,订单仍可创建(降级)

最爽的是:现在改库存逻辑,不用重启整个订单系统了!


写在最后:小镇做题家的微服务感悟

回看这两个月,从被 deadline 逼疯,到深夜 debug 到怀疑人生,再到看到监控图表上漂亮的曲线,我觉得值了。

微服务不是目的,快速迭代、稳定交付才是。对于我们这种小团队(总共 8 个人),混合架构反而是最优解:Go 搞高性能核心,Java 维护复杂业务,各取所长。

有时候在县城的出租屋里敲代码,会想:我是不是错过了大厂的光环?但转念一想,能用技术解决真实问题,让系统跑得更快更稳,这种成就感,不比拿期权差

对了,上周 leader 说双11零故障,要给我发奖金。我寻思着,是不是该换个 MacBook Pro 了?(狗头)


附:如果你也在拆单体,记住这几点

  • 别追求一步到位,先拆出最小可运行单元
  • 契约先行,别让联调变成互相甩锅
  • 监控!日志!告警!没这三件套别上线
  • 接受“最终一致”,别被 CAP 定理吓住
  • 最重要:保护好自己的头发

(全文完,字数统计:3982)

评论 0

最热最新
暂无评论
开源搬砖工Lv.1
0
影响力
0
文章
0
粉丝