
有网友提问 面试最看重系统设计能力 但日常工作比较零散 很难接触系统层面 不知如何开始学习
如果说要从零设计 又怕被说好高骛远
我也曾遇到同样问题 刚开始处理测试问题 负责运维故障定位
但这后来成为优势 能接触到不同模块 更能理解系统全貌 为全栈优化打基础
全栈优化不是简单叠加模块 而是重新设计各层交互 实现整体大于部分之和 达成系统性突破
正如阿姆达尔定律所说 只优化一部分效果有限 系统总被最慢环节限制 必须全面升级才能突破
解决办法:
没有机会设计不会看吗?论文都是免费公开的,从看论文开始

这篇题为《What Modern NVMe Storage Can Do, And How To Exploit It: High-Performance I/O for High-Performance Storage Engines》
论文作者与署名信息
作者:Viktor Leis、Tobias Ziegler、Thomas Neumann、Alfons Kemper
收录与年份:VLDB 2023
主题聚焦:在现代 NVMe SSD 上,如何通过 I/O 路径与存储引擎协同设计,充分释放设备的高并发、低时延与高带宽能力2。
作者背景与研究方向
Viktor Leis
研究方向:数据库系统、存储引擎、NVMe/PCIe 性能优化、查询执行与并发控制。
代表性工作:与团队长期深耕 NVMe 场景,参与 LeanStore 系列工作(如 vldb24: LeanStore: A High-Performance Storage Engine for NVMe SSDs),聚焦在 NVMe 上的高效 I/O 与系统可扩展性2。
Tobias Ziegler
研究方向:NVMe 存储系统、文件系统/块层交互、高性能 I/O 路径设计。
代表性工作:围绕 直接附着 NVMe 阵列 与 I/O 栈优化 的系统性研究与工程实践,持续推动 NVMe 在数据库负载中的极限利用2。
Thomas Neumann
研究方向:数据库内核、索引结构(如 B-Tree/LSM 变体)、查询优化与执行模型。
代表性工作:与团队提出面向现代存储的 TreeLine(vldb23: TreeLine: an update-in-place key-value store for modern storage),强调在新型介质与 NVMe 特性下的结构与访问路径协同优化2。
Alfons Kemper
研究方向:数据库系统、存储与索引设计、高性能事务处理。
代表性工作:长期指导与参与 LeanStore 等面向 NVMe 的高性能存储引擎研究,关注在 超出内存(out-of-memory) 场景下的吞吐与可扩展性突破
问题编号 | 研究问题 | 论文给出的核心解答与发现 |
|---|---|---|
Q1 | NVMe阵列能否达到硬件标称的性能? | 可以,甚至能超越。 实验证实,8块NVMe SSD组成的阵列能够实现1250万次/秒的随机读取IOPS,超过了单个硬盘标称性能的简单叠加。 |
Q2 | 应该使用哪种I/O API?是否需要内核旁路(如SPDK)? | 1. 所有异步接口(libaio, io_uring, SPDK)都能实现高吞吐。2. 内核旁路(SPDK)在CPU效率上具有绝对优势,但io_uring在轮询模式下也能接近其性能。对于追求极致效率的场景,SPDK是最佳选择。 |
Q3 | 存储引擎应使用多大的页大小,才能在获得高性能的同时最小化I/O放大? | 4 KB是最佳权衡点。 这是NVMe SSD随机读取性能的“甜点”,能同时优化IOPS、带宽和延迟。小于4KB会因硬件限制导致性能下降,大于4KB则会造成严重的I/O放大。 |
Q4 | 如何管理实现高SSD吞吐所需的高并发度? | 必须采用用户态协作式多任务(轻量级线程)。 传统“一个查询一个内核线程”的模型会导致数千线程的过度订阅和巨大开销。论文通过用户态任务调度,使少量工作线程能高效管理海量并发的I/O请求。 |
Q5 | 如何让存储引擎足够快,以管理每秒数千万的IOPS? | 内存外(Out-of-Memory)的代码路径必须进行深度优化和并行化。 这包括:采用分区锁消除热点、优化淘汰算法(如引入乐观父指针)、移除内存分配、以及微调热代码路径,确保CPU不会成为瓶颈。 |
Q6 | I/O应由专用的I/O线程执行,还是由每个工作线程执行? | 应由工作线程直接执行(All-to-All模型)。 论文否定了专用I/O线程或SSD绑定的模型,采用对称设计:每个工作线程拥有通往所有SSD的独立I/O通道,无需线程间通信。这简化了设计,并实现了最佳的可扩展性。 |
原文:
Q1
Arrays of NVMe SSDs can achieve the performance promised in hardware specifications.
In fact, we achieved slightly higher throughput at 12.5 M IOPS with our 8×SSD setup.
Q2 Good performance can be achieved with all asynchronous I/O interfaces. ?
Kernel-bypassing is not essential to achieve full bandwidth even with small pages. However, it is more efficient in CPU usage.
Q3 The best trade-off between random IOPS, throughput, latency, and I/O amplification is achieved with 4 KB pages.
Q4 To manage the high parallelism required for large NVMe SSD arrays, the database system must employ a low overhead mechanism to quickly jump between user queries.
To solve that we employed cooperative multitasking using lightweight user-space threads.
Q5 Managing workloads with tens of million IOPS makes out-of-memory code paths hot and performance critical.
This requires scalable I/O management through partitioning relevant data structures to prevent contention hotspots. The replacement algorithm has to be optimized to evict tens of millions of pages per second.
Q6 I/O should be performed directly by worker threads. In our design worker threads in fact perform all duties, like, in-memory work, eviction, and I/O. This symmetric design has conceptual advantages and allows for a more robust system.
翻译:
Q1 NVMe SSD阵列能够实现硬件规格中承诺的性能 实际上,我们通过8块SSD的配置 实现了略高于1250万IOPS的吞吐量
Q2 所有异步I/O接口都能实现良好性能 即使使用小页面 内核旁路也并非实现全带宽的必要条件 但其在CPU使用效率方面更具优势
Q3 4KB页面在随机IOPS、吞吐量 延迟和I/O放大效应之间 实现了最佳平衡
Q4 为管理大型NVMe SSD阵列所需的高并行度 数据库系统必须采用低开销机制 以快速切换用户查询 我们通过轻量级用户空间线程 实现协同多任务处理来解决这一问题
Q5 处理数千万IOPS的工作负载 会使内存外代码路径成为性能关键热点 这需要通过分区相关数据结构 来实现可扩展的I/O管理 以防止争用热点 替换算法必须优化至 每秒可淘汰数千万个页面
Q6 I/O操作应由工作线程直接执行 在我们的设计中 工作线程实际承担所有职责 包括内存工作、页面淘汰和I/O 这种对称设计具有概念优势 并能构建更稳健的系统

