服务边界与拆分证据:什么时候分布式成本值得承担
从变更耦合、数据所有权、团队协作、容量隔离和失败传播判断服务边界,避免把代码目录直接映射成进程。
Spring Cloud 提供的是分布式系统常见模式的编程抽象,不能替代服务边界设计。版本与项目范围参见 Spring Cloud。
进程边界必须围住业务不变量
一个服务不只是若干 Controller,而是对一组业务状态拥有最终写入权并对外承诺稳定契约。订单服务可以接收价格快照,却不应绕过定价服务直接修改价格规则;库存服务拥有预留与释放状态,其他服务通过命令或事件协作。若两个所谓服务共享同一组表、互相更新对方字段并要求同版本发布,它们只是网络连接起来的分布式单体。
先在模块化单体中建立包边界、依赖方向、独立测试和数据访问门面。只有边界在同进程内已经可执行,拆成进程才不会把隐藏耦合变成运行时故障。拆分不是治理混乱代码的捷径,而是把已经明确的自治边界交给独立部署与容量管理。
拆分收益要由变化和运行数据证明
适合拆分的信号包括:能力由独立团队长期拥有;发布频率与其他模块显著不同;峰值容量和资源类型需要独立扩缩;合规或故障域要求隔离;跨模块变更比例持续下降。反面信号是同一需求总要跨五个服务改字段、一次测试必须启动整套环境、事务频繁跨库补偿、接口只转发内部实体。
用版本库统计共同变更率,用调用图统计同步扇出,用数据库审计统计跨所有权写入,用事故记录统计故障传播。收益要与服务发现、网络、超时、重试、契约、部署、监控和值班成本相减。没有可量化收益时,模块化单体通常是更强的默认值。
数据迁移决定拆分是否可回滚
真正困难的不是搬代码,而是把共享表变成单一写者。先封装旧表访问,再建立新服务 API 或事件,使用增量同步与校验比较新旧读模型,最后切换写入权。迁移期间双写必须有失败窗口和对账,不能把两个数据库更新包装在一个 try/catch 里假装原子。
回滚路径要在切流前准备:旧模块是否仍可读取新产生的数据,新字段是否向后兼容,事件能否重放,写入权能否在无冲突时切回。只允许代码回滚、不允许数据回滚的方案,必须明确前滚修复与人工处置。
把同步调用画成预算递减的状态机
设计时为每条边记录 owner、协议、连接模型、超时阶段、重试责任、幂等键、流量上限、降级语义和回滚方式。同步扇出越大,成功率乘积越低,尾延迟取最大值;能异步解耦的非关键副作用不要停留在主请求链。
失败控制必须按依赖隔离
注册中心不可用、无实例、连接池满、连接失败、读取超时、业务拒绝和服务端错误要分型。只有动作未开始或有幂等保障的暂态失败允许重试;结果未知先查询或使用稳定幂等键。限流、隔离和熔断的拒绝应成为可观测结果,不能统一包装成空数据。
用“谁能拒绝这次变化”检验自治
真正的服务 owner 能独立拒绝不符合其不变量的请求、安排自己的 schema 演进并在不协调全系统发布的前提下交付兼容版本。若上游可以直接要求它暴露内部表字段,或一次字段变更必须由中央群聊同步十个团队,组织边界并没有形成。评审一条新接口时,应先问调用方需要的业务结果,而不是让提供方导出内部对象。
共享库也会绕过自治。包含 DTO、数据库实体、异常和业务工具的公共包让所有服务在编译期锁步;公共库应收缩到协议生成物、基础观测和极少稳定原语,并允许多版本并存。业务规则通过服务契约或事件演进,不通过升级一个“common”包强制全网发布。
用两个 Java 状态模型固定决策边界
这两个 Java 17 模型不启动真实注册中心、Gateway 或 RPC Server,而是把本篇的状态、预算和不变量转成确定输出。真实集成测试继续验证客户端协议、自动配置、连接复用、推送延迟与故障时序。
javac --release 17 -Xlint:all -Werror examples/backend-development/microservice/service-boundaries/BoundaryCouplingDemo.java examples/backend-development/microservice/service-boundaries/SplitCostDemo.java
java -cp examples/backend-development/microservice/service-boundaries BoundaryCouplingDemo
java -cp examples/backend-development/microservice/service-boundaries SplitCostDemocapability=pricing crossModuleChanges=2 dataOwners=1 boundaryStable=true
monthlyBenefit=180 monthlyDistributedCost=240 shouldSplit=false rollbackPath=modular-monolith状态模型输出变化时,要解释对应的服务边界、Deadline、版本或治理不变量为什么改变。集成测试必须主动制造连接已写出后超时、实例列表陈旧、配置刷新一半、半开探测和灰度标签断链。
契约、安全与配置共同决定可运营性
服务目录记录 owner、业务能力、数据所有权、同步/异步契约、SLO、容量、依赖、敏感级别和下线路径。serviceId、routeId、client name 与 trace service.name 使用稳定命名,避免注册中心、日志和指标各有别名。接口新增优先保持向后兼容,破坏性变化经过双版本与消费者契约测试。
动态配置和发现元数据都可能陈旧。变更必须带版本、owner、范围、过期时间和回滚值,先灰度到少量实例并比较版本分布。会影响线程池、连接池、路由、限流和鉴权的配置要通过不变量校验,失败时保留上一完整快照。
容量与可观测证据要覆盖控制面和数据面
控制面观察注册/订阅延迟、缓存年龄、配置版本分布和刷新失败;数据面观察入口速率、实例选择、连接池等待、调用尝试、重试放大、熔断拒绝、降级、尾延迟和业务结果。指标按 service、operation class、routeVersion、result 与 failure stage 聚合,不把 userId、URL 或 traceId 作为常驻标签。
上线证据包括新旧契约互调、实例排空、Deadline 传播、幂等重试、治理状态机、灰度指标和回滚后的残留任务。完成标准不是“组件控制台显示健康”,而是业务不变量保持、未知结果可查询、故障被限制在预算内并最终收敛。
