阶段一 · 微服务架构与通信

微服务架构设计

一句话总结

微服务不是为了拆分才拆分,而是围绕业务能力划分独立可部署的服务单元.
拆分的终极判断标准只有一个:能否让不同团队以不同节奏独立交付.

为什么需要拆分单体服务

一个典型的 Go 单体后端:所有业务逻辑在一个 binary 里,共享一个数据库,一次 CI/CD 部署整个应用.
早期团队小,迭代快,运维简单.但问题出在规模上升之后:

痛点 表现 根因
交付速度下降 改一行代码要全量回归 + 全量部署 代码耦合,部署粒度 = 整个应用
故障爆炸半径大 一个 OOM / panic 拖垮整个进程 所有模块共享进程资源
团队协作冲突 10 个人改同一个仓库,频繁 merge conflict 代码所有权边界模糊
技术栈锁定 想升 Go 1.22 但有模块依赖 CGO 旧库 所有模块必须统一编译环境
扩容浪费 只有搜索模块需要 CPU,但只能整体扩容 资源粒度 = 整个应用

单体 = 一栋综合办公楼,所有部门共享一个电梯.早高峰一层拥堵,全楼瘫痪.
微服务 = 各部门搬进独立写字楼,各有自己的电梯和供电系统.楼之间通过班车(RPC / 消息)通勤.

单体应该被淘汰吗?

如果你的团队只有 3-5 人,产品还在探索期,业务逻辑不超过 10 万行.那么单体是最优解.
微服务的代价(网络通信,分布式事务,运维复杂度) 在小团队里远大于收益.后面会详细讨论"何时不该拆".

微服务定义

先给一个工作定义:

定义

微服务架构是一种将应用程序构建为一组小型,自治服务的方法.
每个服务围绕一个业务能力(Business Capability)构建,拥有自己的数据存储,通过轻量级协议(HTTP/gRPC/消息队列)通信,可被独立开发,部署和扩缩容.

关键特征:

特征 含义 反例
围绕业务能力 按业务域而非技术层拆分 把"数据库层"拆成一个服务 ✗
独立数据存储 每个服务拥有私有 DB / schema 10 个服务共享一个大库 ✗
独立部署 改 A 服务不需要重新部署 B 发布前要"集成联调会" ✗
轻量级通信 gRPC / HTTP / 事件 共享内存 / 直接读对方数据库 ✗
去中心化治理 各服务可选择适合的技术栈 强制所有服务同一框架版本 ✗

单体不止一种形态

在讨论"拆分"之前,先识别你手上的单体类型--不同形态的药方不同:

经典单体(Single Deployment Unit)

所有代码编译成一个 binary / 一个 WAR 包部署.内部可能有模块划分(package / module),但运行时是一个进程.

分布式单体(Distributed Monolith)

服务虽然物理拆开了,但因为共享数据库,同步链式调用,联合发布,实际上没有获得独立部署的能力.这是最糟糕的状态--拥有分布式的全部代价,却没有微服务的任何收益.

分布式单体的典型症状

改了 A 服务的一个字段,B/C/D 都要跟着发布;每次上线需要所有服务"同时切流";任何一个下游超时整条链路就瘫痪.

如果你的"微服务"有这些表现--它本质上还是单体,只是多了网络开销.

模块化单体(Modular Monolith)

单一部署单元,但内部有清晰的模块边界(独立 DB schema,接口契约,显式依赖声明).这是许多团队的最优中间态--享受单体的简单运维,同时为未来可能的拆分保留边界.

┌────────────────────────────────────────────────────────────┐
│ 模块化单体 │
│ │
│ ┌──────────┐ 接口调用 ┌──────────┐ 接口调用 ┌──────────┐ │
│ │ 订单模块 │ ─────────→ │ 支付模块 │ ─────────→ │ 库存模块 │ │
│ │ (私有表) │ │ (私有表) │ │ (私有表) │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ 共享部署单元 │
└────────────────────────────────────────────────────────────┘
↓ 未来可独立拆出
┌──────────┐ gRPC ┌──────────┐ 事件 ┌──────────┐
│ 订单服务 │ ──────────→ │ 支付服务 │ ────────→ │ 库存服务 │
└──────────┘ └──────────┘ └──────────┘

用 DDD 划定服务边界

微服务拆分最大的难题不是技术,而是在哪里画线.领域驱动设计(Domain-Driven Design,DDD)给出了最实用的思维框架.

