服务边界不是拆得越细越好:微服务治理真正要管什么

大家好,我是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 服务调用方体验,不服务业务事实。
  • 能力切片要能承载完整业务不变量。
  • 默认不新建服务,不是反微服务,而是避免吞下不必要的分布式成本。

最后我想把判断落到这个标准上:

拆服务前,先讲清楚负责人、事实源、契约、发布和排障方式。

如果这些讲不清楚,就先不要把它变成一个长期服务。

相关文章