去年下半年,我所在团队启动了一项重要的工作——将一个运行了 4 年多的单体应用逐步拆分为微服务架构。这篇文章将真实记录这次架构演进的背景、决策过程、技术选型以及踩过的坑,希望能为面临类似场景的团队提供参考。
1. 为什么需要拆分
决定拆分之前,我们先冷静地评估了当前单体应用面临的具体问题,而不是因为"微服务很流行"就匆忙上马:
- 部署耦合——每次发布都需要整体构建和回归测试,从代码合入到上线平均需要 3 个小时。
- 伸缩粒度粗——系统中最消耗资源的其实是订单计算模块,但单体架构下只能整体扩容,造成资源浪费。
- 团队协作瓶颈——随着团队从 5 人扩展到 20 人,不同小组在同一代码库中频繁产生合并冲突。
- 技术栈锁定——老代码基于旧版框架,部分新功能的实现方案受限于历史技术选型。
经过评估,我们认为拆分的收益大于成本,于是正式启动了迁移项目。
2. 服务边界划分
服务拆分的第一步、也是最关键的一步,是确定服务边界。我们采用了领域驱动设计(DDD)的方法论,通过以下步骤来识别限界上下文:
- 事件风暴——召集各业务线的开发、产品、运营同事,用 3 天时间梳理了整个系统的业务事件流。
- 识别聚合根——找出每个业务领域中的核心实体及其一致性边界。
- 依赖分析——分析现有代码库中模块间的实际调用关系,与理论上的限界上下文进行交叉验证。
最终我们确定了以下服务划分:
- 用户服务——用户注册、登录、权限管理
- 订单服务——订单创建、状态流转、退款处理
- 商品服务——商品信息管理、库存查询
- 支付服务——支付渠道对接、账单记录
- 通知服务——短信、邮件、站内信的统一发送
3. 服务间通信选型
服务间通信方式的选择对系统的可用性和可维护性有深远影响。我们根据场景选择了不同的策略:
同步通信:gRPC + Protocol Buffers
对于需要实时响应的查询类场景(如订单服务查询用户信息),我们统一使用 gRPC 作为同步通信协议。相比 REST,gRPC 具有强类型契约、双向流支持和更优的序列化性能。
// 用户服务 proto 定义
service UserService {
rpc GetUser (GetUserRequest) returns (GetUserResponse);
rpc BatchGetUsers (BatchGetUsersRequest) returns (BatchGetUsersResponse);
}
message GetUserRequest {
int64 user_id = 1;
}
异步通信:Kafka + 事件驱动
对于跨服务的业务流程(如"用户下单 → 扣减库存 → 创建支付单 → 发送通知"),我们采用基于 Kafka 的事件驱动架构,确保服务间的松耦合和最终一致性。
4. 分布式事务的处理
分布式事务是微服务架构中最棘手的问题之一。我们遵循以下原则来处理数据一致性问题:
- 尽可能避免分布式事务——通过合理划分服务边界,将强一致性要求的数据放在同一个服务内。
- Saga 模式——对于不可避免的跨服务业务流程,使用编排式 Saga 配合补偿事务来保证最终一致性。
- 幂等设计——所有跨服务的写操作都通过唯一幂等键来保证"至少一次"投递场景下的数据正确性。
5. 可观测性建设
微服务架构下,一个用户请求可能跨越多个服务,排查问题变得困难。我们在迁移的早期就建立了可观测性基础设施:
- 链路追踪——基于 OpenTelemetry 实现全链路追踪,在请求入口生成 traceId 并跨服务传递。
- 集中日志——所有服务将结构化日志输出到 ELK 平台,通过 traceId 关联单个请求的完整日志。
- 指标监控——使用 Prometheus + Grafana 收集和展示各服务的 QPS、延迟、错误率等核心指标。
6. 渐进式迁移策略
我们没有选择"一刀切"的重写方案,而是采用了绞杀者模式(Strangler Fig Pattern)进行渐进式迁移:
- 建立 API 网关——在所有请求的最前端部署网关,作为流量分发入口。
- 逐模块剥离——每次抽出单体中的一个模块,在微服务中独立实现后,将对应路由的流量切到新服务。
- 灰度验证——新服务上线后先切 5% 流量,观察无异常后逐步放大到 100%。
- 清理老代码——确认新服务稳定运行 2 周后,从单体中删除对应的老模块。
整个迁移过程持续了约 5 个月,期间系统一直保持正常运行,用户侧未感知到明显变化。
总结
微服务不是银弹,它解决了一些问题的同时,也引入了分布式系统的复杂性。这次迁移让我深刻体会到:在决定采用微服务之前,先确保你的团队已经做好了应对分布式复杂性的准备——包括 DevOps 基础设施、监控体系以及团队的技术储备。