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

花好月圆
2025-12-13 17:55
阅读 317

作者:一个刚拿到 offer、等着入职的 CS 应届生,重度 ChatGPT 依赖者,K8s 略懂,通勤一小时,头发略少

上周五晚上,我正坐在回龙观出租屋里,一边啃着外卖炸鸡,一边看着电脑屏幕上那个跑不动的 Spring Boot 单体应用——CPU 打满、GC 停顿 5 秒一次、数据库连接池爆了。这已经是我本周第三次被测试群里@了:

“后端又崩了?这接口不是上周就上线了吗?”
“亲,用户反馈下单卡住,麻烦紧急排查一下~”

我盯着日志里那行熟悉的报错 Too many connections,心里默默问候了一下产品经理——“你非要在首页加个‘智能推荐’模块,结果调用了三个内部服务,每个都串行查 DB,还用的是默认连接池配置……”

但骂归骂,活还得干。好在再过两周我就要入职新公司了,这份实习项目(没错,这是我在某二线电商公司的实习毕设)成了我被迫转型微服务的“导火索”。今天这篇文章,就是想和大家聊聊:一个普通一本 CS 大四菜鸡,是怎么在 deadline 和线上事故的双重压力下,把一个快炸掉的单体应用,硬生生拆成微服务的。


起点:那个“能跑就行”的单体应用

先交代下背景。这个项目是一个小型 B2C 电商系统,用的是 Java + Spring Boot + MySQL。最初只有商品、订单、用户三个模块,代码全塞在一个 repo 里,部署时打一个 fat jar,丢到 ECS 上跑。

早期确实爽——改个 bug,mvn clean package,重启服务,搞定。但随着业务膨胀(尤其是去年双11前,产品一口气加了促销、库存预警、物流跟踪等一堆功能),问题就来了:

  • 启动时间从 10 秒飙到 90 秒
  • 一个模块 OOM,整个服务挂掉
  • 数据库连接池经常被打爆(因为所有模块共用同一个 HikariCP)
  • CI/CD 流水线动不动就超时(测一次全量回归要 40 分钟)

最致命的是:团队协作效率暴跌。前端老哥改个商品详情页,后端得陪他一起测订单流程;测试同学提个 bug,我得翻半天日志才确定是哪个模块出的问题。

当时组里有个老哥(现在已跳槽去字节)说了一句话让我印象深刻:“单体不是原罪,但当它变成‘巨石’时,你就该考虑拆了。

于是,在 Leader 的“建议”(其实是命令)下,我接下了这个“拆单体”的活——美其名曰“技术债治理”,实则是“背锅侠招募”。


拆!但怎么拆?别乱来

说实话,刚开始我真有点懵。网上教程一搜一大把,什么 DDD、Bounded Context、Service Mesh……但我手头连本像样的书都没有。后来咬牙买了本《微服务架构设计模式》(Chris Richardson 写的那本),配合 ChatGPT 当“人肉 IDE”,才慢慢理清思路。

第一步:识别业务边界

我拿着那本书,对照现有代码画了个粗糙的上下文图:

模块 主要功能 调用关系
user-service 用户注册/登录/信息管理 被所有服务调用
product-service 商品 CRUD、分类、搜索 被 order、cart 调用
order-service 下单、支付、状态流转 调用 user、product、inventory
inventory-service 库存扣减、预警 被 order 调用

看起来很清晰,对吧?但现实狠狠打了脸——order 里居然嵌套了物流计算逻辑,product 里混着促销规则!这就是典型的“边界模糊”。

于是我干了件狠事:直接删代码(开玩笑的,其实是重构)。我把所有跨模块的直接方法调用,替换成 REST API 调用(临时方案),强制隔离。虽然性能暂时更差了,但至少明确了依赖关系。

💡 经验贴士:拆之前,先用 Arthas 或 JProfiler 抓一下调用链,比靠脑子猜靠谱一万倍。


通信:REST 还是 RPC?选哪个不重要,稳定才重要

一开始我打算上 gRPC,毕竟“高性能”嘛。但运维大哥一句话给我劝退了:“你们这几个服务,QPS 加起来还没我一台 Nginx 高,别整那些花里胡哨的。

于是老老实实用 Spring Cloud OpenFeign + Ribbon(后来换成了 LoadBalancer)。但问题来了:同步调用链太长,一个慢,全链路雪崩

举个栗子:用户下单 → 调 product 获取价格 → 调 inventory 扣库存 → 调 payment 发起支付。如果 payment 服务响应慢(比如第三方支付回调延迟),整个请求就 hang 住,线程池迅速耗尽。

解决方案?异步 + 超时 + 熔断

// Feign Client 配置
@FeignClient(name = "inventory-service", configuration = FeignConfig.class)
public interface InventoryClient {
    @PostMapping("/deduct")
    Result<Boolean> deductStock(@RequestBody DeductRequest request);
}

// FeignConfig.java
@Configuration
public class FeignConfig {
    @Bean
    public Request.Options options() {
        // 连接 1s,读取 3s,避免长时间阻塞
        return new Request.Options(1000, 3000);
    }
}

同时,在 order-service 里加上 Resilience4j 熔断:

