【论文翻译】(KV存储引擎设计):Fast Scans on Key-Value Stores

摘要

kv存储引擎近些年越来越受欢迎,因为它可以弹性地扩缩容,对于get/put可以维持高吞吐量,有更低的延迟。这些得益于它的简单,然而简单也带来一定的代价:目前的kv存储系统不能很好的支持scan性能, 所以它不适用于处理复杂、分析型的query。分析型的query要求更好的数据局部性,然而get/put的高吞吐要求离散的索引。这篇paper展示了一种折中的方式可以兼具两者。讲述了分布式kv存储系统TellStore,对于kv、分析型和混合场景都表现了不错的性能。在论文最后的章节中展示了benchmark的测试结果。

1. 介绍

相比于传统关系型数据库,kv store由于它的弹性、可扩展性、部署和管理简单而受到欢迎。而且它的性能也是可预期的:每条get/put请求花费确切的常量级时间。这些特性使得一些应用可以建立在kvs之上。

近期的一些工作展示了kvs可以以高效可扩展的方式run oltp的负载。这些工作采用了一种"SQL-over-NoSQL"的方式:数据持久化存储在kvs(NoSQL),应用逻辑(with SQL support)放在一个分隔的处理层。在这个工作中我们要提出的一个重要问题是这样一个"SQL-overNoSQL"的架构使用相同的kvs能否同时满足OLAP和OLTP的负载?

这个问题是有价值的,因为oltp和olap的请求访问方式是不同的。大多数kvs的get/put层是为oltp型负载所设计的,但对于分析型的请求并不是可行的,往往要涉及读全部的数据或者大范围的数据。分析型的系统提供适用于它们的访问方式:允许一次性访问到所有的数据(full table scan),下推select条件或者join条件到存储层。大多数kvs是不具备这样的能力的,所以对于scan的性能是不可接受的。

webpuploading.4e448015.gif正在上传…重新上传取消

屏幕快照 2019-07-17 下午11.43.16.png

