微服务架构设计实战:从单体到分布式
作者:一个刚拿到 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_)。这简直是灾难——一个慢查询就能锁死整个库。
我们做了两件事:
- 物理分库:每个核心服务独立数据库(user_db, order_db, inventory_db)
- 读写分离:主库写,从库读(用 ShardingSphere-JDBC)
但随之而来的是分布式事务问题。比如下单要同时创建订单 + 扣库存,传统 @Transactional 不管用了。
我试过 Seata 的 AT 模式,但发现对业务侵入太大,而且我们 QPS 并不高,最终选择了 Saga 模式 + 补偿机制:
- 创建订单成功 → 发消息到 Kafka
- inventory-service 消费消息,尝试扣库存
- 如果失败,发“取消订单”消息,order-service 回滚状态
虽然最终一致性有延迟,但在我们场景下(非金融级),可接受。
🤯 踩坑现场:有一次 Kafka 消息堆积,导致用户付了钱但库存没扣,结果超卖 200 单……那天晚上我和运维一起蹲机房到凌晨三点,边喝冰红茶边看消费日志。
部署与可观测性:K8s 救我狗命
作为 K8s 略懂选手(其实只会 kubectl get pods 和 kubectl 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