服务边界不是拆得越细越好:微服务治理真正要管什么
大家好,我是Z哥。 你有没有见过这种场景? 一个后台 Job,因为运行时间长、资源消耗大,被提议单独拆一个服务。 一个商品详情页,因为要聚合商品、库存、营销、评价,也被提议单独建一个聚合服务。 App、H5、PC 三端字段不一样,于是有人说,那就每端一个 BFF。 再加上 CI 慢、部署慢、扩缩容策略不同、MQ Con
这里是Z哥的个人公众号
每周五11:45 按时送达
当然了,也会时不时加个餐~
我的第「235」篇原创敬上
大家好,我是Z哥。
你有没有见过这种场景?
一个后台 Job,因为运行时间长、资源消耗大,被提议单独拆一个服务。
一个商品详情页,因为要聚合商品、库存、营销、评价,也被提议单独建一个聚合服务。
App、H5、PC 三端字段不一样,于是有人说,那就每端一个 BFF。
再加上 CI 慢、部署慢、扩缩容策略不同、MQ Consumer 和 API 运行方式不同,最后所有理由都指向同一个结论:
「要不再拆一个服务吧。」
我最近重新梳理服务治理时,越来越强烈地感觉到:很多所谓的微服务治理,真正要解决的不是「服务拆多细」的问题,而是别把几个完全不同的概念混成同一个词。
这个词就是「服务」。
我先讲结论:
服务不是代码分组单位,也不是部署单位。服务是一个团队长期负责某组业务事实,并对外承诺稳定契约、能独立发布和排障的边界。
如果这个定义没对齐,后面的所有治理动作都会走偏。
系统看起来更微服务,实际上只是仓库更多、进程更多、调用链更长、数据归属更乱、故障更难追。
/01 最常见的误区:把运行差异当成服务边界/
很多服务拆错,不是因为大家不懂架构,而是因为日常工程里确实有很多「看起来应该独立」的东西。
比如:
- API 和 MQ Consumer 运行方式不同。
- 定时 Job 需要单独扩容。
- 某个批处理任务失败影响线上进程,想隔离。
这些诉求都真实。
但它们不一定是「新建逻辑服务」的理由。
这里要先拆开四个概念:
逻辑服务:有明确负责人,负责业务事实、稳定契约、独立发布和排障
能力切片:逻辑服务内部的业务能力单元
运行入口:API / MQ Consumer / Job / CLI
部署单元:平台上真正运行、扩缩容、观测和调度的对象
一个能力可能有多个运行入口。
比如「价格」能力里,可以有:
- 给前台页面查询价格的 API;
- 消费价格变更消息的 MQ Consumer;
- 定期重建价格缓存或索引的 Job。
这些入口的运行方式不一样,但它们使用的是同一套价格规则、同一份事实源、同一个长期负责团队。
所以它们不是三个服务。
更合理的表达是:
价格能力切片
•API 入口
•MQ Consumer 入口
•Job 入口
部署方案
•价格 API 常驻单元
•价格消息消费常驻单元
•价格重建临时任务单元
如果 Job 需要独立超时、独立并发、独立资源配额、独立告警,那是「部署方案」要解决的问题。
不应该靠新建服务来解决。
运行差异不等于业务边界差异。
这一步判断很关键。
/02 服务边界真正保护的是事实源/
我判断一个服务能不能独立,第一反应不是看它有没有独立仓库,也不是看它是不是单独部署。
我会先问一个问题:
它有没有自己的「业务事实源」?
比如订单服务拥有订单事实,库存服务拥有库存事实,支付服务拥有支付事实。
这些事实不是简单的数据表,而是业务世界里对某件事的权威解释。
谁决定订单是否已支付?
谁决定库存是否可售?
谁决定优惠是否可用?
谁决定商品是否上架?
这类问题的答案,就是事实源边界。
拿前面的价格 Job 举例。
一个价格重建 Job 跑得很慢,占资源,还可能拖垮线上 API。
这时候最容易发生的误判是:既然运行方式不同,那就拆一个 price-job-service。
但真正该问的是另一组问题:
- 这个 Job 有没有新的价格事实源?
- 有没有新的长期负责团队?
- 有没有新的价格契约?
- 能不能独立构建、发布、回滚和排障?
如果答案都是否定的,它就不是新服务。
它只是同一个价格能力的另一个运行入口。
正确动作不是新建逻辑服务,而是把它变成独立部署单元:单独资源配额、单独超时、单独并发、单独告警、单独 runbook。
这就是服务边界治理最容易被忽略的一点:
不要让「运行隔离」冒充「业务边界」。
当然,真实系统里不一定会出现两个服务直接共享核心表、互相改状态这么夸张的情况。
更常见的问题是:两个服务之间同步调用越来越密,接口粒度越来越细,一次需求经常要两边同时改、同时测、同时发布。
这种情况下,虽然架构图上画的是两个服务,但它们在演进节奏上没有真正分开。
判断一个服务是否独立,不只是看它有没有单独部署,也要看它能不能独立负责事实源、契约、发布、回滚和排障。
如果两个服务长期高频协同发布、互相等待、故障排查也总是绑在一起,那它们更像是一个业务能力被拆成了两个运行单元,而不一定是两个真正独立的逻辑服务。
这会制造一种很隐蔽的坏味道:
代码看起来分开了,责任没有分开。
部署看起来独立了,演进节奏没有独立。
调用链看起来解耦了,发布和故障还是绑在一起。
所以我更愿意用这个公式判断:
逻辑服务 = 谁负责 + 负责什么事实 + 对外承诺什么契约 + 怎么独立发布和排障
这四个问题里,最要先看的是事实源。
负责人、契约、发布和排障方式还可以慢慢补齐;事实源一旦放错,后面很难只靠改代码修回来。
因为那时候数据归属、接口契约、发布节奏和故障排查都会被绑在一起。
/03 场景聚合可以存在,但不要偷业务事实/
再看一个常见场景:商品详情页。
一个商品详情页可能要展示商品基础信息、库存状态、营销活动、评价摘要、推荐内容、配送信息。
于是很容易有人说:这不就是一个 product-detail-service 吗?
答案没那么简单。
如果它只是为了让前端少调几次接口,临时把多个服务的结果拼一下,那我认为不值得新建长期逻辑服务。
但如果商品详情是一个长期稳定的业务场景,它有稳定契约,被 App、H5、PC、小程序、开放 API 多类调用方复用,而且里面有明确的编排、降级、缓存、排序、派生读模型,那么它可以成为一个「场景聚合服务」。
关键是边界要讲清楚:
场景聚合服务可以拥有场景契约,不能拥有业务事实。
它可以做:
- 聚合多个业务域的数据;
- 对展示字段做裁剪;
- 做场景级缓存;
- 做降级兜底;
- 做派生读模型;
- 屏蔽上游服务的拓扑复杂度。
但它不能做:
- 直连商品库、库存库、营销库;
- 绕过业务服务契约;
- 沉淀商品、价格、库存、订单这类核心业务规则;
- 把本该属于业务域的事实搬到聚合层。
这里最危险的不是新建聚合层。
最危险的是,为了改得快,把本该属于业务域的规则写进聚合层。时间一长,业务域服务会退化成数据接口,真正的业务规则反而都留在聚合层。
这时候系统会出现一个看起来矛盾的结果:
你以为自己在解耦前台体验,实际上是在把业务事实搬到体验层。
前几次需求可能会变快,因为聚合层离页面最近,改起来最顺手。但规则一旦从领域服务漂到聚合层,后面再想说清楚价格、库存、订单这些事实到底归谁负责,就会越来越难。
/04 BFF 服务调用方体验,不服务业务事实/
BFF 也一样。
App、H5、PC 字段不同,要不要每端一个 BFF?
我的默认答案是:不要机械按端拆。
BFF 的边界应该看调用方族、渠道族、权限上下文、会话模型和发布节奏,而不是屏幕大小。
同一业务线里,App、H5、PC 只是展示字段略有不同,长期负责团队一致、权限模型一致、发布节奏一致,那更适合放在同一个 BFF 逻辑服务里,用能力切片或运行入口表达差异。
只有当这些东西真的分裂,才考虑拆:
- 契约不一样;
- 权限边界不一样;
- SLO 不一样;
- 发布节奏不一样;
- 长期负责团队不一样;
- 生命周期不一样。
BFF 有一句很重要的边界句:
BFF 只服务调用方体验,不服务业务事实。
它可以做展示 DTO、协议适配、登录态、设备信息、渠道上下文、端侧字段裁剪。
但它不应该决定价格怎么算、库存怎么扣、订单状态怎么流转、权益是否生效。
一旦 BFF 开始写这些规则,它就不是 BFF。
它是在业务域外面复制了一份业务域。
/05 默认不新建服务,不等于反微服务/
我现在越来越倾向于一个保守原则:
默认不新建逻辑服务。
这句话容易被误解成「反微服务」。
我反对的不是微服务,而是在负责人、事实源、契约、发布和排障方式都没讲清楚之前,就先把一个名字变成长期服务。
一个能力如果要成为独立逻辑服务,至少要回答清楚这些问题:
- 它有没有独立业务语言?
- 它有没有自己的事实源?
- 它有没有长期负责人?
- 它能不能独立构建、发布、回滚?
- 它是否维护稳定 API 或事件契约?
- 它的 SLO、告警、日志、链路追踪和 runbook 能不能独立维护?
- 不拆出来,是否真的阻塞团队独立演进?
- 容量、隔离、扩缩容诉求,为什么不能通过部署方案解决?
如果这是一次服务治理评审,我会要求提案里至少写清楚这几项:
service-boundary-proposal:
responsible_team: 谁长期负责
fact_source: 它掌握什么业务事实
contract: 对外稳定 API 或事件契约是什么
lifecycle: 如何独立构建、发布、回滚
operations: SLO、告警、日志、链路追踪、runbook 怎么维护
alternative: 为什么能力切片或部署方案承载不了
offset: 是否有存量服务需要吸收、转入口、保留兼容层或下线
这不是流程洁癖。
它是在逼团队把「我想拆」翻译成「这个边界真的能长期负责」。
如果这些问题答不出来,它大概率不是新服务。
它可能只是:
- 一个已有能力切片里的新功能;
- 一个新的能力切片;
- 一个新的 API 入口;
- 一个新的 MQ Consumer;
- 一个新的 Job;
- 一个新的部署单元;
- 一个临时兼容层。
这里可以用这个标准判断:
如果没有明确的独立边界收益,就不要引入分布式系统成本。
每拆一个服务,你得到的不只是一个新的目录结构。
你同时得到:
- 多一次网络调用;
- 多一份契约维护;
- 多一套发布链路;
- 多一份监控告警;
- 多一个故障边界;
- 多一个数据一致性问题;
- 多一个责任归属问题。
如果收益只是「代码看起来更干净」,这笔账大概率不划算。
/06 能力切片要承载完整不变量/
服务不应该乱拆,但这不代表所有东西都塞进一个大泥球。
中间还有一层很重要的概念:能力切片。
能力切片可以理解为逻辑服务内部的业务能力边界。
比如一个电商商品域里,可能有:
- 类目;
- 商品档案;
- 属性;
- 品牌;
- 价格;
- 上下架;
- 搜索索引;
- 商品侧展示数据。
它们可能属于同一个大业务域,但不一定共享同一套领域语言、变化节奏和运行诉求。
所以需要切片。
但切片也不能太碎。
我的判断标准是:
能力切片要足够小,小到能表达清晰业务能力;也要足够大,大到能承载一个完整业务不变量。
太粗,会变成谁都说不清的大服务。
太细,会制造高频调用、循环依赖、同步发布和分布式事务。
一个能力切片是否合理,可以反向问这些问题:
- 是否有清晰业务语言?
- 是否有明确负责人?
- 是否有事实源归属?
- 是否能承载一个完整业务不变量?
- 是否避免和其他切片高频、细碎、来回调用?
- 是否只是新增 API、CRUD、Job 或 Consumer?
- 容量、隔离、扩缩容诉求能否通过部署方案解决?
拿不准时,我倾向于先粗一点。
因为把一个粗切片拆成两个,通常比把一堆拆碎的服务重新揉回正确边界容易。
这也是很多团队后来很难处理的地方:
真正麻烦的地方不在拆分当天,而在后面重新收敛时要处理调用链、数据归属、发布节奏和责任边界。那时候每一项都会变成迁移成本。
/07 存量服务收敛,比新增服务审批更重要/
服务治理如果只管新增,不管存量,最后一定失败。
因为组织里永远有新需求,永远有临时项目,永远有人觉得「再建一个服务最快」。
如果治理动作只增不减,系统复杂度会单向膨胀。
存量服务是否应该收敛,我会看几个组合信号:
- 同一个负责团队;
- 同数据源;
- 同业务语义;
- 强同步调用;
- 高频协同发布;
- 没有独立 SLO;
- 出问题时需要同一批人一起排查;
- 下游依赖的只是接口形态,不是真正的独立业务能力。
命中越多,越要进入收敛评估。
收敛不一定是直接删除。
更现实的方式有几种:
- 保留为独立逻辑服务;
- 吸收到目标服务,变成能力切片;
- 转成已有能力切片的运行入口;
- 只保留兼容 API 或事件契约,内部实现迁走;
- 等调用方迁移后下线。
这里有个很实用的治理规则:
如果某个业务域已经拆出了太多服务,那么新增一个服务时,最好同时说清楚一个抵消项。
比如:
新增 1 个服务,同时明确 1 个服务要吸收、转入口、保留兼容层或下线。
这不是做数字游戏。
它是在提醒团队:服务数量不是资产,边界清晰才是资产。
/08 一个简单的决策流/
原因清楚了,我们具体可以怎么做呢?
我会用下面这个决策流。
第一步,先判断它到底是什么类型。
沉淀业务事实和规则 -> 业务域服务
稳定场景契约 + 跨域聚合 + 不拥有事实源 -> 场景聚合服务
调用方族 / 渠道族适配 -> BFF
通用技术能力 + 平台负责团队 -> 平台服务
只是 API / MQ Consumer / Job 差异 -> 运行入口
第二步,看已有能力切片能不能承载。
如果只是某个业务能力的新接口、新字段、新 Job、新 Consumer,优先放回已有切片。
第三步,看运行诉求能不能通过部署方案解决。
比如资源隔离、扩缩容、超时、重试、并发、限流、告警、凭证权限,这些很多时候应该由部署单元表达。
第四步,最后才评估新建逻辑服务。
只有当它真的具备明确负责人、事实源、契约、独立发布和排障方式,并且能力切片和部署方案都无法承载时,才值得新建服务。
可以把它压缩成一个判断函数:
shouldCreateService(capability):
if onlyEntrypointChanged:
return “新增运行入口”
if existingCapabilitySliceCanOwnIt:
return “放入已有能力切片”
if newCapabilitySliceCanOwnIt:
return “新建能力切片”
if deploymentPlanCanSolveRuntimeNeed:
return “调整部署方案”
if hasResponsibleTeam
and hasFactSource
and hasStableContract
and hasIndependentReleaseAndDebug:
return “例外新建逻辑服务”
return “不要新建服务”
这个函数不完美,但它能阻止很多「拍脑袋拆服务」。
/09 几个强反模式/
最后列几个我认为要高度警惕的反模式。
第一,按运行入口拆服务。
同一业务能力的 API、MQ Consumer、Job 共享同一套规则和事实源,却拆成三个服务。
这是把运行差异当成业务边界。
第二,按页面或按端机械建体验层服务。
每个页面一个长期聚合服务,或者 App、H5、PC 只是字段不同就拆三套 BFF,最后体验层变成新的业务规则中心。
这是把前端调用便利当成服务边界。
第三,跨服务共享事实源。
多个服务共享同一批核心表、Redis key、MQ topic,甚至共享高权限连接串。
这是最危险的。
因为它让所有服务边界都变成纸面边界。
第四,为「以后可能独立」提前建服务。
如果现在没有明确负责人、事实源、契约、发布和排障方式,大概率以后也不会自动长出来。
提前建服务不会自动长出独立边界,反而会先引入一套发布、监控、契约维护和排障成本。
这里还有一个更底层的治理原则:
不要只问「这个服务能不能建」。
还要问「如果建了,哪个旧边界要被收敛」。
好了,总结一下。
这篇呢,Z哥和你分享了我对「服务边界治理」的一点思考。
核心不是反对微服务,而是反对把所有工程差异都包装成服务边界。
真正有用的判断是:
- 服务不是代码分组单位,而是负责人、事实源、契约、发布和排障方式都清楚的边界。
- API、MQ Consumer、Job 是运行入口,不天然是服务。
- 容量、隔离、扩缩容诉求,优先用部署方案解决。
- 场景聚合可以拥有场景契约,但不能拥有业务事实。
- BFF 服务调用方体验,不服务业务事实。
- 能力切片要能承载完整业务不变量。
- 默认不新建服务,不是反微服务,而是避免吞下不必要的分布式成本。
最后我想把判断落到这个标准上:
拆服务前,先讲清楚负责人、事实源、契约、发布和排障方式。
如果这些讲不清楚,就先不要把它变成一个长期服务。