I/O has long been considered slow. 长久以来,I/O一直被认为是慢的。
As a result, in most systems, 因此多数系统中,
the I/O path is less optimized I/O路径的优化往往
than the in-memory components. 不如内存部分充分。
Once the system can schedule 一旦系统能调度
millions of IOPS, 数百万IOPS,
the CPU often becomes CPU反而常常
the performance bottleneck. 成为性能瓶颈。
想象一个高速收费站: 旧系统(硬盘):车流稀疏 → 收费站空闲 新系统(NVMe):车流如潮 → 收费站拥堵
具体开销: ├─ 每个I/O请求需要~1000-5000 CPU周期 ├─ 上下文切换(用户态↔内核态) ├─ 中断处理开销 ├─ 内存拷贝操作 └─ 锁竞争与同步
在原始LeanStore设计中 使用一个全局锁 保护I/O操作和冷却阶段 防止并发访问冲突
原论文认为此锁不构成瓶颈
I/O操作耗时仍远高于锁获取
然而面对现代NVMe SSD 这个全局锁迅速演变为性能瓶颈 原因解析: NVMe将I/O延迟降至微秒级 每秒可处理千万级请求 原设计假设被彻底颠覆
锁开销相对比例剧增:
•
单个I/O:锁耗时占比微不足道
•
千万级IOPS:锁操作累积成巨大开销
•
多核并发:全局锁引发严重竞争
Page eviction is crucial. Database systems are not only running user queries, but also performance-critical tasks such as page evic- tion
先看三幅图
图7(问题)→ 图8(整体架构)→ 图10(关键实现)
特性 | 图7(问题) | 图8(架构) | 图10(实现) |
|---|---|---|---|
核心内容 | 定义性能目标与矛盾:高并发I/O需求 vs. 有限CPU核心。 | 提出系统级解决方案:用协作式多任务集成I/O处理,消除专用I/O线程。 | 选择最优I/O连接模型:全对全模型,实现无锁、直接的硬件访问。 |
I/O读写角色 | 指出I/O读写必须达到高并发(>1000未完成请求) 才能利用SSD性能。 | 规定I/O读写是工作线程的核心职责之一,与任务执行、页面淘汰集成。 | 规定I/O读写通过工作线程专属的I/O通道直接进行,无需线程间协调。 |
关键贡献 | 揭示了传统同步I/O/多线程架构的根本性不足(线程过度订阅)。 | 提供了管理海量I/O请求的CPU高效方法(用户空间任务,极低切换开销)。 | 提供了与SSD硬件通信的最高效路径(全对全模型,消除通信开销)。 |
关系 | 为什么需要新设计(传统方法无法满足并行性要求)。 | 整体上如何做(用协作式多任务架构管理I/O)。 | 具体如何做(用全对全I/O模型实现高效硬件交互)。 |
因此,I/O读写的区别在于:
•
传统方式
•
:每个查询一个OS线程,使用同步阻塞I/O(如pread)。
•
线程在I/O时被阻塞,需要数千个线程来产生足够并发I/O,导致极端过度订阅和巨大上下文切换开销。
•
LeanStore方式(图7+8+10):
•
少量工作线程运行异步非阻塞I/O。
•
每个线程管理大量用户空间任务,任务在I/O时让出CPU;
•
线程通过专属I/O通道直接、异步地向所有SSD提交和轮询I/O。
•
这实现了用少量CPU核心高效管理海量并发I/O,最终达到硬件极限性能。

