微服务拆分实战:一个北漂码农的血泪复盘
上周五晚上十点半,我瘫在工位上盯着屏幕上那行熟悉的报错:“Service timeout after 30s”,心里默念着房贷还款日。这已经是这个月第三次线上告警了。我们的单体应用又双叒叕扛不住流量高峰了——而这次,是在公司“618大促预演”最关键的链路上。
作为一个刚在北京上车、背负三十年房贷的普通后端程序员,我其实挺怕这种半夜被 PagerDuty 叫醒的日子。但没办法,技术债总得有人还,尤其当你所在的团队正处在“从单体迈向分布式”的生死路口。
今天这篇博客,就是想把过去半年我们团队拆微服务的真实经历掏出来晒一晒。不灌鸡汤,不画大饼,就讲实打实踩过的坑、熬过的夜,以及那些让我差点辞职的瞬间。
起点:那个臃肿到令人发指的单体
我们原来的系统,说白了就是一个 Spring Boot 单体应用,代码量超过 40 万行,模块之间耦合得像北京早高峰的国贸地铁站——谁也别想轻松下车。
最夸张的是什么?用户下单逻辑和后台报表生成居然在同一个事务里跑! 每次财务月底导出 Excel,整个订单服务就卡成 PPT。产品经理每次看到监控图都摇头:“你们这系统是不是用胶带粘起来的?”
更惨的是部署。改一行日志级别,全量发布要停机十分钟。运维兄弟私下跟我说:“你再这样搞,我就把你工牌挂服务器机柜上辟邪。”
转折点发生在去年双11前两周。那天下午三点,订单服务因为数据库连接池耗尽直接雪崩,导致支付回调全部失败。老板在会议室拍桌子:“必须拆!下个季度完不成微服务改造,年终奖全部泡汤!”
于是,我们五个后端,外加一个兼职写代码的产品经理(别问,问就是互联网公司特色),硬着头皮开始了这场“技术赎罪之旅”。
第一步:别急着拆,先画边界!
很多团队一听说“微服务”,立马开干:Spring Cloud + Nacos + Gateway 一套组合拳打出去,结果拆完发现服务间调用比原来还慢,监控全是乱码,连个链路追踪都拼不完整。
我们吸取了教训——拆之前,先做领域建模。
参考了《领域驱动设计》(DDD)那本书里的限界上下文(Bounded Context)概念,我们花了整整两周时间,拉着产品、测试、甚至客服,一起梳理业务流程。最终划分出四个核心域:
| 领域 | 职责 | 关键实体 |
|---|---|---|
| 用户中心 | 注册、登录、权限 | User, Role, Session |
| 商品服务 | SKU管理、库存、分类 | Product, Inventory, Category |
| 订单服务 | 下单、状态机、超时取消 | Order, OrderItem, Payment |
| 营销中心 | 优惠券、满减、秒杀 | Coupon, Promotion, Activity |
注意,这里没有把“支付”单独拆出来!为什么?因为我们调研发现,支付回调其实高度依赖订单状态机,强行拆开会引入大量分布式事务问题。与其为了“微”而微,不如先保证业务一致性。这点后来被证明非常关键——隔壁组拆得太细,光是订单创建+库存扣减+优惠计算三个服务之间的最终一致性,就让他们熬了三个月。
技术选型:务实比时髦更重要
说实话,作为技术宅,我也曾幻想过用 Istio + Envoy + Knative 搞一套云原生全家桶。但现实是:我们只有两个懂 K8s 的人,其中一个还在准备跳槽。
所以最终架构很“朴素”:
- 注册中心:Nacos(国产,文档友好,运维能看懂)
- 网关:Spring Cloud Gateway(团队熟悉,性能够用)
- RPC:Feign + Ribbon(虽然老,但稳定)
- 链路追踪:SkyWalking(开源,UI 直观,老板看了都说好)
- 配置中心:还是 Nacos(别搞太多中间件,求你了)
数据库层面,我们坚持“每个服务独享数据库”。但初期为了降低迁移成本,允许新服务读取旧单体的只读副本——这招叫“绞杀者模式”(Strangler Fig Pattern),出自 Martin Fowler 的文章。简单说,就是新功能走新服务,老功能逐步下线,像藤蔓一样慢慢绞杀旧系统。
举个例子:商品详情页原本全由单体提供。现在,我们将“库存查询”剥离到新商品服务,通过 Feign 调用:
// 商品服务中的库存查询接口
@RestController
public class InventoryController {
@GetMapping("/inventory/{skuId}")
public StockInfo getStock(@PathVariable String skuId) {
// 从独立库存表查,不再依赖订单库
return inventoryService.queryBySku(skuId);
}
}
而前端页面,通过网关聚合数据:
# gateway 路由配置
spring:
cloud:
gateway:
routes:
- id: product-service
uri: lb://product-service
predicates:
- Path=/api/product/**
- id: inventory-service
uri: lb://inventory-service
predicates:
- Path=/api/inventory/**
这样,前端几乎不用改代码,后端却完成了初步解耦。
坑最多的环节:数据一致性
如果说微服务有“地狱难度”关卡,那一定是分布式事务。
我们遇到的第一个大坑是:用户领券后,营销中心要扣减库存,同时用户中心要记录领取记录。如果只成功一半,用户白嫖了怎么办?
一开始想上 Seata 的 AT 模式,但发现对 MySQL 表结构有侵入(需要 undo_log 表),而且我们有些表是分库分表的,兼容性存疑。
最后采用“本地消息表 + 定时补偿”方案:
- 营销服务在扣减优惠券库存的同时,往本地
message_outbox表插入一条待发送消息 - 后台定时任务扫描该表,调用用户中心的“记录领取”接口
- 用户中心收到请求后,先查是否已处理(幂等),再写入
- 营销服务收到成功响应,删除消息表记录;若失败,重试三次后告警人工介入
代码示意:
@Transactional
public void claimCoupon(String userId, String couponId) {
// 1. 扣减库存(本地事务)
couponRepo.reduceStock(couponId);
// 2. 写消息表(同事务)
messageOutboxRepo.insert(new Message(
"USER_COUPON_CLAIMED",
JSON.toJSONString(Map.of("userId", userId, "couponId", couponId))
));
}
这套机制上线后,我们做了压力测试:模拟 5000 TPS 下,消息丢失率为 0,最终一致性延迟平均 < 2s。虽然不够“高大上”,但胜在可控、可追溯。
运维视角:别让微服务变成“微痛苦”
拆完服务只是开始,真正的挑战在运维。
以前单体时代,看一个 JVM 监控就行。现在十几个服务,每个都要盯 CPU、内存、GC、线程池……运维小哥直接摆烂:“你们自己 watch 去吧!”
我们赶紧补救:
- 统一日志格式:强制所有服务使用 Logback + MDC,注入 traceId,方便 ELK 聚合
- 健康检查标准化:每个服务暴露
/actuator/health,包含 DB、Redis、第三方依赖状态 - 自动化扩缩容:基于 CPU 和 Pending Requests 设置 HPA 策略(不过目前只敢在非核心服务试用)
最救命的是 SkyWalking。有一次订单服务突然变慢,通过链路追踪一眼就定位到是调用商品服务时 DNS 解析超时——原来是 K8s 的 CoreDNS 配置有问题。要是以前,这种跨服务的问题至少要排查半天。
效果如何?数字不会骗人
经过四个月迭代,我们完成了核心链路的微服务化。对比数据如下:
| 指标 | 拆分前 | 拆分后 | 提升 |
|---|---|---|---|
| 平均响应时间(首页) | 1200ms | 450ms | ↓ 62.5% |
| 部署频率 | 每周1次 | 每天5+次 | ↑ 500% |
| 故障隔离能力 | 全站宕机 | 单服务降级 | ✅ |
| 新人上手时间 | 2周 | 3天 | ↓ 78% |
最关键的是,618 大促当天零重大事故。虽然凌晨三点我还是被叫起来处理了个缓存穿透问题(又是产品经理临时加的“猜你喜欢”模块惹的祸),但至少没影响主流程。
给同行的几句真心话
作为一个既要还房贷又要写代码的北漂,我想说:
- 微服务不是银弹:如果你的业务还没到单体扛不住的地步,别跟风拆。我见过太多团队为了简历好看硬拆,结果维护成本翻倍。
- 先治理,再拆分:确保你的 CI/CD、监控、日志体系到位。否则拆完就是一场运维灾难。
- 人比技术重要:我们能成功,很大程度是因为团队愿意一起加班、一起背锅。技术方案可以抄,但协作文化抄不来。
- 多读书,少焦虑:除了《领域驱动设计》,我还推荐《微服务架构设计模式》(Chris Richardson 著)。这本书把各种场景下的解决方案讲得很透,比网上碎片化教程靠谱多了。
最后,分享一句我在书里看到的话,也是我现在贴在显示器边上的话:
“微服务的目标不是拆分,而是提升交付速度和系统韧性。”
写完这篇博客,已经是凌晨一点。窗外北京的夜依然灯火通明,而我的房贷APP提醒明天是还款日。但至少今晚,我能睡个安稳觉——因为系统没崩。
(完)

评论 0