Scott Shi
当写入超长键时,LSM树内存块索引会因急剧膨胀耗尽物理内存并引发系统换页崩溃,底层存储必须设置硬性键长阈值并引入布隆过滤器以阻断空查导致的读放大。
AI-assisted summary of [Chinese] Mock Interview: Design KV store — original source. Check the original for full context.
Source published:
Key ideas
- 针对写密集型(Write-heavy)分布式 KV 存储,采用 LSM-Tree 架构通过 WAL 追加写和内存 MemTable 缓冲提供高吞吐低延迟写能力,后台借助 Compaction 异步回收旧版本和墓碑(Tombstone)。 (4:24)
- 存储引擎必须对 Key/Value 设置硬性边界防护,生产级 SSTable 通常切分为 10MB 至 100MB,避免过长 Key 导致内存块索引膨胀并引发频繁的操作系统页面置换(Page Swap Thrashing)。 (36:24)
- 点查非存在记录会触发全层 SSTable 遍历与最恶劣的读放大,系统需借助布隆过滤器(Bloom Filter)在内存中提前拦截无效磁盘 I/O。 (50:18)
- 分布式路由应通过 Partition Key 和一致性哈希管理逻辑分片,而非在业务主键中硬编码机架等物理拓扑;跨节点多对象事务则需权衡性能并引入两阶段提交(2PC)。 (54:24)
Detailed notes
## Background 该视频由 Scott Shi 于 2020 年 12 月 29 日发布在 YouTube 频道上。视频记录了一场针对“从零设计分布式键值存储系统(KV Store)”的高阶模拟系统设计面试,并在简介中提供了配套的讨论设计文档链接。
## TL;DR 面试围绕高写吞吐、支持复合主键与事务隔离的分布式 KV 存储展开,深入探讨了存储引擎与分布式架构的设计细节。双方首先基于 LSM-Tree 架构深入剖析了 WAL、MemTable、SSTable 分层落盘与 Compaction 机制,并针对超大 Key 引发的内存交换、读放大及布隆过滤器的优化展开了激辩。随后,讨论进一步扩展到使用一致性哈希进行数据路由分片、基于 MVCC 的读写互不阻塞并发控制,以及 LSM-Tree 与 B-Tree 的性能权衡。最后,面试官与候选人梳理了动态网络下的节点迁移成本,并对比了单对象事务与基于两阶段提交(2PC)的分布式多对象事务的复杂度与隔离级别。
## Atmosphere & style 整场面试呈现出专业务实且极具思辨张力的硬核技术交锋氛围。面试官擅长通过极端场景(如超长 Key 耗尽内存、全量数据加载磁盘开销、无序数据点查)持续挑战候选人的前置假设,推着讨论向操作系统底层 I/O 与内存管理层级下潜。候选人在面对高难度追问时既有据理力争的细节辩论,也有在触及知识盲区(如快照 Dump 和复杂分布式事务)时的坦诚应对,整体互动真实还原了顶级大厂系统设计面试中高强度的思维推演与工程权衡过程。
## Bulletin - [04:24](https://www.youtube.com/watch?v=cG5eyid8kpA&t=264s) 针对写密集型(Write-heavy)分布式 KV 存储,采用 LSM-Tree 架构通过 WAL 追加写和内存 MemTable 缓冲提供高吞吐低延迟写能力,后台借助 Compaction 异步回收旧版本和墓碑(Tombstone)。 - [36:24](https://www.youtube.com/watch?v=cG5eyid8kpA&t=2184s) 存储引擎必须对 Key/Value 设置硬性边界防护,生产级 SSTable 通常切分为 10MB 至 100MB,避免过长 Key 导致内存块索引膨胀并引发频繁的操作系统页面置换(Page Swap Thrashing)。 - [50:18](https://www.youtube.com/watch?v=cG5eyid8kpA&t=3018s) 点查非存在记录会触发全层 SSTable 遍历与最恶劣的读放大,系统需借助布隆过滤器(Bloom Filter)在内存中提前拦截无效磁盘 I/O。 - [54:24](https://www.youtube.com/watch?v=cG5eyid8kpA&t=3264s) 分布式路由应通过 Partition Key 和一致性哈希管理逻辑分片,而非在业务主键中硬编码机架等物理拓扑;跨节点多对象事务则需权衡性能并引入两阶段提交(2PC)。
## Key takeaways - [04:24](https://www.youtube.com/watch?v=cG5eyid8kpA&t=264s) 写密集型场景应优先选择 LSM-Tree 架构,写入操作顺序落入 Commit Log(WAL)并在 MemTable 排序缓冲,满阈值后刷盘为不可变的 SSTable。 - [10:40](https://www.youtube.com/watch?v=cG5eyid8kpA&t=640s) LSM-Tree 的更新与删除本质上均为追加写入:更新写入带新时间戳的新版本,删除写入 Tombstone 标记,最终由后台 Compaction 进程完成多路归并和空间物理回收。 - [21:33](https://www.youtube.com/watch?v=cG5eyid8kpA&t=1293s) MemTable 无法采用无序纯追加数组,因点查复杂度会退化为 O(N) 线性扫描,必须采用支持高效查找与有序落盘的内存索引结构(如跳表或哈希结构)。 - [21:33](https://www.youtube.com/watch?v=cG5eyid8kpA&t=1293s) 底座系统必须对上游输入施加防御性约束(如限制 Key 上限为 1KB 或 64MB),防止异常长 Key 耗尽内存导致节点崩溃。 - [36:24](https://www.youtube.com/watch?v=cG5eyid8kpA&t=2184s) 在以完整 URL 作为 Key、整型计数器作为 Value 的倾斜场景下,内存中常驻的 SSTable 块索引会迅速耗尽物理 RAM,引发灾难性的系统 Swap 抖动。 - [36:24](https://www.youtube.com/watch?v=cG5eyid8kpA&t=2184s) 生产级 LSM-Tree(如 RocksDB)将 SSTable 控制在 10MB 到 100MB 之间,通过切分小文件保持索引轻量,避免大 SSTable 带来的加载与 Compaction 瓶颈。 - [42:25](https://www.youtube.com/watch?v=cG5eyid8kpA&t=2545s) 单机检索不存在的记录会遭遇最坏读取路径,必须依次排查 MemTable 和每一层 SSTable,造成严重的读放大(Read Amplification)。 - [50:18](https://www.youtube.com/watch?v=cG5eyid8kpA&t=3018s) 引入布隆过滤器(Bloom Filter)可以极快判定 Key 是否可能存在,若判定不存在则直接跳过磁盘 I/O 与各级 SST 检索。 - [51:10](https://www.youtube.com/watch?v=cG5eyid8kpA&t=3070s) 复合主键应拆解为用于分片寻址的 Partition Key(如 ZIP Code + User ID)和节点内有序排列的 Clustering Key,不能将机房、机架等物理拓扑信息硬编码到业务键中。 - [01:01:14](https://www.youtube.com/watch?v=cG5eyid8kpA&t=3674s) 通过多版本并发控制(MVCC)与时间戳可见性检查,读请求访问已提交快照,写请求写入独立临时标记版本,实现读写互不阻塞。 - [01:12:04](https://www.youtube.com/watch?v=cG5eyid8kpA&t=4324s) 相比 B-Tree 在随机写时引发的磁盘随机 I/O 和页分裂,LSM-Tree 在追加写及最新热点数据点查上具备吞吐和延迟优势,但深层冷数据读取与删除较依赖 Compaction 优化。 - [01:15:10](https://www.youtube.com/watch?v=cG5eyid8kpA&t=4510s) 集群水平扩容通常基于一致性哈希环,使节点增减时的数据迁移仅局限于相邻环段,保证迁移与发现开销与节点数量成比例可控。 - [01:25:35](https://www.youtube.com/watch?v=cG5eyid8kpA&t=5135s) 单对象事务可局限于单个分片本地原子提交,而跨分片的多对象分布式事务必须引入两阶段提交(2PC)协议,需在一致性保证与网络通信延迟、锁竞争间进行权衡。
## Detailed walkthrough ### [00:23](https://www.youtube.com/watch?v=cG5eyid8kpA&t=23s) 面试开场与题目需求阐释
面试官 Scott Shi 简单开场后,明确了本次系统设计模拟面试的主题:从零设计一个键值存储系统(Design a KV Store from scratch)。面试官梳理并给出了多层面的系统需求:
在 API 层面,系统需要支持基础的写(`write(key, value)`)、读(`read(key)`)与删除(`delete(key)`)。`value` 应该支持任意的编码格式(如 JSON、Protobuf、纯二进制等),并建议在存储时引入数据压缩。此外,系统要求支持类似 Apache Cassandra 的复合主键(composite primary key),允许用户分别指定 primary key 和 partition key;其中 partition key 用于决定数据具体分片(shard)到哪个节点,从而让用户对数据物理存放位置拥有充分的控制权。
在事务与隔离级别(Isolation Level)层面,系统需要做到“读不阻塞写,写也不阻塞读”;面试官建议先从单对象事务(single-object transaction)入手,后续再进一步讨论多对象事务(multiple-object transaction)的实现方案。系统还要求支持打快照(dump snapshot),以便在发生数据故障或灾难时能够依靠快照进行恢复。
在系统级性能与架构指标上,核心要求对写吞吐量(write throughput)进行深度优化。该系统定位为偏向 OLTP 的事务型数据库,必须具备良好的水平扩展能力(horizontal scalability),当有新增请求或容量增加时,能够简单地通过添加新节点进行横向扩容。最后,系统需要高度可配置(highly configurable),支持用户调节持久化级别:如果用户不在意数据丢失,可以牺牲持久性来换取极致的高吞吐和低延迟;如果业务对数据持久性有极高要求,则可以配置成类似于传统数据库或 RocksDB 那样严密的持久化策略。
- **核心 API**:`write(key, value)`、`read(key)`、`delete(key)`,支持任意格式 value 及数据压缩 - **数据路由**:支持类似 Cassandra 的复合主键设计,通过 partition key 控制 Sharding 路由 - **并发与事务**:读写互不阻塞,优先实现单对象事务,讨论扩展多对象事务 - **容灾与运维**:支持 Dump snapshot 与快照恢复机制 - **性能目标**:针对写吞吐优化(Write-heavy),支持水平线性扩展,持久化等级可灵活配置
---
[04:24](https://www.youtube.com/watch?v=cG5eyid8kpA&t=264s) 架构选型讨论:基于 LSM-Tree 与 Cassandra 模式
针对上述需求,候选人分析认为 Apache Cassandra 的架构非常契合当下的场景。尽管需求中提到了事务支持和快照导出等比原生 Cassandra 更严格的特性(候选人不太确定 Cassandra 原生对 Dump snapshot 的支持成熟度),但整体方向上,采用 Cassandra 底层的 LSM-Tree(Log-Structured Merge-tree)架构绝对是应对写密集(write-heavy)场景的标准范式。
候选人梳理道,基于 LSM-Tree 的架构在逻辑上如同一个大哈希表(Big Hash Table),但在底层存储上却由一系列有序结构协同工作:数据写入时先记录 Commit Log(即 WAL),随后写入内存中的 MemTable;当 MemTable 写满达到一定阈值后,会作为不可变数据下刷(flush)到磁盘上的 SSTable(Sorted String Table,候选人口误提到了 stream table,后纠正为 Sorted String Table);在后台,则依赖 Compaction 机制持续合并不同层级的 SSTable。此外,由于 Cassandra 本质上是面向宽行(wide-column / row-oriented)且具备分区键索引的分布式数据库,利用 Partition Key 决定 Shard 归属完全契合分布式节点路由的设计。
```mermaid flowchart TD accTitle: LSM-Tree Architecture Flow accDescr: High-level overview of write path in an LSM-tree based storage engine
subgraph MemoryPath ["Memory Path"] WAL["Commit Log / WAL\n(Append-only for Durability)"] MEM["MemTable\n(In-memory sorted structure)"] end
subgraph DiskStorage ["Disk Storage (SSTables)"] L0["L0 SSTable\n(Flushed from MemTable)"] L1["L1 SSTable"] L2["L2 SSTable"] end
WAL --> MEM MEM -- "Flush on threshold" --> L0 L0 -- "Background Compaction" --> L1 L1 -- "Background Compaction" --> L2 ```
---
[07:05](https://www.youtube.com/watch?v=cG5eyid8kpA&t=425s) 深入剖析 LSM-Tree:Commit Log、MemTable 与 SSTable
面试官在此处打断,要求候选人暂停一下高阶概念,花一两分钟时间深入讲解 LSM-Tree 的底层运行细节与原理。
候选人详细展开了 LSM-Tree 的机制与生命周期: 1. **追加写与持久化保证(Commit Log)**:LSM-Tree 采用类似“Kafka 风格”的纯追加写日志(append-only log)。每次写请求到来时,首要任务是将操作追加写入 Commit Log。这一步纯粹出于持久性(Durability)考虑,一旦发生崩溃或断电,Commit Log 是唯一的恢复源泉,日常正常读请求绝对不会去读取 Commit Log。 2. **内存缓冲与高速写(MemTable)**:日志刷盘后,数据随即写入内存中的 MemTable。在操作系统层面,写操作通常先进入内核缓冲(Kernel Space Buffer)或借助 Direct I/O 跳过标准 I/O 缓冲以压榨性能,因此内存写入耗时极低、吞吐极高。 3. **分层刷盘与冷热沉淀(SSTable)**:当 MemTable 达到指定容量阈值时,会被冻结并作为不可变的有序数据块导出到磁盘冷存储中,即 Google Bigtable 论文中定义的 SSTable(Sorted String Table)。磁盘上通常维护着多个不同层级(Level 0、Level 1 等)的 SSTable。 4. **读写性能权衡(Read vs Write Trade-off)**:这种架构换来了极高的写性能和低写延迟。然而代价在于读取路径变得相对沉重,读操作需要自顶向下遍历并检查 MemTable 以及多个层级的 SSTable,并在必要时执行多路归并,这就是典型的“读合并”(Merge on read)。
---
[10:40](https://www.youtube.com/watch?v=cG5eyid8kpA&t=640s) 数据记录生命周期、Tombstone 与后台 Compaction 机制
面试官进一步引导候选人深入细节,提出具体要求:“能否落到一个具体的记录上?在 MemTable 里一条 Record 长什么样?到了 SSTable 里它又是什么样子的?能给出一个具体的例子吗?”
候选人以自己在实际项目中存储事件(Event Store)的经历为例进行推演。候选人提到,Cassandra 非常适合做 Event Store,类似于 Uber 将日期字符串作为生命周期表的主键: - **Schema 与数据示例**:可以采用日期字符串(如 `2020-08-11`)或者微秒/毫秒级时间戳作为 Key,而 Value 则包含具体的附随数据(satellite data),比如事件类型、位置信息(如 `Zoom` 会议打卡或某个行程事件)等。这类事件数据通常只追加、不修改历史,只会在审计(auditing)或探索性数据分析(exploratory analysis)等低频场景下被检索,非常契合 LSM-Tree 的特性。 - **写入流转**:写请求携带着该事件记录,首先落入 Commit Log,紧接着写入 MemTable。 - **删除与更新(Upsert & Delete via Tombstone)**:在 LSM-Tree 中,更新本质上就是插入一条带有更新时间戳的新记录;而删除操作同样不是就地物理擦除,而是写入一个特殊的删除标记——墓碑(Tombstone),带有操作时间戳。 - **后台合并清除(Compaction Daemon)**:系统中运行着后台守护进程(Compaction daemon),持续对各个 SSTable 进行多路归并压缩。当后台 Compaction 发现某个 Key 对应着一个 Tombstone,且该 Tombstone 的时间戳晚于旧版本的真实数据时,就会在生成新的 SSTable 时彻底丢弃旧数据和墓碑记录,完成物理清除并回收磁盘空间。
在此基础上,候选人准备顺着面试官前面提到的要求,进一步探讨如何将这一套机制推演到多对象事务(multi-object transaction)的场景中。
[16:57](https://www.youtube.com/watch?v=cG5eyid8kpA&t=1017s) Write-Ahead Log (WAL) 与 MemTable 的写入流转
面试官梳理并可视化了候选人目前提出的写入流程:每当有新记录到来时,系统首先将其追加到磁盘上的 Write-Ahead Log(WAL,或称为 Commit Log)中。一旦 WAL 的数据完成落盘(flush),系统再将记录写入内存中的 MemTable。面试官以此为基准,向候选人抛出了一个具体的数据更新场景:假设先前写入了记录 R2,键为时间戳 `12345`,值为 `"mock interview"`;随后又写入了记录 R4,键同样是 `12345`,但值更新为了 `"cooking class"`。面对这种相同 Key 的更新操作,MemTable 应该如何处理?
```mermaid flowchart LR accTitle: WAL to MemTable write flow accDescr: Data flow from incoming record to write-ahead log on disk and then to in-memory memtable. subgraph Ingestion["Write Path"] A["Incoming Record<br/>(e.g., R4: key=12345)"] --> B["Append to WAL<br/>(Commit Log on Disk)"] B --> C["Flush to Disk"] C --> D["Write to MemTable<br/>(In-Memory)"] end ```
候选人最初提出 MemTable 本质上就是一个大哈希表(Hash Table),在更新时可以直接进行覆盖(overwrite)。他认为系统可以通过配置项来决定具体行为,例如设定总是覆盖旧值以保证获取到最新数据。随后候选人补充解释,他之所以最初假设可以直接追加,是因为在先前举例时 Key 使用了纳秒级时间戳(nanosecond timestamp),他预设了所有事件的 Key 都是全局唯一、不重复的。面试官随即指出,作为底层通用数据库的设计者,不能将“Key 必须是纳秒时间戳且永不重复”作为强行强加给业务开发者的硬性前置假设;业务方完全可以使用非唯一的业务键来执行常规更新。候选人认可了这一点,随之改变策略,提出放弃在内存中原地覆盖,而是将 MemTable 设计为一个巨大的数组或向量(huge array / vector),对所有写入始终执行追加(always append),后续重复 Key 的去重和更新处理则完全交由后台的 Compaction 守护进程(compaction daemon)来完成。
[21:33](https://www.youtube.com/watch?v=cG5eyid8kpA&t=1293s) MemTable 内存数据结构选型与边界约束
由于候选人提出了使用“大数组(Vector / Array)作为 MemTable”的方案,面试官要求其深入评估该内存数据结构的合理性与实现可行性。候选人解释说,可以预先在内存中分配一个巨大空间(例如 8 GB 的向量),维护一个内部计数器不断执行追加,一旦写入量达到设定的容量阈值,就将整个 MemTable 一次性 dump 到磁盘生成 SSTable。
面试官针对这一设想连续提出了关键质疑。首先是关于单条记录大小的限制(Key/Value size limitation):如果允许用户写入 8 GB 的数据,是否应该对单条 Record 设定上限?候选人起初认为不需要限制,系统可以单独起一个监控线程(thread)或者直接通过 Linux 的 `/proc/<pid>/status` 去检查进程的内存占用,一旦总内存达到阈值就触发落盘,因此无需限制 Record 数量。面试官立刻提醒,作为平台底层的架构师,如果完全不做边界约束,上游客户端可能会做出极其极端的行为,比如单个请求写入 200 GB 的超大 Key 或 Value,这会直接打崩系统。候选人随即修正说法,同意必须设置硬性阈值以规避极端情况,例如限制 Key 的最大尺寸为 64 MB。面试官将具体的尺寸参数暂定为 Key 上限 $X$ KB、Value 上限 $Y$ Bytes,随后转向了更为致命的读取性能问题:如果 MemTable 只是一个无序的纯追加 Array,并且内存中积压了高达 8 GB 的数据,当客户端发起点查(Point Query)请求检索单条记录时,如何在数组中高效查找?候选人意识到无序数组无法进行高效检索,只能推翻数组方案,重新回到 Hash Table 上,承认内存数据结构必须能够支持高效的查找操作。
- **写入边界防护**:必须对单条 Record 的 Key/Value 大小设置上限(例如 Key 最大 64 MB),防止上游恶意或异常写入耗尽系统资源。 - **内存监控方案**:候选人提出通过 Linux 系统的 proc 文件系统(如 `/proc/[pid]/status`)监控进程内存水位,到达阈值后触发 Dump。 - **数据结构权衡**:无序 Array 虽然追加写入成本为 $O(1)$,但点查退化为全表线性扫描($O(N)$),无法满足高性能 KV 存储的需求,因而被迫退回 Hash Table。
[26:42](https://www.youtube.com/watch?v=cG5eyid8kpA&t=1602s) 分层 SSTable 检索与读取合并(Read Merge)机制
讨论随即推进到系统的全链路点查与分层 SSTable 架构。面试官勾勒出经典的分层存储模型:最顶层是驻留在内存中的 MemTable,下方则是持久化在磁盘上的分层 Sorted String Table(如 Level 1 SSTable、Level 2 SSTable 等)。
```mermaid flowchart TD accTitle: Tiered KV Store Read Path accDescr: Lookup path starting from in-memory MemTable down to leveled SSTables on disk. subgraph Memory["In-Memory"] M["MemTable<br/>(Hash Table)"] end subgraph Disk["Disk Storage"] L1["Level 1 SSTable<br/>(On-Disk)"] L2["Level 2 SSTable<br/>(On-Disk)"] LN["Level... SSTable<br/>(On-Disk)"] end M -->|"Miss"| L1 L1 -->|"Miss"| L2 L2 -->|"Miss"| LN ```
面试官提出核心问题:当系统要查找一条记录(如 `R100`)时,如果该记录存在于内存的 Hash Table 中,可以以极快的速度命中并返回(两人围绕 Hash Table 的查询复杂度是 $O(1)$ 还是在特殊树形 Hash 实现下的 $O(\log N)$ 进行了简要讨论与澄清);但如果该记录在 MemTable 中未命中(Miss),系统就必须下潜到磁盘上的各层 SSTable 中去检索。由于不能在磁盘上进行代价高昂的全盘线性扫描,候选人指出必须为 SSTable 构建索引(Index),并将索引常驻于内存中,通过内存索引快速定位磁盘文件中的目标 Record 位置。
随后,面试官针对跨层数据的读取一致性提出了具体的场景追问:假设 `R100` 在系统不同层级中存在多个版本,例如在时间戳 4、时间戳 3 和时间戳 6 分别有写入记录,系统在读取时应该如何执行合并(Merge)?候选人对此提出,在执行读取操作时需要执行 Read Merge,按照版本对应的时间戳顺序,将变更像堆栈一样依次叠加(apply the change over the stack of different versions)。为了佐证该机制,候选人还类比了 Apache Hudi 的更新设计,说明通过保存记录的不同版本差量并在读取时进行多版本合并以计算出最终状态。
[31:21](https://www.youtube.com/watch?v=cG5eyid8kpA&t=1881s) Read Path and Full Record Storage Across SSTable Levels
The candidate and interviewer dig into the mechanics of handling updates, tombstones, and point lookups across different storage layers. The candidate explains that if a key was previously deleted with a tombstone at version 4, recreated at version 6, and recently modified in the MemTable, the latest version must supersede the older ones. The interviewer clarifies the read path mechanics, asking whether reading a record requires traversing every single SSTable level to perform "read consolidation and read repair." The candidate clarifies that full traversal is not necessary if the latest version is already sitting in the in-memory MemTable; the database can simply return it immediately.
```mermaid flowchart TD accTitle: LSM-Tree Point Read Path accDescr: Flowchart illustrating the lookup hierarchy from MemTable down through SSTable levels.
subgraph Memory["In-Memory Layer"] Q["Read Request (Key)"] --> M["Check MemTable"] end
subgraph Disk["On-Disk SSTable Levels"] L0["Level 0 SSTables"] L1["Level 1 SSTables"] LN["Level N SSTables"] end
M -- "Hit (Latest Version)" --> R["Return Record Immediately"] M -- "Miss" --> L0 L0 -- "Miss" --> L1 L1 -- "Miss" --> LN LN -- "Hit / Miss" --> R ```
Even when a lookup must fall back to disk, the candidate notes that the engine does not record incremental diffs or change deltas—it stores the "full record" snapshot at each write. Consequently, the query engine only needs to locate the newest timestamped version of that key rather than reading and reconciling every historical revision. Older superseded entries and purged tombstones are deferred to the background compaction process to be reclaimed asynchronously.
- MemTable lookups short-circuit disk reads when a fresh record or tombstone is present in memory. - Writes represent complete row snapshots rather than deltas, eliminating the need to consolidate multi-version diffs on typical transactional reads. - Historical versions and deleted records are consolidated later during background SSTable compaction.
---
[33:14](https://www.youtube.com/watch?v=cG5eyid8kpA&t=1994s) The Large Key Problem: YouTube URL View Count Scenario
The interviewer introduces a concrete use case: designing the database to store YouTube video view counts, where the key is the full YouTube URL (which can be quite long) and the value is a simple integer counter. To illustrate the skew, the interviewer establishes a scenario where out of 8 gigabytes of stored data, more than 7 gigabytes are consumed strictly by the keys. He asks the candidate what fundamental issue this creates for the database.
The candidate initially guesses that the primary challenge is high update frequency and hot partitions, identifying it as a "heavy hitter issue" where view counts constantly increment. The interviewer dismisses this, explaining that whether updates hit the same row or insert new rows, high write throughput is standard operating behavior for a database and not inherently fatal. Instead, he warns that with keys vastly outstripping values, the current design has a fundamental architectural defect that will cause severe degradation or crash the node entirely.
- **Key-to-Value Skew**: Long URL strings paired with 64-bit integer counters create a disproportionate 10:1 ratio where the vast majority of storage footprint is metadata/keys rather than payload values. - **Divergence of Concerns**: The candidate suspects high-throughput write contention ("heavy hitter"), whereas the interviewer directs focus toward memory allocation and index overhead.
---
[36:24](https://www.youtube.com/watch?v=cG5eyid8kpA&t=2184s) Memory Thrashing, Key-to-Value Ratios, and SSTable Sizing
Stepping in to move the discussion along, the interviewer explains the hardware-level bottleneck. Typical production servers operate with between 32 GB and 250 GB of RAM. In an LSM-tree-based engine, block indexes for SSTables must reside in memory to provide acceptable read latencies. When key sizes balloon, the in-memory index size explodes. If the index exceeds available physical memory, the operating system triggers continuous page faults and aggressive swap thrashing. The system ends up "basically just page swap, page swap always," freezing the database and rendering it unresponsive.
The candidate acknowledges this index overhead and suggests replacing raw YouTube URLs with compact hash keys to shrink the in-memory footprint, though he concedes that hashing introduces potential hash collisions. The conversation turns to real-world SSTable sizing: the interviewer notes that an 8 GB SSTable file is far too large. Production engines like RocksDB typically configure individual SSTable/SST files between 10 MB and 100 MB, or at most a few hundred megabytes, specifically to keep index structures lightweight and prevent memory exhaustion during lookups and compactions.
- Index memory footprint is directly proportional to key length; oversized keys exhaust RAM and trigger constant OS-level page swapping. - SSTables in engines like RocksDB are partitioned into 10 MB to 100 MB files to keep block indexes manageable. - Hashing URLs shrinks index entries but introduces collision handling overhead and alters lexical sort order.
---
[40:05](https://www.youtube.com/watch?v=cG5eyid8kpA&t=2405s) Real-World Precedents: Bigtable Reversed URLs and Key Size Limits
The discussion touches on database responsibilities versus application developer freedom. The interviewer stresses that an infrastructure engineer cannot simply assume application developers will behave nicely and use compact keys: "You don't really actually tell what application developer do... you should assume that application developer can do whatever they like." The candidate playfully pushes back, claiming the interviewer is "deliberately making up the question hard" because standard schema designs typically avoid massive primary key strings.
The interviewer counters that storing full URLs as database keys is not a contrived theoretical puzzle but an established, foundational pattern at Google: Google's Bigtable paper explicitly documents storing reversed domain URLs (e.g., `com.google.maps/index.html`) as row keys to naturally group subdomains and URL hierarchies together. To defend the system without banning long keys entirely, the interviewer points out that databases must enforce strict upper boundaries—such as a 1 KB key size ceiling—or provide internal key normalization to keep indices from overwhelming host memory.
- **Bigtable Precedent**: Google Bigtable uses reversed URLs as row keys to achieve lexical co-location of domain hierarchies on disk. - **Defensive Safeguards**: Databases must enforce hard constraints (such as a maximum key length of 1 KB) to prevent arbitrary application payloads from crashing storage node indexes.
---
[42:25](https://www.youtube.com/watch?v=cG5eyid8kpA&t=2545s) Point Lookup Misses on a Single Node and Read Amplification
The interviewer shifts to a classic retrieval edge case: querying for a record that does not exist in the database (such as searching for `record 501` when it has never been written). The candidate begins outlining a distributed coordinator architecture, explaining how the coordinator broadcasts range requests across multiple worker nodes, aggregates responses, and resolves quorums. The interviewer quickly reins him in, clarifying that distributed record discovery across thousands of nodes is a separate topic: "I'm talking about something very simple: you just give a record 501, you're looking for this single node."
Focusing strictly on a single node, the candidate realizes the penalty of a non-existent key. Without an early-exit mechanism, the node cannot confirm the absence of a record until it has checked the in-memory MemTable and then serially traversed down through every level of on-disk SSTables. Because `record 501` is not present in any block, the read path suffers maximum read amplification, triggering unnecessary disk I/O at every level before it can definitively return a "not found" response.
- Querying for non-existent records represents the worst-case read path in an LSM-tree if unmitigated. - The storage engine is forced to inspect the MemTable and search block indexes across every on-disk SSTable level before determining the key is absent.
[45:21](https://www.youtube.com/watch?v=cG5eyid8kpA&t=2721s) SST 文件检索性能优化与加载开销的争论
在探讨单机 LSM-Tree / SST (Sorted String Table) 的读取性能时,候选人注意到按层逐个排查 SST 文件会带来较高的延时,于是提议维护一个全局索引(Global Index)或者全局范围(Global Range)来加速查找。面试官立刻指出这一思路的硬伤:全局索引本身占用的空间可能非常庞大,甚至可能超过数据本身的体积,因此在内存中维护全量全局索引并不现实。既然无法建立全局索引,候选人转而提出利用 SST 文件内部有序(sorted string)的物理特性,直接通过二分查找(Binary Search)定位目标记录。
```mermaid flowchart TD accTitle: SST Loading Strategy Debate accDescr: Comparison between candidate's header offset approach and interviewer's full table loading view.
subgraph CandidateView["Candidate's View: Metadata & Offsets"] direction TB M1["Read SST Header / Metadata"] --> M2["Get Record Offsets & Ranges"] M2 --> M3["Direct Pinpoint Seek to Disk Block"] end
subgraph InterviewerView["Interviewer's View: Disk-to-Memory Penalty"] direction TB L1["SST Levels: L1 to L3"] --> L2["Load Entire Block / Table into RAM"] L2 --> L3["Scan Keys in Memory"] end ```
针对二分查找方案,面试官指出了主要的延迟瓶颈:磁盘到内存的加载过程(loading from disk to memory)成本极其高昂,其加载量与需要读取的索引大小成正比;如果 Key 本身很大,这个开销几乎与扫描整个 SST 文件相当。候选人坚决反对“需要将整个 SST 从磁盘载入内存”的看法。他辩称,既然单层 SST 内部是有序的,只需要读取文件头部第一条记录和尾部最后一条记录来确认范围区间,这种开销是非常轻微的("very trivial cost")。当面试官反问“你的意思是说我们不需要加载整个 SST?”并强调通常必须把各层(如 L1 到 L3)全盘读入时,候选人重申并不需要全量装载:可以在 SST 的头部存储元数据(Metadata),记录首尾边界、每个记录的偏移量(Offset)与长度,借助类似于分区路径(Partition Path)的机制直接精确定位(Pinpoint)具体的数据块。
- 候选人坚持从磁盘全量加载 SST 属于巨大的开销("loading all from disk to memory that is a huge cost"),主张通过文件头部的元数据记录偏移信息以实现局部 Seek。 - 面试官认为在实际大规模场景下,字典范围可能非常宽泛(例如从 A 到 Z),首尾边界检查并不能避免大量的磁盘 I/O。
---
[50:18](https://www.youtube.com/watch?v=cG5eyid8kpA&t=3018s) 布隆过滤器(Bloom Filter)的引入与应用场景澄清
面试官见双方在底层磁盘读取细节上僵持不下,主动建议跳过具体的 Seek 细节,并提出了业界的经典优化手段——布隆过滤器(Bloom Filter)。听到布隆过滤器,候选人起初有些疑虑,认为客户端发起请求是为了读取完整的记录数据("get the whole record"),而布隆过滤器只能判断记录是否存在,无法返回具体内容。
面试官随即阐明了布隆过滤器的核心价值:在多层 SST 结构中,如果布隆过滤器判定某个 Key 在某层甚至整个存储中根本不存在(Negative),系统就可以彻底跳过昂贵的磁盘查找与内存表检索,从而节省大量无谓的 I/O。“如果不存在,你直接就知道它不存在了,甚至不需要去查 MemTable/SST”,候选人豁然开朗,认同如果仅为了快速排除不存在的记录、避免无效加载,布隆过滤器确实是最优解("Bloom filter is the best solution")。
---
[51:10](https://www.youtube.com/watch?v=cG5eyid8kpA&t=3070s) 复合主键(Composite Primary Key)的转换与结构定义
在确认单机读、写、删除与 SST 流程已基本闭环后,面试官将讨论推进到更高层级的 Schema 设计:分布式 KV 存储如何支持复合主键(Composite Primary Key)。面试官以 Yelp 类本地生活点评应用为例,给出了一个多字段组成的 Key 定义:
| 字段名 | 推荐数据类型 | 概念层级角色 | |:--- |:--- |:--- | | **ZIP Code** | String | 分区键(Partition Key)候选 | | **User ID** | UUID | 用户唯一标识 | | **Activity ID** | UUID | 业务活动/行为标识 | | **Timestamp** | Timestamp | 时间戳 / 排序标识 |
```mermaid flowchart LR accTitle: Composite Key Decomposition accDescr: Composite key split into partition key and clustering/sort columns.
subgraph CompositeKey["Composite Key"] Z["ZIP Code (String)"] U["User ID (UUID)"] A["Activity ID (UUID)"] T["Timestamp"] end
CompositeKey --> PK["Routing / Partition Key\n(ZIP Code + User ID)"] CompositeKey --> CK["Clustering / Local Sort Key\n(Activity ID + Timestamp)"] ```
面试官抛出核心问题:在分布式底层,系统应该如何把这组包含地理位置、用户标识、活动和时间戳的复合字段内部转换并映射为存储引擎可处理的 Key?候选人分析指出,首先必须确定分区键(Partition Key)以决定数据落在哪个节点;鉴于邮编(ZIP Code)在业务上数量相对固定,而用户的 UUID 范围非常广,候选人提议将 ZIP Code 与 User ID 组合起来作为路由依据。
---
[54:24](https://www.youtube.com/watch?v=cG5eyid8kpA&t=3264s) 路由分片与物理网络拓扑抽象的误区
在讨论如何把复合主键路由到物理节点时,候选人提出了一个多级分片构想:假设集群有 24 到 26 台机器,可以通过 User UUID 的首字母映射到具体节点;更进一步,候选人试图将复合主键中的不同字段分别映射到物理基础设施的各个层级——将某个字段用于选择数据中心(Data Center),将 Activity UUID 用于映射服务器机架(Rack),再用时间戳定位具体服务器内的分片。
面试官对此提出了强烈质疑,指出这种设计混淆了逻辑抽象与物理网络拓扑。面试官强调,在现代互联网架构中,存储引擎的上层逻辑根本不应该、也不可能硬编码感知服务器位于哪个具体的机架(Rack);内部通常使用 DNS(如根据机器名 `adhoc1.sfo` 解析到 IP 地址),机器随时可能下线或漂移更名,在业务逻辑 Key 中绑定物理机房与机架层级信息完全是过度暴露("too much information for server to know which rack")。
```mermaid flowchart TD accTitle: Coordinator Routing Flow accDescr: Client connects to coordinator gateway which routes request via partition key.
C["Client"] -->|Writes Record| G["Coordinator / Gateway\n(Known IP / DNS)"] subgraph Cluster["Distributed Cluster Topology"] G -->|Dynamic Snitch / Network Status| DC["Target Data Center"] DC --> R["Target Rack"] R --> N["Target Node / Machine"] end ```
候选人随即澄清并修正了自己的表述:客户端在发起写请求时,仅需连接协调节点(Coordinator Gateway),客户端本身并不需要知晓物理节点的拓扑细节;真正负责将分区键映射到机房、机架和物理节点的是协调节点。候选人举例说明,这类似于 Apache Cassandra 内部的机制,通过名为 Snitch(网络嗅探器)的组件动态探测网络延迟和机架拓扑,从而自动决定最快、最合理的物理写入副本位置,而业务数据本身只需要依赖哈希分区键来确定数据归属。
[58:38](https://www.youtube.com/watch?v=cG5eyid8kpA&t=3518s) Data Modeling: Composite Keys and Wide-Column Storage
The discussion begins with the interviewer clarifying an earlier architectural detail about data placement and key representations. Rather than getting bogged down in which physical directory, rack, or data center a record resides in, the interviewer poses a more fundamental schema question: how does the system take a composite key and convert it into a hashmap key? The candidate tries to frame the structure around a row-oriented versus wide-column model, explaining that at the root level there is a primary identifier—such as a `zip_code` concatenated with a `user_id`. Underneath this primary row key, multiple activities and timestamp attributes are maintained in what resembles a wide-column or column-family layout.
```mermaid flowchart TD accTitle: Composite Key to Storage Hierarchy accDescr: Mapping composite row keys and nested column family attributes into memory storage.
subgraph RowKey["Row Key"] K["Composite Key<br/>(Zip Code + User ID)"] end
subgraph ColFamily["Column Family / Values"] A1["Activity A + Timestamp 1"] A2["Activity B + Timestamp 2"] A3["Activity C + Timestamp 3"] end
K --> A1 K --> A2 K --> A3 ```
The interviewer steps in to ground the terminology, noting that the column family is essentially the value component inside the underlying key-value pair. The candidate concedes that they had gotten somewhat turned around on the database modeling terminology: "I think I got confused here, some concept get confused." Acknowledging that only about fifteen minutes remain in the interview session, the interviewer moves the conversation toward transactional isolation levels and system requirements.
[01:01:14](https://www.youtube.com/watch?v=cG5eyid8kpA&t=3674s) Concurrency Control via MVCC and Snapshot Isolation
The interviewer highlights a core functional requirement: reads must not block writes, and writes must not block reads. The candidate immediately identifies that this demands Multi-Version Concurrency Control (MVCC) to prevent dirty reads and uncommitted writes. When asked to walk through an end-to-end example, the candidate explains that incoming writes create a dedicated snapshot or copy for the mutating thread, while concurrent read threads continue serving queries against the established base version of the record.
```mermaid flowchart LR accTitle: MVCC Read and Write Concurrency Flow accDescr: Concurrent read and write threads accessing committed records versus uncommitted write snapshots.
subgraph Storage["Storage Engine (MVCC)"] V1["Record v1<br/>Commit: T10 (Visible)"] V2["Record v2<br/>Commit: T20 (Visible)"] Draft["Record v3<br/>UID Tag / In-flight (Invisible)"] end
Reader["Read Thread<br/>(Reads snapshot <= T25)"] -->|Sees committed| V2 Writer["Write Thread<br/>(Txn ID: UID_99)"] -->|Writes draft| Draft ```
The interviewer probes deeper into how the storage engine manages visibility between active snapshots and committed data. The candidate explains that records track commit timestamps (often paired with unique transaction IDs or UUIDs): - **Commit Timestamps:** Each committed version stores a canonical commit time. - **Transaction Visibility Tagging:** While a transaction is actively being written, it carries a unique transaction identifier or tag that designates it as uncommitted. - **Visibility Boundaries:** If a record's commit timestamp precedes the read snapshot timestamp, the record is visible to that reader; if the timestamp is in the future or tied to an ongoing uncommitted transaction, it is hidden from external readers. - **Materialization:** Once a writing transaction successfully commits, the ephemeral transaction tag is cleared, making the new state visible to subsequent read operations.
[01:05:40](https://www.youtube.com/watch?v=cG5eyid8kpA&t=3940s) Transaction Boundaries and Database Snapshot Dumps
The interviewer then turns to transaction scope: "How would you like to handle like single object or multi object transaction?" The candidate openly acknowledges a gap in their preparation, admitting, "I still haven't read about this part... familiar, not familiar... I still haven't read about this part." After a period of quiet contemplation and note-taking around optimistic transaction control and recovery mechanisms, the interviewer decides to pivot rather than letting time slip away.
With roughly five minutes left, the interviewer asks how the system would dump a full database snapshot for backups or point-in-time recovery. The candidate again confesses unfamiliarity with the specific mechanics of snapshot dumping in this architecture ("No idea, no idea"). The interviewer reassures them ("That's fine, let's look at the last few requirements") and shifts focus to storage engine performance trade-offs.
[01:12:04](https://www.youtube.com/watch?v=cG5eyid8kpA&t=4324s) Engine Performance Trade-offs: LSM-Tree vs. B-Tree
The interviewer asks the candidate to contrast their proposed append-heavy log-structured engine (LSM-tree style) against a traditional B-Tree under varied read and write access patterns. The candidate explains that B-Trees typically excel at arbitrary point reads, but suffer severely on writes because insertions cause random disk I/O, node page splits, and tree rebalancing: "The read for the B-Tree will be definitely faster, but the write of the B-Tree will be much slower than this design."
The interviewer and candidate break down nuances across specific query patterns: - **Append and Write Throughput:** The proposed append-only design delivers near-optimal write throughput and high availability because writes sequentially stream to a write-ahead log and in-memory structure without immediate page rewriting. - **Recent Data Lookups:** For freshly written keys, the proposed engine is actually faster than a B-Tree because the data resides directly in the in-memory MemTable, bypassing disk lookups entirely. - **Compaction and Deletion Penalties:** If workloads contain numerous updates, upserts, or tombstones, the LSM design must search through multiple SSTables and run background compactions, dragging down read latency unless Bloom filters and compact files mitigate the overhead.
[01:15:10](https://www.youtube.com/watch?v=cG5eyid8kpA&t=4510s) Horizontal Scalability via Consistent Hashing
For the final technical topic, the interviewer inquires how the key-value store scales horizontally across a growing fleet of machines. The candidate proposes consistent hashing as the partitioning and distribution mechanism across cluster nodes. This setup offers predictable, near-linear capacity expansion: adding a new server node to the ring requires migrating only an isolated subset of key ranges from adjacent nodes rather than rehashing the entire keyspace.
The interviewer agrees that consistent hashing is standard, but nudges the candidate to consider operational assumptions: in a controlled enterprise data center, physical nodes do not churn, join, and disconnect as erratically as peers in a peer-to-peer network like BitTorrent or Chord. The topology remains comparatively stable, meaning cluster scaling events are deliberate, controlled administrative operations rather than volatile churn.
[01:17:18](https://www.youtube.com/watch?v=cG5eyid8kpA&t=4638s) Dynamic Network Node Churn and Data Migration Overhead
The candidate and interviewer evaluate how the key-value store architecture behaves under dynamic cluster conditions where nodes frequently join and depart ("the node come that go very fast"). Despite operating in a "very dynamic network," the system must preserve throughput and maintain low latency. The candidate pauses to evaluate the operational boundaries, asking for a brief moment to review the scheme: "give me 10 second let me see... I don't see a issue here." The analysis quickly turns to node membership changes and how partition data is discovered and migrated.
The central consideration is that migration overhead and record lookup complexity scale alongside cluster growth, remaining "proportional to the node number." When nodes churn, the cluster must balance partition reassignments, record discovery, and data replication without saturating internal network bandwidth or stalling live client read and write operations.
- **Dynamic network tolerance**: Ensuring consistent key-value operations even when nodes join and fail in rapid succession. - **Migration cost proportionality**: Data discovery and partition migration overhead remain proportional to total node count. - **Discovery stability**: Maintaining accurate record discovery paths during active cluster rebalancing.
[01:21:22](https://www.youtube.com/watch?v=cG5eyid8kpA&t=4882s) Distributed Transactions and Isolation Level Requirements
The discussion turns to transactions in a distributed storage environment, acknowledging that distributed transactional consistency is notoriously "hard." Rather than attempting to support heavyweight transactional guarantees blindly, the interviewer steers the focus back toward the specific "requirements of system." Understanding client expectations dictates which database "isolation level" (such as read committed, repeatable read, or snapshot isolation) the architecture must actually enforce.
The discussion also highlights the trade-offs of the system's design envelope ("envelope is a good point"). The candidate and interviewer contrast local replication mechanisms with distributed transaction processing. When operations cross partition boundaries, ensuring that transactions are safely committed across multiple nodes introduces serialization delays and complex failure-handling paths.
- **System-driven isolation levels**: Aligning transactional isolation guarantees with concrete application requirements rather than defaulting to full serializability. - **Replication vs. distributed transactions**: Balancing high-availability data replication against the coordination overhead of transactional atomicity. - **Cross-partition scope**: Managing the complexity and lock management required when transaction boundaries span multiple shards or storage topics.
[01:25:35](https://www.youtube.com/watch?v=cG5eyid8kpA&t=5135s) Single vs. Multi-Object Transactions, Two-Phase Commit, and Wrap-Up
The session closes with an in-depth look at single-object versus multiple-object transactions. Handling a single object is relatively straightforward because the mutation can be isolated and committed atomically on the hosting partition or replica set. However, operations involving multiple objects across disparate partitions ("sorry multiple object... associate impact object") require cross-node coordination. For multi-object cross-partition transactions, the system must coordinate via protocols like Two-Phase Commit (2PC, "two commit"), which amplifies latency, resource contention, and lock duration across all associated objects.
```mermaid flowchart TD accTitle: Single vs Multiple Object Transaction Handling accDescr: Flowchart contrasting the execution path of single-object transactions against distributed multi-object transactions using two-phase commit. subgraph Input["Request Evaluation"] Req["Transaction Request"] --> Check{"Single or Multi-Object?"} end subgraph Single["Single Object Path"] Check -->|"Single Object"| Local["Local Partition Execution"] Local --> FastCommit["Atomic Commit & Return"] end subgraph Multi["Multi-Object Path"] Check -->|"Multiple Objects"| Coord["Transaction Coordinator"] Coord --> Phase1["Phase 1: Prepare Across Shards"] Phase1 --> Phase2["Phase 2: Two-Phase Commit (2PC)"] end ```
With the core architectural trade-offs explored, the interview wraps up in a cordial atmosphere. The candidate and interviewer exchange mutual thanks ("thank you Scott", "thank you everybody"), concluding the mock system design session with a round of goodbyes.
- **Single-object atomicity**: Confined to local nodes with low latency and minimal locking. - **Multi-object distributed execution**: Relies on multi-phase protocols such as Two-Phase Commit (2PC) to maintain consistency across shards. - **Session conclusion**: Final appreciative sign-offs between interviewer, candidate, and listeners.
## Notable quotes > [36:24](https://www.youtube.com/watch?v=cG5eyid8kpA&t=2184s) basically just page swap, page swap always — Scott Shi
> [40:05](https://www.youtube.com/watch?v=cG5eyid8kpA&t=2405s) You don't really actually tell what application developer do... you should assume that application developer can do whatever they like. — Scott Shi
> [40:05](https://www.youtube.com/watch?v=cG5eyid8kpA&t=2405s) deliberately making up the question hard — Candidate
> [42:25](https://www.youtube.com/watch?v=cG5eyid8kpA&t=2545s) I'm talking about something very simple: you just give a record 501, you're looking for this single node. — Scott Shi
> [45:21](https://www.youtube.com/watch?v=cG5eyid8kpA&t=2721s) loading all from disk to memory that is a huge cost — Candidate
> [50:18](https://www.youtube.com/watch?v=cG5eyid8kpA&t=3018s) Bloom filter is the best solution — Candidate
> [54:24](https://www.youtube.com/watch?v=cG5eyid8kpA&t=3264s) too much information for server to know which rack — Scott Shi
> [58:38](https://www.youtube.com/watch?v=cG5eyid8kpA&t=3518s) I think I got confused here, some concept get confused. — Candidate
> [01:05:40](https://www.youtube.com/watch?v=cG5eyid8kpA&t=3940s) I still haven't read about this part... familiar, not familiar... I still haven't read about this part. — Candidate
> [01:05:40](https://www.youtube.com/watch?v=cG5eyid8kpA&t=3940s) No idea, no idea — Candidate
> [01:12:04](https://www.youtube.com/watch?v=cG5eyid8kpA&t=4324s) The read for the B-Tree will be definitely faster, but the write of the B-Tree will be much slower than this design. — Candidate
> [01:17:18](https://www.youtube.com/watch?v=cG5eyid8kpA&t=4638s) give me 10 second let me see... I don't see a issue here. — Candidate
## Mentioned Scott Shi, Apache Cassandra, RocksDB, LSM-Tree, Google Bigtable, Kafka, Uber, Zoom, Apache Hudi, YouTube, Yelp, BitTorrent, Chord
## Video description In this mock interview session, we discuss about design a KV store.
doc could be found here: https://docs.google.com/document/d/13P7paQ8niP-aHALKIK51tePZgsFg_4g8OHm_Ig-OGMg/edit?usp=sharing
[Source](https://www.youtube.com/watch?v=cG5eyid8kpA)