CircuitBreaker circuitBreaker = CircuitBreaker.ofDefaults("inventory");
Supplier<Result<Boolean>> decorated = CircuitBreaker
    .decorateSupplier(circuitBreaker, () -> inventoryClient.deductStock(request));

try {
    return decorated.get();
} catch (CallNotPermittedException e) {
    log.warn("Inventory service熔断,走降级逻辑");
    return fallbackDeduct(); // 比如先占位,异步扣库存
}

上线后,平均响应时间从 2.1s 降到 800ms,P99 也从 8s+ 控制在 2s 内。虽然还是不够快,但至少不会拖垮整个系统了。


数据库:别再共用一个库了!

最开始所有服务都连同一个 MySQL 实例,不同模块用不同表前缀区分(比如 user_, order_)。这简直是灾难——一个慢查询就能锁死整个库

我们做了两件事:

  1. 物理分库:每个核心服务独立数据库(user_db, order_db, inventory_db)
  2. 读写分离:主库写,从库读(用 ShardingSphere-JDBC)

但随之而来的是分布式事务问题。比如下单要同时创建订单 + 扣库存,传统 @Transactional 不管用了。

我试过 Seata 的 AT 模式,但发现对业务侵入太大,而且我们 QPS 并不高,最终选择了 Saga 模式 + 补偿机制

  • 创建订单成功 → 发消息到 Kafka
  • inventory-service 消费消息,尝试扣库存
  • 如果失败,发“取消订单”消息,order-service 回滚状态

虽然最终一致性有延迟,但在我们场景下(非金融级),可接受

🤯 踩坑现场:有一次 Kafka 消息堆积,导致用户付了钱但库存没扣,结果超卖 200 单……那天晚上我和运维一起蹲机房到凌晨三点,边喝冰红茶边看消费日志。


部署与可观测性:K8s 救我狗命

作为 K8s 略懂选手(其实只会 kubectl get podskubectl logs -f),这次终于有机会实践了。

我们在测试环境搭了 Minikube,生产用阿里云 ACK。每个服务打包成 Docker 镜像,通过 Helm Chart 部署。

关键配置:

# deployment.yaml 片段
resources:
  requests:
    memory: "512Mi"
    cpu: "200m"
  limits:
    memory: "1Gi"
    cpu: "500m"

限制资源很重要! 之前有个服务 OOM,因为没设 limit,直接吃光节点内存,把其他 pod 全挤出去了。

另外,日志统一收集到 ELK,指标用 Prometheus + Grafana 监控。最常用的面板就三个:

  • 各服务 QPS & 错误率
  • JVM GC 时间 & 堆内存
  • 数据库连接池使用率

有了这些,再也不用半夜被 PagerDuty 叫醒后一脸懵了。


性能优化:微服务不是银弹,搞不好更慢

很多人以为拆成微服务性能自动提升,大错特错!网络开销、序列化、服务发现……每一环都在拖慢你。

我们的优化重点:

优化点 措施 效果
序列化 JSON → Protobuf(仅内部服务间) 序列化耗时 ↓40%
服务发现 Eureka → Nacos(支持 DNS-F) 服务调用延迟 ↓15%
缓存 Redis 缓存商品信息、用户会话 DB QPS ↓60%
异步化 非核心操作(如发通知)走 MQ 主链路 RT ↓30%

特别说下缓存。我们用 Spring Cache + Redis,但踩了经典坑:缓存穿透

@Cacheable(value = "product", key = "#id", unless = "#result == null")
public Product getProduct(Long id) {
    return productMapper.selectById(id);
}

如果传一个不存在的 ID,每次都会打到 DB。后来加了 布隆过滤器 + 空值缓存(短 TTL) 才解决。


心得:微服务不是目的,解决问题才是

折腾了两个月,系统终于稳住了。上周压测,单节点 order-service 能扛 800+ TPS(对比之前单体 300 TPS),而且某个服务挂了,其他功能基本不受影响。

但我也深刻体会到:微服务不是万能药。它带来了复杂度、运维成本、调试难度。如果你的业务还没到那个规模,别为了“高大上”而拆。

对我这种应届生来说,这次经历最大的收获不是技术,而是工程思维

  • 如何在有限时间内做 trade-off
  • 如何和运维、测试、产品沟通协作
  • 如何在“能跑”和“健壮”之间找平衡

顺便安利几本对我帮助巨大的书:

  • 《微服务架构设计模式》——理论扎实,案例贴近国内
  • 《凤凰项目》——虽然是小说,但讲透了 DevOps 和协作
  • 《Java 并发编程实战》——拆完才发现并发问题更多了……

最后:别怕,慢慢来

写这篇文章时,我已经拿到 offer,下周就要入职新公司了。据说他们用的是 Service Mesh + K8s 全托管,想想还有点小紧张。

但回头看看自己从那个连 Feign 都配不明白的菜鸟,到现在能独立拆服务、调性能、扛线上事故,其实也没那么难——无非是多查文档、多问 ChatGPT、多熬几个夜

所以,如果你也在面对一个快炸掉的单体应用,别慌。拆就完了,大不了……再回滚嘛(手动狗头)。

P.S. 如果你也在北京,通勤一小时,头发越来越少,欢迎留言交流!说不定哪天在西二旗地铁站,咱俩还能互相递瓶红牛 😉

评论 0

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