Figure 7不是展示我们做了什么,而是展示我们需要解决什么问题
这张图的核心是说明一个高性能存储引擎必须同时有效管理以下三种并行性:
1
请求级并行性(3.2章节)
•
含义:系统需要同时处理大量并发的用户查询。
•
挑战:在内存外(out-of-memory)场景下,每个查询都可能触发多个I/O操作。为了保持高吞吐量,系统必须能够同时管理成千上万个并发的I/O请求。
The problem with high oversubscription. Most database systems run queries in independent threads and use synchronous I/O requests (i.e., pread) to handle page faults.
In this design, to keep the SSDs busy there must be more than 1000 threads running simultaneously for user requests only
2
CPU级并行性
•
含义:现代服务器拥有数十至数百个CPU核心。
•
挑战:软件必须能够有效地利用所有这些计算核心来处理请求、管理I/O和运行后台任务(如页面淘汰)。传统的基于操作系统线程的模型(每个查询一个线程)会导致极端过度订阅,因为需要数千个线程来保持SSD繁忙,但核心数远少于线程数,从而产生巨大的上下文切换开销。
Page eviction is crucial. Database systems are not only running user queries, but also performance-critical tasks such as page eviction. Once the system runs out of free pages because the page replacement algorithm is too slow, the system will stall.
核心问题
•
数据库系统不仅运行用户查询,还执行性能关键的后台任务,如页面淘汰。
•
如果页面替换算法太慢,导致系统耗尽空闲页面,系统就会停滞。
•
同样,如果脏页不能足够快地写出,系统也会停滞。 传统方法的缺点
•
在原始的LeanStore版本中,页面淘汰和脏页写入由称为 “页面提供者” 的专用后台线程批量处理。
•
为了达到每秒数百万次页面淘汰(及相应的I/O操作),需要多个这样的后台线程。
•
缺点:很难确定需要多少后台线程,尤其是在工作负载变化时。这增加了系统配置和管理的复杂性。
3.3 节的核心思想是打破“前台工作线程”和“后台线程”的严格分离。 通过将页面淘汰等关键后台工作作为系统任务集成到工作线程的协作式多任务调度循环中, 系统实现了:
•
更简单的架构:无需管理和调整专用后台线程的数量。
•
自适应的资源分配:CPU时间在用户查询和后台工作之间动态平衡。
•
更高的效率:减少了线程间协调和上下文切换的开销。
这种设计是使系统能够高效管理每秒数千万次I/O操作所需的、极高的页面淘汰率的关键。
1
SSD级并行性
•
含义:为了充分利用NVMe SSD的内部并行性(多个通道和闪存芯片),需要保持超过1000个未完成的I/O请求(高I/O深度)才能使SSD阵列保持繁忙。
•
挑战:系统必须能够高效地调度和管理如此大量的并发I/O操作,并确保它们被均匀地分发到所有SSD上。
图7揭示了现代硬件与软件设计之间的根本矛盾:
•
硬件能力:SSD阵列需要极高并发度(>1000个未完成I/O)才能达到峰值性能。
•
传统软件模型:使用同步阻塞I/O(如pread)的数据库系统,每个查询线程在遇到页错误时会被阻塞,直到I/O完成。
•
导致的后果:为了产生足够的并发I/O来喂饱SSD,系统需要运行数千个查询线程。但这远远超过了CPU核心数(通常只有几十到上百个),导致极端的线程过度订阅,CPU时间大量浪费在操作系统线程的上下文切换上,而非实际工作。
图7提出的挑战直接引出了后续图8和图10的设计方案:
1
解决请求级与CPU级并行性的矛盾(图8的核心):
•
采用用户空间协作式多任务处理。
•
系统只启动与CPU核心数相等的工作线程。
•
每个工作线程内部运行一个DBMS内部调度器,管理成千上万个轻量级用户空间任务(协作式线程)。
•
优势:任务切换成本极低(~20 CPU周期),避免了操作系统线程上下文切换的数千周期开销。这使得系统能够用有限的核心高效管理海量并发请求。
2
解决高并发I/O需求(图10的核心):
•
采用异步非阻塞I/O接口(如libaio、io_uring或SPDK)。
•
当用户空间任务遇到页错误时,它异步提交I/O请求并让出CPU,而不是阻塞整个线程。
•
工作线程随后可以处理其他任务或轮询I/O完成事件。
•
优势:单个线程可以同时管理数百个未完成的I/O请求,从而用少量线程即可满足SSD对高I/O深度的需求。
3
连接工作线程与SSD(图10的模型选择):
•
采用全对全I/O模型(All-to-All)。
•
每个工作线程拥有独立的I/O通道,可以直接访问所有SSD,无需线程间消息传递或同步。
•
优势:消除了专用I/O线程或SSD分配模型中的通信开销,实现了对称、高效、可扩展的I/O提交。

