如何设计一个数据系统

2025-06-062026-07-15

在给定 Workload 下,选择一组数据模型 + 存储与执行结构 + 并发与分布式策略,在满足正确性约束的前提下,对 性能 / 成本 / 灵活性 / 复杂度 做权衡,以逼近 可靠性 / 可扩展性 / 可维护性 的目标。

整个框架的四个层次:

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:让运维、演进、接手的人活下去——可观测、可演化、低心智负担。

三者不是"挑一个",是全都要。它们是约束,不是选项。

Workload 工作负载(面对什么)

不画清 workload,所有"手段"的选择都是空中楼阁。至少要回答:

  • 读写比与形态:读多 / 写多 / 均衡?点查、范围扫描、全表聚合、多表关联?
  • 数据分布:均匀 / 长尾 / 热点?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

四层手段(怎么做)

把手段堆成列表会丢掉"谁服务于谁",按职责分层才能看清依赖关系:上层定义"做什么",下层支撑"怎么做到"。

  1. 抽象层: 数据模型 + 查询语言 → 决定"怎么用"
  2. 执行层: 存储结构 + 索引 / 访问路径 → 决定"多快"
  3. 分布层: 复制 + 分区 → 决定"多大 & 多稳"
  4. 演化层: 编码格式 + schema 演进 → 决定"能不能活久"

1. 抽象层(对用户暴露)—— 决定"怎么用"

  • 数据模型:关系 / 文档 / 图 / KV / 列族 / 时序。
  • 查询语言:声明式(SQL、Cypher、GraphQL)vs 命令式(KV API、MapReduce)。
  • 模型与语言是一体的:模型限定了能自然表达的形状,语言限定了能高效问的问题。

2. 执行层(性能核心)—— 决定"多快"

  • 存储结构:B-Tree(读优)、LSM(写优)、列存(OLAP / 压缩)、倒排(搜索)、堆 + 索引。
  • 索引 / 访问路径:主键、二级索引、覆盖索引、多列索引;点查、范围扫描、全表扫描。
  • 这一层直接决定读放大 / 写放大 / 空间放大三角。

3. 分布层(规模 & 可用性)—— 决定"多大 & 多稳"

  • 复制:单主 / 多主 / 无主;同步 / 半同步 / 异步;副本拓扑(链式、星型、Quorum)。
  • 分区:按范围 / 按哈希 / 一致性哈希;分区再平衡;二级索引的本地 vs 全局策略。
  • 这一层把单机能力放大到集群,同时引入了 CAP / 网络分区 / 时钟漂移这套新麻烦。

4. 演化层(长期维护)—— 决定"能不能活久"

  • 编码格式:JSON / Protobuf / Avro / Thrift;自描述 vs 带 schema;紧凑度与可读性。
  • Schema 演进:向前兼容 / 向后兼容;滚动升级、双写迁移、影子表;在线 DDL。
  • 没有这一层,系统在第一年风光无限,第三年寸步难行。

分层的实战价值:

  • 排障自上而下:用户喊慢 → 是查询语言写法、索引缺失,还是分区热点、网络复制延迟?
  • 设计自下而上:分布层选了无主复制 → 抽象层就别承诺线性一致。

三条主轴(权衡)

零散的权衡条目能列十几条,但本质冲突只有三类。每条主轴都是一个反问

轴一:正确性 vs 性能 —— "要不要等所有人都同意?"

囊括:一致性 vs 延迟、隔离级别 vs 吞吐、同步 vs 异步复制。

  • = 协调成本(锁、共识、跨节点 RTT)
  • 不等 = 不一致窗口、丢数据窗口、并发异常

表现:CAP / PACELC、Serializable → RU、同步 → 异步复制。 本质:协调(coordination)是正确性的代价,也是性能的敌人。

轴二:读 vs 写 vs 空间 —— "成本放在哪一侧?"

囊括:读放大、写放大、空间放大三角。任一存储引擎只能优化两端,第三端必然恶化。

  • B-Tree:读放大低,写放大高
  • LSM:写放大低,读放大与空间放大高
  • 列存 + 压缩:空间小、扫描快,但点查与更新昂贵

本质:没有免费的索引,只有把痛点搬去别处。

轴三:灵活性 vs 约束 —— "约束提前还是延后?"

囊括:强 schema vs 弱 schema、静态类型 vs 动态、预建索引 vs 即席查询、预聚合 vs 实时计算。

  • 提前约束(写时校验、强 schema、预建索引):写入慢、迁移痛,但读取/查询便宜、错误早暴露。
  • 延后约束(读时校验、schemaless、即席分析):写入自由,但每次读都要付解释代价、错误晚暴露。

本质:约束是"现在花钱"还是"以后花钱",总账谁也省不了。

评论