关于 ddia-principles
《Designing Data-Intensive Applications》的精编学习指南,面向学习数据系统设计与运维的读者。内容按可靠性、存储、分布式一致性及派生数据等主题组织。
主要功能
- 01概述可靠性与可扩展性原则
- 02比较数据模型和存储引擎
- 03讲解复制与分区
- 04梳理事务隔离级别与异常
- 05涵盖分布式一致性与共识
适用场景
- 01复习数据系统设计概念
- 02比较存储或复制方案
- 03学习一致性与事务取舍
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ luoling8192/ ai-coding-principles --skill ddia-principles安装后在 Claude Code 中输入/ddia-principles确认已加载。
详细介绍
数据密集型应用系统设计——精要指南
来源:Martin Kleppmann,《数据密集型应用系统设计》 核心论点:现代应用的核心挑战在于数据,而非计算。
第一部分:数据系统基础
第 1 章:可靠性、可扩展性、可维护性
三大支柱
| 支柱 | 定义 | 关键指标 |
|---|---|---|
| 可靠性 | 系统即使发生故障也能正常工作 | 故障 ≠ 失效;容忍故障,防止失效 |
| 可扩展性 | 系统能够平稳应对负载增长 | 用百分位数衡量:p50、p95、p99、p999 |
| 可维护性 | 系统易于运维、理解和演进 | 可运维性 + 简洁性 + 可演进性 |
故障类别
- 硬件:随机、相互独立(磁盘、RAM、电源)。通过冗余(RAID、双电源)缓解。
- 软件:同时影响所有节点的系统性错误(闰秒错误)。通过进程隔离、监控、混沌工程缓解。
- 人为因素:宕机的首要原因(配置错误)。通过良好的抽象、沙箱、金丝雀部署和快速回滚缓解。
可扩展性模式
- 纵向扩展(scale-up):使用更大的机器。简单,但存在上限。
- 横向扩展(scale-out):增加机器(无共享架构)。复杂,但扩展空间不受限。
- 弹性扩展:检测到负载后自动扩展。适用于不可预测的工作负载。
Twitter 扇出案例研究:每秒 4.6k 次写入,但每秒 300k 次读取。解决方案:为大多数用户预先计算时间线(写入扇出);对名人用户在读取时合并。
性能:使用百分位数,而非平均值
- p50 = 中位数。p99 = 尾部延迟,对用户体验至关重要。
- Amazon:延迟增加 100ms = 收入损失 1%。
- 尾部延迟放大:一次缓慢的后端调用会拖慢整个并行请求。
第 2 章:数据模型与查询语言
模型选择指南
| 模型 | 最适合 | 弱点 |
|---|---|---|
| 关系型 | 结构化数据、复杂联接、ACID 事务 | 模式僵化,与 OOP 存在阻抗失配 |
| 文档型 | 层次化数据、灵活模式、数据局部性 | 联接能力弱,多对多关系处理不佳 |
| 图 | 高度关联的数据、可变深度遍历 | 工具链不够成熟,更难分区 |
模式策略
- 写入时模式(关系型):在写入时强制执行结构。可及早发现错误,但需要承担迁移成本。
- 读取时模式(文档型):在读取时解释结构。灵活,但验证责任落在应用上。
规范化与反规范化
- 规范化:单一事实来源、更新一致,但需要联接。
- 反规范化:读取更快,但存在不一致风险和更新异常。
趋势:各种模型正在融合——PostgreSQL 支持 JSON,MongoDB 增加了联接。应根据访问模式选择,而非意识形态。
第 3 章:存储与检索
存储引擎对比
| 特性 | B-Tree | LSM-Tree |
|---|---|---|
| 写入吞吐量 | 较低(原地更新 + WAL) | 较高(顺序追加) |
| 读取延迟 | 更可预测 | 可能需要检查多个 SSTable |
| 写放大 | 较高 | 较低 |
| 空间效率 | 可能产生碎片 | 压缩效果更好 |
| 事务支持 | 更简单(锁定树节点) | 更复杂 |
| 使用方 | PostgreSQL、MySQL、Oracle | LevelDB、RocksDB、Cassandra |
OLTP 与 OLAP
| 方面 | OLTP | OLAP |
|---|---|---|
| 访问方式 | 随机访问,少量记录 | 顺序扫描,数百万行 |
| 用户 | 终端用户 | 分析师 |
| 数据 | 当前状态 | 历史事件 |
| 规模 | GB–TB | TB–PB |
| 优化目标 | 低延迟 | 高吞吐量 |
数据仓库
- ETL:从 OLTP 提取 → 转换 → 加载到仓库。
- 星型模式:中央事实表(事件)+ 维度表(属性)。事实表可包含 100 多列,规模可达 PB 级。
- 列式存储:每列单独存储。当查询只涉及少数列时,可大幅节省 I/O。支持位图编码、游程压缩和向量化处理。
第 4 章:编码与演进
格式比较
| 格式 | 大小(示例) | 架构 | 演进 | 跨语言 |
|---|---|---|---|---|
| JSON | 81 字节 | 隐式 | 手动 | 极佳 |
| Thrift | 59 字节 | 必需 | 字段标签 | 良好 |
| Protobuf | 33 字节 | 必需 | 字段标签 | 极佳 |
| Avro | 32 字节 | 必需 | 名称匹配 | 良好 |
兼容性规则
- 向后兼容:新代码读取旧数据。(始终必需)
- 向前兼容:旧代码读取新数据。(滚动升级时必需)
- 规则:只添加或删除带有默认值的字段。切勿重复使用已删除的字段标签。
数据流模式
- 通过数据库:滚动部署期间,多个代码版本并存。数据的生命周期长于代码。
- 通过服务(REST/RPC):先更新服务器,再更新客户端。请求需保持向后兼容,响应需保持向前兼容。
- 通过异步消息:解耦生产者和消费者。支持独立演进版本。
避免:特定于语言的序列化(Java Serializable、Python pickle)——会导致供应商锁定,并带来安全风险。
第二部分:分布式数据
第 5 章:复制
复制模型
| 模型 | 写入 | 冲突 | 用例 |
|---|---|---|---|
| 单主 | 一个节点 | 无 | 最常见(PostgreSQL、MySQL) |
| 多主 | 多个节点 | 必须解决 | 多数据中心、离线客户端 |
| 无主 | 任意节点 | 必须解决 | Cassandra、Riak、Voldemort |
同步与异步复制
- 同步:持久可靠,但副本故障时会阻塞。
- 异步:速度快,但主节点故障时有数据丢失风险。
- 半同步:一个副本同步,其余副本异步。实用的折中方案。
复制延迟问题与解决方案
| 问题 | 症状 | 解决方案 |
|---|---|---|
| 读己之写 | 用户看不到自己写入的数据 | 读取用户自己的数据时,从主节点读取 |
| 单调读 | 数据时间倒退 | 将用户固定到同一个副本 |
| 一致前缀读 | 因果顺序被破坏 | 将存在因果关系的数据写入同一分区 |
冲突解决
- 最后写入胜出(LWW):简单但会丢失数据。仅当键不可变时才安全。
- 合并:对值取并集、进行拼接,或使用 CRDT 数据结构。
- 应用层处理:返回所有版本(“兄弟版本”),由应用决定。
- 版本向量:跟踪每个副本的因果依赖关系。
仲裁:w + r > n
- w = 写入确认数,r = 读取查询数,n = 副本总数。
- 宽松仲裁:分区期间接受写入到非归属节点(提示移交)。提高可用性,但会降低一致性。
第 6 章:分区(分片)
分区策略
| 策略 | 优点 | 缺点 |
|---|---|---|
| 键范围 | 范围查询高效 | 顺序键可能导致热点 |
| 哈希 | 分布均匀 | 不支持范围查询 |
| 复合 | 对第一部分进行哈希,其余部分排序 | 更复杂,但兼具两者优点 |
二级索引分区
- 本地(基于文档):每个分区为自己的数据创建索引。写入简单,读取需要分散收集。
- 全局(基于术语):按术语对索引分区。读取高效,写入需要更新多个分区。
重新平衡
- 固定分区数:分区数多于节点数。节点变更时重新分配。(Riak、Elasticsearch)
- 动态:根据大小进行拆分/合并。(HBase、RethinkDB)
- 与节点数成比例:每个节点的分区数固定。(Cassandra)
请求路由
- 轮询到任意节点(节点会在需要时转发)
- 路由层(感知分区的代理)
- 感知客户端(客户端了解分区映射)
- ZooKeeper:权威的分区 → 节点映射。HBase、Kafka 使用此方式。
第 7 章:事务
隔离级别(从弱到强)
| 级别 | 防止 | 允许 | 实现方式 |
|---|---|---|---|
| Read Committed | 脏读、脏写 | 不可重复读、丢失更新 | 行锁 + 旧值副本 |
| Snapshot Isolation | + 不可重复读 | 写偏斜、幻读 | MVCC(多版本) |
| Serializable | 所有异常 | 无 | 2PL、串行执行或 SSI |
并发异常
| 异常 | 描述 | 示例 |
|---|---|---|
| 脏读 | 读取未提交的数据 | 读取只写了一半的转账记录 |
| 脏写 | 覆盖未提交的数据 | 两位买家都“赢得”同一件商品 |
| 丢失更新 | 读-修改-写竞争 | 两次并发的计数器递增 |
| 写偏斜 | 基于过期读数做决策 | 两位医生都停止值班 |
| 幻读 | 新行改变查询结果 | 会议室被重复预订 |
可串行化实现
- 串行执行:单线程、内存中执行。速度快,但吞吐量有限。(VoltDB、Redis)
- 两阶段锁(2PL):共享锁 / 排他锁一直持有到提交。保证性强,但速度慢,容易发生死锁。
- SSI(Serializable Snapshot Isolation):乐观方式——自由执行,在提交时检测冲突。最适合读密集型工作负载。(PostgreSQL 9.1+)
第 8 章:分布式系统中的问题
三种不可靠性
网络:异步分组网络——不保证送达,也不保证时序。无法区分崩溃和网络延迟。超时是唯一的故障检测器,但不存在正确的超时值。
时钟:
- 墙上时钟:可能向后跳(NTP 校正)。绝不要用来排列事件顺序。
- 单调时钟:可安全测量经过的时间,但不能用于跨节点比较。
- 石英时钟漂移:约 200ppm → 每 30 秒误差 6ms。
进程:GC 暂停、VM 挂起、缺页——线程可能毫无预警地停止。暂停的节点不知道时间已经过去。
核心原则
- 故障 ≠ 失效:针对部分故障进行设计。某些节点正常工作,而其他节点可能无法工作。
- 真相由多数派决定:单个节点无法独自确定系统状态。由法定人数投票决定。
- 隔离令牌:单调递增的令牌可防止僵尸进程破坏状态。
- 安全性与活性:安全性(坏事永不发生)必须始终成立。活性(好事最终发生)可能需要满足条件。
第 9 章:一致性与共识
一致性模型(从强到弱)
| 模型 | 保证 | 代价 |
|---|---|---|
| Linearizability | 行为如同只有一个副本,所有操作都是原子的 | 高延迟,分区期间可用性降低 |
| Causal consistency | 遵循因果顺序 | 性能更好,可容忍分区 |
| Eventual consistency | 副本最终会收敛 | 性能最佳,保证最弱 |
线性一致性的使用场景
- Leader 选举 / 分布式锁
- 唯一性约束(用户名、文件名)
- 跨通道协调(消息队列 + 存储)
共识算法
- 2PC:基于协调者,协调者故障时会阻塞。实用但脆弱。
- Paxos/Raft/Zab:基于 Epoch 的 Leader 选举 + 法定人数投票。不会阻塞。etcd、ZooKeeper、Consul 使用此方式。
- FLP 不可能性:在存在崩溃的纯异步系统中,共识不可能实现。实际算法使用超时。
全序广播 ≡ 共识 ≡ 线性一致的 CAS
这三个问题在数学上等价。解决其中一个,就能解决全部。
ZooKeeper / etcd 模式
- 用于协调的小型共识集群(3–5 个节点)。
- 线性一致的原子 CAS 操作。
- 通过会话心跳检测故障。
- 应用场景:领导者选举、分区分配、分布式锁、服务发现。
第三部分:派生数据
第 10 章:批处理
Unix 哲学 → MapReduce
- 每个程序都专注做好一件事。
- 一个程序的输出 = 另一个程序的输入。
- 输入不可变,处理结果确定。
MapReduce 流水线
输入 → Mapper(提取键值对)→ 排序/分区 → Reducer(按键聚合)→ 输出
分布式联接策略
| 联接类型 | 适用场景 | 实现方式 |
|---|---|---|
| 排序归并 | 两个输入都很大 | 按联接键排序,在 Reducer 中归并 |
| 广播哈希 | 一个输入较小(可放入 RAM) | 将较小的一侧加载为哈希表 |
| 分区哈希 | 两个输入采用相同方式分区 | 对每个分区执行哈希联接 |
超越 MapReduce:数据流引擎(Spark、Flink、Tez)
- 将整个工作流视为单个作业。
- 流水线处理中间结果(避免将数据完整物化到 HDFS)。
- 尽可能将数据保留在内存中。
- 跟踪计算血缘以便在故障后恢复(RDD)。
- 支持迭代算法(通过 Pregel/BSP 模型进行图处理)。
第 11 章:流处理
消息代理模型
| 模型 | 投递方式 | 顺序保证 | 重放 | 使用场景 |
|---|---|---|---|---|
| AMQP/JMS | 逐条确认,确认后删除 | 不保证顺序 | 否 | 任务队列、异步 RPC |
| 基于日志(Kafka) | 基于偏移量,消息保留 | 分区内有序 | 是 | 事件流、CDC |
变更数据捕获(CDC)
将数据库变更提取为事件流 → 让派生系统(搜索索引、缓存、数据仓库)保持同步。事实来源仍在数据库中;派生视图是消费者。
事件溯源
将状态建模为只追加的业务事件序列(而非数据库操作)。事件是不可变的事实。当前状态 = 对事件历史进行折叠计算。
流联接
| 联接 | 输入 A | 输入 B | 状态 |
|---|---|---|---|
| 流-流 | 事件 | 事件 | 按时间窗口缓冲 |
| 流-表 | 事件 | 数据库快照(通过 CDC) | 本地物化表 |
| 表-表 | CDC 流 | CDC 流 | 派生物化视图 |
时间与窗口
| 窗口 | 描述 |
|---|---|
| 滚动窗口 | 固定大小、不重叠(例如,每 1 分钟) |
| 跳跃窗口 | 固定大小、彼此重叠(例如,每 30 秒计算一个 1 分钟窗口) |
| 滑动窗口 | 时间间隔不超过阈值的所有事件 |
| 会话窗口 | 按活动间隔分组(例如,30 分钟无活动) |
事件时间 ≠ 处理时间。 为确保正确性,始终使用事件时间。使用水位线或更正发布来处理迟到事件。
处理保证
- 微批处理(Spark Streaming):延迟约 1 秒,采用原子化的小批次。
- 检查点(Flink):通过消息屏障定期创建快照。
- 幂等性:使用消息偏移量或唯一 ID 去重。
- 端到端恰好一次需要幂等输出和去重。
第 12 章:数据系统的未来
数据集成模式
没有任何单一数据库可以处理所有事情。使用事件日志作为集成骨干:
- 所有写入都经过权威事件日志。
- 派生系统(索引、缓存、机器学习模型)消费该日志。
- 使用确定性、幂等函数在各层之间进行转换。
数据库解耦
分离不同职责:
- 记录系统:记录权威写入。
- 派生系统:索引、缓存、物化视图消费变更流。
- 支持渐进式迁移——让新旧系统并行运行。
端到端恰好一次
底层保证(TCP、数据库事务)无法确保应用正确性。需要:
- 操作标识符:在应用层使用基于 UUID 的去重。
- 幂等操作:执行一次或多次,效果相同。
- 唯一约束强制执行:通过分区流处理实现。
异步约束强制执行
不使用分布式事务,而是:
- 按约束字段将请求路由到分区日志。
- 流处理器对竞争请求排序。
- 拒绝违反约束的请求,并通过输出流通知客户端。
- 有些应用可以容忍暂时违反约束,并通过补偿事务处理。
可审计性
- 将数据视为不可变事件日志,以便重建和验证。
- 实施加密审计追踪(Merkle 树)。
- 定期测试备份恢复和数据重建。
决策框架:快速参考
如何选择数据模型
Many-to-many relationships? → Relational or Graph
Hierarchical / nested data? → Document
Highly connected data? → Graph
Flexible / evolving schema? → Document (schema-on-read)
Strong consistency required? → Relational (ACID)
如何选择存储引擎
Write-heavy workload? → LSM-tree (RocksDB, Cassandra)
Read-heavy, predictable? → B-tree (PostgreSQL, MySQL)
Analytical queries? → Column store (ClickHouse, Redshift)
Full-text search? → Inverted index (Elasticsearch)
如何选择复制策略
Single datacenter? → Single-leader
Multi-datacenter? → Multi-leader
Offline-first clients? → Multi-leader or Leaderless
Maximum availability? → Leaderless with sloppy quorum
Strong consistency? → Single-leader with sync replication
如何选择隔离级别
Read-only analytics? → Snapshot isolation
General OLTP? → Read committed (default in most DBs)
Financial / critical? → Serializable (prefer SSI over 2PL)
High write contention? → Serial execution (if data fits in RAM)
如何选择批处理或流处理
Historical data reprocessing? → Batch (Spark, Flink batch mode)
Real-time derived views? → Stream (Kafka + Flink/Spark Streaming)
Both needed? → Unified engine (Flink) over Lambda architecture