NUMA感知的持久内存存储引擎优化设计
发布日期:2025-01-04 15:20 点击次数:202
数据库是高效组织、存储、管理数据的软件. 随着互联网、物联网和移动计算技术的发展, 人们能够获取的数据规模爆炸式增长. 如何高效地存储、管理和查询这些海量多模的数据, 是当前数据管理领域面临的严峻挑战. 近年来, 新型硬件技术突飞猛进, 特别是非易失性内存(non-volatile memory, NVM)的出现, 打破了传统内外存之间的界限, 对现有的软件体系结构带来了颠覆性的影响. 传统的数据库系统是基于慢速块存储设备构建的, 采用了经典的“高速缓存+内存+磁盘”三层存储架构设计, 由于磁盘和内存的硬件延迟相差两个数量级, 导致数据在内存和外存之间移动或交换时会引发系统性能颠簸的问题. 计算机硬件的创新为数据库系统带来了全新的机会, 数据库系统需要根据新型硬件特性重新优化设计软件的架构和算法.
非易失性内存也称为持久内存(persistent memory, PM)或存储级内存(storage-class memory, SCM), PM具有非易失性、字节寻址、低时延、大容量等优势, 但是当前的PM硬件也存在着磨损不均衡、读写不对称等问题[1]. 文献[2]给出了英特尔®傲腾™ DC持久内存(DCPMM)和DRAM的时延和带宽对比, 其中, DCPMM的读写时延约是DRAM的4倍, 说明PM的读写能力较DRAM还有一定的差距; DCPMM的读带宽是其写带宽的10倍以上, 即存在巨大的读写非对称性. 因此, 业界普遍认为, PM短期内还无法完全替代DRAM, 采用DRAM+PM的混合内存架构更能发挥各自的优势[3].
近年来, 一些先进的商业数据库已开始尝试应用PM来提升性能. Oracle 20c增加了PM Database[4]功能, 使数据库绕过DRAM Buffer, 直接读取PM上的数据, 并能跟踪数据访问频率, 自动将热点数据从PM中调入DRAM. SQL Server 2019则增加了混合缓冲池, 使缓冲池对象能够直接引用PM设备上的数据页. 尽管这些商业数据库推出了PM数据库功能, 但大多处于初级的DAX (direct access)[5]接口适配阶段, 并未针对PM新特性对数据库系统做深度优化.
NUMA (non uniform memory access)架构将内存和处理器分成组, 每组称为一个NUMA节点, 从任一个处理器角度看, 与该处理器位于同一NUMA节点中的内存称为本地, 其他NUMA节点中的内存称为远端, CPU访问本地内存的速度远高于远端内存, 并且节点之间的距离越大, 访问时延越高[6]. PM作为内存使用时同样存在这个问题, 而且与DRAM相比, CPU跨NUMA节点访问PM的性能衰减更加明显[7]. 因此, 构建PM存储引擎时, 需要把NUMA特性作为重要的设计考量, 确保合理使用PM数据存储空间的同时, 降低跨NUMA节点访问PM的开销.
本文探讨了中兴通讯新一代数据库系统GoldenX的PM存储引擎设计实践, 重点研究了NUMA感知的PM存储引擎下的数据空间分布、事务处理和异常恢复等问题. 本文主要工作和贡献如下: (1) 提出了一种DRAM+PM混合内存架构下跨NUMA节点的数据空间分布策略和分布式存取模型, 实现了PM数据空间的高效使用. (2) 针对跨NUMA访问PM的高开销问题, 提出了I/O代理例程访问方法, 将跨NUMA访问PM开销转化为一次远程DRAM内存拷贝和本地访问PM的开销; 设计了CLA缓存页机制, 缓解I/O写放大问题, 提升了本地访问PM的效率. (3) 扩展了传统的表空间概念, 让每个表空间既拥有独立的表数据存储, 也拥有专门的WAL日志存储. 针对该分布式WAL存储架构提出一种基于全局顺序号的分布式WAL事务处理机制, 解决了单点WAL性能问题, 并实现了NUMA架构下的事务处理、检查点和灾难恢复等优化机制及算法.
本文第1节介绍国内外相关研究工作. 第2节阐述NUMA感知的PM存储引擎的数据分布模型及其优化设计机制. 第3节通过实验评估该数据分布模型及相关设计对PM存储引擎的优化效果. 最后总结全文.
1 相关工作
PM是一种极具发展前景的存储技术. 近年来, 基于PM优化的数据库管理系统在访问接口、索引结构、数据组织、事务日志、存储架构、查询优化等方面得到了广泛的关注和研究[8−12]. SNIA (storage networking industry association)建议通过文件系统管理NVM设备, 目前, PM的访问方法包括DAX文件系统标准接口和PMDK API两种: 前者无须修改应用程序即可利用PM的性能优势; 而PMDK接口完全在用户态执行, 无须经过内核上下文切换, 可获得最佳性能.
为了高效管理PM中的数据, 业界出现了许多索引结构的相关研究, 例如wB+Tree[13]、DPTree[14]、CDDS-Tree[15]、FPTree[16]、BzTree[17]等. Chen等人[13]提出的wB+Tree结构让数据页内部的元组保持无序, 以减小因排序造成的额外写, 但仍会引起大量mfence和cflush, 以维护数据的一致性. Zhou等人[14]提出了一种专为DRAM-PM混合系统设计的写优化索引结构DPTree, 其核心思想是, 利用批量更新和原地合并来降低元数据的更新代价.同时, 该设计采用的写优化持久日志、粗粒度版本控制、基于哈希的节点设计和就地合并算法, 使DPTree能够在多核环境中提高并发度和可扩展性. 此外, 在SQL执行引擎方面也需要针对PM读写不对称和有限的写耐久性的特点重新设计执行引擎的算法, 以减少写入PM的次数. Arulraj[11]介绍了在查询计划阶段进行改良的混合写限制排序算法和分段Grace hash join算法, 二者都是通过写限制算法来降低写入强度. 其中, 混合写限制排序算法是先使用写密集型更快的外部合并排序算法对一部分数据进行排序, 其余数据则使用写受限的选择排序算法进行排序, 以降低性能为代价减少写入强度.
在数据组织方面, 传统的面向磁盘的数据库系统以页为单位持久化数据, 可字节寻址的PM为更小粒度的数据I/O提供了可能. Renen等人[18]利用PM字节可寻址特性提出了Cache-Line-Grained页和Mini页结构, 允许以缓存行粒度加载或刷写PM上的数据页, 减少了数据的搬运量. 同时, Mini页通过slot结构标识DRAM和PM页内缓存行的对应关系, 进一步提升了DRAM空间利用效率. 在此基础上, 构建DRAM-PM-SSD三层存储架构, 实现数据的热、温、冷分离, 进一步提升系统性能. Joy等人[19]在Renen等人成果的基础上设计了Spitfire, 进一步探讨三层缓存管理机制, 引入了数据页概率迁移策略并利用机器学习技术, 动态调整迁移概率值, 使之能够适应任意工作负载.
在事务日志方面, 为了解决预写式日志(WAL)在高并发下写日志时锁争抢严重和重复写数据等问题, 学术界基于PM提出了NV-logging[20]、WBL[21]和分布式日志[22]等改进机制. NV-logging充分利用PM字节寻址功能, 简化了日志结构, 通过给每个事务分配单独日志, 以分散日志缓冲区并减少潜在的锁竞争, 从而降低了系统开销. WBL机制完美利用了PM的非易失性和按字节寻址等优点. 相比WAL, WBL不会记录数据库元组的修改信息, 而是在数据持久化到存储设备之后, 记录commit标记信息用于回滚未提交的事务. 因此, WBL具备更好的写入性能和更快的故障恢复速度. 但是, WBL无法支持数据库集群(或节点)间的数据同步与备份容灾, 难以在商用系统中使用.
在存储架构方面, 有学者研究PM用在主存数据库以减少日志量. 然而, 由于目前PM与DRAM相比具有更高的时延, 难以实现事务的高吞吐量; 而且直接在PM上频繁读写会耗尽其有限的耐久性, 存在硬件故障的潜在风险. 因此, 采用DRAM+PM混合内存架构更能发挥各自的优势. 一种为层级架构, 将DRAM作为PM的Buffer或者Cache, 根据特定策略将冷热数据在DRAM和PM之间动态移动. Kimura[23]基于DRAM和PM两层混合存储架构设计了可扩展的FOEDUS引擎. FOEDUS基于dual page机制管理PM和DRAM中数据页, 并通过WAL机制在DRAM中构建快照页, 然后顺序写入PM, 从而实现数据的持久化与冷热管理. 平行架构是将PM与DRAM构成的混合内存处于同一层级, 利用PM的非易失性直接对一部分数据做持久化. 这种架构能充分利用PM字节寻址的新特性, 为数据库存储架构的发展带来了新思路. Renen等人[18]对比了几种NVM存储方式的优势和劣势, 提出了一种新的基于NVM的存储引擎, 它同时支持DRAM、NVM和SSD. 在SSD中存储冷数据, 在DRAM中访问(读写)数据页, 通过WAL日志确保持久性, 当DRAM中数据页被驱逐时, 根据数据冷热程度要么写入NVM, 要么写入SSD. 充分利用NVM的字节寻址能力, 避免了性能悬崖, 并且性能接近于纯NVM存储引擎. 通过支持SSD, 它可以管理非常大的数据集, 而且比其他方法更经济.
传统的NUMA内存管理策略无法有效地管理PM+DRAM混合内存, 业界出现了一些NUMA感知(NUMA-aware)的DRAM+PM混合内存解决方案, 其主要思想是尽可能地降低混合内存系统中的内存访问成本, 提升系统性能. Kim等人[24]提出了一种面向众核NUMA感知的NOVA文件系统, 该系统虚拟化了NUMA节点上的PM设备, 采用本地优先存放策略, 将文件数据和元数据优先放在本地PM设备上, 以减少远程访问. Duan等人[25]提出了NUMA感知的混合内存系统HiNUMA, 该系统采用NUMA拓扑感知的混合内存策略初始化数据, 综合考虑数据访问频率及内存带宽利用率将远端数据页迁移到本地内存, 降低了混合存储系统内存访问成本. 上述DRAM+PM混合内存架构下的NUMA感知设计思想同样可以应用到数据库管理系统[26, 27].
在NUMA架构下, DBMS需要进一步优化事务日志机制, 以发挥PM硬件潜力. Haubenschild等人[22]提出了两阶段分布式日志算法, 每个线程维护一个日志分区, 首先将日志写入NUMA本地节点的PM中, 然后由WAL写线程异步将本地PM中的日志持久化到SSD, 通过GSN保证分布式日志的逻辑顺序. 但是这种方法在恢复时需要扫描全部日志, 将日志按页及GSN进行排序后才能回放, 当日志量非常大时, 恢复效率比较低. Wang等人[28]提出了基于PM的分布式日志机制, 将日志基于页或者事务进行分区存储到每个NUMA节点, 通过GSN唯一标识每一个日志记录及其顺序. 然而, 一个事务可能会修改多个页, 这两种日志分区机制均没考虑数据分区, 存在跨NUMA持久化数据页的问题.
综上所述, 以上研究未能有效解决数据库跨NUMA访问PM的性能问题. 本文基于实测数据评估了跨NUMA节点访问不同存储介质的I/O性能衰减模型, 提出了跨NUMA节点的数据空间分布策略和代价最小的PM访问路径, 并以此为基础展开NUMA感知的PM存储引擎设计, 实现了PM存储空间的合理利用, 并充分挖掘其I/O潜力.
2 NUMA感知的PM存储引擎设计
现代计算机仍然是冯⋅诺依曼体系结构, 计算与存储密不可分. PM存储引擎设计不仅要考虑PM的非易失、字节寻址等特性, 也要考虑处理器因素, 特别是NUMA特性带来的影响. 因此, PM存储引擎设计必须把支持NUMA特性作为最重要考量.
GoldenX PM存储引擎的总体设计原则是, 让DBMS既能充分调度全部CPU算力, 又能发挥本地访问PM的低时延优势.图 1展示了本文的设计思想.
图 1 NUMA感知的PM存储引擎设计
在顶层设计上, 让每个NUMA节点负责管理一部分数据, 有着相对独立的表空间和WAL日志空间. 当需要访问多个表空间时, 为了降低跨NUMA节点的访问开销, 事务执行器会预先在每个NUMA节点内启动一个I/O代理例程, 委托其执行数据扫描或写入操作, 这样就把代价较大的远端访问PM转换为代价小得多的远端访问DRAM+本地访问PM, 从而提升事务处理性能.
在底层设计上, GoldenX重点针对PM硬件特性扬长避短. 在访问方法方面, GoldenX直接使用DAX文件系统管理各种PM数据库文件. 当需要访问这些文件时, GoldenX先使用PMDK API将其映射到进程虚拟地址空间中, 然后像访问普通内存一样直接读写PM数据, 图 1中带箭头的虚线展示了Table、Index和WAL的这种内存映射关系. 在事务提交时, 脏页无须立即刷写回PM, 而是延迟到检查点周期时, 再由checkpoint线程写回PM.
2.1 数据空间分布与访问路径
对于NUMA感知的PM存储引擎, 一个理想的访问模型是处理线程总能被调度到待访问数据所在的本地CPU上. 然而, 将一个线程从本地CPU迁移到远端CPU的代价很高, 包含内核上下文切换和上下文切换信息传输装载到远端CPU等, 其总开销甚至高于CPU通过NUMA互联模块直接访问远端节点PM的开销. 另一个思路是, 在每个NUMA节点上固定驻留一部分服务线程. 但DBMS处理复杂事务时需要同时访问多个NUMA节点上的数据, 这种方式行不通.
为了更接近理想数据访问模型, GoldenX在系统设计目标上要做到两点: 一是要最大程度地激发所有PM的I/O潜力, 确保PM数据存储空间被合理利用; 二是要尽量降低跨NUMA节点访问的总开销, 探索一条代价最小的访问PM路径.
2.1.1 跨NUMA节点I/O性能衰减模型
存储介质厂家一般会提供两个性能曲线: IOPS(每秒读写次数)性能曲线和带宽性能曲线. IOPS性能曲线会随着数据块大小的增大而减少; 带宽的性能曲线会随着数据块大小的增大而增大, 并最终趋于稳态. 数据库的应用场景有两个显著特点: 一是小数据块读写, 二是高并发操作. 在小数据块读写时, PM的带宽不容易成为瓶颈, 这是因为IOPS往往率先成为瓶颈, 从而限制了其读写带宽潜力. 针对数据库的高并发特点, 本文通过实际测试后发现: PM的读写性能随并发数的增大而增大, 并在10个线程后趋于稳态. 以256 B读写操作为例: 随机读场景下, 10个并发的带宽是单个并发的6倍; 随机写场景, 10个并发的带宽是单个并发的3倍.
为了进一步考察不同存储介质跨NUMA节点访问后的I/O衰减程度, 本文采用FIO和MLC工具对DRAM、PM和NVMe SSD的读写带宽进行对比测试. 基于上述分析结果, 本次测试采用10个并发, 以获得他们在小数据块I/O场景下的最高带宽, 测试其结果如图 2所示.
图 2 DRAM、PM、NVMe SSD跨NUMA节点读写带宽对比
对于随机读场景, PM跨NUMA节点读取数据性能衰减了88%−96%, 而NVMe SSD仅衰减了0.7%−4%; 对于随机写场景, PM跨NUMA节点读取数据性能衰减了44%−64%, 而NVMe SSD仅衰减了1%−7%; 而DRAM跨NUMA节点访问时, 读写性能均下降了35%−66%. 可见, 不同存储介质的I/O性能衰减程度差别很大. 其中, NVMe SSD衰减程度最低, 可以忽略; DRAM次之; PM读带宽衰减程度最严重, 必须在设计机制上进行规避.
为了简化I/O性能衰减模型, 本文以读写粒度256 B为基准进行估算: 根据上述测试结果, CPU访问本地PM比访问远端PM的读带宽高10倍, 写带宽高1倍; 而访问本地DRAM比访问远端DRAM的读写带宽均高3倍.
2.1.2 跨NUMA节点数据空间分布
为简化模型描述, 本文以2个NUMA节点为基础模型, 每个NUMA节点各配置1根DRAM和1根PM, 其他配置场景可看成该模型的组合变化. 首先要做的设计选择是如何分配这2个PM的存储空间. GoldenX把数据空间大致划分为两部分.
● Tablespace: 表数据存储, 包括表、索引等.
● WAL: REDO、UNDO等事务状态信息.
这里有多种潜在的空间组合, 表 1展示了有独立意义的所有组合, 其中, T表示Tablespace, W表示WAL.
表 1 Tablespace和WAL在PM上的空间组合
下面分析这些组合的优缺点.
● 组合1: 优点是没有跨NUMA节点访问问题, 缺点是无法利用其他NUMA节点的PM存储.
● 组合2−组合4: 特点是总有一部分T和与其对应的W不在一起, 这会造成部分数据更变操作所产生的WAL必须跨NUMA节点存储, 这容易导致单点WAL成为性能瓶颈.
● 组合5: 优点是变更本地数据所产生的WAL也存储在本地, 且数据与WAL均衡分布, 可充分利用各个PM的I/O潜能.
基于上述分析和设计目标1, GoldenX选择组合5数据分布策略, 但需要解决以下问题.
(1) 跨NUMA节点访问数据的I/O效率提升;
(2) 分布式表空间下的事务处理机制优化.
2.1.3 跨NUMA节点访问代价估算
先来描述部署场景: PM1在NUMA Node0, 其上有Tablespace1和WAL1; PM2在NUMA Node1, 其上有Tablespace2和WAL2, 表t1在Tablespace1, 表t2在Tablespace2. 在评估不同数据访问路径时, 本文只考虑纯写事务模型和纯读事务模型的执行代价, 读写混合事务模型的执行代价可基于这2个基础模型按比例叠加进行估算. 记纯写事务为TX_write, 它同时变更表t1和表t2; 记纯读事务为TX_read, 它同时读取表t1和表t2. 假设事务执行器运行在NUMA Node 0上, 考虑到线程切换代价, 在一个事务处理过程中, 不允许线程来回在不同NUMA节点间切换. 假设从本地PM读取单位数据量(记为δ)的时间代价是100, 那么根据第2.1.1节的带宽实验数据估算可得: 从远端PM读取δ数据量的时间代价约为1 000, 向本地PM写入δ数据量的时间代价约为1 000, 向远端PM写入δ数据量的时间代价约为2 000.
如图 3(a)所示, 在纯写事务模型中, 事务TX_write包含对表t1的写入操作WRITE t1和对表t2的写入操作WRITE t2. 记向表t1和t2写入的数据量分别为λ1和λ2, 由此生成的WAL数据量分别为ω1和ω2. WRITE t1属于本地PM写, 其中, 写表数据文件Data1的时间代价CostLocalDataWrite=(λ1÷δ)×1000, 写WAL日志文件WAL1的时间代价CostLocalWALWrite=(ω1÷δ)×1000, 则WRITE t1的总时间代价:
$
{ Cost }_{{WriteT1 }}={ Cost }_{{LocalDataWrite }}+{ Cost }_{{LocalWALWrite }}=\left(\lambda_{1}+\omega_{1}\right) \div \delta \times 1000 {. }
$
图 3 跨NUMA节点写事务数据访问路径
如果WRITE t2跨NUMA节点直接写远端PM, 则写Data2和写WAL2的总时间代价为
$
{ Cost }_{{WriteT2 }}={ Cost }_{{RemoteDataWrite }}+{ Cost }_{{RemoteWALWrite }}=\left[\left(\lambda_{2} \div \delta\right)+\left(\omega_{2} \div \delta\right)\right] \times 2000.
$
可得, 写事务的总时间代价为
$ \begin{array}{*{20}{l}}
Cos{t_{TotalWrite}} = Cos{t_{WriteT1}} + Cos{t_{WriteT2}} \hfill \\
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ = Cos{t_{LocalDataWrite}} + Cos{t_{LocalWALWrite}} + Cos{t_{RemoteDataWrite}} + Cos{t_{RemoteWALWrite}} \hfill \\
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ = [({\lambda _1} + {\omega _1}) + ({\lambda _2} + {\omega _2}) \times 2] \times 1000 \div \delta . \hfill \\
\end{array} $
为了消除跨NUMA节点访问PM产生的代价, GoldenX在每个NUMA节点中驻留运行一个I/O代理例程I/O Agent, 当需要访问远端PM时, 事务执行器向I/O Agent发送请求消息, 委托后者执行(本地)读写操作, 并通过消息链路传回处理结果. 由于线程(进程)间跨NUMA节点通信本质上属于DRAM内存拷贝过程, 不涉及网络通信开销, 因此可按照DRAM远端内存访问代价进行估算. 根据文献[2]提供的带宽对比数据, DRAM本地写带宽约为PM本地读带宽的5.8倍, 再根据第2.1.1节的带宽对比实验结果可知, DRAM本地写带宽约为远端写带宽的1倍, 得出DRAM远端写带宽约为本地PM读带宽的2.9倍. 因此, 当假设本地PM读的时间代价为100时, 则跨NUMA节点的线程通信时间代价可估算为100÷2.9≈35.
由上述分析, 引入I/O代理例程之后, 对于WRITE t2操作, 事务执行器通过线程间通信分别把待写入的表数据及其WAL日志发送给NUMA Node 1的I/O Agent, 后者采用PM本地写方式完成持久化, 则有:
$
{Cost}_{{RemoteDataTransmit }}=\lambda_{2} \div \delta \times 35, { Cost }_{{RemoteWALTransmit }}=\omega_{2} \div \delta \times 35.
$
其总时间代价为
$
{Cost}_{{OptWriteT } 2}={Cost}_{{RemoteDataTransmit }}+{Cost}_{{LocalDataWrite }}+{Cost}_{{RemoteWALTransmit }}+{ Cost }_{{LocalWALWrite }}\\
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ =\left[\left(\lambda_{2}+\omega_{2}\right) \div \delta\right] \times(35+1000).
$
优化后的写事务总时间代价为
$ \begin{array}{*{20}{l}}
Cos{t_{TotalOptWrite}} = Cos{t_{WriteT1}} + Cos{t_{OptWriteT2}} \hfill \\
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ = Cos{t_{LocalDataWrite}} + Cos{t_{LocalWALWrite}} + Cos{t_{RemoteDataTransmit}} + \hfill \\
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ Cos{t_{LocalDataWrite}} + Cos{t_{RemoteWALTransmit}} + Cos{t_{LocalWALWrite}} \hfill \\
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ = [({\lambda _1} + {\omega _1}) \times 1000 + ({\lambda _2} + {\omega _2}) \times 1035] \div \delta . \hfill \\
\end{array} $
假设写入表t1和t2的数据量相等且为λ, 且表数据产生的日志量也相等且为ω, 则优化前的总时间代价为(λ+ω)×3000÷δ, 优化后的总时间代价为(λ+ω)×2035÷δ, 那么在纯写事务模型中, GoldenX优化后的总时间代价同比下降了32% (即1−CostTotalOptWrite÷CostTotalWrite).
如图 3(b)所示, 在纯读事务模型中, 事务TX_read包含对表t1的扫描读取操作SCAN t1和对表t2的扫描读取操作SCAN t2. 记从表t1和t2读取的数据量分别为μ1和μ2. 采用跨NUMA节点直接访问方式时, TX_read的总时间代价为
$
{Cost}_{{TotalRead }}={Cost}_{{ReadT1 } 1}+{ Cost }_{{ReadT2 }}={Cost}_{{LocalDataRead }}+{Cost}_{{RemoteDataRead }}=\left(100 \mu_{1}+1000 \mu_{2}\right) \div \delta.
$
其中, CostLocalDataRead=(μ1÷δ)×100, CostRemoteDataRead=(μ2÷δ)×1000.
采用I/O代理方式后, TX_read的总时间代价为
$
{ Cost }_{{TotaloptRead }}={ Cost }_{{ReadT1 }}+{ Cost }_{{OptReadT } 2}={ Cost }_{{ReadT1 }}+\left({ Cost }_{{ReadT1 }}+{ Cost }_{{RemoteScanDataTransmit }}\right)=\left(100 \mu_{1}+135 \mu_{2}\right) \div \delta {. }
$
其中, CostRemoteScanDataTransmit)=μ2÷δ×35. 假设读取表t1和t2的数据量相等, 那么在纯读事务模型中, GoldenX优化后的总时间代价相比下降了78%(即1−CostTotalOptRead÷CostTotalRead).
2.1.4 NUMA节点内I/O访问优化
在NUMA节点内部, GoldenX存储引擎利用PM低时延和字节可寻址特性提升数据页的I/O效率. 传统存储引擎的页缓存机制有效解决了慢速磁盘I/O与高速CPU之间矛盾, 然而以页为单位的刷写粒度存在严重的写放大问题, PM的字节可寻址特性为解决该问题提供了可能性. 一个解决思路是: 记录缓存页的每一个改动点, 类似Page n Position x To y, 持久化时, 只把改动部分的数据刷写回PM. 这样做的优点是不存在任何写放大现象, 但详细记录改动点信息本身就是一笔巨大的开销.
为解决此问题, GoldenX借鉴文献[18]的思想提出了Cache Line Area (CLA)方法, 通过只记录改动区域来降低改动点信息的精确度. 把每个缓存页划分成固定大小的区域, 在页描述符中增加一个位图, 每个比特位代表一个区域, 当某个区域被改动时, 就把该比特位设置为1(称为变脏). 这种方式的优点是管理开销非常低, 只需一个位图即可快速定位所有改动点.
由于CPU与内存之间数据交换的最小单位是1个Cache Line, 因此把区域大小设置成1个Cache Line可获得最佳性能. 假设1个缓存页大小为8 KB, 1个Cache Line大小为64 B, 则每个缓存页被分割为128个区域, 每个这样的区域就是一个CLA.
如图 4所示, 每个缓存页在DRAM中设置一个页描述符, 同一个数据库表的所有页描述符组成一个页描述符表. 页描述符除了记录页号pno、缓存页指针dram_ptr、脏页标志位dirty_flag、PM物理页内存映射指针pm_ptr等信息外, 还有1个16字节大小的bitmap表(简记为CLA bitmap), 用于标记哪些区域变脏了. 在checkpoint线程刷写脏页过程中, 首先通过脏页标志位dirty_flag判断该缓存页是否变脏, 若是再进一步通过CLA bitmap快速判断哪些CLA变脏, 直接定位并刷写该CLA.
图 4 基于Cache-Line-Area的缓存页结构
同时, GoldenX会根据每个页面的访问频度, 动态地为高频数据页开启驻留DRAM的缓存, 事务处理线程通过页描述符中的dram_ptr字段找到其DRAM缓存页进行读写操作.
2.2 分布式表空间下的事务处理
传统数据库系统也支持创建多个表空间, 但只能有一个WAL日志空间, 这必然使得某些表空间与WAL日志空间不在同一个NUMA节点内, 导致相关事务处理过程存在跨节点访问开销. 针对这种情况, GoldenX让每个表空间不仅拥有独立的表数据存储(简记为Data), 而且拥有专门的WAL日志文件, 使得整个DBMS的WAL变成分布式存储架构. 同时规定: 每个表只属于一个特定的表空间, 事务对一个表的变更所产生的WAL日志必须存储在相同表空间下的WAL日志文件中. 这既消除了WAL单点写瓶颈, 也减少了跨NUMA节点额外访问开销. 大多数情况下, 业务系统都是一些只涉及单表修改的简单事务, 这些事务只需访问NUMA节点本地的PM数据, I/O效率非常高.
然而, 当一个事务同时修改不同表空间下的多个表时, 与该事务关联的WAL日志记录就会分散到多个表空间下的WAL日志文件中. 在这种情况下, WAL原先用于唯一标识每一个日志记录的LSN (log sequence number)机制就会失效. 原因是分布式WAL在系统中存在着多个日志文件, 单独一个LSN号区别不了它属于哪个日志文件. 更为严重的是, 由于落入到不同日志文件中的事务日志的LSN是无序的, 这使得原先依赖LSN来决定事务执行顺序的机制也失效了.
2.2.1 全局顺序号
解决上述问题的办法是, 引入一个全局顺序号来唯一标识每一个日志记录. 我们借鉴并改进了文献[28]所提出的GSN (global sequence number)概念及方法. DBMS维护着一个全局逻辑时钟作为GSN生成器, 简称全局授时器, 系统初始值为0, 每次授予一个新的时间戳时递增1. 每个页面和日志记录都永久保存着一个它们被修改时刻的GSN. 每个事务启动时都必须事先向全局授时器申请一个初始GSN, 简称事务初始GSN. 在此基础上, 本文进行了总结和改进, 并给出GSN偏序关系的数学定义. 为了便于表述, 约定: 当前事务的GSN记为txGSN, 当前页面的GSN记为pageGSN, 当前日志文件的GSN记为logGSN, 当前日志记录的GSN记为logrecGSN.
GSN变化规则如下:
规则1. 每当事务t修改页面p时, 令txGSN(t)=pageGSN(p)=max{txGSN(t), pageGSN(p)}+1.
规则2. 每当事务t读取页面p时, 令txGSN(t)=max{txGSN(t), pageGSN(p)}.
规则3. 每当因修改页面p而引发的向日志文件f插入日志记录l时, 令logGSN(f)=max{txGSN(t), pageGSN(p), logGSN(f)+1}, 然后令txGSN(t)=logrecGSN(l)=logGSN(f).
除了GSN之外, 每个日志记录依然保存了原先的LSN, 用于指示该日志记录在所属日志文件中的偏移位置. 尽管上述GSN方法无法获得全部日志记录之间的全序关系, 但可获得3个严格偏序关系≤, 使得对于任意一个页面(或日志文件或事务), 与之相关联的所有日志记录可以确立顺序关系. 根据GSN变化规则, 可获得在页面p所关联的全部日志记录集合上的严格偏序关系, 其定义如下.
定义1. 给定集合S(p)={l|l∈L∧Page(l)=p}, L是给定DBMS系统全部日志记录的集合, Page(1)函数将日志记录l映射到它所修改的页面号p, ≤是S(p)上的GSN大小关系且满足:
1) 反自反性: ∀x∈S(p), 有x$\not < $x;
2) 非对称性: ∀x, y∈S(p), x≤y⇒y$\not < $x;
3) 传递性: ∀x, y, z∈S(p), x≤y且y≤z, 则x≤z.
同理可定义日志文件和事务上的严格偏序关系. 有了上述3个严格偏序关系, 就能实现:
(1) 判断一个日志记录能否应用于它的目标页面, 即是否满足pageGSN≤logrecGSN;
(2) 判断同一事务中任意2个日志记录的次序, 无论它们是否存储在同一日志文件中;
(3) 判断同一日志文件中任意2个日志记录的次序, 但这属于冗余功能, 因与同一页面关联的所有日志记录都存储在同一个日志文件中, 故仅靠LSN即可判断任意2个日志记录的次序.
2.2.2 事务提交与回滚
● 事务提交
一个复杂事务会对不同表空间的多个表进行数据变更, 它们可能存储在不同NUMA节点的PM中, 变更所产生的日志记录将分别写入不同WAL日志文件. 为了跟踪每个事务涉及哪些WAL文件, 每个事务维护一个WAL文件列表wal_file_list以及该事务在每个WAL文件的最后刷写LSN位置tx_latest_wal_pos. 每当向一个WAL文件写入日志记录时, 首先判断该WAL文件是否在列表中, 若不在, 则将该文件添加到wal_file_ list, 然后更新tx_latest_wal_pos值. 同时, 每个WAL日志文件维护一个最新刷写位置latest_flush_pos, 表示在该位置之前的所有数据已被正确刷写到PM存储中. 当有多个事务并发执行时, 不同事务产生的日志记录交错写入这些WAL文件中. 当一个事务提交时, 事务执行器在各个WAL日志文件中写入Commit及End特殊日志记录, 然后发起事务日志持久化流程, 其算法参见算法1.
算法1. WAL日志持久化伪代码.
输入: 待提交事务对象tx.
输出: 无.
1. f=tx.wal_file_list.getNext(⋅); //获取第1个文件
2. WHILE (f!=NULL)
3. IF (f.latest_flush_pos < f.tx_latest_wal_pos) THEN
4. sfence;
5. 循环调用clwb指令刷写CPU Cache, 直至刷写到不小于f.tx_latest_wal_pos的位置p
6. sfence;
7. f.latest_flush_pos=p;
8. END IF
9. f=tx.wal_file_list.getNext(⋅);
10. END WHILE
在算法1中, 首先从事务的WAL文件列表wal_file_list中取出第1个文件f (line 1). 如果该文件f对应的日志还未被刷写(line 3), 则执行sfence指令和clwb指令将其刷写到PM (line 4−line 6). 接着更新文件f日志刷写位置(line 7), 然后继续从事务的WAL文件列表wal_file_list中取出下一个文件f (line 9), 并重复上述步骤, 直至该事务所有WAL文件刷写完成.
● 事务回滚
日志记录无法通过GSN本身识别其所属的日志文件及其偏移量, 为了避免事务回滚时产生大量日志扫描开销, GoldenX为每个事务开启了一个DRAM UNDO Stack(简称DUS). 这是一个堆栈数据结构, 每当向日志文件f插入LSN为lsn的日志记录时, 将一个三元组(f, lsn, len)压入栈中, 其中, len表示该日志记录的长度. 当一个事务被主动撤销或意外终止时, 事务管理器从该事务的DUS中依次弹出每个三元组, 再用f和lsn计算出指向该日志记录在PM中位置的指针p, 读出日志记录内容后做UNDO操作. DUS之所以不存储完整日志记录内容, 一是为了节省DRAM空间占用, 二是利用PM低时延随机读的优势. 复杂事务的回滚操作同样会涉及跨NUMA节点的日志访问, 如果日志文件f在本NUMA节点内, 事务管理器就直接从所在PM读取日志记录; 否则, 委托I/O Agent去读取.
2.2.3 检查点与灾难恢复
● 检查点
事务在提交过程中只需保证与之相关的所有日志记录被正确持久化到WAL文件中, 不会立即把被修改的脏页同步刷写到PM, 而是由后台辅助线程pages_writer和checkpointer延期刷写到PM. pages_writer定期遍历每个表空间的脏页链表, 根据既定策略, 将一定数量的脏页持久化到PM. checkpointer被定期或即时触发, 将所有脏页持久化到PM, 完成一次检查点(checkpoint)操作.
为了避免远端刷写PM开销, 在每个NUMA节点中, 分别启动一个checkpointer与pages_writer线程, 各自负责本地表空间脏页刷写. 每当一个检查点操作被触发时, 其中一个checkpointer线程会被选举为主checkpointer线程, 负责搜集其他checkpointer线程的工作状态, 如图 5所示. 首先, 各checkpointer线程在各自WAL日志文件中写入特殊日志记录BEGIN CHECKPOINT cno (cno是单调递增的顺序号, 由主checkpointer线程统一申请); 之后, 各线程开始刷写脏页, 直至脏页链表为空.主线程跟踪其他线程工作状态, 在确保所有表空间脏页链表都已刷写完毕后, 通知所有线程在各自WAL日志文件中写入特殊日志记录END CHECKPOINT cno. GoldenX事务在执行过程中直接对PM数据页进行改写(但不立即刷写), 并在CLA bitmap中记录被改写的页面位置, 在刷写脏页时调用sfence+clwb+sfence指令, 完成指定PM内存区域的持久化.
图 5 跨NUMA节点Checkpoint机制
● 灾难恢复
系统异常掉电后重启, DBMS自动进入灾难恢复(crash recovery)过程, 以将数据库恢复到一致性状态, 这包括3个阶段: Analysis、Redo、Undo. Analysis阶段的工作目标是构建系统异常时刻的活跃事务列表和脏页列表, 其基本方法是找到最后一个完整检查点(即同时包含了BEGIN CHECKPOINT和END CHECKPOINT)之后, 开始顺序扫描后续的所有WAL日志记录, 从中分析出所有没有正常结束的事务和被更新过的页面. Redo阶段则从脏页列表中找到最小的页面LSN, 将其作为Redo操作的起始位置, 再次顺序扫描WAL日志记录并逐条回放所有日志记录. UNDO阶段则在Redo阶段基础上把所有没有结束标记的事务做撤销操作. 上述传统实现方法将出现两遍扫描和解析WAL日志过程, 代价非常高昂.
为了规避这种情况, GoldenX做了改进.
● 首先, DBMS系统将记录并保存最近2次检查点操作信息, 包括该检查点的操作顺序号cno、BEGIN CHECKPOINT和END CHECKPOINT在各个分布式WAL文件中的LSN、奇偶检验值等, 系统重启之时, GoldenX首先找到最近的第1个检查点, 验证不通过才找第2个最近检查点, 2个检查点都检验失败则启动Analysis过程. 通过上述方法, 可以快速定位到最近一次完整的检查点位置, 并从这个位置开始扫描和回放WAL日志.
● 其次, GoldenX把Analysis阶段和Redo阶段合并, 只需扫描一遍WAL日志. 传统Analysis阶段的目的是构建系统异常时刻的活跃事务列表和脏页列表, 并由此确定Redo回放的起始位置(即最小的脏页LSN), 但活跃事务列表和脏页列表其实没有必要提前构建好, 可边Redo回放每个日志记录边动态维护这2个列表, Redo回放的起始位置就是BEGIN CHECKPOINT的LSN位置.
各个表空间的WAL文件单独记录本表空间的数据变更情况, 因而相对独立, 各个表空间的Redo过程可并行启动和推进. 每个表空间在本地执行各自的日志扫描回放算法, 参见算法2.
算法2. NUMA本地日志扫描回放算法伪代码.
输入: WAL日志扫描起始位置start_scan_pos.
输出: 活跃事务列表active_tx_table和脏页列表dirty_pages_list.
1. 初始化active_tx_table和dirty_pages_list为空
2. startWALScan(start_scan_pos); //从起始位置开始扫描
3. rec=getNextWALRecord(⋅); //获取下一个日志记录
4. WHILE (rec!=NULL)
5. IF (!active_tx_table.contain(rec.tid)) THEN
//将该事务加入活跃事务列表, 并创建事务对象
6. active_tx_table.add(rec.tid);
7. END IF
8. IF (rec.type==‘END’ OR rec.type==‘COMMIT’ OR rec.type==‘ABORT’) THEN
//将该事务从活跃事务列表中移除
9. active_tx_table.remove(rec.tid);
10. ELSE IF (rec.type==‘UPDATE’) THEN
11. IF (!dirty_pages_list.contain(rec.pno)) THEN
//将该数据页号加入脏页列表中
12. dirty_pages_list.add(rec.pno);
13. END IF
//将该日志记录压入该事务的本地DUS中
14. active_tx_table.getTX(rec.tid).dus.push(rec);
15. replay(rec); //回放该日志记录
16. END IF
17. rec=getNextWALRecord(⋅);
18. END WHILE
在算法2中, 从起始位置开始扫描, 并获取一个WAL日志(line 2, line 3). 如果该日志中的事务ID不在活跃事务列表active_trx_table, 则将其加入该列表(line 5, line 6). 如果该日志中的事务已提交或已回滚, 则将其从活跃事务列表中移除(line 8, line 9). 如果该日志记录中的事务是写事务且未提交(line 10), 则将其加入脏页列表dirty_pate_list (line 11, line 12). 然后, 将该日志压入该事务本地DUS中(line 14). 最后, 将该日志进行回放(line 15). 然后获取下一个日志(line 17)并重复以上操作, 直至将所有日志记录扫描和回放完.
当完成所有日志记录的扫描及回放之后, 各表空间将分别得到一个活跃事务列表active_tx_table和一个脏页列表dirty_pages_list. 没有正常终止的事务将残留在活跃事务列表中, 这些事务对象的本地DUS包含了回滚该事务所需的全部信息. 进入Undo阶段后, DBMS利用上述DUS信息回滚这些事务, 具体步骤如下:
(1) 归并各个表空间活跃事务列表, 得到一个全局活跃事务列表g_active_tx_table.
(2) 从g_active_tx_table中取出下一个事务tx.
(3) 采用多路归并算法, 从各个表空间的tx.DUS获取GSN最大的日志记录.
(4) 撤销该日志记录的操作.
(5) 重复步骤(3)、步骤(4), 直到tx的所有DUS为空.
(6) 重复步骤(2)−步骤(5), 直到g_active_tx_table为空.
3 实验结果及分析
本节通过实验评估GoldenX PM存储引擎应用NUMA感知数据分布模型后的性能优化效果, 实验环境配置见表 2.
表 2 实验环境软硬件配置
3.1 WAL和表数据放置到PM后的性能提升
在评估完整的NUMA感知数据分布模型之前, 先考察将PM存储引擎的不同功能数据放置到PM上对系统总体性能的影响. 为了更真实地模拟实际工业应用场景下的性能表现, 本节基于单个NUMA节点上的SATA SSD、NVMe SSD、PM这3种设备评估GoldenX分别将WAL、表数据(含索引)放置到PM后的性能提升情况. 以TPC-B基准测试作为评估标准, 被测试表基础数据量为1亿条记录(约130 GB), 测试时仅剩余约8 GB的DRAM缓冲区大小, 每个场景测试时间30 min.
如图 6(a)所示, 在SELECT场景中, PM最高查询性能比NVMe SSD提升了53%, 比SATA SSD提升了1100%. 说明消除了跨NUMA访问所带来的开销后, 同等配置下PM比NVMe SSD具有更高的读性能优势. 这是因为GoldenX充分利用PM字节可寻址特性, 每次以粒度更小的256 B为单位读取PM数据, 而不是读取整个8 KB数据页, 减少了无效I/O带宽消耗.
图 6 TPC-B性能对比
在UPDATE场景中, PM最高性能比NVMe SSD提升了45%, 比SATA SSD提升了588%. 这是因为UPDATE操作包含了数据查询过程, 可利用字节可寻址特性减少无效读I/O; 同时, 由于开启CLA缓存页机制后, Checkpoint线程刷写脏页时只刷写变脏的CLA到PM, 减小了部分无效写I/O.
在INSERT场景中, PM最高性能比NVMe SSD提升了25%, 比SATA SSD提升了310%. 这是由于GoldenX改进型B+树减少了因排序造成的额外写开销, 有效提升了索引操作效率. 与SELECT场景相比, PM在纯写模式下优势有所减少, 原因是数据库以8 K为单位刷写数据页, NVMe SSD的4 K刷盘机制只需2次I/O, 而PM每次刷写256 B, 需刷写32次, 这部分抵消了PM的低时延优势.
从图 6(b)可以看出, 各场景下的CPU利用率与其性能表现密切相关. 需要指出的是, SATA SSD和NVMe SSD在所有场景下, CPU wait均不为0, 数据磁盘繁忙程度均达到100%. 说明磁盘I/O达到了瓶颈. 以SELECT测试为例: NVMe SSD场景下, CPU wait为10%; SATA SSD场景下, CPU wait为30%; 而PM场景下, CPU wait为0. 从图 6(c)可以看出, SELECT场景下, NVMe SSD吞吐量是SATA SSD吞吐量的8倍; UPDATE场景下, NVMe SSD吞吐量是SATA SSD吞吐量3.8倍; INSERT场景下, NVMe SSD吞吐量是SATA SSD吞吐量的3.2倍. 这与它们的TPC-B性能表现是一致的.
从系统角度看, 相对于传统SSD+DRAM架构, 引入PM之后的性能优势得益于如下3个方面.
● 一是更低的读写时延. 在DBMS这类小数据块读写场景下, 读写吞吐量一般不会率先成为瓶颈. PM相对于SATA SSD及NVMe SSD具有更低的访问时延, 因此可获得更高的IOPS能力.
● 二是减少或避免了数据的写放大现象. 在传统存储介质下, DBMS以数据页为单位进行I/O, 即使事务只修改了一个字节, 也需要将整个数据页持久化到磁盘, 写放大现象严重. 引入PM后, 利用其字节可寻址特性, 仅需将数据页改动的字节(或CLA)刷写到磁盘, 这减小或消除了写放大现象.
● 三是更短的I/O访问路径. 一方面, 通过PMDK, 以用户态方式读写PM, 既避免了内存上下文切换开销, 又旁路了操作系统内核, 缩小了I/O软件栈长度; 另一方面, 通过CPU指令, MOVNT向PM拷贝数据, 拷贝不必经过CPU CACHE, 进一步减小了I/O访问路径.
此外, 当数据存放在SATA SSD或NVMe SSD时, 不论WAL是否放置在PM上, 上述4种测试场景的TPS性能都波动不大. 这是因为基于顺序I/O的WAL写操作不是系统性能瓶颈. 所以, 仅仅把WAL日志存放到PM上, 并不能从根本上提升整个DBMS的处理能力. 当前最新的相关研究[22, 28]仅把WAL放置到了PM上, 并且其在实验时预置数据量要么远小于DRAM缓冲区大小, 要么与DRAM缓冲区大小相当, 这导致DBMS直接从缓冲区读取大部分数据页, 无须频繁磁盘I/O, TPS性能被高估. 文献[18]则采用DRAM+NVM+SSD三级架构, 性能与数据量的大小关系密切, 从其测试结果也可以看出, 当数据量越大时, 访问SSD上的冷数据概率越大, 导致其性能衰减幅度很大. 而本文实现方案采用DRAM+NVM两级架构, 在NVM容量满足数据存储的前提下, 数据量大小对事务处理性能的影响不大. 根据实际测试: 当数据量分别为50 GB, 100 GB, 150 GB, 200 GB时, 在SELECT、UPDATE、INSERT场景下, 其TPS变化均在1.5%以下.
如图 6(d)所示, 测试了DRAM缓存容量超过数据存储规模场景下不同存储组合的UPDATE性能. 当DRAM缓存容量超过数据存储规模时, 存放在NVMe SSD上的数据可全部缓冲于DRAM缓存中, 数据访问I/O不再是关键瓶颈. 此时, WAL在PM上比WAL在NVMe SSD上性能提升18%. 这说明仅仅改造WAL适配PM, 只有在数据文件I/O不是主要瓶颈的情况下才能提升数据库性能. 当数据、WAL均在NVMe SSD时, DRAM缓存为200 GB比DRAM缓存为8 GB的性能提升了23%; 而当数据、WAL均在PM时, DRAM缓存为8 GB和200 GB的性能变化不大. 这是因为GoldenX直接从PM上读写数据, 对DRAM缓存大小不敏感.
3.2 NUMA感知的数据分布模型性能评估
本节选取开源PostgreSQL12 (下面简称PG)作为参照对象, 设计了3个对比测试案例.
● Case1: 两个PM卡都在NUMA Node 0, PG进程绑定在NUMA Node 0.
● Case2: 两个PM卡分别位于NUMA Node 0和Node 1, PG进程不绑定NUMA节点.
● Case3: 两个PM卡分别位于NUMA Node 0和Node 1, 直接启动GoldenX进程.
测试环境中, 每个PM各创建1个表空间, 因PG不支持分布式WAL, 故选择其中一个PM存放WAL日志. 基础数据为1亿条(约130 GB), 使用YCSB进行对比测试. 共设计了6个不同测试场景(其中, B和C属于自定义场景): A为100% SELECT, B为100% UPDATE, C为100% INSERT, D为50%SELECT+50%UPDATE, E为95%SCAN+5%INSERT, F为50%SELECT+50%READ-MODIFY-WRITE.
如图 7所示, 在YCSB-A场景中, GoldenX的SELECT性能比Case1提升了199%, 比Case2提升了317%.一方面, 尽管Case2拥有两个NUMA节点的CPU计算资源, 但受制于跨NUMA节点I/O性能衰减问题, 其TPS性能反而不如仅有一半CPU资源的Case1. 另一方面, 虽然Case1避免了跨NUMA节点访问PM问题, 但只能使用一半的CPU资源, 导致算力不足, CPU成为性能瓶颈. 这表明GoldenX基于I/O代理模式的只读事务数据访问路径是高效的, 它既降低了跨NUMA节点访问PM的I/O性能损耗, 又能发挥出两个PM的读性能潜力.
图 7 GoldenX和PG适配PM性能对比
在YCSB-C场景中, GoldenX的INSERT性能比Case1提升了64%, 比Case2提升了190%. 这说明GoldenX的NUMA感知架构有效提升了数据库的写入性能. Case1高于Case2的主要原因是, Case1消除了跨NUMA节点访问PM所产生的额外开销. 而Case3高于Case1的原因是: Case3把刷写WAL日志的I/O均摊到2个PM上, 而Case1则集中刷写到一个PM上, 导致该PM成为性能瓶颈. 与SELECT相比, 在INSERT性能上, GoldenX对PG的优势有所减小. 其原因是: 跨NUMA节点访问时, PM读带宽衰退程度比写带宽高10倍.
在YCSB-B场景中, GoldenX的UPDATE性能比Case1提升了105%, 比Case2提升了257%, 提升幅度处于只读与只写场景之间. 这是因为UPDATE时, 数据库要先从PM上读取旧数据, 更新后再将其写回, 属于读写混合场景, 参见YCSB-A和YCSB-C的原因分析. 其他几个YCSB测试场景也属于读写混合模型, 在此不一一叙述.
在上述对比分析过程中, 我们都分别选择了各个场景下的最高TPS性能做比较. 在YCSB各场景中, GoldenX随着并发数的增加性能呈线性提升, 并发数为50左右时达到性能拐点, 然后随并发数的增加略有提升但趋于平稳. 相比于Case1和Case2, 随并发数的增加, GoldenX性能提升更加明显. 这是由于GoldenX设计的数据空间分布和最小访问路径能够有效减少跨NUMA节点访问PM的I/O瓶颈, 从而提升系统的整体性能.而Case2并发数为10时就达到了峰值, 性能拐点更低. 说明跨NUMA节点访问PM的I/O性能衰减, 是影响性能提升的主要瓶颈之一. 观察所有测试场景可以发现: 即使空闲了一半CPU算力, Case1的TPS性能也都明显高于Case2. 这说明了NUMA感知对于PM存储引擎的重要性.
在50个并发数的YCSB-A纯读场景下, Case3的事务平均处理时长比Case2下降了76.02%; 在50个并发数的YCSB-C纯写场景下, Case3的事务平均处理时长比Case1下降了33.8%. 这是因为Case1只存在WAL集中存储在一个PM所导致的I/O性能瓶颈问题, 不存在跨NUMA节点写PM的I/O性能衰减问题以及CPU算力不足问题; Case3的事务平均处理时长比Case2下降了64.8%, 其原因是Case2不仅存在着跨NUMA节点写PM问题, 还存在着WAL集中存储在一个PM所导致的I/O性能瓶颈问题. 对照Case1测试结果可估算: 若排除WAL集中存储在一个PM所带来的I/O性能衰减因素, Case3优化跨NUMA处理机制后相比Case2的事务处理时长下降幅度为64.8%−33.8%=31%. 上述纯读和纯写事务处理性能提升情况与第2.1.3节所述的代价估算模型基本一致, 这说明本文设计的读/写事务跨NUMA节点访问PM模型是有效性的, 其代价估算模型基本准确.
3.3 基于TPC-C模型的事务处理性能评估
TPC-C是衡量联机事务处理系统性能与可伸缩性的行业标准基准测试. 本节使用BenchmarkSQL工具评估GoldenX分别将WAL、表数据(含索引)移动到PM后的性能提升情况. 测试场景是1 000个仓库,初始数据量是100 GB, 实验场景参照第3.1节和第3.2节.
从图 8(a)可见, 在DRAM缓存为8 GB时, 仅仅将WAL放置到PM上并不能明显提升性能, 原因是其性能瓶颈不在于写WAL日志, 而在于读写NVMe SSD上的数据文件; 将数据也放置到PM上后, TPC-C性能比数据在NVMe SSD上提升了24.4%. 当DRAM缓存为200 GB时, 3个测试场景的测试结果变化并不大. 主要原因是TPC-C场景下数据读取占比较高, CPU利用率已经近100%, 性能瓶颈由磁盘I/O变成了CPU.
图 8 TPC-C性能对比
从图 8(b)可见, GoldenX的TPC-C性能比Case1提升了90%. 主要原因是PG绑定了单个NUMA节点, 性能瓶颈在CPU; 而GoldenX可以同时利用两个NUMA节点的算力, 从而使其性能近乎翻倍. GoldenX的TPC-C性能比Case2性能提升了134%, 这说明如果不绑定单个NUMA节点, PG更加难以有效发挥PM的优势. 这与上述YCSB读写混合场景下的测试结论是一致的.
4 总结
新型硬件技术的发展, 为数据库系统带来新的机遇和挑战. 特别是持久性内存的出现, 打破了传统内外存之间的界限, 对现有软件体系结构带来颠覆性影响. 传统的数据库系统软件架构及主要算法都是基于经典的三层存储架构设计的, 需要在数据和日志布局、存储引擎设计等方面重新优化设计, 来适配新型计算存储硬件, 以充分发挥新型硬件的潜力, 提升数据库系统的性能. 本文介绍了中兴通讯新一代关系数据库GoldenX在适配NUMA特性和持久内存所进行的设计实践, 重点探讨了NUMA感知的PM存储引擎下的数据空间分布、事务处理和异常恢复等问题. 最后, 实验结果表明, 本文提出的方法能够显著降低数据库日志和数据跨NUMA节点访问PM时的开销. 但是本方法仍有需要进一步完善之处, 比如跨NUMA节点的分布式WAL日志机制, 在主备集群(或节点)间同步数据时, 需要获取每个事务的完整日志, 这时需遍历多个PM, 其时间代价略高于集中式WAL日志.
展望未来, 数据库运行环境将逐步转移到以FPGA/GPU/TPU为代表的异构算力、以NVM为代表的新型存储和以RDMA为代表的新型网络的硬件平台上. 如何设计和优化数据库系统软件以便能更有效地发挥新型硬件的潜能, 是数据库系统重要发展方向之一, 我们将持续探索和不断优化数据库系统对新型硬件的感知、适配和优化技术.