E2 - 微服务基础:拓扑 + 事件类型 + Single Writer
事件驱动微服务的"建筑学"——微服务拓扑、事件结构、Single Writer 原则、微服务税。
一句话
微服务拓扑 = 输入流 + 处理 + 物化 + 输出流;事件 = 业务事实的不可变记录;Single Writer = 一聚合一服务一写者。
这本书在这一层说了什么
本层基于 EDM Ch 2 完整精读(32KB / 5016 词)。
Bellemare Ch 2 开篇:
"An event-driven microservice is a small application built to fulfill a specific bounded context. Consumer microservices consume and process events from one or more input event streams, whereas producer microservices produce events to event streams for other services to consume."
Bellemare 关键洞察:
"Communication between event-driven microservices is completely asynchronous."
核心论点 5 个:
- Building Topologies — 拓扑 2 种(微服务 vs 业务)
- The Contents of an Event — 事件是业务事实的记录
- The Structure of an Event — Key/Value 结构
- Three Main Event Types — Unkeyed / Entity / Keyed
- Materializing State from Entity Events — 状态物化
是什么
1. 微服务拓扑 2 种
Microservice Topology(微服务拓扑)
Bellemare 定义:
"A microservice topology is the event-driven topology internal to a single microservice. This defines the data-driven operations to be performed on incoming events, including transformation, storage, and emission."
微服务拓扑(内部):
输入流 → 转换 → 物化 → 输出流
示例:
输入流 A → 物化到数据存储
输入流 B → 过滤 + 转换 → 与存储 JOIN → 输出流关键:微服务拓扑只描述"单个服务内部",不描述服务间关系。
Business Topology(业务拓扑)
Bellemare 定义:
"A business topology is the set of microservices, event streams, and APIs that fulfill complex business functions."
业务拓扑(全局):
微服务 1 → 事件流 A → 微服务 2 (REST API + 流消费)
→ 微服务 3 → 事件流 CBellemare 关键洞察:
"A microservice topology details the inner workings of a single microservice. A business topology, on the other hand, details the relationships between services."
关键区分:
- 微服务拓扑 = "how"(内部怎么工作)
- 业务拓扑 = "who talks to whom"(谁和谁通信)
2. 事件的本质(The Contents of an Event)
Bellemare 关键定义:
"An event is a recording of what happened, much like how an application's information and error logs record what takes place in the application. Unlike these logs, however, events are also the single source of truth... they must contain all the information required to accurately describe what happened."
事件可以是任何业务事实:
- 收到发票(Receiving an invoice)
- 预订会议室(Booking a meeting room)
- 咖啡机做咖啡(Requesting a cup of coffee)
- 雇佣新员工(Hiring a new employee)
- 代码完成构建(Successfully completing arbitrary code)
关键洞察:事件是 SSOT——必须包含描述"发生了什么"的全部信息。
3. 事件结构 = Key + Value
Bellemare 关键论断:
"Events are typically represented using a key/value format. The value stores the complete details of the event, while the key is used for identification purposes, routing, and aggregation operations on events with the same key."
Key 的用途:
- 唯一标识
- 路由(路由到特定 partition)
- 聚合(同 Key 事件聚合)
Value 的要求:
- 完整详情(自描述)
- 不依赖外部上下文
4. 事件 3 种类型(Three Main Event Types)
Bellemare 关键分类:
类型 1: Unkeyed Event(无键事件)
定义:"Unkeyed events are used to describe an event as a singular statement of fact."
例:用户打开书的页面(ISBN: 372719, Timestamp: 1538913600)
Key: N/A
Value: ISBN: 372719, Timestamp: 1538913600用途:简单通知(不需要被路由/聚合)。
类型 2: Entity Event(实体事件)
定义:"An entity is a unique thing and is keyed on the unique ID of that thing."
Bellemare 论断:
"Entity events are particularly important in event-driven architectures. They provide a continual history of the state of an entity and can be used to materialize state... Only the latest entity event is needed to determine the current state of an entity."
例:书的实体(ISBN: 372, Author: Adam Bellemare)
Key: ISBN: 372
Value: 完整属性(Author / Title / Price / ...)关键洞察:Entity Event = 业务实体状态变化的历史——只读最新事件 = 当前状态。
类型 3: Keyed Event(键控事件)
定义:"A keyed event contains a key but does not represent an entity. Keyed events are usually used for partitioning the stream of events to guarantee data locality within a single partition of an event stream."
例:用户与书的交互(按 ISBN 分区)
Key: ISBN: 372719
Value: UserId: A537FE关键洞察:Keyed Event 用于保证同 Key 事件的顺序处理(同 partition 内有序)。
5. 状态物化(Materializing State from Entity Events)
Bellemare 关键论断:
"You can materialize a stateful table by applying entity events, in order, from an entity event stream to a data store."
物化策略:
- 应用 Entity Events 顺序写入数据存储
- 最新 Entity Event = 当前状态
- 历史 Entity Events = 状态变化历史
为什么重要:
- 查询响应快(直读物化数据)
- 事件流仍是 SSOT(可重放)
6. Consumer Group + Partition Assignment(消费者组 + 分区分配)
Bellemare Ch 2 关键定义:
"The consumer group allows for multiple consumers to be viewed as the same logical entity and can be leveraged for horizontal scaling of message consumption."
核心论点:
- Consumer Group(消费者组) = 逻辑消费者,多实例视为同一实体
- 每个分区只被组内一个消费者消费(互斥消费)
- 每消费者可有多个分区(负载均衡)
- 新消费者加入 → 自动 rebalance 分区分配
关键约束:
"The number of active consumer instances in the group is limited to the number of partitions in the event stream."
——消费者数超过分区数 = 多余消费者闲置。
两种消费模式:
| 模式 | 行为 | 顺序保证 | 支持的 Broker |
|---|---|---|---|
| Consumer Group(流消费) | 每事件被组内一个消费者处理 | ✅ 同分区有序 | Apache Kafka / RabbitMQ |
| Queue(队列消费) | 每事件被唯一消费者处理,标记 consumed | ❌ 顺序不保证 | Apache Pulsar / 传统 MQ |
Bellemare 关于队列消费的关键警告:
"Event order is not maintained when processing from a queue. Parallel consumers consume and process events out of order, while a single consumer may fail to process an event, return it to the queue for processing at a later date, and move on to the next event."
Bellemare Ch 5 三大分区分配策略:
- Round-robin(轮询):所有分区列表 → 轮流分给每个消费者。默认策略。
- Static(静态):固定分区到固定消费者。用于有状态实例(materialized state)。
- Custom(自定义):基于 lag/容量等外部信号定制分配。
Rebalance(再平衡)触发条件:
- 新消费者加入 consumer group
- 消费者离开(崩溃 / 优雅关闭)
- 分区数变化
- 静态分配超时未归位
为什么重要:
- 水平扩展:加消费者 = 加并行度(不超过分区数)
- 故障恢复:消费者崩溃 → 自动重新分配其分区
- 状态物化分片:rebalance 期间 materialized state 需重新分片(ch07 详细讨论)
- 再平衡成本:分区重新分配 = 处理暂停 + 状态恢复(特别对有状态服务)
与 Single Writer 的关系:
- Single Writer = 一聚合一服务一写者(ch11 详)
- Consumer Group = 多消费者读同一流(ch02 详)
- 两者不冲突:写者唯一 + 读者可多个 reader group 独立消费
与 Partition 的关系:
- Partition = 物理并行单元(每个分区一个写者 + 独立 offset)
- Consumer Group = 逻辑并行单元(同一服务多实例)
- 一服务一 group + 每分区一消费者 = 最大并行度
为什么
为什么微服务要小?
Bellemare 2 周写完 + 一脑子装下——本质是认知负载而非代码量。
根因:
- 大服务 → 多业务规则 → 测试难
- 大服务 → 多变更原因 → 修改影响大
- 大服务 → 单点故障 → 局部崩溃全局
为什么需要 Single Writer?
反例:订单服务和支付服务都写"订单状态"字段 → 谁先谁后冲突、谁覆盖谁难定、谁负责的 Bug 难定位。
Single Writer:
- 一个聚合 = 一个微服务 = 唯一写者
- 其他服务只能消费,不能修改
- 即使"协调"也只是发事件,不直接写
根因 1:避免分布式写冲突根因 2:简化一致性模型(无须分布式锁) 根因 3:责任清晰(Bug 定位容易)
为什么 Entity Event 是事件驱动核心?
Bellemare 关键洞察:
"Entity events are particularly important in event-driven architectures. They provide a continual history of the state of an entity and can be used to materialize state."
Entity Event 的核心价值:
- 历史可追溯(不只是当前状态)
- 状态可物化(历史 → 当前)
- 业务变化不破坏历史(追加而非修改)
为什么事件必须自描述?
反例:消费者代码有 event.getOrderId().getUserId() 这种链式调用 → 修改生产者后消费者崩溃。
根因:Value 包含 ID 而非完整对象 → 消费者 JOIN 数据库 → 耦合到数据库 schema。
修复:Value 包含完整所需字段(denormalize),消费者不依赖任何外部查询。
怎么做
Step 1:划分微服务拓扑
输入:
- 业务需求文档
- DDD 限界上下文(来自 IDDD)
操作:
1. 每个 BC 映射到一个微服务
2. 每个微服务画出"输入流 + 处理 + 物化 + 输出流"
3. 验证 BC 之间的边界(= Single Writer 边界)
产出:
- 微服务拓扑图
- 事件流清单(输入/输出)
- 物化状态清单Step 2:选择事件结构
判断树:
Q1: 业务事件有"实体"吗?
否 → Unkeyed Event(通知)
是 ↓
Q2: 实体有"当前状态"吗?
否 → Entity Event(OrderCreated(orderId=123))
是 ↓
Q3: 需要事件溯源吗?
否 → Entity Event + 独立状态存储
是 → Keyed Event(Order(orderId=123, status=CONFIRMED))Step 3:应用 Single Writer
规则:
1. 每个聚合在一个微服务中
2. 写操作只通过该微服务的 API
3. 其他服务只能"读"或"消费事件"
4. 跨 BC 写通过"事件 + Saga"实现,不是直接写
反模式检测:
- 两个服务写同一张表 → 拆分
- 一个服务写另一个服务的 DB → 改为事件
- 分布式锁协调写 → 重构为 Single WriterStep 4:评估微服务税
微服务税评估:
- 部署复杂度 ↑(容器/编排)
- 监控复杂度 ↑(分布式追踪)
- 测试复杂度 ↑(跨服务)
- 团队协作 ↑(接口协调)
决策:
- 收益 > 税 → 微服务
- 收益 < 税 → 单体 + 模块化什么时候用 / 什么时候不用
用事件驱动微服务的 2 大场景
| 场景 | 关键判断 |
|---|---|
| 多 BC 需要独立演化 | 每个 BC 团队节奏不同,Single Writer 让 BC 间无写冲突 |
| 需要事件溯源 / 时间旅行调试 | 业务需要"为什么状态是这样"的历史,Entity Event 完整保留 |
不用的 3 大场景
| 场景 | 原因 | 替代 |
|---|---|---|
| 简单 CRUD + 单团队 | Single Writer 收益 = 0,但 Microservice Tax > 0 | 单体 + 模块 |
| 强一致 + 同步 RPC 必要 | 事件最终一致延迟不可接受 | gRPC + 同步数据库事务 |
| 小消息 + 通知场景 | 消息销毁是特性不是缺陷 | Message Broker(RabbitMQ) |
看不见的地方
失效模式 1:把 Message Broker 当 Event Broker 用
症状:用 RabbitMQ 默认队列做事件流 → 新增消费者失败 → 历史数据无法重放。
根因:RabbitMQ 默认行为是"消费即销毁",不是"持久可重放"。
修复:换 Kafka / Pulsar,或开启 RabbitMQ Streams 插件。
失效模式 2:事件 Value 不自描述
症状:消费者代码有 event.getOrderId().getUserId() 链式调用 → 修改生产者后消费者崩溃。
根因:Value 包含 ID 而非完整对象。
修复:Value 包含完整所需字段(denormalize),消费者不依赖任何外部查询。
失效模式 3:跨 BC 直接写数据库
症状:订单服务直接 UPDATE 库存表 → 库存服务不知道被改 → 数据漂移。
根因:违反 Single Writer 原则。
修复:订单服务发布 OrderCreated 事件 → 库存服务订阅 → 库存服务自己写库存表。
失效模式 4:把"事件"等同于"通知"
症状:团队认为"事件 = WebSocket 推送" → 推送失败就丢消息 → 业务状态丢失。
根因:未理解事件的"事实"语义——事件是"已经发生"的事实,不是"将要发生"的命令。
修复:事件必须有"过去时态"命名(OrderCreated 而非 CreateOrder)+ 持久化 + 多消费者。
失效模式 5:微服务税吃掉所有收益
症状:5 个微服务 + 3 个团队 + 单一业务领域 → 协调成本 > 收益。
根因:过早微服务化。
修复:先单体 + 模块化,到 50+ 开发者或 BC 边界清晰时再拆。
相关节点
骨架 A — 世界观
| 节点 | 关联 | 解释 |
|---|---|---|
| wonder-02-emergence-action | 强 | Single Writer = "涌现作为"的物理实现——BC 间通过事件"作为",不直接写 |
| wonder-03-emergence-time | 中 | 事件 = 时间序列 = "时"的物化——Entity Event 把"时"压缩成"事件" |
骨架 B — 工程实践
| 节点 | 关联 | 解释 |
|---|---|---|
| value-stream | 强 | 事件流 = 价值流——价值从生产端流到消费端 |
| feedback-mechanisms | 中 | 事件订阅 = 反馈机制——消费方通过消费事件反馈到系统 |
| change-management | 中 | 微服务拓扑 = 变更边界——一服务修改不波及其他 |
| quality-assurance | 中 | Single Writer = 质量保障——责任唯一 → Bug 定位简单 |
与 IDDD 对位
| IDDD | EDM |
|---|---|
| Ch 8 领域事件 | Ch 2 事件 3 类型 |
| Ch 10 聚合 | Ch 2 Single Writer(聚合边界 = 写者边界) |
状态
E2 概念递进落地完成(基于原书 ch02 32KB / 5016 词)。
- 5 个核心论点(拓扑 2 种 / 事件本质 / Key-Value / 事件 3 类型 / 状态物化)
- 4 步怎么做
- 5 个失效模式
- 骨架 A/B + IDDD 完整映射