如何设计一个数据系统
在给定工作负载的情况下,选择一组数据模型 + 存储与执行结构 + 并发与分布式策略,对性能/成本/灵活性/复杂度做权衡,以逼近可靠性/可扩展性/可维护性的目标。
整个框架的四个层次:
flowchart TB
why["目标 Why(可靠/扩展/维护)"]
workload["工作负载 Workload(面对什么)"]
how["手段 How(数据模型/执行路径/分布式策略)"]
tradeoff["权衡 Tradeoff(正确/性能/灵活)"]
why -- 约束 --> workload -- 驱动 --> how -- 暴露 --> tradeoff
- 目标(Why)= 可靠性/可扩展性/可维护性
- Workload(What kind)= 读写形态/数据分布/体量/SLO/正确容忍度
- 手段(How)= 数据模型+执行结构+分布式策略(+演化策略)
- 权衡(Where it hurts)= 正确性 vs 性能 / 读 vs 写 vs 空间 / 灵活性 vs 约束
下文按这四层依次展开。
三个目标(要什么)
- 可靠性 Reliability:在硬件故障、软件 bug、人为错误下仍能正确工作
- 可扩展性 Scalability:负载(数据量、读写吞吐、并发)增长时,性能可控地退化或线性扩展
- 可维护性 Maintainability:让运维、演进、接手的人活下去(可观测、可演化、低心智负担)
三者不是"挑一个",是全都要。它们是约束,不是选项。
工作负载(面对什么)
不弄清工作负载,所有手段的选择都是空中楼阁。至少要回答:
- 读写比与形态:读多 / 写多 / 均衡?点查、范围扫描、全表聚合、多表关联?
- 数据分布:均匀 / 长尾 / 热点?key 空间是否连续?
- 体量与增速:总数据量、单条大小、写入 QPS、年增长
- 时序特征:突发(秒杀) vs 平稳;时间局部性(最近写入最常被读)
- SLO:P50/P99 延迟、可用性目标(几个 9)、一致性窗口
- 正确性容忍度:能丢多少(RPO)、能停多久(RTO)、能不一致多久?
Workload 决定了手段的"梯度方向":
| Workload 特征 | 倾向的手段 |
|---|---|
| 读多写少 + 点查 | 哈希索引 / KV / 缓存 / 读副本 |
| 写多 + 顺序写入 | LSM / 日志结构 / 批量提交 / append-only |
| 范围扫描 / 排序读 | B-Tree / 按 key 排序分区 / 列存 |
| 写多 + 强热点 | 哈希分区 + 随机后缀 / 本地聚合 / 限流 |
| 时序数据 + TTL | TSDB / 按时间分区 / 冷热分层 |
| 强一致 + 跨分区事务 | 单主+ 2PC / Spanner 式 TrueTime |
| 高可用 + 容忍不一致 | 多主 / 无主 + Quorum / CRDT |
四层手段(怎么做)
把手段堆成列表会丢掉谁服务于谁,按职责分层才能看清依赖关系:上层定义做什么,下层支撑怎么做到
- 抽象层: 数据模型 + 查询语言 → 决定
怎么用 - 执行层: 存储结构 + 索引 / 访问路径 → 决定
多快 - 分布层: 复制 + 分区 → 决定
多大 & 多稳 - 演化层: 编码格式 + schema 演进 → 决定
能不能活久
1. 抽象层(对用户暴露)—— 决定怎么用
- 数据模型:关系 / 文档 / 图 / KV / 列族 / 时序
- 查询语言:声明式(SQL、Cypher、GraphQL)vs 命令式(KV API、MapReduce)
- 模型与语言是一体的:模型限定了能自然表达的形状,语言限定了能高效问的问题
2. 执行层(性能核心)—— 决定多快
- 存储结构:B-Tree(读优)、LSM(写优)、列存(OLAP / 压缩)、倒排(搜索)、堆 + 索引
- 索引 / 访问路径:主键、二级索引、覆盖索引、多列索引;点查、范围扫描、全表扫描
- 这一层直接决定
读放大 / 写放大 / 空间放大三角
写优先的引擎(LSM / 日志结构)会把数据路径拆成两条:前台提交路径负责低延迟与持久性,后台整理路径异步把数据变成可查询、可归档的形态:
sequenceDiagram
autonumber
box rgb(225,240,255) 前台提交路径 commit path(同步 · 决定延迟与持久性)
participant C as Client
participant E as 存储引擎 (Leader)
participant W as WAL / replicated log
participant D as 本地 SSD
participant F as Follower (副本)
end
box rgb(255,244,220) 后台整理路径 materialization path(异步 · 决定存储形态与成本)
participant M as 内存状态 (MemTable / Buffer Pool)
participant B as 后台任务 checkpoint / flush / compaction
participant T as SSTable / segment / snapshot
participant O as 对象存储 S3
end
Note over C,F: ① 提交路径:写入先过日志,返回前必须 durable
C->>E: 写入请求 (put / txn commit)
E->>W: append log record + fsync
W->>D: 顺序写日志段
W-->>E: 本地落盘确认(单机持久性)
E->>F: 复制日志记录
F-->>E: quorum ack(分布式持久性)
E->>M: 更新内存状态(无 I/O,立即可读)
E-->>C: 返回提交成功(commit ack)
Note over M,O: ② 整理路径:提交之后才动,慢一点没关系
B->>M: checkpoint / flush(周期触发)
M->>T: 刷出不可变快照(SSTable / segment)
B->>T: compaction(合并 / 排序 / 清理)
B->>O: 上传快照到对象存储(廉价归档)
B->>W: 截断已 checkpoint 的日志
两条路径的本质分工:提交路径只做顺序写(日志追加 + fsync,或复制到 quorum),把随机写和重活全甩给后台路径批量消化;写放大 / 空间放大因此发生在后台,不挡前台延迟。本地 SSD 与 quorum 满足其一即可保证 durable。
3. 分布层(规模 & 可用性)—— 决定多大 & 多稳
- 复制:单主 / 多主 / 无主;同步 / 半同步 / 异步;副本拓扑(链式、星型、Quorum)
- 分区:按范围 / 按哈希 / 一致性哈希;分区再平衡;二级索引的本地 vs 全局策略
- 这一层把单机能力放大到集群,同时引入了 CAP / 网络分区 / 时钟漂移的新麻烦
4. 演化层(长期维护)—— 决定能不能活久
- 编码格式:JSON / Protobuf / Avro / Thrift;自描述 vs 带 schema;紧凑度与可读性
- Schema 演进:向前兼容 / 向后兼容;滚动升级、双写迁移、影子表;在线 DDL
- 没有这一层,系统在第一年风光无限,第三年寸步难行
5. 总结(分层的实战价值):
- 排障自上而下:用户喊慢 → 是查询语言写法、索引缺失,还是分区热点、网络复制延迟?
- 设计自下而上:分布层选了无主复制 → 抽象层就别承诺线性一致
三条主轴(权衡)
零散的权衡条目能列十几条,但本质冲突只有三类。每条主轴都是一个反问:
轴一:正确性 vs 性能 —— 要不要等所有人都同意?
囊括:一致性 vs 延迟、隔离级别 vs 吞吐、同步 vs 异步复制。
- 等 = 协调成本(锁、共识、跨节点 RTT)
- 不等 = 不一致窗口、丢数据窗口、并发异常
表现:CAP / PACELC、Serializable → RU、同步 → 异步复制。 本质:协调(coordination)是正确性的代价,也是性能的敌人。
轴二:读 vs 写 vs 空间 —— 成本放在哪一侧?
囊括:读放大、写放大、空间放大三角。任一存储引擎只能优化两端,第三端必然恶化。
- B-Tree:读放大低,写放大高
- LSM:写放大低,读放大与空间放大高
- 列存 + 压缩:空间小、扫描快,但点查与更新昂贵
本质:没有免费的索引,只有把痛点搬去别处。
轴三:灵活性 vs 约束 —— 约束提前还是延后?
囊括:强 schema vs 弱 schema、静态类型 vs 动态、预建索引 vs 即席查询、预聚合 vs 实时计算。
- 提前约束(写时校验、强 schema、预建索引):写入慢、迁移痛,但读取/查询便宜、错误早暴露。
- 延后约束(读时校验、schemaless、即席分析):写入自由,但每次读都要付解释代价、错误晚暴露。
本质:约束是
现在花钱还是以后花钱,总账谁也省不了。
评论