未来数据库硬件-计算篇

本文在绿泡泡"狗哥琐话"首发于2025.2.21 <-关注不走丢。

在计算章节这部分,关于性能部分提到了一篇论文,非常的有意思,叫做:"Cloud-Native Database Systems and Unikernels: Reimagining OS Abstractions for Modern Hardware"。

1.动机

众所周知,通用操作系统首先要做的就是提供了一个统一的抽象接口来访问硬件,并隔离了不同进程对硬件的访问。但这对数据库来说并不合适,在前面的网络篇我们就聊过,为了避免内核带来的开销,我们用了很多少手段来绕开通用内核。

所以,操作系统硬件抽象和进程隔离是有代价的,特别是对于需要对硬件进行细粒度控制以优化性能和资源利用率的高性能数据库系统。因此,定制的操作系统内核可以提供更适合数据库系统的OS API,从而实现更好的性能。

所以有一种叫做unikernels的内核架构,就是冲着数据库来做的专有系统。

2 一些细节

2.1 为啥之前的定制内核操作系统没有成功

首先,很少有用户愿意购买一个需要安装自定义操作系统的DBMS,想想就烦的1p;其次,由于用户的硬件配置各不相同,这样的自定义操作系统必须实现并维护众多设备的驱动程序。因此,除了少数特定应用场景外,数据库系统不得不适应通用操作系统带来的限制。

2.2 现在为啥行了

因为公有云。数据库系统越来越多地作为托管服务在公共云上提供。这一转变意味着自定义内核只需由DBMS供应商安装,使用户能够在无需承担管理负担的情况下受益于自定义内核。而且很多公有云虚拟机对操作系统的要求很低。

比如AWS EC2支持启动自定义操作系统内核,任何运行在最近EC2实例上的DBMS仅需实现两个驱动程序:一个用于网络的AWS特定EFA驱动程序和一个用于存储的通用NVMe驱动程序。

3.Unikernel究竟好在哪里

在一个unikernel中,任何线程都可以直接访问任何内核数据结构和硬件设备。基于hypervisor的虚拟化确保了每个VM运行单一服务时的租户隔离。对于许多云服务来说,运行像Linux这样的通用操作系统是多余的,因为没有其他进程或用户需要隔离。因此,unikernels消除了这一不必要的隔离层。

总得来说,有这么几个点是非常有优势的:

  1. 没有用户态和内核态的切换成本
  2. 应用程序可以直接控制硬件并操作虚拟内存页表
  3. 去除虚拟化相关的开销,整体系统变得更快更简单更小,启动也更快
  4. 不去适配老的POSIX API,这意味着可以做出一个更简单高效的设计。(很多旧API是为单核系统和稀缺内存设计的)

4.通用操作系统究竟不好在哪里

前面我们说了,操作系统最核心的任务之一就是给硬件提供一个抽象。

操作系统提供了一个统一的硬件抽象,并将其分配给不同的用户。对于数据库系统最为相关的四种主要硬件资源是CPU核心、主内存、存储和网络。在传统模型中,操作系统提供了执行线程的抽象,操作系统调度器将这些线程动态映射到CPU核心上。存储和网络I/O通过同步阻塞系统调用完成。当一个线程在I/O操作上被阻塞时,直到设备通过中断信号通知完成才会重新调度该线程。在这个模型中,操作系统控制所有的调度决策并接收所有硬件信号。

那我们以传统操作系统之上的数据库为例。一般服务端会为每一个客户端分配一个线程。每个线程使用阻塞系统调用监听网络套接字上的传入请求。当请求到达时,操作系统会在某个CPU核心上调度该线程执行。然后,线程可以处理查询并将结果通过另一个阻塞套接字系统调用返回给客户端。在查询执行过程中,缓冲池页面缺失可能导致存储I/O系统调用和额外的上下文切换。这种模型易于使用,概念清晰,并且在DBMS和OS之间提供了明确的责任分离。DBMS严重依赖于操作系统提供的抽象。通过阻塞系统调用(以及必要时的抢占式多任务处理),操作系统完全掌控并确保所有硬件资源得到充分利用。在一个I/O(磁盘和网络)相对较慢而CPU相对快速的世界里,由于上下文切换开销可以忽略不计,这种同步模型非常高效。

但是,现代存储设备速度非常快,这意味着I/O堆栈的CPU开销可能相当大。存储设备也具有高度并行性:例如,单个现代SSD能够同时处理大约100个I/O请求,每秒超过一百万个请求。因此,几乎不可能通过阻塞I/O系统调用来利用现代存储设备。传统模型的第二个更概念化的问题在于它没有考虑查询内的并行性,这在拥有数十个CPU核心的现代硬件上至关重要。查询内的并行性具有挑战性,因为它打破了客户端连接与线程之间的一对一映射。

