DDIA 1-5 编码与演化
这章主要围绕两个核心问题展开:
- 数据是如何被编码的
- 数据是如何在系统中流动的
这两个问题看似基础,但它们直接决定了系统的可演化性、兼容性以及长期维护成本。本文尝试从工程实践的角度,对这两个问题做一次系统性的梳理。
一、数据是如何编码的?
在分布式系统中,数据几乎总是在内存结构与字节序列之间不断转换。编码方式的选择,决定了数据能否被安全、高效、长期地使用。
1. 基于特定语言的编码方式
许多编程语言都提供了原生的对象序列化能力,例如:
- Java:
java.io.Serializable - Python:
pickle - Ruby:
Marshal
优点
- 使用方便
- 几乎不需要额外建模
- 可以快速保存和恢复内存对象
缺点
- 语言强绑定
数据几乎无法被其他语言安全解析,极大限制了系统的演化空间。 - 安全风险
反序列化任意字节流可能导致远程代码执行。 - 可演化性差
对向前 / 向后兼容支持不足。 - 性能与体积问题
典型代表是 Java 原生序列化,体积大、性能差。
👉 结论:仅适合临时用途,不适合长期数据存储或跨服务通信。
2. 文本编码格式(JSON / XML / CSV)
常见的文本编码格式包括:
- JSON
- XML
- CSV
优点
- 人类可读
- 跨语言支持广泛
- 非常适合系统间数据交换
主要问题
- 数字语义不清晰
JSON 无法区分整数和浮点数,大整数在 JavaScript 中存在精度问题。 - 不支持二进制数据
只能通过 Base64 间接编码,体积膨胀约 33%。 - 模式复杂或缺失
- XML / JSON Schema 功能强大但复杂
- CSV 完全没有模式,需手动维护行列含义
- 编码/解码逻辑容易散落在代码中
3. 二进制编码格式
为了提升效率和减少存储体积,出现了大量二进制编码方案,主要包括:
- MessagePack
- Protocol Buffers
- Avro
MessagePack
- 本质上是 JSON 的二进制形式
- 保留字段名
- 体积优化有限,但解析更快
Protocol Buffers
- 基于明确的 Schema
- 使用 字段编号(tag) 而非字段名
- 天然支持向前 / 向后兼容
- 非常适合 RPC 场景(如 gRPC)
Avro
- 不在数据中存储字段标识
- 通过 写入者模式(Writer Schema) 与 读取者模式(Reader Schema) 解码
- 对动态生成 Schema 非常友好
- 广泛用于数据管道和大数据系统
👉 总体结论:
基于 Schema 的二进制编码是构建可长期演化系统的关键基础设施。
二、数据是如何流动的?
数据编码并不是孤立存在的,它总是伴随着数据流动的方式。
1. 流经数据库
数据库可以理解为:给未来的自己发送消息。
关键问题
- 不同时间写入不同格式的数据
- 数据比代码活得更久
- 新代码必须能读取旧数据(向后兼容)
- 多版本程序同时访问数据库
- 滚动升级期间,新旧代码并存
- 数据格式必须具备向前兼容能力
- 归档与分析存储
- 数据通常被一次性写入后不可变
- 更适合 Avro、Parquet 等分析友好格式
2. 流经服务:REST 与 RPC
Web / REST 服务
- 基于 HTTP
- 通常使用 JSON
- API 通过 OpenAPI / Swagger 定义
- 添加字段通常是兼容的
RPC
- 试图让远程调用“看起来像本地函数”
- 常见实现:
- gRPC(Protocol Buffers)
- Avro RPC
RPC 的现实问题
- 网络不可靠
- 超时与重试
- 幂等性问题
- 延迟不可预测
👉 结论:
RPC 并不是“远程函数调用”,而是一次有失败风险的分布式交互。
3. 负载均衡与服务发现
为了应对服务实例动态变化,系统通常引入:
- 硬件 / 软件负载均衡器
- DNS
- 服务发现系统(如 Consul)
- 服务网格(Istio / Linkerd)
服务网格通过 sidecar 模式,将:
- 负载均衡
- 加密
- 可观测性
从业务代码中剥离出来。
4. 持久化执行与工作流
在复杂业务中,单个请求往往涉及多个服务调用。
问题:如何保证“只执行一次”?
这正是持久化执行框架的核心价值:
- 工作流由工作流引擎调度
- 所有 RPC 与状态变更都会被持久化
- 失败后可重放执行
- 对外表现为 精确一次语义
典型代表:Temporal
挑战
- 对代码确定性要求极高
- 修改执行顺序可能破坏重放
- 通常需要版本隔离执行
5. 事件驱动架构
另一种重要的数据流动方式是异步消息:
- 消息队列 / 事件代理
- 发布-订阅模型
发送者与接收者解耦 优势
- 提升系统弹性
- 天然支持削峰填谷
- 适合构建松耦合系统
注意点
- 消息本质仍是字节序列
- 仍需处理 Schema 演化
- 需要防止未知字段丢失
总结
数据编码与数据流动并不是底层细节,而是系统可演化性的核心支柱。
- 编码方式决定了数据是否能被长期、安全地使用
- 数据流动方式决定了系统是否能独立演进
- 向前 / 向后兼容是分布式系统的基本生存能力
数据比代码更长寿。
设计系统时,我们本质上是在为未来的系统版本铺路。