关于 data-storage
用于选择数据存储、数据模型和扩展策略的系统设计技能,帮助架构师根据访问模式与一致性需求匹配存储方案,并优先考虑简单方案,再引入副本或分片。
主要功能
- 01比较关系型与 NoSQL 存储
- 02设计键、查询与索引
- 03评估副本与数据库联邦
- 04规划分区与分片
- 05分析存储故障场景
适用场景
- 01为新系统选择存储方案
- 02根据查询设计键与索引
- 03判断何时需要副本或分片
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ proyecto26/ system-design-skills --skill data-storage安装后在 Claude Code 中输入/data-storage确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/deep-dive.mdreferences/providers/aws.mdazure.mdgcp.mdgeneric.md详细介绍
数据存储
选择记录的存放位置、键和查询方式,以及存储如何扩展到单台机器之外。存储层是后期最难更改的层:错误的数据模型或分片键会固化为扩展瓶颈,而副本配置错误则会在不知不觉中提供过期或丢失的数据。
何时使用
任何需要持久化状态的系统:选择 SQL 还是 NoSQL、设计架构及其访问路径、添加索引、拆分热点表、在节点间分布数据(分片/分区)、添加只读副本,或决定要反规范化哪些数据。当“存储数据”需要明确具体的键和查询形式时,就该参考这里。
何时不使用
在数据规模迫使你这么做之前,不要分片、添加副本或使用 NoSQL(YAGNI)。单个索引完善的关系型节点可以轻松处理约 1k QPS,以及数十 GB 到低 TB 的数据——大多数系统永远不会超出这个范围。分片会增加运维成本,并破坏联接/事务;只有在确实超出单节点的写入吞吐量或数据集容量时才添加分片(→ back-of-the-envelope)。缓存读取(→ caching)和添加只读副本都是比分片更便宜的优先选择。
先明确需求
选择存储或拓扑之前,请先回答以下问题——它们决定设计方案:
- 数据形态与关系 — 扁平的键值对?需要联接的复杂关系?文档块?连接关系图?这决定选择 SQL 还是 NoSQL。
- 访问模式 — 数据如何读写,而不只是存在哪里。按键点查、范围扫描、临时查询、聚合?根据必须支持的查询来设计存储。
- 读写比例与规模 — 读写各自的 QPS,以及当前和保留期内的数据总量。(关于 QPS、存储和分片数量,参见 →
back-of-the-envelope。) - 一致性需求 — 读取必须看到最新写入,还是最终一致性也可以?是否需要多记录事务?(CAP/一致性理论 →
consistency-coordination。) - 延迟与持久性目标 — p99 读写预算,以及节点故障时系统能够承受丢失多少近期数据。
选项
**关系型(SQL — Postgres、MySQL):**严格的架构、联接、ACID 事务。适用场景:数据具有关系,数据完整性很重要,且查询多样或临时性强——在有数据指标证明不适用之前,这是安全的默认选择。
**文档型(MongoDB 等):**架构灵活,可按结构查询独立完整的类 JSON 文档。适用场景:记录整体读写,且架构不断演进;关系较少。
**键值型(Redis、DynamoDB、Riak):**按键执行 O(1) 的 get/put,不支持复杂查询。适用场景:访问完全基于已知键,且需要极高吞吐量。
**宽列型(Cassandra、Bigtable、HBase):**按分区键组织行,列稀疏,并按顺序保存键以支持范围扫描。适用场景:写入量巨大,且表可以根据少数已知查询模式设计。
**图型(Neo4j):**节点和边。适用场景:核心查询需要遍历多对多关系(社交图谱、推荐)。
扩展方式(适用于任何存储):
- 索引 — 添加辅助结构,让查询无需再扫描数据。这是首要手段。
- 只读副本(主从模式) — 将写入复制到负责处理读取的从节点。 适用场景:读取占主导,且可以接受轻微的数据过期。
- 联邦化 — 按功能拆分数据库(用户/产品/论坛)。适用场景:各功能域独立扩展,且很少需要联接。
- 分片/分区 — 使用分片键将一张逻辑表拆分到多个节点。适用场景:超出单节点的写入能力或数据集容量。(本技能负责此项。)
- 反规范化 — 存储冗余副本以跳过联接。适用场景:读取量远超写入量,且联接是瓶颈。
**多态持久化:**使用上述多种存储,各自承担最适合的工作(例如,Postgres 存储订单,Redis 存储会话,搜索索引存储全文)。代价是需要运维并协调多个存储。
权衡
| 选项 | 解决的问题 | 带来的问题 | 何时更换 |
|---|---|---|---|
| 关系型/SQL | 联接、ACID、临时查询、完整性 | 单节点写入上限;架构迁移;难以横向扩展 | 写入量/数据量超出单个节点的承载能力,或架构确实灵活多变 → NoSQL/分片 |
| 文档型 | 架构灵活;读取完整对象 | 不支持联接;跨文档一致性需手动维护;查询引擎较弱 | 数据变为关系型,或需要跨文档事务 → SQL |
| 键值型 | 极高吞吐量;运维简单 | 只能按键访问;不支持范围查询/二级查询 | 需要按键以外的查询 → 文档型/宽列型 |
| 宽列型 | 写入密集型扩展;按排序键范围扫描 | 必须预先确定查询;键设定后难以更改;默认最终一致 | 访问模式未知/多样 → 关系型 |
| 图型 | 以低成本深度遍历关系 | 工具生态小众;难以分片;不适合批量扫描 | 关系较浅 → 关系型/文档型 |
| 索引 | 将扫描转为查找 | 写入变慢;存储占用增加;索引膨胀 | 写放大带来的影响超过读取收益 |
| 只读副本 | 分担读取;提供冗余 | 复制延迟 → 读取到陈旧数据;需要处理故障转移/提升逻辑 | 无法接受陈旧数据 → 从主节点读取 / consistency-coordination |
| 联邦 | 按领域扩展;缩小工作集 | 跨领域联接会失效;需要应用路由逻辑 | 单个领域仍无法容纳 → 对该领域分片 |
| 分片 | 横向扩展写入和存储 | 跨分片联接/事务难以处理;重新分片麻烦;热点分片 | 单个节点可以承载,或热点分片影响过大 → 合并/重新设定键 |
| 反规范化 | 消除读取路径上昂贵的联接 | 数据重复;写入时扇出;一致性漂移 | 写入负载使扇出成为新的瓶颈 → 规范化/缓存 |
压力下的行为
负载和故障会在存储层被放大,最终引发服务中断。
- 热点分片 / 热门键: 分布不均的分片键会将不成比例的流量发送到 一个节点(某个爆红用户、单个租户),而其他节点却空闲。集群看起来 负载不高,但一个分片已濒临崩溃。缓解措施: 使用基数高且分布均匀 的分片键;隔离热点键或对其进行子分区。
- 复制延迟: 写入突增时,从节点会落后于主节点,导致读取到陈旧数据, 而故障转移还可能提升一个丢失了近期写入的从节点。缓解措施: 为实现 读己之写,从主节点读取;对延迟设置上限并配置告警。
- 连接耗尽: 每个数据库连接都会消耗内存和一个后端进程/线程;流量 激增(或重试风暴)会打开超出数据库处理能力的连接数,导致所有查询 都变慢或出错。前置连接池(有界连接池)才能让数据库保持运行——没有 它,数据库就会成为在负载下故障的单点故障。
- 写入风暴 / 锁争用: 热点行或单个序列/计数器会使写入串行化;索引 更新和锁等待不断累积。
- 高压下重新分片: 在系统已过载时进行再平衡,会移动海量数据,可能 使集群不堪重负。应在临界点到来前规划容量。
监控: 复制延迟、各分片的 QPS 和大小(分布偏斜)、连接池饱和度/等待 时间、慢查询率、锁等待,以及磁盘/IOPS 余量。
如何应用
- 明确输入 — 确定数据形状、访问模式、读写比例、一致性需求以及延迟/持久性目标(参见 先明确需求)。不了解访问模式,就无法选定适用的数据存储。
- 根据权衡表选择数据存储 — 将数据形状与选项匹配;除非某个数字或关系模式排除了关系型数据库,否则默认选择关系型数据库。说明每个候选方案会牺牲什么,而不只说它能解决什么。
- 确定接口 — 在引入扩展机制前,为每种查询写明主键、分区键/排序键和二级索引(参见 接口草图)。键才是关键决策。
- 按成本从低到高设置扩展参数 — 依次使用索引、缓存(→
caching)、只读副本、联邦,最后才是分片。达到目标后就停止。 - 对方案进行压力测试 — 检查热点分片、复制延迟、连接耗尽和重新分片(参见 压力下的行为);确认键分布偏斜或写入突发不会压垮单个节点。
- 先估算规模,再选择提供商 — 根据
back-of-the-envelope计算分片数量和每个节点的负载;如果只需一个节点,就不要分片。默认采用通用方案,仅在指定云平台时阅读对应的提供商文件。 - 在答案中展示权衡 — 提供
Decision | Solves | Worsens | Change it when…表格,每个已做出的选择占一行(例如选项、数据存储及其故障策略);最后一列要写明具体触发条件(一个数字或可观察到的信号)。然后说明最先会出现什么故障。
应做和不应做
应做
- 从查询出发:围绕访问路径设计数据存储,并在画任何架构图之前写出具体的键。
- 默认使用单个索引完善的关系型节点;先充分利用索引、缓存和只读副本,再考虑分片。
- 选择基数高、分布均匀的分片键;隔离已知热点键,或将其进一步划分为子分区。
- 在数据库前部署有上限的连接池器,并针对复制延迟和各分片的数据偏斜设置告警。
- 根据峰值写入 QPS 和数据集大小计算分片数量,取较大的结果。
不应做
- 不要在数字表明有必要之前就采用 NoSQL、只读副本或分片(YAGNI)。
- 不要在了解数据形状和一致性需求之前,因炒作而选择数据存储。
- 不要以为只读副本能提供最新读取结果——复制延迟会导致读取到陈旧数据,故障转移还可能丢失近期写入。
- 不要使用基数低或单调递增的键进行分片;这会造成热点分片和写入热点。
- 不要等集群已经过载后再重新分片;应在到达容量瓶颈之前规划容量。
关键数字
不要重复表格中的内容——从 back-of-the-envelope 中提取数据。影响存储决策的数字包括:单个 RDBMS 节点约为 1k QPS;键值节点约为 10k QPS;10 GB 可放入 RAM,10 TB 需要分布式存储。使用这些数据计算 分片数量 = 峰值写入 QPS ÷ 单节点 QPS(还要按大小计算一次:总字节数 ÷ 单节点容量),然后取较大的结果。如果结果是一个节点,就不要分片。
接口草图
数据模型就是契约(GUIDE 故障模式 #8)——在写明键之前,“NoSQL box” 并不能决定任何事。请为每种实体明确以下内容:
- 主键 — 唯一标识某行/项的内容,以及查找方式。
- 分区键(分片键)+ 排序键 — 例如,宽列数据库/DynamoDB:
PK = user_id(哈希,用于分散负载),SK = created_at(排序,用于支持范围扫描,例如“最新的 N 条帖子”)。PK 必须具有高基数且分布均匀。 - 二级索引 — 必须支持的非键访问路径,以及每条路径对应的查询。
- 关系型数据库 — 表、带类型的列、外键,以及支持每条查询的索引;注明有意进行反规范化的部分。
如果没有具体的键,也没有说明它服务于哪条查询,设计的其余部分就只能靠猜测。
选择提供商
默认使用上面的通用方案。如果用户指定了云服务,请阅读
references/providers/<provider>.md,了解托管服务映射、
配额和限制,以及提供商特有的权衡。如果该提供商没有对应文件,则采用通用方案。
图示
要直观展示数据层——带只读副本的主节点、带路由器的分片集群,或多语言拆分——请使用插件内的 architecture-diagram 技能。
在路由箭头上标出分片键,并明确标示复制方向;
此处不要嵌入 Mermaid。
相关构建模块
caching— 与此模块配合使用,用于分担读取负载,也是成本最低的首选措施; 在分片之前,可作为只读副本的替代方案。consistency-coordination— 此模块负责的概念包括 CAP/一致性模型、 一致性哈希、仲裁,以及分布式事务/saga。如果无法接受过时读取或跨分片原子性问题,可与此模块 配合使用。back-of-the-envelope— 为此模块提供依据:给出 QPS、存储量和 分片数量,帮助判断此处的每种措施是必要还是可省略。scaling-evolution— 依赖此模块;随着系统增长,安排引入各项存储措施的时机。system-design— 将工作流导向此模块的协调器。
参考资料
references/deep-dive.md— 分区方案(范围/哈希/目录)、 复制机制与冲突解决、索引内部实现(B-tree 与 LSM 对比)、 规范化与反规范化、连接池、重新分片。在详细设计数据层时阅读。references/providers/{generic,aws,azure,gcp}.md— 服务映射、影响决策的 限制,以及各环境的常见问题。