万字长文!对比分析了多款存储方案,KeeWiDB最终选择自己来

大数据时代,无人不知Google的“三驾马车”。“三驾马车”指的是Google发布的三篇论文,介绍了Google在大规模数据存储与计算方向的工程实践,奠定了业界大规模分布式存储系统的理论基础,如今市场上流行的几款国产数据库都有参考这三篇论文。

  • 《The Google File System》,2003年
  • 《MapReduce: Simplified Data Processing on Large Clusters》,2004年
  • 《Bigtable: A Distributed Storage System for Structured Data》,2006年

其中,Bigtable是数据存储领域的经典论文,这篇论文首次对外完整、系统的叙述了Google是如何将LSM-Tree架构应用在工业级数据存储产品中的。熟悉数据库的朋友,一定对LSM-Tree不陌生。LSM-Tree起源于上世纪70年代,1996年被正式提出,之后Google成功实现商业化应用。

LSM-Tree的核心思想是“Out-of-Place Update”,可以将离散随机写转化为批量顺序写,这对机械硬盘作为主流存储介质的时代而言,能大幅度提升系统吞吐。现如今,已经有一大批成熟的KV存储产品采用了LSM-Tree架构,例如DynamoDB, HBase, Cassandra和AsterixDB等。然而,工程实践往往存在一种取舍,几乎很难找到一个完美契合所有应用场景的设计。LSM-Tree在带来优秀的写入性能的同时,也带来了读写放大和空间放大问题。

随着硬件技术的发展,固态硬盘逐渐替代机械硬盘成为存储的主流,曾经的核心因素(随机写和顺序写的性能差异)现在也不再那么核心。那么现在存储系统设计的核心是什么呢?KeeWiDB倒是可以给你答案图片

高性能、低成本!如何减小固态硬盘擦除次数?如何延长使用寿命?这些都是KeeWiDB研发团队重点突破的地方。基于此,本文将重点阐述KeeWiDB中存储引擎的设计概览,详细介绍数据如何存储、如何索引,给读者提供一些KeeWiDB的思考和实践。

一、存储层

图1 展示的是存储在磁盘上的数据文件格式,数据文件由若干个固定大小的Page组成,文件头部使用了一些Page用于存储元信息,包括和实例与存储相关的元信息,元信息后面的Page主要用于存储用户的数据以及数据的索引,尾部的Chunk Page则是为了满足索引对连续存储空间的需求。Page至顶向下分配,Chunk Page则至底向上,这种动态分配方式,空间利用率更高

file 图1 KeeWiDB的存储层架构

和主流涉盘型数据库相似,我们使用Page管理物理存储资源,那么Page大小定为多少合适呢?

我们知道OS宕机可能产生Partial Write,而KeeWiDB作为一个严格的事务型数据库,数据操作的持久性是必须满足的核心性质之一,所以宕机不应该对数据的可用性造成影响

针对Partial Write问题,业界主流的事务型数据库都有自己的解决方案,比如MySQL采用了Double Write策略,PostgreSQL采用了Full Image策略,这些方案虽然可以解决该问题,但或多或少都牺牲了一定的性能。得益于SSD的写盘机制,其天然就对物理页写入的原子性提供了很好的实现基础,所以利用这类硬件4K物理页写入的原子特性,便能够在保证数据持久性的同时,而不损失性能。此外,由于我们采用的索引相对稳定,其IO次数不会随着Page页大小改变而显著不同。故权衡之下,我们选择4K作为基本的IO单元

至此,我们知道KeeWiDB是按照4K Page管理磁盘的出发点了,那么是否数据就能直接存储到Page里面呢?

如你所想,不能。针对海量的小值数据,直接存储会产生很多内部碎片,导致大量的空间浪费,同时也会导致性能大幅下降。解决办法也很直观,那就是将Page页拆分为更小粒度的Block

图2 展示了Page内部的组织结构,主要包括两部分:PageHeaderData和BlockTable。PageHeaderData部分存储了Page页的元信息。BlockTable则是实际存储数据的地方,包含一组连续的Block,而为了管理方便和检索高效,同一个BlockTable中的Block大小是相等的。通过PageHeaderData的BitTable索引BlockTable,结合平台特性,我们只需要使用一条CPU指令,便能够定位到页内空闲的Block块

file 图2 Page组成结构

而为了满足不同用户场景的数据存储,存储层内部划分了多个梯度的Block大小,即多种类型的Page页,每种类型的Page页则通过特定的PageManager管理。

图3 展示了PageManager的主要内容,通过noempty_page_list可以找到一个包含指定Block大小的Page页,用于数据写入;如果当前noempty_page_list为空,则从全局Free Page List中弹出一个页,初始化后挂在该链表上,以便后续用户的写入。当Page页变为空闲时,则从该链表中摘除,重新挂载到全局Free Page List上,以便其它PageManager使用。

file 图3 PageManager组成结构

想必大家已经发现上面的数据块分配方式,和tcmalloc,jemalloc等内存分配器有相似之处。不同的是,作为磁盘型空间分配器,针对大块空间的分配,KeeWiDB通过链表的方式将不同的类型的Block链接起来,并采用类似Best-Fit的策略选择Block。如图4所示,当用户数据为5K大小时,存储层会分配两个Block,并通过Block头部的Pos Info指针链接起来。这种大块空间的分配方式,能够较好的减小内部碎片,同时使数据占用的Page数更少,进而减少数据读取时的IO次数

file 图4 Block链式结构

以上便是用户数据在KeeWiDB中存放的主要形式。可以看出,用户数据是分散存储在整个数据库文件中不同Page上的,那么如何快速定位用户数据,便是索引的主要职责

二、索引层

2.1 选型

KeeWiDB定位是一个KV数据库,所以不需要像关系型数据库那样,为了满足各种高性能的SQL操作而针对性的建立不同类型的索引。通常在主索引上,对范围查询需求不高,而对快速点查则需求强烈。所以我们没有选择在关系型数据库中,发挥重要作用的B-Tree索引,而选择了具有常数级等值查询时间复杂度的hash索引

hash索引大体上存在两类技术方案Static Hashing和Dynamic Hashing。前者以Redis的主索引为代表,后者以BerkeleyDB为代表。如图5所示,Static Hashing的主要问题是:扩容时Bucket的数量翻倍,会导致搬迁的数据量大,可能阻塞后续的读写访问。基于此,Redis引入了渐进式Rehash算法,其可以将扩容时的元素搬迁平摊到后续的每次读写操作上,这在一定程度上避免了阻塞问题。但由于其扩容时仍然需要Bucket空间翻倍,当数据集很大时,

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值