微服务拆分边界与何时不该拆¶
30 秒版(开场)¶
拆分边界按 业务域(Bounded Context)+ 变更频率 + 团队 Conway 定律,不是按表或按层。过早拆分带来 分布式事务、链路延迟、运维爆炸。生产关键词:高内聚低耦合、数据所有权、API 契约。
3 分钟版(精讲深度)¶
- 是什么:每个服务拥有独立数据与发布周期,通过 API/MQ 协作。
- 为什么:单体过大时编译慢、部署牵一发动全身;但微服务不是银弹。
- 怎么做:识别核心域(订单、支付、用户);每个服务拥有自己的数据写权限,物理上可阶段性共享数据库集群/schema;同步还是异步由返回时必须满足的业务语义决定,不按数量教条选择。
10 分钟版(原理 + 图示)¶
flowchart TB
subgraph order_ctx[订单上下文]
OrderSvc[order-service]
OrderDB[(order_db)]
end
subgraph pay_ctx[支付上下文]
PaySvc[payment-service]
PayDB[(payment_db)]
end
subgraph user_ctx[用户上下文]
UserSvc[user-service]
UserDB[(user_db)]
end
OrderSvc -->|RPC 创建支付| PaySvc
OrderSvc -->|MQ order.created| UserSvc
拆分原则
| 原则 | 说明 |
|---|---|
| 业务能力 | 按域而非技术层(auth-service 全栈) |
| 数据所有权 | 其他服务不能绕过 owner 直接写表;跨域读用 API、投影或受控迁移方案 |
| 变更耦合 | 总是一起改的不拆 |
| 团队边界 | 服务边界应匹配稳定 ownership;一个团队可拥有多个相关服务 |
| 性能边界 | 高频本地调用不拆 RPC |
何时不该拆
- 小团队且没有自动化交付、可观测和 on-call 能力。
- 强一致跨表事务为主,无 Saga 能力。
- 没有独立扩缩、独立发布或隔离故障的实际需求,模块化单体已足够。
- 「先拆再说」无域建模,变成分布式单体。
容量与成本
- 单体 1 万 QPS,P99 10ms;拆 5 服务串行 RPC 各 5ms → P99 25ms+(含网络)。
- 5 服务 × 3 副本 = 15 部署单元,日志/trace 成本 ×5。
生产场景¶
- 电商:订单、库存、支付、营销——库存与订单可拆,营销读多可独立扩。
- 错误拆分:
user-service+user-db-service+user-cache-service过度碎片化。 - 可观测:服务间调用拓扑、循环依赖检测。
排查与工具¶
| 工具 | 用途 |
|---|---|
| 依赖图 / service mesh | 调用链复杂度 |
| 变更共现分析 | Git 哪些模块总是一起改 |
| 领域事件风暴 | DDD 工作坊划界 |
| OpenAPI 契约测试 | 跨团队 API 稳定 |
架构取舍¶
| 方案 | 适用 | 不适用 |
|---|---|---|
| 模块化单体 | 中小团队、快速迭代 | 多团队抢同一代码库 |
| 微服务 | 多团队、独立扩缩 | 小团队、强一致 |
| BFF 聚合 | 移动端多样 API | 简单 CRUD |
| 共享物理 DB、逻辑 ownership | 迁移期或小团队可接受 | 多服务任意跨表读写、共同修改 schema |
深挖问答¶
- 订单和库存拆不拆? → 可拆,用 Saga/事件;秒杀期库存可独立扩。
- 分布式单体是什么? → 拆了很多服务但共享 DB、紧耦合 RPC,最糟组合。
- Go 微服务通信用什么? → gRPC 内部、REST 对外;MQ 解耦。
- 如何防止循环依赖? → 单向依赖规则、MQ 事件、防腐层 ACL。
- 何时从单体再合并? → 调用 1:1、无独立扩缩需求、运维成本 > 收益。
反模式与事故¶
- 按「Controller/Service/DAO」三层拆三个服务。
- 服务 A 直接读服务 B 的数据库。
- 无 API 版本,改字段拖垮所有消费者。
- 20 个微服务,On-call 一人看不懂全局。
代码示例¶
// 防腐层 — 订单域不依赖支付内部模型
type PaymentPort interface {
CreatePayment(ctx context.Context, req CreatePaymentCmd) (PaymentRef, error)
}
type paymentACL struct {
client payv1.PaymentServiceClient
}
func (a *paymentACL) CreatePayment(ctx context.Context, cmd CreatePaymentCmd) (PaymentRef, error) {
resp, err := a.client.Create(ctx, &payv1.CreateRequest{
OrderId: cmd.OrderID,
Amount: cmd.AmountCents,
})
if err != nil {
return PaymentRef{}, err
}
return PaymentRef{ID: resp.PaymentId}, nil
}