限界上下文(Bounded Context)

核心概念:每个限界上下文是一个自洽的业务领域,内部的术语和规则在上下文内有明确含义,出了边界可能语义不同.

"账户"在不同上下文里含义完全不同:
用户中心的"账户" = 登录凭证 + 个人信息.
财务系统的"账户" = 资金余额 + 交易记录.
权限系统的"账户" = 角色 + 权限列表.
把它们强行塞进一张 Account 表,就是混淆了限界上下文.

实操步骤:

  1. 事件风暴(Event Storming):召集业务方和开发,在白板上列出所有业务事件("订单已创建","支付已完成","库存已扣减"...)
  2. 聚合命令和事件:将相关事件归入同一个聚合(Aggregate),聚合是事务一致性的边界
  3. 识别上下文边界:当两组事件之间的交互变成"通知"而非"直接调用"时,它们大概率属于不同上下文
  4. 上下文 = 服务候选:每个限界上下文对应一个或一组微服务

上下文映射(Context Mapping)

服务之间不是孤岛--它们必须协作.DDD 定义了几种典型的上下文间关系:

关系模式 含义 微服务场景
防腐层(ACL) 在边界处翻译外部模型,不让外部概念污染内部 调用第三方支付 API 时,封装一层适配器
开放主机服务(OHS) 提供标准化 API 供多方调用 用户中心对外暴露 gRPC 接口
共享内核(SK) 两个上下文共享一小部分模型 订单和物流共享 Address 值对象
客户/供应商 下游需求驱动上游 API 演进 前端 BFF 驱动后端接口变更

防腐层是微服务最重要的模式之一

当你需要对接外部系统(第三方 API,遗留单体,另一个团队的服务),永远在边界处加一个翻译层.

它的价值:

  1. 外部模型变更不会扩散到你的核心领域
  2. 你可以随时替换外部依赖而不改内部逻辑
  3. 测试时可以 mock 防腐层而非整个外部系统

拆分策略与粒度

常见拆分维度

维度 适用场景 示例
按业务能力 最常见,DDD 导出 订单服务,支付服务,库存服务
按数据所有权 数据隔离需求强 用户数据服务(GDPR 合规要求独立)
按变更频率 部分模块迭代极快 营销活动服务(每周变)vs 结算服务(每季度变)
按团队组织 康威定律驱动 A 团队负责搜索,B 团队负责推荐
按扩缩容需求 资源画像差异大 CPU 密集的图片处理 vs IO 密集的消息推送