为了说明目前kvs不适合分析型的应用,table1展示了简单的查询5000wkv对数据的时间,返回了特定字段(YCSB# Query 1 in Section 7.2.1)的最大值(?最大值是什么意思?)。对于这个实验,论文中用了四种机型(在7.1章节中展示了配置)。因为rocksdb是一个单机存储引擎,测试它只用了1/4的数据量。Cassandra花费了大约19m处理这个简单的查询。RAMCloud和HBase需要大约半分钟。对于这三种kvs,使用服务端聚集数据,因为传输数据在客户端聚集会使得性能更差(???)。即使将这些数据集全部放在内存中,性能也是不可接受的。在这个实验中可以接受的系统是rocksdb,memsql和kudu。rocksdb是一个适用于oltp系统的开源数据库存储引擎,被facebook所使用。MemSQL是分布式的、内存关系型数据库系统,经过很好的优化(相比与其它的有很快表达式编译??)。kudu 是一种基于列存的kvs,适用于混合负载(分析型和get/put)。但是如果有实时的延迟的要求,这7种系统都还远达不到要求。这篇论文将提出TellStore的不同变种,可以实现更低的延迟,更快的响应时间。

kudu的性能表现说明了即使为这种查询做了特定优化的系统也不容易在kvs实现快速查询。这里的问题在于同时支持get/put和scan操作是一个矛盾的目标。有效的scan要求很好的数据局部性,而get/put要求离散的索引。多版本和回收机制的实现也会很大的影响性能,是需要考虑的点。这篇论文展示了一种折中的方式,说明在相同kvs上支持混合负载是可能的。

这篇论文主要贡献了以下几个点:
第一,论文提到了Sql-over-NoSql架构,展示了它如何处理混合负载的。
第二,论文提出了开发一个同时支持点查和范围查询的kvs的存储格式设计(列存/行存等)。
第三,论文介绍了TellStore,一个分布式的内存kvs和两种不同kvs的实现(Sections 4 to 6)。
最后,论文给出了用扩展的YCSB benchmark测试的实验性能和工业界(the telecommuni- cations industry )使用变体实现kvs支持有效查询的负载。
这个工作的主要思想是构建一个支持混合型负载的kvs是可能的,而且性能是可接受的。相比于从文献中简单提出概念,从整体上考虑和罗列这些设计问题是比较重要的。

REQUIREMENTS

2.1 SQL-over-NoSQL架构

webpuploading.4e448015.gif正在上传…重新上传取消

屏幕快照 2019-07-19 下午10.35.20.png

Figure 1 描述了在kvs之上支持OLTP/OLAP混合负载的SQL-OVER-NOSQL架构。在这种架构中,数据持久化在分布式的包含get/put和scan接口的kvs中,事务和查询的逻辑在单独的处理层。处理层也需要处理并发的查询和事务。在整个过程中,我们使用SI(Snapshot Isolation)作为隔离级别,使用Figure 1 中展示的Commit Manager来实现。SI(或者多版本并发控制的其它形式)被作为是默认的隔离级别实现,尤其是在分布式系统和数据库混合负载的场景下,因为oltp事务从来不会阻塞或者干扰olap的查询。在SI隔离级别下,Commit Manager 简化设计了事务的时间戳,保留了对活跃、已提交和回滚事务的追踪,因此几乎不会成为系统的瓶颈。
SQL-Over-NoSQL架构的一个主要优势是elastic(弹性)。机器可以独立的增加到存储层或者处理层(计算层)。比如,可以新增OLAP节点放在处理层来处理复杂的分析型查询。当query完成时,这些节点可以关闭或者更改属性来处理其它任务。这种结构也能有效地支持混合型的事务/分析型的负载:两种负载都可以并发的跑在单独的数据副本上。
为了高效地实现SQL-OverNoSQL架构,分布式kvs必须满足以下的要求:

Scans
除了get/put请求,kvs还必须支持高效地scan操作。为了减少网络传输的代价,kvs应该支持selections、projections和简单的聚合,以实现只有相关的数据从存储层传输到处理层。另外,支持sharded scan对于许多应用来讲是一大优势。

Versioning
为了支持多版本的并发控制,kvs必须要每条记录的不同版本,根据事务的时间戳来返回正确的数据版本。Versioning需要有旧版本回收来释放旧版本数据占用的空间。

Batching and Asynchronous Communication
为了实现高oltp性能,oltp处理节点将多个请求打包发给存储层是关键的。使用这种方式,从处理层到存储层传输信息的往返代价就被分摊到了多个并发事务中。而且,这种打包请求的方式需要异步执行,使得处理层在等待前一次请求被kvs处理完成之前可以收集下一轮的请求。

2.2 实现的难点

实现这三个需求的最大难点在于它们是矛盾的。这也是为什么今天大多数kvs只支持get/put(例如Cassandra 和 HBase),有的实现了多版本(例如RAMCloud 和 HBase),有些实现了异步传输。所有的这些特征都能很好的支持离散的数据结构,为get/put操作而设计。当查找一行特定版本的记录时,是否它是紧凑的存储是不重要的。然而对于scan,要求很高的数据局部性和数据紧密的存储使得每一次存储层的访问能够返回尽可能多的相关数据。数据局部性对于磁盘和内存中的scan都很重要。而且,加入scan的特征也带来如下的矛盾点:
Scan vs. Get/Put
大多数的分析型系统使用列向存储层来增加数据的局部性。然而,kvs,不需要物化数据,典型的使用行向存储来处理get/put请求。
Scan vs. Versioning
不想关的多版本记录会减少数据的局部性而降低scan的性能。而且,scan过程中check记录的版本相关,也会花费高昂的代价。
Scan vs. Batching
像get/put请求那样打包scan请求并不是有利的。OLTP的负载要求常量级的返回时间和对get/put请求可预期的返回时间。相反,scan会带来返回时间的多变性,取决于谓词的选择性和需要处理的复杂query涉及的字段数量。

幸运地,如我们所描述的,这些矛盾点是可以以一种折中的方式来解决的。这篇论文的目的是去研究kvs的存储结构设计和通过实验证明哪种方式最有效。

3. DESIGN SPACE

这部分给出了一个整体的概括,关于构建一个支持bulk操作和scan的kvs的大多数重要的设计问题。

3.1 Where to Put Updates?

这里有三种可能的设计方式来实现updates(put, delete和insert):update-in-place, log-structured, and delta-main。
update-in-place是大多数关系型数据库系统所采用的方式。新的记录(insert)存储在已存在的空闲页上,更新已有记录(puts or deletes)通过在原有记录的地方进行覆盖写来实现。如果记录是fixed,这种方式是很好的,因为这样几乎就不会有碎片产生。然而,这种方式对于多版本维护是tricker的。如果多版本数据是在原地进行维护,则会引起明显的存储空间碎片和失去局部性。这种方式的另一个问题是限制来并发:为了创建一条记录的新版本,整个page都要被latched(与锁的区别??)(a latch是lock-term的锁,一旦page被更新完就可以释放)。

Log-structured的存储设计第一次被提出是在文件系统的应用中【40】。这种方式被一些kvs所使用,例如RAMCloud。主要想法是通过追加写的方式实现所有的update操作。 Log-structured的存储有两个重要的优势:(1) 不会有空间碎片产生。(2)不会有并发问题,因为追加写可以以一种不阻塞的方式实现【29】。一个主要的缺点是如果没有做优化的话,对于scan是很费的,scan需要读旧版本的数据。而且,如果一个记录很少被更新,是很难去做旧版本回收的。一个被称为LSM-Tree的数据结构被用在LevelDB [23], RocksDB [16], and Kudu [19]中。这种变体会定期重组数据来提升读的性能。

第三种方式delta-main,是被SAP Hana所提出的【17】,也被用在几个research 工作中,例如AIM【10】。这种方式以一种写优化的数据结构来收集所有的update请求(called delta),并且以一种读优化的数据结构来维护大部分的数据(called main)。这两种数据结构会定期的进行merge,这种方式尝试将log-structed的优点(fast get/put)和update-in-place(fast scan)的优点结合起来。

3.2 How to Arrange Records?

最流行的两种设计方式是row-major和column-major。row-major以连续的字节流方式将数据存储在页中[24],这种布局通常对于get/put在整条记录上很有效。column-major将数据垂直分区,将表的整列存储为连续的字节流。这种column-major组织方式对于分析型的query是有益的,这种query经常会涉及到scan列的一个子集[25, 3, 2, 45, 9, 8]。另外,column-major支持向量操作(SIMD)来加速批量操作和在现代硬件上的scan [47, 49]。一种column-major的变体称作PAX[5],在每个page中存储多条记录,而在页内,所有的记录以一种column-major的方式存储。PAX是一个在纯列向设计和行向设计之间很好的折中。

Column-major在定长的value上表现比较好。这就是state-of-the-art系统避免使用变长值的原因,要么简单的不允许使用它们(as AIM[10),要么使用在堆上分配内存,存储对应指针的方式(as in MonetDB [8] and HyPer [33]),或者用一个字典并存储固定大小的字典代码(as e.g. in SAP/HANA [18] and DB2/BLU [39]))。

3.3 How to Handle Versions?

如第二部分所描述的,我们需要支持记录的多版本来实现多版本并发控制。这里有两种主要的实现方式:(a) 存储一条记录的所有版本在相同的位置 (b) 将记录的多个版本使用链表链起来。第一种变体通常与update-in-place结合使用,用这种方式创建新版本的成本更低,但是合并页面做垃圾回收时会花费高昂的代价。第二种变体更适合log-structured的存储,clustering versions in an append-only data structure would require a costly copy of all previous versions to the head.
链接记录的指针会消耗空间,遍历链表会涉及到很多cache miss也会花费很大的代价。好的一方面,第二种方式简化了垃圾回收,因为它可以以一种旧版本数据日志流截断的方式来实现。而且可以减少空间的碎片化。

3.4 When to do Garbage Collection?

回收旧版本数据的方式有两种:(a) 使用单独的线程做周期的回收。(b) scan的过程中做piggy-back回收。方法b增加了scan的时间,但是也换回了垃圾回收的收益:相比其它table,scan频繁从垃圾回收中受益的table会更频繁的做垃圾回收。piggy-back回收的另一个优点是数据处理过程中随时随地做回收,也避免了拿取数据引起的cache miss。

3.5 Summary

屏幕快照 2019-07-24 下午11.59.27.png

table 2给出了在存储效率(碎片)、并发、实现多版本和垃圾回收的花费和scan、get/put操作的效率的前三个纬度之间权衡的方式。第四个纬度垃圾回收正是实现这些性能特征的基础。一个kvs的性能是由这些技术的组合所决定的。总而言之,构建一个kvs总共有24种不同的方式:
(update-in-place vs. log-structured vs. delta-main)
×(row-major vs. column-major / PAX)
×(clustered-versions vs. chained-versions)
×(periodic vs. piggy-backed garbage collection)
另外,还有混合的方式,例如periodic和piggy-backed回收方式可以组合实现。幸运的是,仅仅只有几种变体方式是有意义的:Log-structured方式的updates 和 列向的存储结构设计是没有意义的,因为明显“delta-main & column-major”的组合方式更有效。另外“log-structured & chained-version”的方式明显比"log-structured & clustered-versions"更优。

这其中最有的两个组合方式是log- structured with chained-versions in a row-major的方式和使用delta-main的数据结构with clustered-versions in a column-major的格式。接下来的两个章节将会描述我们使用这两种方式在TellStore上的实现,叫做TellStore-Log 和 TellStore-Col。 第六章给出了TellStore的实现细节,对于所有的TellStore的变体都是很重要的。第七章包含了通过比较不同变体之间的权衡得出的性能评估,使用已存在的一些kvs作为baseline。出了tellstore-log和tellstore-col,第七章节包含了第三种变体的结果,tellstore-row,与tellstore-col是相似的,但是使用row-major的存储格式可以更好的研究对于混合oltp/olap的负载权衡行存和列存。

4. TELLSTORE-LOG

TellStore-Log的实现由RAMCloud所启发,并加入了新的修改以支持有效的scan。

Where to Put Updates?

屏幕快照 2019-07-29 下午10.50.41.png

Figure 2描述了TellStore-Log的主要数据结构:详细描述了记录的布局(kv对)以及使用hashtable来索引日志中的记录。日志本身是被分为多个片段使用链表链起来的多个pages来存储所有的kv对。

日志是使用追加写的数据结构:内存分配以一种无锁的方式自动增加到page的头部。一旦记录追加到日志中,将变为不可变的。这个属性使得恢复和复制日志变得简单,因为复制过程只需要监视头部。因为无锁的特性,相同key的记录可以并发的追加到日志中。哈希表始终是同步点,一条记录只有等指向它的指针成功地插入或者更新到哈希表中才认为生效。以防冲突,数据变为immutable之前在日志中是无效的状态。delete以一种特殊标记的没有数据的update的形式被写入。

hashtable使用锁实现会变成性能点。并发无锁的hashtable设计对于特定的路径是一种常用的优化手段。尤其,在实现扩容时面临点查和update性能的权衡。TellStore预先分配一个定长大小的hashtable,为所有在存储节点上的表共同使用。实现上采用线性探测的开放寻址算法以解决数据冲突的问题。不利的是,开放寻址在高负载下往往表现不佳。为了节省内存以使得可以使用较小的内存保存更大的表,哈希表只记录表的id、记录的key和指向记录的指针。

How to Arrange Records?

追加写的方式本质上与行向存储结构相关联。为了支持有效的scan,日志中的记录必须是完全独立的。我们尤其想要避免去哈希表中查找来确定一条记录是否有效(没有被delete或者覆盖写)。这个约束对版本控制有一些影响,将在下一节中提到。

TellStore-Log在存储节点上为每个表分配一段单独的log。这使得scan可以只处理相关的pages,提高了数据的局部性。sca对有效的记录的数量是敏感的,这对局部性的要求有影响,4.4节将会提到。

4.3 How to Handle Versions?

日志的不可变性使得一条记录的新版本只能追加到日志的头部。为了能够定位到旧版本的数据,我们在每个记录旁边存储指向前一个版本的指针来形成多版本链。另外事务为每条记录分配一个时间戳保存在元信息中,多版本的链表总是根据snapshot由新到旧严格排序的,哈希表总是指向最新的记录。给定一个snapshot,get请求会沿着链表查找直到遇到第一个满足条件的记录。遍历链表涉及到cache miss的代价,与fast put是一种权衡。
这种设计看起来是与所有数据都必须独立的要求是矛盾的:当scan日中中记录时,只有记录的时间戳可以拿到,不能判断是否数据是过期了的。为了避免哈希表的查找,我们为每条记录在元信息中增加了过期的时间戳((valid-to)作为不可变字段。一条记录被成功写之后,旧版本的记录的valid-to将会更新(lazily updated)为新版本的valid-to。给定一个snapshot, scan 可以通过比较两个时间戳来确定记录是否满足条件。

哈希表仍然是唯一的同步点,始终指向最新的元素。更新哈希表和设置valid to字段之间没有竞争条件,TellStore的SI实现不能确保在线事务的可见性(???)。

When to do Garbage Collection?

scan的性能被不再被任何活跃事务所看见的过期数据的数量所影响。为了更频繁合并那些经常被scan的table,垃圾回收放在正常scan的一部分中。扫描一个页面时,其运行状况按照页面中有效记录的数量与总记录数量的比率来计算,一旦该值降到某个阀值以下,页面将会被标记为垃圾回收。被标记的页面将在下一次scan时将剩余有效数据copy到log的头部并将回收后的页面放入空闲页面的池子中。一条记录被copy到日志头部之后,该键的多版本链的指针需要重新调整。为此,我们需要查找哈希表,沿着版本链找到正确的位置。这个操作由于很差的缓存局部性而成本高昂。为了从非频繁扫描的表中回收空间,由后台线程定期去扫描回收。

Summary

尽管Ramcloud是日志结构kvs的典型实现,但其scan性能较差。同时构建一个支持快速get/put请求、版本控制和scan的log-structed kvs是可能的。主要的要点是日志中多版本的仔细组织、有效的垃圾回收策略、延迟的元信息/时间戳实现来使得记录独立,无锁算法和良好的工程设计。

TELLSTORE-COL

屏幕快照 2019-07-31 下午10.42.34.png

TellStore-Col采用delta-main方式的核心理念,维护两种数据结构:main for read, delta for update。如Figure3所示,我们的实现实际涉及到4种数据结构:page list用来维护主要的数据,两个log存储delta(一个for insert, 一个for update)和一个哈希表来索引数据。

5.1 Where to Put Updates?
除了查询元信息字段,在main中的数据只用于read,所有update都被写入一个追加写的存储中。与TellStore-Log不同,delta被分成两个独立的log:update已存在的记录被写到update-log中,update不存在的记录被写到insert-log中。如第5.4节所示,这种分离使得从delta构建main更容易。索引中保留了一个标志位来标示指针指向delta或者main。除了这个标志位,索引表使用与TellStore-Log相同的哈希表。

5.2 How to Arrange Records?

       尽管对于两个delta-logs而言,行优先格式都是唯一合理的选择(如第4.2节所述),但main允许我们以行优先格式或列优先格式存储记录。

TellStore-Col是一种delta-main实现,它以称为ColumnMap [10]的列主格式存储其主页面,如图4所示。该想法是遵循Partition AttributesAcross Paradigm[5] (PAX),它将记录分组为多个页面并在其中这样的页面以列为主的格式组织它们。

如果表的每个字段的大小都是固定的,那么知道记录的第一个属性的位置就足够了。其他属性的位置可以根据页面中的记录数和每个属性的数据类型大小来计算。但是,如果字段可以具有任意大小(例如可变长度字符串),则这种简单的计算方法会失败。

我们在每页末尾分配一个堆,存储所有可变大小的字段。通过存储了4字节前缀的4字节偏移量的固定大小的元数据来索引此堆。虽然元数据字段以列主格式存储,但字段内容以行主格式存储在堆中。

这有两个优点:首先,在提取记录时,可变大小字段已经是行主格式,可以简单地复制到输出缓冲区。其次,通过以固定大小的列主格式存储前缀,我们可以加快常用的前缀扫描查询的速度,因为我们可以缩小候选元组的集合而不必查看堆。

5.3 how to handle version?

       与在log-structured方法中实现版本控制的方式类似(请参见第4.3节),TellStore-Col将创建时间戳记存储在一个valid-from字段中,作为记录元数据的一部分。 更新日志中的记录使用先前的指针从最新到最旧的链接在一起。 与主记录和插入日志中的记录一起存储的最新指针始终指向更新日志中的最新元素。 为了避免循环,从更新日志到主目录之间没有反向指针。

在主页面内,同一键的不同版本以列主格式从最新到旧连续存储。 Valid-from时间戳和new pointers也被转换为主要列格式,并作为普通属性存储在meta-data节中。 索引始终指向ColumnMap或插入日志中最新元素的元数据字段。 给定快照时间戳记,将从新元素扫描valid-from字段,直到找到快照中包含的时间戳记。

需要将最新指针存储在记录旁边而不是存储在哈希表中,以使记录自包含。 否则将需要进行扫描以在哈希表中执行查找以确定是否写入了新记录。 两个delta-logs中的记录仅存储创建它们的事务的时间戳,因此也不是独立的。 如第5.4节所述,这是扫描和垃圾收集性能之间的权衡。

5.4 when to do Garbage Collection?

       垃圾收集负责定期将两个增量日志中的更新合并到经过压缩和读取优化的main中。 因此,它在保证对良好scan性能的局部性要求方面起着至关重要的作用。 所有main pages都是不可变的,并使用写时复制方法重写。 为了避免干扰get / put和scan的并发访问,这是必要的,因为就地更新将需要在页面上锁存(请参阅第3.1节)。与TellStore-Log相比,压缩页面的代价更高,因为涉及转换 从行主格式更新为列主格式。 为此,垃圾收集器在单独的线程中运行,而不是扫描的一部分。 专用线程会定期扫描主页面中每个页面的元数据部分。 一旦它遇到一个包含已更新记录(通过检查最新字段)或不再包含在任何活动快照中(通过检查valid-fromfield)的记录的页面,该页面将被重写。

同一键的所有版本均从main和update-log中按照版本链进行收集。 任何活动快照中都不包含的带有时间戳的元素将被丢弃,而从更新日志中收集的元素将转换为以列为主的格式。 然后将元素从最新到最旧排序,并附加到新的主页上。 重定位记录后,最新字段将更新为指向新记录,以使并发更新知道重定位。 最后,垃圾收集器扫描插入日志中的所有记录,从更新日志中收集同一键的所有版本,并将它们以列主要格式写入主页面。 之后,两个增量日志都可以被截断,旧的主页可以释放到空闲页池中。

通过从update-log拆分插入,收集器只需扫描插入日志即可收集所有键,这些记录中的键都不是main的一部分。不利的一面是,这会减少扫描的数据局部性,因为在遵循版本链时,更新会强制扫描对更新日志执行随机查找。这里的前提是通过提高垃圾收集的效率,它可以以更高的频率运行,从而将更新日志的大小保持为最小。

积极地压缩页面:整个页面很快就会被重写,因为其中的单个元素变得无效。 这会导致在高负载下,高写入会被放大,这将对基于磁盘的系统产生很大的影响,而对基于内存的系统的影响则较小。 这种方法的扩展将是基于脏的紧凑化,类似于TellStore-Log。 另一方面,延迟压缩将使数据中的大部分保留在增量日志中,从而影响扫描性能

5.5 summary

       与TellStore-Log相似,仔细地平衡获取/放置,扫描和版本控制的不同需求,使我们能够设计一种支持事务和分析访问模式的数据结构。 扫描虽然受益于大部分数据都以紧凑的列主格式驻留的事实,但更新得益于增量的写优化日志实现。 可以通过将相同键的记录聚在一起并将其时间戳作为列主格式的常规字段进行处理来实现版本控制。 垃圾回收在增量主方法中很昂贵,因此是在专用线程中执行的,该线程定期垃圾回收陈旧的记录。

6 Implementation

TellStore是一个分布式的内存KVS。 诸如SQL-over-NOSQL系统处理层的节点之类的客户端使用Infiniband连接到Tell-Store。 每个TellStore实例存储一个不同的数据库子集,并支持第2节中所述的get/put,scan和版本控制接口。

尽管第4节和第5节介绍了存储引擎的总体设计,但本节将TellStore描述为可以使用任何如同上面描述的存储引擎的分布式KVS。 更多细节和不完整的描述可以在[36]中找到。 此外,TellStoreis开源[1]。

6.1 异步通讯

为了充分利用可用的CPU时间和网络带宽,在等待存储请求完成时,处理实例不应处于空闲状态。 这就是为什么TellStore使用一个称为InfinIO的异步通信库与处理层进行通信的原因。 InfinIO是专门为在Infiniband之上运行而构建的,它通过类似于Boost.Asio的API来利用用户级线程和回调函数,用于以太网通信。 对TellStore的所有请求都会立即返回调用对象级线程可以在其上等待的afuture对象。然后,InfinIO在将网络上的所有请求实际发送到TellStore之前透明地批处理所有请求。 同样,来自存储的响应在发送回处理节点之前被批处理在一起。 这种批处理大大降低了Infiniband链路上的消息速率,从而极大地提高了整体性能,否则它将成为性能瓶颈(请参见第7.2.3节)。

6.2 线程模型

       TellStore运行三种不同类型的线程,如图5所示:获取/放置线程,扫描线程和垃圾回收线程。 每当客户端打开连接时,get / putthread之一都会接受该连接,并独自承担处理该特定客户端发出的传入请求的责任。 每当客户端发出扫描请求时,get / put线程都会将请求放置在队列中,以由专用扫描线程处理。 为了确保扫描和获取/放入操作的吞吐量保持一致,TellStoreonly使用无锁数据结构。其中一个扫描线程具有扫描协调器的作用。 扫描协调器使用所有排队的扫描请求,并将其捆绑为一个共享的扫描。 协调器对存储引擎的页面集进行分区,并在扫描线程之间平均分配它们。 然后,所有扫描线程(包括协调器)都独立地并行处理其分区。 (部分)扫描结果使用RDMA直接写入客户端的内存中。借助此线程模型,TellStore可以根据预期的工作量灵活地配置资源以获取/放置和扫描请求。

6.3 数据索引

       第4和第5节介绍了TellStore如何组织和处理记录。 两种方法都使用无锁哈希表来索引存储在单个节点内的记录。 为了跨节点定位键,TellStoreimplements类似于Chord [44]的分布式哈希表。 但是,使用哈希表的选择与如何在KVS上实现快速扫描的问题正交,就像使用DHT的决定一样。 范围分区可以使用相同的技术。对于范围分区,通常会使用像B树或LSM这样的聚簇索引来对页面内的数据进行索引。 但是,这通常会使get / put操作变得更加昂贵。 为了支持范围查询,Tell使用了无锁的B树,该树仅在处理层中实现,如[30]中所述。

发布了13 篇原创文章 · 获赞 7 · 访问量 379

猜你喜欢

转载自blog.csdn.net/weixin_39966701/article/details/105135857