LabHub
学习 学习路径 课程

微服务架构

边界画在哪里

在 LabHub 中继续学习

一句话总结

边界不是从名词中产生,而是从语言中产生。如果两个团队使用同一个词,却表达不同含义,那里就是边界。

概念图: 事件风暴。 · 谁会对该事件作出反应 · 计算耦合程度。 · 从单体开始。

为什么需要了解这些

“把系统拆成订单服务、商品服务、会员服务吧。”这是会议室里最常见的一句话,也是最常失败的设计。这种方式不过是把数据库表名直接提升为服务名称。结果可以预见:创建一笔订单,需要分别调用商品服务和会员服务;为了一个页面,三个服务始终必须一起行动。独立发布能力并没有出现。

领域驱动设计提出的视角不同。边界不是来自数据种类,而是来自围绕这些数据进行交流的人所使用的语言。

工作原理

以“商品”这个词为例。对销售团队而言,商品意味着价格、折扣和是否展示;对物流团队而言,商品意味着体积、重量和储存温度;对结算团队而言,商品意味着手续费率和税务代码。如果把这三种模型合并到一张商品表中,每次因物流需求新增字段,销售团队的代码也必须重新发布。

这里就是限界上下文。每个上下文拥有自己的商品模型,上下文之间只通过标识符(例如 SKU)连接。跨越上下文边界时转换模型,就是前面介绍过的防腐层。

实际工作中,可以通过三种方式寻找边界候选。第一,同一个词的含义发生分歧的位置。第二,真正需要事务的范围——必须位于同一事务中的数据,应留在同一服务内。第三,变更频率——每周变化的部分与一年只变化一次的部分,很可能属于不同服务。

生产现场中的常见情况

边界错误的信号,首先出现在会议而不是代码中。如果发布一个功能总是需要协调两个团队的日程,说明边界划错了。纠正方式通常不是继续拆分,而是把两个服务重新合并。合并决策远少于拆分决策,却更经常是正确答案。

还有一点:微服务中的“公共库”是一种安静的耦合。如果把公共 DTO 放在同一个仓库,升级该仓库版本时,所有服务都必须一起行动。更安全的方式不是共享代码,而是通过模式文档(OpenAPI、protobuf)共享契约。

寻找边界的实际流程

“从语言中寻找”虽然正确,却不容易执行。实际现场有一套可用流程。

事件风暴。 把领域专家与开发人员集中到同一个房间,在墙上用过去时贴出“已经发生的事情”:订单已受理支付已批准库存已扣减配送已开始。随后按时间顺序排列事件,并标记谁会对该事件作出反应。反应聚集成块的位置,就是边界候选。

计算耦合程度。 画出候选边界后,统计真实代码中的调用次数。

指标 理想值 数值过差意味着什么
绘制一个页面所需的服务数 1~2 3 个以上说明边界错误
发布一个功能所涉及的团队数 1 2 个以上说明边界跨越团队
服务间同步调用深度 1~2 3 层以上会导致故障级联

从单体开始。 一开始就拆分通常会划错,因为此时还不了解领域。先在同一仓库中通过模块划定边界(模块化单体),当边界持续六个月都没有变化,再把模块拆成服务。错误的模块边界半天就能移动,错误的服务边界却可能需要数月修正。

不应拆分的时候

与正确划定边界同样重要的是,决定不拆分也是设计的一部分。满足以下条件时,拆成服务几乎总是得不偿失。

还必须计算运维成本。每增加一个服务,就会分别增加一个仓库、一套 CI、一条发布流水线、一块仪表板、一组告警和一份值班文档。拥有五个服务的系统,不是代码量增加五倍,而是运维表面积增加五倍

数据跨越边界时

一旦划出边界,立刻会遇到“那要怎么做 Join”的问题。订单列表需要显示商品名称,但商品属于另一个服务。答案有三种,每一种代价不同。

第二种方式成为正确答案的情况,比想象中更多。先确定一个值应被视为**“引用”,还是“当时的事实”**,答案自然会出现。

下一次测验要确认什么

本模块只介绍概念。紧接着的测验会检查边界判断标准;下一模块则会亲手验证,如此拆分的服务在真正通信时需要付出什么代价。