在微服务与SOA架构体系下,如何合理地划分服务是系统设计的核心问题之一。服务划分过粗会导致单体系统的弊端重现,划分过细则会带来运维复杂度激增、分布式事务等问题。本文从实践出发,结合领域驱动设计(DDD)和康威定律,系统性地阐述服务划分的因素、原则、方法论以及常见反模式。
服务划分不是拍脑袋决定的,需要综合考虑多个维度的因素。以下是实践中验证有效的划分依据。
服务本身的可用性要求可以作为划分服务边界的重要依据,包括服务的可用性、稳定性及性能指标。
典型场景:
| 维度 | 前台系统 | 后台系统 |
|---|---|---|
| 可用性要求 | 7×24小时不间断 | 工作时间可用即可 |
| 响应延迟 | 要求更快响应、更低延迟 | 对吞吐量要求更高 |
| 故障影响 | 直接影响用户体验和收入 | 影响内部运营效率 |
| 扩容策略 | 按峰值流量预留容量 | 按平均负载规划 |
实践经验:在支付系统中,支付网关(前台)和交易对账(后台)的SLA差异极大。前者要求99.99%可用性,后者允许在凌晨低峰期进行维护。将二者分离,可以避免后台系统的维护窗口影响前台支付。
把相对不变的部分独立出来,是控制变更成本的有效策略。
核心逻辑:
案例:支付核心 vs 支付方式
支付核心(不变)
├── 交易流水记录
├── 资金冻结/解冻逻辑
└── 状态机流转
支付方式(常变)
├── 微信支付接口升级
├── 支付宝新功能接入
└── 新兴支付方式(数字人民币等)
踩坑记录:早期将支付方式与支付核心耦合,每次新增支付方式都需要回归测试整个支付链路,测试周期从2天延长到2周。拆分后,新增支付方式仅需独立测试,周期缩短到半天。
领域驱动设计是服务划分最重要的理论依据。通过识别限界上下文(Bounded Context),自然形成服务边界。
关键概念:
| 概念 | 说明 | 示例 |
|---|---|---|
| 限界上下文 | 领域模型的边界,同一概念在不同上下文含义不同 | "订单"在支付上下文和物流上下文中含义不同 |
| 聚合根 | 领域对象的集群,外部通过聚合根访问 | 支付聚合根包含支付单、支付流水、支付状态 |
| 领域事件 | 领域内发生的有意义的事情 | PaymentCompleted、OrderPaid |
划分步骤:
内部约定:我们在做领域划分时,要求每个限界上下文必须有明确的"通用语言"(Ubiquitous Language)。同一个术语在不同上下文中可以有不同含义,但在同一个上下文中必须唯一。
数据所有权是服务划分的硬边界。一个数据实体应该只被一个服务所拥有,其他服务只能通过API访问。
原则:
反例警示:
❌ 错误做法:
订单服务直接查询支付数据库获取支付状态
✅ 正确做法:
订单服务调用支付服务API或订阅PaymentCompleted事件
"设计系统的组织,其产生的设计等同于组织之内、组织之间的沟通结构。" —— Melvin Conway
康威定律的启示:
注意事项:
经验之谈:曾经按前端/后端团队划分服务,导致一个业务功能需要跨团队协调3个服务。后来调整为按业务域划分(每个团队负责完整业务域),沟通成本降低了70%。
当不同部分需要不同的技术栈时,是天然的拆分点。
常见场景:
服务应该能够独立开发、部署、运行和扩展,不依赖其他服务的内部实现细节。
自治性的四个维度:
| 维度 | 说明 | 检查标准 |
|---|---|---|
| 开发自治 | 可以独立修改代码 | 修改不需要通知其他团队 |
| 部署自治 | 可以独立发布 | 发布不强制其他服务同步发布 |
| 运行自治 | 可以独立扩缩容 | 根据自有负载调整资源 |
| 技术自治 | 可以独立选择技术栈 | 不受其他服务技术选型约束 |
故障应该在服务边界内隔离,避免级联失败。
隔离策略:
故障隔离层次
├── 进程隔离(服务独立进程)
├── 资源隔离(线程池、连接池独立)
├── 数据隔离(独立数据库/schema)
└── 部署隔离(独立实例、独立集群)
踩坑记录:曾经多个服务共享一个数据库实例,一个服务的慢查询导致整个数据库CPU打满,所有服务受影响。拆分数据库后,故障隔离性大幅提升。
一个服务应该包含完成其职责所需的所有能力,避免功能分散导致的事务和一致性问题。
完整性检查:
示例:
❌ 不完整划分:
用户服务:管理用户基本信息
用户认证服务:管理用户登录态
用户权限服务:管理用户角色
→ 查询一个用户的完整信息需要调用3个服务
✅ 完整划分:
用户服务:管理用户所有信息(基本信息、认证、权限)
→ 单一服务提供完整用户视图
一个服务应该只负责一个明确的业务领域或功能集合。
判断标准:
高内聚: 服务内部的功能紧密相关,共享数据和行为。
松耦合: 服务之间通过定义良好的接口交互,减少相互依赖。
耦合度评估:
| 耦合类型 | 说明 | 可接受度 |
|---|---|---|
| 数据耦合 | 通过API传递数据 | ✅ 推荐 |
| 控制耦合 | 一个服务控制另一个服务的逻辑 | ⚠️ 尽量避免 |
| 外部耦合 | 共享外部接口/格式 | ⚠️ 谨慎使用 |
| 内容耦合 | 直接访问内部数据 | ❌ 禁止 |
步骤:
上下文关系类型:
| 关系类型 | 说明 | 适用场景 |
|---|---|---|
| 合作关系 | 两个上下文团队协作紧密 | 紧密关联的核心域 |
| 共享内核 | 共享部分模型 | 高度耦合的子域 |
| 客户-供应商 | 上游优先满足下游需求 | 内部服务依赖 |
| 遵奉者 | 下游完全遵循上游模型 | 外部系统对接 |
| 防腐层 | 下游隔离上游模型 | 遗留系统对接 |
| 开放主机服务 | 上游提供标准化接口 | 对外暴露服务 |
| 发布语言 | 定义明确的数据交换格式 | 跨团队集成 |
按组织的业务能力进行分解,每个服务对应一个业务能力。
示例(电商平台):
业务能力
├── 商品管理
├── 库存管理
├── 订单管理
├── 支付处理
├── 物流配送
├── 用户管理
├── 营销促销
└── 售后服务
需要强一致性的事务应该在一个服务内完成,最终一致性可以跨服务。
决策树:
是否需要强一致性?
├── 是 → 必须在同一服务内
└── 否 → 可以考虑跨服务(最终一致性)
├── 实时性要求高? → 同步API调用
└── 实时性要求低? → 异步消息/事件
实践经验:支付和订单状态更新需要强一致性,必须在同一事务边界内。但支付完成后通知物流发货,可以用异步事件,允许短暂延迟。
对于遗留单体系统,不建议大爆炸式拆分,应采用渐进式策略。
绞杀者模式(Strangler Fig Pattern):
阶段1: 单体应用
[ 单体应用 ]
↑
用户
阶段2: 提取第一个服务
[ 单体应用 ] ←→ [ 新服务A ]
↑
用户
阶段3: 逐步提取
[ 单体应用 ] ←→ [ 服务A ] ←→ [ 服务B ]
↑ ↑
用户 ←───────────────────────┘
(API Gateway路由)
阶段4: 单体被"绞杀"
[ 服务A ] ←→ [ 服务B ] ←→ [ 服务C ]
↑
用户
"如果一个服务可以在2周内重写,那它的粒度就是合适的。" —— 业界经验法则
更实用的判断标准:
| 指标 | 建议范围 | 说明 |
|---|---|---|
| 代码行数 | 1000-10000行 | 因语言而异 |
| 团队规模 | 2-8人 | 康威定律适配 |
| 接口数量 | 5-20个 | 对外暴露的API |
| 数据库表 | 5-15张 | 服务拥有的表 |
| 部署频率 | 每周1-5次 | 独立部署能力 |
特征: 服务看似独立部署,但高度耦合,必须同时发布。
识别信号:
解决方案:
特征: 服务粒度过细,功能过于单一。
危害:
解决方案:
特征: 多个服务直接操作同一数据库。
危害:
解决方案:
特征: 按UI层、业务层、数据层划分服务。
危害:
解决方案:
在确定服务划分方案前,逐项检查:
服务划分是架构设计的核心决策,影响系统的可维护性、可扩展性和团队效率。好的服务划分应该:
核心原则:服务划分没有银弹,需要在实践中不断调整。初期可以粗一些,随着业务发展和团队成熟逐步细化。重要的是保持服务的自治性和完整性,而不是追求理论上的完美划分。