图8
原文: Figure 8: Design overview. The system is handling many incoming requests in parallel. Worker threads are running these requests cooperatively as tasks, while also taking care of page eviction, I/O submission, and polling
工作线程是系统执行的核心单元,采用用户空间协作式多任务模型。
1
启动与数量:系统启动与硬件核心数量相等的工作线程。
2
任务调度:每个工作线程内部运行一个DBMS内部调度器,负责执行轻量级的用户空间任务(使用Boost Context库实现)。
3
职责集成:每个工作线程身兼多职,不仅执行用户查询任务,还负责处理页面淘汰、I/O提交和轮询完成事件。
4
协作式执行流程:
•
当用户任务遇到页错误时,会提交异步I/O请求,并将任务状态设置为“等待I/O”,然后主动让出(yield) CPU控制权给调度器。
•
调度器随后会处理I/O提交、页面淘汰和轮询I/O完成事件。
•
当I/O完成时,I/O后端会回调工作线程,工作线程将对应的任务状态设置为“I/O完成”,并将其重新放入就绪队列,等待调度器恢复执行。
I/O后端的工作原理
I/O后端是一个软件抽象层,负责管理与底层NVMe SSD硬件的所有通信。
1
统一抽象:它封装了不同异步I/O库(libaio、io_uring、SPDK)的实现细节,为上层(工作线程)提供统一的接口。
2
内部RAID 0条带化:在DBMS内部实现数据条带化,将多个SSD在逻辑上组合成一个设备,以提高性能和容量。
3
I/O通道模型(全对全模型):
•
每个工作线程拥有一个独立的I/O通道,该通道管理着到所有SSD的直接连接(如NVMe队列对)。
•
这种全对全模型意味着无需任何线程间消息传递或同步。工作线程可以直接向任何SSD提交I/O请求。
4
请求处理流程:
•
接收来自工作线程的异步I/O请求。
•
将请求放入对应的NVMe提交队列。