康威定律(Conway's Law)

系统架构 = 组织沟通结构

"设计系统的组织,其产出的设计等同于组织的沟通结构."--Melvin Conway, 1967

康威定律不是建议,是物理定律级的观察.实践中的含义:

  • 如果你的组织是按"前端组 / 后端组 / DBA 组"划分的,你很难做出围绕业务能力的微服务--因为每个功能都要跨三个组协调
  • 反向操作(逆康威定律):先设计你想要的系统架构,再按这个架构调整团队结构
  • 一个服务的最佳 owner 数量 = 一个团队(5-9 人).超过这个数字通常意味着服务太大了

粒度判断:太粗 vs 太细

太粗(本质还是单体) 太细(Nano-service) 合适
部署频率 改一行要全量发布 一个功能要改 5 个服务 改动通常只影响 1 个服务
团队所有权 多个团队争抢同一个服务 一个人维护 10 个服务 一个团队完整拥有
数据一致性 事务在内部即可保证 到处都是分布式事务 服务内强一致,跨服务最终一致
通信模式 内部模块调用 大量同步链式 RPC 核心路径 ≤ 3 跳

粒度经验法则

如果你无法在两周内完全重写一个服务的核心逻辑--它可能太大了.
如果一个用户请求需要串行调用 5 个以上的服务--你可能拆太细了.

常见反模式

反模式一:共享数据库

多个服务直接读写同一个数据库的同一张表.

为什么有害:

  • 数据库 schema 变更影响所有服务--耦合回来了
  • 无法独立扩缩容数据层
  • 数据所有权不清,谁都能改,没人敢动

正确做法: 每个服务拥有自己的 schema(甚至独立数据库实例).需要对方数据时,通过 API 或事件获取,不直接查表.

反模式二:同步链式调用

A → B → C → D → E,链路上任何一环超时,整个请求失败.

为什么有害:

  • 可用性 = 各环节可用性的乘积(99.9%5 = 99.5%)
  • 延迟 = 各环节延迟之和
  • 任何一个下游的部署或故障都影响上游

正确做法: 异步解耦(事件驱动),并行调用,缓存,设置合理超时 + 降级.

反模式三:分布式单体

前面已经提到--物理拆分但逻辑耦合.核心症状:不能独立部署.

检测方法: 问自己"我能不能只发布 A 服务而不通知其他团队?"如果答案是"不能"--你有一个分布式单体.

反模式四:按技术层拆分

把"数据访问层","业务逻辑层","API 层"各自拆成独立服务.

为什么有害: 一个业务需求改动要同时修改 3 个服务--拆分没有减少协调成本,反而增加了网络调用和部署风险.

正确做法: 按业务能力纵向拆分,每个服务内部自己包含 API → 业务逻辑 → 数据访问的完整栈.

┌───────────────────────┐
│ 按技术层拆分(错误) │
├───────────────────────┤
│ • direction TB │
│ • API 层服务 │
│ • 业务逻辑服务 │
│ • 数据访问服务 │
│ • API --> BIZ --> DAL │
└───────────────────────┘
┌─────────────────────┐
│ 按业务能力拆分(正确) │
├─────────────────────┤
│ • direction TB │
│ • 订单服务
API+逻辑+数据 │
│ • 支付服务
API+逻辑+数据 │
│ • 库存服务
API+逻辑+数据 │
└─────────────────────┘

何时不该拆微服务

这可能是整篇笔记最重要的一节.微服务有真实的代价:

代价 具体表现
网络不可靠 RPC 超时,重试风暴,消息丢失--单体里不存在这些问题
数据一致性难 跨服务没有数据库事务,需要 Saga / 补偿 / 最终一致
运维复杂度 10 个服务 = 10 套 CI/CD,10 套监控告警,10 套日志收集
调试困难 一个请求经过 5 个服务,分布式追踪成为刚需
测试复杂 集成测试要启动整个依赖链,或维护大量 mock
团队认知负担 开发者需要理解服务间的交互协议,数据流向,故障模式

以下场景建议保持单体(或模块化单体):

  • 团队 ≤ 5 人
  • 产品处于 MVP / 探索期,业务边界还不清晰
  • 没有 K8s / 容器化基础设施,运维能力不足
  • 不存在独立部署的需求(所有功能总是一起发布)
  • 核心路径对延迟极度敏感(每多一跳网络增加 1-5ms)

渐进式演进策略

最务实的路径:先构建模块化单体(内部清晰边界 + 独立 schema),等真正出现独立部署 / 独立扩容的需求时,再沿着已有边界拆出去.
这比一开始就强行拆微服务安全得多.

从单体到微服务的演进路径

如果决定要拆,不要一次性重写,而是渐进式拆分(Strangler Fig Pattern):

┌────────────┐
│ 第一步:识别边界 │
├────────────┤
│ • 单体应用 │
└────────────┘
┌─────────────────────────────┐
│ 第二步:模块化 │
├─────────────────────────────┤
│ • 模块化单体
清晰接口 + 独立 schema │
└─────────────────────────────┘
┌────────────┐
│ 第三步:逐步抽取 │
├────────────┤
│ • 服务 A │
│ • 缩小的单体 │
└────────────┘
┌────────────────────────┐
│ 第四步:持续演进 │
├────────────────────────┤
│ • 服务 B │
│ • 服务 C │
│ • MOD2 --> |"继续拆"| MS3 │
└────────────────────────┘

绞杀者模式(Strangler Fig Pattern)

名字来自热带的绞杀榕--种子落在宿主树上,缓慢包裹宿主,最终取而代之.

应用到迁移中:

  1. 在单体前面加一个路由层(API Gateway / Nginx / Envoy)
  2. 新功能直接写成新服务,流量通过路由层导入
  3. 逐步迁移旧功能:从低风险模块开始,迁移完后把流量切到新服务
  4. 旧单体逐渐缩小,直到只剩空壳(或彻底下线)

第一刀切哪里

优先拆出满足以下条件的模块:

  1. 变更频率高(拆出后能加快交付)
  2. 与其他模块耦合度低(拆分成本小)
  3. 有独立扩容需求(拆出后能降本)

通常"用户认证"或"通知推送"是不错的第一刀--它们职责清晰,接口简单,几乎所有系统都需要.

数据所有权与数据库拆分

微服务最难的不是代码拆分,而是数据库拆分.代码拆完可以独立部署,但如果还共享数据库,本质上没有解耦.

Database per Service 原则

每个服务拥有私有数据存储,其他服务不能直接访问--只能通过该服务的 API 或发布的事件获取数据.

落地方案有几个层次:

方案 隔离程度 适用场景
独立数据库实例 最强 安全性要求高(如支付),独立扩缩容
同实例不同 schema 中等 大多数场景,运维成本可控
同 schema 不同表前缀 最弱 迁移过渡期的临时方案

拆库后的 JOIN 问题

单体里一个 SQL JOIN 就能拿到的数据,拆分后怎么办?

  • API 组合:在上层服务里调两个下游 API,在内存中组装(适合低频查询)
  • 数据冗余:通过事件同步一份只读副本到本地(适合高频查询)
  • CQRS:写入走各自服务,查询走专门的读模型(适合复杂报表)

不要为了避免网络调用而走捷径

"直接查对方数据库不就行了?"--这会让两个服务通过 schema 耦合.对方改一个字段名,你就挂了.
这正是前面"共享数据库反模式"的问题.宁可多一次 RPC 调用,也不要偷看别人的数据库.

服务间通信概览

微服务之间的通信方式是架构设计的核心决策.整体分为两大类:

同步通信 异步通信
模式 请求-响应(Request/Response) 事件驱动(Event-Driven)
协议 gRPC / HTTP REST / GraphQL Kafka / RabbitMQ / NATS / Redis Streams
耦合度 时间耦合(调用方等待响应) 解耦(发布方不关心谁消费)
适用场景 需要立即结果(查询,校验) 通知,异步处理,跨域解耦
失败处理 重试 / 超时 / 熔断 重试消费 / 死信队列 / 幂等

同步通信 = 打电话,必须对方接了才能说事,对方不在就只能等.
异步通信 = 发微信,发完就不用等,对方看到了自然会处理.
关键业务对话用电话(确认收到),日常通知用微信(异步即可).

下一篇将深入讲解 gRPC 同步通信,第三篇讲异步消息--两种模式互补使用是微服务通信的最佳实践.

真实世界的微服务长什么样

以一个电商系统为例,展示合理的服务划分:

┌─────────────┐
│ API Gateway │
└─────────────┘


┌──────────┐
│ 用户服务 │
└──────────┘
┌──────────┐
│ 订单服务 │
└──────────┘


┌──────────┐
│ 商品服务 │
└──────────┘
┌──────────┐
│ 搜索服务 │
└──────────┘
┌──────────┐
│ 支付服务 │
└──────────┘
┌──────────┐
│ 库存服务 │
└──────────┘

注意几个设计决策:

  • 订单 → 支付 / 库存:同步 RPC(创建订单时需要立即确认扣款和锁库存)
  • 订单 → 通知 / 数据分析:异步事件(不需要等它们处理完)
  • 支付 → 订单:支付结果通过事件回调(解耦支付渠道的异步性)
  • API Gateway 统一对外,内部服务之间直接 gRPC 通信

快速回顾

  • 微服务的本质:围绕业务能力划分的,可独立部署和演进的服务单元
  • 拆分判据:能否让不同团队以不同节奏独立交付--不是代码行数或技术层
  • DDD 是最佳工具:限界上下文 = 服务边界,事件风暴帮你发现边界
  • 康威定律:系统架构反映组织结构,拆服务前先看团队怎么分
  • 四大反模式:共享数据库,同步链式调用,分布式单体,按技术层拆分
  • 渐进式优于激进:模块化单体 → 绞杀者模式逐步拆出 → 持续演进
  • 数据所有权:每个服务私有数据库,跨服务通过 API / 事件获取数据
  • 通信两种模式:同步(gRPC/HTTP)+ 异步(事件/消息),互补使用

动手练习

  1. 画模块依赖图:画出你当前工作项目的模块依赖图,尝试识别哪些模块之间是"通知"关系(可异步)vs "直接调用"关系(需同步)
  2. 事件风暴划边界:用事件风暴的方式,列出你最熟悉的一个业务流程中的所有事件,尝试划分限界上下文
  3. 检查共享数据库:检查项目中是否存在"共享数据库"反模式--多个模块直接 JOIN 对方的表?如果要拆分,数据怎么解耦?
  4. 评估拆分条件:评估你的项目是否满足拆微服务的前提条件(团队规模,基础设施,部署频率差异),给出"拆/不拆/先模块化"的结论