那么现代高性能数据库是怎么做的呢?自行调度I/O请求和CPU核心。为此,它们通常为每个CPU核心启动一个工作线程,依赖于异步I/O接口(如io_uring),并避免使用软件RAID和文件系统等操作系统服务。在这种模型中,数据库系统必须决定为每个查询使用多少线程,这意味着它需要一个调度器,理想情况下应考虑其他当前正在运行的查询及其是否受I/O或CPU限制。DBMS还必须管理I/O请求,操作系统仅作为中介将异步请求传递给异步硬件设备。请注意,这些调度和I/O决策曾经是操作系统的主要目的。在现代硬件上,唯一获得良好性能的方法是通过低级操作系统接口,这将大部分责任推给了应用程序。因此,操作系统无法履行其作为应用程序与硬件之间协调者的传统角色。

不仅如此,现代I/O设备的高带宽仅用于将数据传输到CPU就造成了相当大的CPU负载。这个前面的视频我们也讲过,就不再赘述了。

5.Unikernels是什么样的

Unikernels 是为云环境特别设计的轻量级操作系统。核心理念是将应用程序和操作系统内核编译成一个系统镜像,并在基于hypervisor的虚拟化环境中运行。如图1(c)所示,hypervisor允许应用程序以内核模式运行,并与内核共享相同的内存地址空间。正如引言中提到的,这种特性使得Unikernels既简单(代码行数从百万减少到几万)又高效(没有系统调用开销,也没有进程/用户隔离成本)。

