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

这里是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 服务调用方体验,不服务业务事实。

  • 能力切片要能承载完整业务不变量。

  • 默认不新建服务,不是反微服务,而是避免吞下不必要的分布式成本。

最后我想把判断落到这个标准上:
拆服务前,先讲清楚负责人、事实源、契约、发布和排障方式。
如果这些讲不清楚,就先不要把它变成一个长期服务。


原创文章,转载请注明本文链接: https://zacharyfan.com/archives/1628.html

关于作者:张帆(Zachary,个人微信号:Zachary-ZF)。坚持用心打磨每一篇高质量原创。欢迎扫描二维码~

微信公众号

定期发表原创内容:架构设计丨分布式系统丨产品丨运营丨一些思考。

如果你是初级程序员,想提升但不知道如何下手。又或者做程序员多年,陷入了一些瓶颈想拓宽一下视野。欢迎关注我的公众号「跨界架构师」,回复「技术」,送你一份我长期收集和整理的思维导图。

如果你是运营,面对不断变化的市场束手无策。又或者想了解主流的运营策略,以丰富自己的“仓库”。欢迎关注我的公众号「跨界架构师」,回复「运营」,送你一份我长期收集和整理的思维导图。

Leave a Reply

发表评论

电子邮件地址不会被公开。 必填项已用*标注

ZacharyFan.com © 2019 | WordPress Theme: BlogGem by TwoPoints.