•
定期轮询(或通过中断接收通知)NVMe完成队列,获取已完成的I/O操作。
•
触发回调函数,通知相应的工作线程I/O已完成。
不共享,但每个工作线程拥有自己独立的I/O通道。
•
设计模型:系统采用全对全模型。
•
具体实现:每个工作线程都拥有一个独立的I/O通道,该通道是I/O后端抽象的一部分,负责管理该线程到所有SSD的直接连接(如NVMe队列对)。
•
关键优势:这种设计无需线程间消息传递或同步。
•
每个工作线程都可以直接、独立地向任何SSD提交I/O请求,从而实现了高效、无锁的并行I/O访问。
结论:I/O后端是一个软件抽象层,它为每个工作线程提供了独立的I/O通道。 工作线程不共享同一个I/O后端实例,而是每个线程通过自己独立的通道与所有SSD通信。
不是。I/O后端不是协程,而是一个管理硬件通信的软件抽象层。
•
I/O后端的本质:
•
它是一个库或模块,封装了与底层NVMe SSD硬件通信的细节。
•
它支持多种异步I/O库(如libaio、io_uring、SPDK),
•
并为上层(工作线程)提供统一的、高性能的I/O接口。
1
每个工作线程拥有独立的I/O通道
•
在 全对全模型 中,每个工作线程都拥有一个独立的 I/O通道。
•
这个I/O通道是I/O后端抽象的一部分,它封装了与所有SSD的直接连接(如NVMe队列对)。
•
绑定方式:工作线程在初始化时,会向I/O后端申请并建立自己的I/O通道。这个通道专属于该线程,无需与其他线程共享或同步。
2
工作线程直接通过其I/O通道执行I/O
•
当工作线程中的用户任务需要执行I/O(如发生页错误)时,它直接调用自己I/O通道的接口来提交异步I/O请求。
•
提交后,任务让出CPU,工作线程的调度器继续执行其他任务或进行页面淘汰。
•
工作线程定期轮询自己I/O通道的完成队列,检查I/O是否完成。
•
完成后,I/O后端通过回调机制通知该工作线程,工作线程再将对应的任务标记为就绪并恢复执行。
工作流程(绑定关系图示)
工作线程 (Worker Thread) | |---> 内部调度器 (Scheduler) | | | |---> 执行用户任务 (User Task) | | | (遇到页错误) | | V | |---> 调用 **本线程的I/O通道** 提交异步I/O请求 | | | (任务让出CPU,状态设为“等待I/O”) | | V | |---> 调度器选择下一个任务执行 | | | |---> 执行系统任务 (如页面淘汰) | | | |---> **轮询本线程的I/O通道**,检查完成事件 | | (I/O完成) | V | |---> I/O通道回调,将对应任务状态设为“I/O完成” | | | (任务被重新放入就绪队列) | | V | |---> 调度器恢复执行该任务 | VI/O后端 (I/O Backend) | |---> 为每个工作线程维护一个 I/O通道 | | | |---> 通道1: 管理到 SSD 1, SSD 2, ..., SSD N 的连接 | |---> 通道2: 管理到 SSD 1, SSD 2, ..., SSD N 的连接 | |---> ... | |---> 通道M: 管理到 SSD 1, SSD 2, ..., SSD N 的连接 | VNVMe SSD 阵列
•
描述对象:工作线程如何与SSD硬件连接的具体I/O模型。
•
核心内容:比较了三种模型:
1
专用I/O线程模型:工作线程需通过消息传递将I/O请求交给专门的I/O线程处理。
2
SSD分配模型:每个SSD被分配给特定线程,跨SSD访问需要线程间通信。
3
全对全模型(作者采用):每个工作线程都能直接访问所有SSD,拥有自己到每个SSD的I/O通道(队列对),无需任何线程间消息传递或同步。
图10对比了三种将工作线程连接到SSD阵列的模型:
1
专用I/O线程模型 (a)
2
SSD分配模型 (b)
3
全对全模型 (c) — LeanStore采用
在LeanStore采用的全对全模型中,I/O读写流程如下:
1
I/O通道抽象:
•
每个工作线程都拥有一个独立的I/O通道。
•
这个I/O通道是I/O后端的一部分,它封装了与所有SSD的直接连接(NVMe队列对)。
2
直接、无锁的I/O提交:
•
当工作线程中的用户任务(例如B树查找)遇到页错误时,它会直接调用其所属工作线程的I/O通道接口,提交一个异步I/O请求(例如读取一个4KB页面)。
•
关键点:此过程无需任何线程间消息传递或同步。工作线程不需要与专用I/O线程通信,也不需要与其他工作线程协调以访问特定的SSD。
3
硬件访问:
•
该请求通过该线程的专属NVMe提交队列直接发送到目标SSD。
•
每个工作线程都可以直接向任何SSD提交I/O请求。
4
I/O完成处理:
•
工作线程定期轮询其I/O通道的完成队列,检查I/O操作是否完成。
•
当SSD完成操作后,会将完成事件写入对应的完成队列。
•
工作线程在轮询时发现完成事件,并执行相应的回调函数,将等待该I/O的任务状态设置为就绪,以便调度器恢复其执行。
•
与专用I/O线程模型 (a) 对比:
•
在模型(a)中,工作线程不能直接访问SSD。它们必须通过消息传递或同步将I/O请求发送给专用的I/O线程,由后者负责与SSD交互。
•
问题:I/O线程成为瓶颈,且线程间通信开销大。文档指出,单个I/O线程只能达到约630k IOPS,无法满足高吞吐需求。
•
与SSD分配模型 (b) 对比:
•
在模型(b)中,每个SSD被分配给特定的线程。如果工作线程需要访问非其“拥有”的SSD,则仍需线程间通信。
•
问题:引入了跨线程通信的复杂性和开销。
1
无消息传递:完全消除了线程间协调开销。
2
对称性:所有工作线程具有相同的职责和结构,没有特殊的“I/O线程”或“SSD专属线程”。
3
高效与可扩展:由于没有通信开销,该模型可以实现极高的可扩展性。系统可以高效地运行在单个CPU核心上,也可以轻松扩展到所有可用核心。
4
简单健壮:编程模型更简单,系统更健壮。
特性 | 图7(挑战) | 图8(架构解决方案) | 图10(I/O实现方案) |
|---|---|---|---|
层级 | 问题定义层:指出硬件和软件需求之间的并行性鸿沟。 | 系统架构层:展示整体的线程和任务管理模型。 | I/O实现层:展示工作线程与存储硬件之间的具体连接方式。 |
焦点 | 是什么(需要处理什么并行性)。 | 怎么做(如何组织线程和任务来处理这些并行性)。 | 怎么连接(如何以最低开销将I/O请求送达硬件)。 |
关系 | 提出了设计图8和图10所要解决的根本问题。 | 图8的架构要求图10的全对全模型。因为每个工作线程需要独立处理所有任务(包括I/O),所以必须采用允许直接访问所有SSD的模型。 | 图10的全对全模型是实现图8架构的关键技术支撑。它使得“工作线程负责一切”的设计能够高效、无冲突地执行。 |
简单来说:
图7 说明了 “我们需要处理海量并行请求、有限CPU核心和需要上千个并发I/O” 这个难题。
图8 给出了答案:“让每个CPU核心上的工作线程通过协作式任务处理所有事情(查询、淘汰、I/O)”。
图10 则解决了图8方案中的一个关键子问题:“如何让每个工作线程高效地访问所有SSD?” 答案是:采用全对全I/O模型,让每个线程拥有到所有SSD的直接通道,避免线程间通信。
因此,图8和图10不是替代关系,而是互补关系。