目前比较常见的操作系统是OSv和Unikraft,他们都支持标准的Unix API POSIX。

  • POSIX vs 新抽象: OSv可以直接运行许多应用程序,或者只需进行少量修改。然而,由于POSIX较大且包含许多晦涩的功能,所有Unikernels只实现了一部分POSIX。完全兼容Linux并不是一个好的选择,因为这会破坏Unikernels的简洁性。例如,OSv的整个虚拟内存子系统位于一个2,100行的C++文件(core/mmu.cc)中。相比之下,Linux中的相应代码(mm/*)超过110,000行C代码。大部分子系统实现的是隔离或数据库系统无关的晦涩功能。
  • 对于硬件的直接访问。原本在内核态的能力直接暴露给数据库,提升效率:
    • CPU: 抢占、睡眠/唤醒
    • 虚拟内存: 页表和TLB操作
    • Hypervisor: 内存气球和CPU热插拔
    • I/O: 直接访问提交和完成队列、中断控制、设备配置
  • Unikernel 安全性: 由于其简洁性,Unikernels极大地减少了攻击面。然而,它们可能缺乏标准的安全措施(如地址和特权隔离)和常见的强化技术(如保护页面、地址空间布局随机化)。虽然后者只是实现上的不足,但前者在Unikernel仅托管单个租户的情况下并不成问题。毕竟,攻击者的主要目标是非法访问数据库,将用户空间漏洞升级到内核空间带来的额外好处有限。

5.1计算调度

正如前面说的,如果现代数据库系统希望充分利用现代硬件,则必须自行调度CPU任务和I/O操作。例如,LeanStore使用固定核心的工作线程维护一个用户空间任务队列。当任务出现页面缺失时,它提交一个异步I/O请求,将其自身排队,然后切换到另一个任务(使用用户空间堆栈切换)。尽管这种方法效率高,但它实际上是在用户空间中重新实现了操作系统调度器,并且由于两个原因不够健壮:首先,CPU密集型任务可能会独占CPU核心,因为用户空间调度器无法访问原始抢占机制(即硬件定时器、跨核心激活)。其次,如果用户空间任务无意中阻塞了,核心将暂时浪费,因为操作系统不了解其他等待的任务。

就算把不健壮的问题放在一边,线程的切换代价也差距很大。在Linux中,上下文切换至少需要1微秒,而在用户空间中切换堆栈只需10纳秒。在Unikernels中,用户空间任务和内核级线程之间没有区别;线程可以自行挂起,操纵其他核心上的运行队列,阻止抢占或像协程一样交错执行。同时,OSv调度器对所有线程有全面了解,可以在它们独占CPU时抢占每个线程。摆脱不可见的用户空间任务避免了调度间隙,使所有逻辑DBMS作业可见,并且DBMS可以跟踪系统资源利用率。

一旦调度器获得对任务的控制权和可见性,我们也可以解决第2节中提出的查询内并行性问题。目前,DBMS必须决定为每个查询使用多少线程。但我们认为,如果调度器能够要求逻辑作业自我并行化会更好。在这种设计中,做出良好调度决策所需的所有信息(CPU负载、I/O利用率、作业优先级等)都集中在一个地方且易于获取。在Unikernel中,这样的调度器更容易实现,因为上下文切换便宜并且我们可以预抢占式地中断线程。

5.2虚拟内存

使用虚拟内存实现功能而非隔离: 通用操作系统主要使用虚拟内存硬件(如MMU和TLB)来分离用户空间和内核空间,并隔离进程。无需隔离的情况下,Unikernels可以提供对虚拟内存硬件的直接控制,从而实现新的用例。具体来说,我们看到了数据库系统的三个机会:

  • 缓存: 虚拟内存可以用来实现高效的缓冲管理,通过将虚拟内存页表作为硬件辅助间接表。早期工作中展示了如何在Linux中使用现有系统调用(vmcache)或扩展Linux以更快的系统调用(exmap)实现这一点。使用Unikernel,实现这样的缓存设计变得更快更简单。
  • VM感知算法和数据结构: 虚拟内存也被用于实现快照、动态数据结构和可变页面大小。尽管概念优雅,这些提案并未广泛采用。我们认为主要原因在于VM原语在Linux中速度慢且不易扩展。例如,在16核CPU上使用pagemap接口检查4KiB随机页面是否存在于4GiB区域中需要1.8至4.8微秒,而在OSv中仅需40至44纳秒。减少VM操作开销的替代方法是使用大页(2 MiB),我们计划支持这两种页面大小。然而,对于具有随机访问模式的OLTP工作负载,大页并不总是有益的。因此,可扩展且高效的4KiB VM操作能实现更多用例。
  • 内存分配: 另一个VM用途是中间查询处理结果的内存分配,这对于内存查询处理发生率很高,通常需要大的连续VM范围(如哈希表)。现有的内存分配器有两种选择:要么直接使用mmap()和munmap()将每个分配传递给OS,导致每次新分配后发生按需页面错误。在Linux中,这样的页面错误扩展性差:使用1个线程安装4KiB页面需要1.05微秒,使用16个线程则需要4.17微秒。通用分配器(如jemalloc)可能会将内存保留在进程中,这可能导致由于分配大小变化引起的内存碎片。在Unikernels中,选项A变得更加吸引人,因为巨大的VM地址空间(≥ 256 TiB)缓解了碎片问题,页面错误更快且潜在更具扩展性:无锁页面错误快速路径安装预分配帧在1个线程下需要0.5微秒,在16个线程下需要1.29微秒。

5.3 Hypervisor的改善

之前都是数据和内核交互,然后内核和hypervisor交互,现在变成允许DBMS与hypervisor交换信息并动态改变内存和CPU分配,可以用来改善资源利用率。

5.4 IO的改善

当今,几乎所有的存储设备都使用标准化的NVMe协议,这意味着只需要一个驱动程序即可管理存储。VMe基于队列,这些队列可以由主机CPU和存储设备访问。在分配一个或多个提交和完成队列并将它们注册到设备后,提交操作仅涉及向提交队列写入数据。一旦I/O操作完成,它将出现在完成队列中,并通过DMA数据会出现在内存中的预期位置。值得注意的是,这反映了现代操作系统接口(如io_uring)所采用的异步、基于队列的I/O模型。实际上,io_uring只是在NVMe队列之上提供了一个额外的队列,这导致了显著的CPU开销。基于Unikernel的设计可以直接将NVMe队列暴露给DBMS,绕过这一不必要的层。

还有一个比较显著的就是对于存储和网络的IO方式------轮询以及中断。那么在事件速率低时,它在功耗方面变得非常浪费。而速率高的时候,一直发生中断其实开销也不低。与内核绕过方法不同,Unikernels允许根据工作负载动态启用、禁用和路由中断。

5.5测试结果

然后呢,用debian和OSv做了一系列的基准测试,在16C 12G的虚拟机上做了一些测试,诸如创建快照、OLAP、TP的多线程计算等等。总体测下来结果是不错的,有兴趣的同学可以自己翻看论文看里面的细节。

6.小结

通用操作系统抽象与数据库系统需求之间的紧张关系早在五十年前就被观察到了。尽管存在针对DBMS特定操作系统的尝试,例如作为Gamma项目的一部分,几乎所有广泛使用的数据库系统都依赖于标准操作系统。最近的DBMS/OS协同设计项目包括DBOS、MxKernel和COD。

像我们的提议一样,DBOS专注于云环境,特别是分布式计算节点的编排。目前,DBOS基于Firecracker,并且可以与Unikernel结合使用。因此,我们相信本文提出的想法与DBOS项目是互补的。

MxKernel强调了不可中断的运行至完成任务在异构架构上执行查询计划时可能优于线程的优势。相反,我们将考虑所有重要的资源(CPU、内存、I/O),并旨在创建一个云原生DBMS系统,在这个系统中硬件池高度标准化。

相关推荐
NCIN EXPE2 小时前
redis 使用
数据库·redis·缓存
MongoDB 数据平台2 小时前
为编码代理引入 MongoDB 代理技能和插件
数据库·mongodb
极客on之路3 小时前
mysql explain type 各个字段解释
数据库·mysql
代码雕刻家3 小时前
MySQL与SQL Server的基本指令
数据库·mysql·sqlserver
lThE ANDE3 小时前
开启mysql的binlog日志
数据库·mysql
yejqvow123 小时前
CSS如何控制placeholder文字的颜色_使用--placeholder伪元素
jvm·数据库·python
oLLI PILO3 小时前
nacos2.3.0 接入pgsql或其他数据库
数据库
m0_743623923 小时前
HTML怎么创建多语言切换器_HTML语言选择下拉结构【指南】
jvm·数据库·python
pele3 小时前
Angular 表单中基于下拉选择动态启用字段必填校验的完整实现
jvm·数据库·python