本文是 DDIA 第一部分的完整读书笔记,涵盖第 1-4 章:可靠性与可扩展性、数据模型、存储引擎、数据编码。
第1章:可靠性、可扩展性与可维护性
1.1 数据密集型应用的组成
现代数据密集型应用通常由多个组件组合:
1 2 3 4 5 6 7 8 9 10
| ┌─────────────────────────────────────────────────────────────┐ │ 数据密集型应用架构 │ ├─────────────────────────────────────────────────────────────┤ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ 数据库 │ │ 缓存 │ │ 搜索索引 │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ 流处理 │ │ 批处理 │ │ 消息队列 │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ │ └─────────────────────────────────────────────────────────────┘
|
| 类型 |
特点 |
主要瓶颈 |
典型场景 |
| 数据密集型 |
数据量大、复杂、变化快 |
磁盘I/O、网络带宽 |
Web应用、社交网络 |
| 计算密集型 |
计算量大、算法复杂 |
CPU处理能力 |
科学计算、图形渲染 |
1.2 可靠性(Reliability)
可靠性:系统在面对故障时,仍能正确运行
| 术语 |
英文 |
定义 |
| 故障 |
Fault |
系统中某个组件偏离其规格 |
| 失效 |
Failure |
整个系统停止为用户提供服务 |
| 容错 |
Fault-tolerant |
系统能够处理某些类型的故障 |
故障类型及应对
1. 硬件故障
- 硬盘损坏、内存故障、电源中断
- 应对:RAID、双电源、热备份、多副本
2. 软件错误
- 比硬件故障更难预测,可能导致系统性问题
- 应对:全面测试、进程隔离、监控告警
3. 人为错误
- 研究表明,运维人员的配置错误是系统中断的首要原因
- 应对:良好的API设计、沙箱环境、快速回滚
1.3 可扩展性(Scalability)
可扩展性:系统应对负载增长的能力
描述性能:百分位数
| 百分位 |
含义 |
用途 |
| p50 |
中位数 |
典型响应时间 |
| p95 |
95%的请求快于此值 |
较高标准 |
| p99 |
99%的请求快于此值 |
SLA标准 |
| 方案 |
描述 |
优点 |
缺点 |
| 拉模式 |
读取时间线时查询关注者的推文 |
写入简单 |
读取开销大 |
| 推模式 |
发推时写入所有关注者的缓存 |
读取快速 |
写入开销大(大V问题) |
| 混合模式 |
普通用户推模式,大V拉模式 |
平衡读写 |
实现复杂 |
扩展策略
- 纵向扩展:使用更强大的机器(简单但有上限)
- 横向扩展:使用多台普通机器(需要复杂设计)
- 弹性扩展:根据负载自动增减资源
1.4 可维护性(Maintainability)
软件的大部分成本不在于最初的开发,而在于后续的维护
| 方面 |
目标 |
| 可操作性 |
让运维团队能够轻松保持系统运行 |
| 简单性 |
让新工程师能够轻松理解系统 |
| 可演化性 |
让工程师能够轻松对系统进行修改 |
第2章:数据模型与查询语言
2.1 数据模型分层
1 2 3 4 5 6 7 8 9
| ┌─────────────────────────────────────────────┐ │ 应用程序层(对象、数据结构、API) │ ├─────────────────────────────────────────────┤ │ 数据模型层(表、文档、图) │ ├─────────────────────────────────────────────┤ │ 存储层(字节序列、内存、磁盘) │ ├─────────────────────────────────────────────┤ │ 硬件层(电信号、磁场) │ └─────────────────────────────────────────────┘
|
2.2 关系模型
| 概念 |
描述 |
示例 |
| 关系(Relation) |
表 |
users表 |
| 元组(Tuple) |
行/记录 |
一个用户记录 |
| 属性(Attribute) |
列 |
name, email, age |
1 2 3 4 5 6 7 8 9 10 11
| CREATE TABLE positions ( position_id INT PRIMARY KEY, title VARCHAR(100) );
CREATE TABLE users ( user_id INT PRIMARY KEY, name VARCHAR(100), position_id INT REFERENCES positions(position_id) );
|
优势:数据一致性、灵活查询、ACID事务、成熟稳定
局限:对象-关系阻抗不匹配、模式僵化、扩展困难
2.3 文档模型
代表产品:MongoDB, CouchDB, Elasticsearch
1 2 3 4 5 6 7 8 9
| { "user_id": 1, "name": "张三", "positions": [ {"title": "软件工程师", "company": "ABC公司"}, {"title": "技术总监", "company": "XYZ公司"} ], "skills": ["Java", "Python", "分布式系统"] }
|
| 特性 |
文档模型 |
关系模型 |
| 数据结构 |
嵌套/层次化 |
扁平/规范化 |
| 模式 |
灵活(Schema-on-read) |
严格(Schema-on-write) |
| 局部性 |
好(整个文档一起存储) |
差(需要JOIN多表) |
| 多对多关系 |
较难处理 |
容易处理 |
2.4 图模型
适用场景:社交网络、知识图谱、推荐系统
1 2 3 4 5 6 7 8
| // Neo4j Cypher 示例 CREATE (alice:Person {name: 'Alice', age: 30}) CREATE (bob:Person {name: 'Bob', age: 25}) CREATE (alice)-[:FOLLOWS {since: '2020-01-01'}]->(bob)
-- 找出 Alice 关注的人所关注的人 MATCH (alice:Person {name: 'Alice'})-[:FOLLOWS]->()-[:FOLLOWS]->(fof) RETURN fof.name
|
2.5 数据模型选择指南
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| ┌───────────────┐ │ 数据关系复杂吗?│ └───────────────┘ │ │ 复杂 简单 │ │ ▼ ▼ ┌──────────┐ ┌─────────────┐ │多对多关系?│ │树状/层次结构?│ └──────────┘ └─────────────┘ │ │ │ │ 是 否 是 否 ▼ ▼ ▼ ▼ 图模型 关系模型 文档模型 关系模型
|
第3章:存储与检索
3.1 两大存储引擎家族
| 类型 |
优化目标 |
典型应用 |
代表产品 |
| 日志结构存储 |
写入优化 |
高写入负载 |
LevelDB, RocksDB, Cassandra |
| 原地更新存储 |
读取优化 |
事务处理 |
MySQL InnoDB, PostgreSQL |
3.2 哈希索引
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| ┌─────────────────────────────────────────────┐ │ 内存中的哈希表 │ ├─────────────────────────────────────────────┤ │ key1 ──> 字节偏移量 0 │ │ key2 ──> 字节偏移量 128 │ │ key3 ──> 字节偏移量 256 │ └─────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────┐ │ 磁盘上的日志文件 │ ├─────────────────────────────────────────────┤ │ offset 0 : key1,value1 │ │ offset 128: key2,value2 │ └─────────────────────────────────────────────┘
|
局限:所有键必须在内存中、范围查询慢
3.3 LSM-Tree(Log-Structured Merge-Tree)
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| Level 0 (内存): ┌────────────┐ │ Memtable │ ← 当前写入(平衡树) └────────────┘
Level 1 (磁盘): ┌────┐ ┌────┐ ┌────┐ │SS1 │ │SS2 │ │SS3 │ ← 较新的 SSTable └────┘ └────┘ └────┘
Level 2 (磁盘): ┌────────┐ ┌────────┐ │ SS4 │ │ SS5 │ ← 经过合并的 SSTable └────────┘ └────────┘
|
优化技术:
- 布隆过滤器:快速判断键是否存在(避免无效磁盘读取)
- 压缩策略:Size-Tiered(写密集)、Leveled(读密集)
3.4 B-Tree
1 2 3 4 5 6 7 8 9
| ┌───────────────┐ │ [30, 70] │ ← 根节点 └───────────────┘ / │ \ ┌───────┐ ┌───────────┐ ┌───────┐ │[10,20]│ │[40,50,60] │ │[80,90]│ ← 内部节点 └───────┘ └───────────┘ └───────┘ / │ \ / │ \ \ / │ \ 叶子节点(包含实际数据或指向数据的指针)
|
| 特性 |
B-Tree |
LSM-Tree |
| 写入 |
原地更新 |
追加写入 |
| 读取 |
快(一次定位) |
可能需要检查多个文件 |
| 写放大 |
较低 |
较高(压缩开销) |
| 空间利用 |
可能碎片化 |
更紧凑 |
3.5 OLTP vs OLAP
| 特性 |
OLTP |
OLAP |
| 全称 |
在线事务处理 |
在线分析处理 |
| 主要操作 |
增删改查 |
复杂查询、聚合 |
| 访问模式 |
基于键的随机访问 |
扫描大量记录 |
| 数据量 |
GB~TB |
TB~PB |
| 响应时间 |
毫秒级 |
秒~分钟级 |
列式存储优势:
第4章:数据编码与演化
4.1 兼容性概念
1 2 3 4 5 6 7
| 后向兼容(Backward Compatibility): 新代码能读取旧代码写的数据 v3 代码 ─── 读取 ───> v1 数据 ✓
前向兼容(Forward Compatibility): 旧代码能读取新代码写的数据 v1 代码 ─── 读取 ───> v3 数据 ✓
|
滚动升级:不停机部署新版本,同时运行新旧版本代码
4.2 编码格式对比
| 特性 |
JSON |
Protobuf |
Thrift |
Avro |
| 可读性 |
高 |
无 |
无 |
无 |
| 空间效率 |
低 |
高 |
高 |
最高 |
| 模式 |
可选 |
必须 |
必须 |
必须 |
| 模式演化 |
手动 |
支持 |
支持 |
支持 |
| 动态模式 |
天然 |
困难 |
困难 |
支持 |
4.3 Protocol Buffers 示例
1 2 3 4 5
| message Person { required string user_name = 1; optional int64 favorite_number = 2; repeated string interests = 3; }
|
兼容性规则:
| 操作 |
后向兼容 |
前向兼容 |
| 添加可选字段 |
✓ |
✓ |
| 删除可选字段 |
✓ |
✓ |
| 添加必填字段 |
✗ |
✗ |
| 修改字段标签 |
✗ |
✗ |
4.4 数据流模式
1. 数据库:数据可能比代码更持久,需要能够读取多年前写入的数据
2. 服务调用(REST/RPC):
- REST:基于HTTP,资源导向
- gRPC:基于Protobuf,高性能
3. 消息传递:
- 解耦、缓冲、异步、可靠性
- 代表:Kafka, RabbitMQ
本章关键要点
- 可靠性不是追求零故障,而是在故障发生时系统仍能正常工作
- 百分位数比平均值更能反映用户真实体验
- 抽象是管理复杂性的最重要工具
- 没有万能的数据模型,选择取决于应用场景
- LSM-Tree优化写入,B-Tree优化读取
- 兼容性是渐进式部署的前提
延伸阅读
- 《Site Reliability Engineering》:Google SRE 实践
- 《Database Internals》:数据库内部原理深入指南
- 《Building Microservices》:服务间通信详解