文章目录
Abstract
内存中键值存储(KVS)广泛用于缓存热数据,以解决基于磁盘的存储或分布式系统中的热点问题。但是,内存KVS内部的热点问题却被忽略了。由于近来热点问题变得更加严重的趋势,现有KVS中缺乏热点意识,这使得它们的性能不佳且对高度倾斜的工作负载不可靠。在本文中,我们探索了KVS中内存索引结构的热点感知设计。我们首先分析理想的热点感知索引可能带来的好处,并讨论有效利用热点感知的挑战(即热点转移和并发访问问题)。基于这些见解,我们提出了一种名为HotRing的新型热点感知KVS,该KVS已针对大规模并发访问一小部分项目进行了优化。 HotRing基于有序环哈希索引结构,该结构通过将头部指针移近热项目,从而快速访问热项目。它还应用了轻量级策略来在运行时检测热点转移。 HotRing在设计中全面采用了无锁结构,用于常见的操作(即读取,更新)和特定于HotRing的操作(即热点移动检测,头指针移动和有序环重新哈希),因此可以并发大量请求更好地利用多核架构。广泛的实验表明,在高度倾斜的工作负载上,与其他内存KVS相比,我们的方法能够实现2.58倍的性能提升。
1 Introduction
内存中键值存储(KVS)是存储基础结构(例如数据库,文件系统)中的重要组件,可将经常访问的数据缓存在内存中以加快访问速度。 KVS有助于提高这些系统的性能和可伸缩性,其中每秒需要处理数十亿个请求。 许多最先进的KVS(例如Memcached,Redis及其变体)已广泛开发并部署在企业的生产环境中,例如Facebook,Amazon,Twitter和LinkedIn。
热点问题(即在高度偏斜的工作负载中经常访问的一小部分项目)是现实情况中的常见问题,并且在文献中已进行了广泛的研究。 有许多集群范围内热点的解决方案,例如一致的哈希,数据迁移和前端数据缓存。 此外,单节点热点问题也得到了很好的解决。 例如,计算机体系结构利用分层存储布局(例如,磁盘,RAM,CPU高速缓存)来将经常访问的数据块高速缓存在低延迟存储介质中。 许多存储系统,例如LevelDB和RocksDB,都使用内存中的KVS来管理热点。
但是,这些方案通常会忽略内存KVS内部的热点问题。如图1所示,我们从内存中收集了来自阿里巴巴生产环境中的KVS的访问分布。我们观察到50%(每天的情况)到90%(极端的情况)的访问仅涉及总项目的1%,这表明热点问题在互联网时代变得空前严重。此现象背后有几个原因。首先,在线应用程序中的活跃用户数量一直在增长。实时事件(例如,在线促销,突发新闻)能够在短时间内吸引数十亿次访问某些项目,而快速访问这些热点至关重要。据报道,每0.1s的加载延迟将使亚马逊的销售损失1%,而每0.5s的Google搜索结果额外的加载延迟将导致流量下降20%。其次,此类应用程序下的基础架构变得复杂。管道(pipeline)中某处的小错误(例如由于软件错误或配置错误)通常会导致(不可预测的)重复访问某项(例如不断读取并返回错误消息)。我们希望这些不可预测的热点不会崩溃或阻塞整个系统。因此,在热点存在的情况下保持KVS的性能和可靠性非常重要。
许多索引结构可用于实现KVS,例如跳跃表,平衡/前缀( balanced/trie)树(例如Masstree)和哈希(例如Memcached,MemC3,MICA,FASTER),其中哈希因查找速度更快而最受欢迎。但是,我们观察到,大多数方法都无法察觉热点,因为它们通过同一策略管理所有数据。在这种情况下,与其他的非热点数据相比,读取热点数据会涉及相同数量的内存访问次数。从理论分析(在第2.2节中有详细介绍),我们发现在当前哈希索引中查找热点需要比最理想情况下做更多的内存访问。尽管存在减少内存访问的机制,但它们仅提供有限的作用。例如,CPU缓存有助于加快热点访问速度,但只有32MB的容量。rehash操作有助于减少每个冲突链的长度,但会显着增加内存占用量。这种情况为我们提供了进一步优化高度偏斜的工作负载中的热点访问的机会。
在本文中,我们提出了HotRing,这是一种可感知热点的内存KVS,它利用哈希索引进行了优化,该哈希索引针对大规模并发访问一小部分项目(即热点)进行了优化。最初的想法是对于某一个数据项的查找与其热度相关,即较热的项应被更快地读取。为了实现这一目标,必须解决两个挑战:热点转移-热点项的集合不断变化,我们需要及时发现并适应这种变化;并发访问热点本质上是由大量并发请求访问的,我们需要为它们维持高并发性。对于热点转移问题,我们将哈希索引每一项中的冲突链替换为有序环结构,以便在热点转移时,桶标头可以直接指向热点项目,而不会影响正确性。另外,我们使用轻量级机制在运行时检测热点转移。对于并发访问问题,我们采用了受现有无锁结构启发的无锁设计,并将其扩展为支持HotRing所需的所有操作,包括热点移位检测,头部指针移动和有序环rehash。
我们已经在模拟实际工作负载的基准下进行了广泛的实验评估,并将HotRing与基于无锁链的哈希和其他基准进行了比较。 结果表明,在工作负载极度不均匀的情况下,HotRing每秒可处理多达565M读取请求,与其他系统相比,提高了2.58倍。 对于 in-placeupdates(这是啥) 和 read-copy-updates respectively(分散的RCU?),它也分别实现了2.17倍和1.32倍的改进。 这证明HotRing是提高每个节点上热点处理能力的有效数据结构,从而使其成为一个高性能且高可靠的内存KVS。
我们的主要贡献概述如下:
- 我们在现有的内存索引中确定了热点问题,并证明了可感知热点的设计具有极大的潜力来提高热点的性能。
- 我们建议采用有序环哈希结构HotRing,这是利用热点感知设计的第一步。 通过将头部指针移近热数据项,可以快速访问热数据项。 它还采用轻量级策略在运行时检测热点转移。
- 我们使HotRing无锁化,以支持大规模并发访问。 特别是,我们从头开始设计特定于HotRing的操作,包括热点移位检测,头部指针移动和有序环重新哈希。
- 我们在基于实际工作量的基准上评估了我们的方法。 结果表明,当访问高度不对称时,HotRing明显优于其他KVS。
本文的其余部分安排如下。 §2介绍了哈希索引和热点问题,并讨论了可识别热点哈希的机遇和挑战。 §3阐述了HotRing的详细设计,而§4评估了其性能。 最后,第5节回顾了相关工作,第6节总结了本文。
2 Background & Motivation
在本节中,我们首先介绍现有KVS中的哈希索引和热点问题。 然后,我们将从理论上展示理想的热点感知散列的潜在好处。 最后,我们讨论了在实际指标中有效利用热点感知的挑战以及我们的设计原则。
2.1 Hash Indexes and Hotspot Issues
哈希索引是KVS中使用的最流行的内存结构,尤其是在上层应用程序不需要范围查询时。图2说明了哈希索引的典型结构,该索引包含一个全局哈希表和该表中每个条目的一个冲突链。要访问key,我们首先计算其哈希值 h h h 以定位相应的入口头,然后检查冲突链中的数据,直到找到该key或到达链的末端(即key不存在)为止。 n n n位哈希值可以进一步划分为哈希表部分(例如, k k k位)和tag部分(例如, ( n − k ) (n-k) (n−k)位)。tag可以包含在每个项目中,以避免比较长键。如图2所示,哈希索引无法识别热点,即,热点项目可能在碰撞链中平均分布。对于靠近冲突链尾部放置的热点项目(例如,图中的Item3),它比前面的其他数据需要更多的内存访问。但是,在工作负载高度倾斜的情况下,热点数据访问成本的略微增加可能会导致整体性能严重下降。
有几种减少热点数据访问成本的方法,但是效果有限。首先,CPU缓存可以加快对热点数据的访问速度(即以64字节缓存行为单位)。但是,对于大多数商用服务器,CPU缓存的容量约为32 MB,而整个内存量则超过256 GB。只能缓存0.012%的内存,远低于图1中观察到的热点比率。为了更好地利用CPU缓存,提出了许多对缓存友好的索引结构。其次,可以扩大哈希表(即,通过重新哈希)以减少冲突链的长度,以便定位热门项目需要较少的内存访问。但是,当哈希表已经很大时,不再建议重新哈希。例如,对于两个连续的重新哈希操作,第二个需要两倍的存储空间,但仅带来一半的功效(就减少链长而言)。总而言之,所有现有方法都只能在较小程度上缓解热点问题。
2.2 Potential Benefits of Hotspot-Awareness
随着热点问题变得越来越严重(如图1所示),它为热点识别哈希索引的设计提供了越来越多的机会。 首先,有趣的是对利用热点感知设计可以获得多少潜在利益进行粗略的估计和分析。
在传统的基于链的哈希索引中,热项目被随机放置在冲突链中,因此,热数据项和冷数据项在访问成本方面是等效的。 假设我们在具有B个存储桶的哈希表中存储了N个项(即键值对),则每个存储桶链的平均长度为 L = N / B L = N / B L=N/B。 检索 E c h a i n Echain Echain链中的项目的预期内存访问次数为:
E c h a i n = 1 + L 2 = 1 + N 2 ∗ B Echain = 1+\frac {L} {2} = 1+ \frac {N} {2*B} Echain=1+2L=1+2∗BN
公式中的前导1代表哈希表中的查找。(我个人的理解就是根据哈希值找到冲突链这一步)
在理想的热点感知哈希索引中,检索项目所需的内存访问应与此负相关,例如,最热的项目需要最少的内存访问才能检索。 我们在Zipfian分布中对商品热度进行建模,其中第 x x x 个最热商品的访问频率 f f f 表示为:
f ( x ) = 1 x θ ∑ n = 1 N 1 n θ f(x) = \frac {\frac {1} {x^\theta}} {\sum_{n=1}^N \frac {1} {n^\theta}} f(x)=∑n=1Nnθ1xθ1
其中 θ \theta θ 是偏度因子。 为了简化分析,我们假设热点均匀分布在B个存储桶中,即每个存储桶恰好包含前B个最热项目中的一个,前B + 1至2B个最热项目中的一个(笔者:这其实就是最理想的访问情况),依此类推。 在这种情况下,如果我们可以按它们的访问频率(降序)对链中的所有项目进行排序,则用于检索项目Eideal的预期内存访问次数为:
E i d e a l Eideal Eideal
= 1 + ∑ k = 1 L F ( k ) ∗ k = 1 + \sum_{k=1}^L F(k)*k =1+k=1∑LF(k)∗k
= 1 + ∑ k = 1 N B [ ∑ i = ( k − 1 ) ∗ B + 1 k ∗ B ) f ( i ) ] ∗ k = 1 + \sum_{k=1}^\frac{N}{B}[\sum_{i=(k-1)*B+1}^{k*B}) f(i)]*k =1+k=1∑BN[i=(k−1)∗B+1∑k∗B)f(i)]∗k
其中 F ( k ) F(k) F(k)代表每个链条上第k个项目的累积访问频率。
为了估算热点感知设计的潜在好处,我们计算了传统哈希和理想热点感知哈希的预期内存访问次数,如图3所示。我们可以观察到,随着冲突链长度的不断增长,热点 感知哈希显着提高了访问效率。 该结果证实,在哈希索引中考虑热点感知是提高性能的有希望的方向。
2.3 Challenges and Design Principles
我们已经讨论了使索引具有热点意识是有益的。 但是,在我们将这些见解应用于实际设计之前,仍然存在一些挑战:
- Hotspot Shift. 在实际应用中,访问模式会随着时间不断变化。 用每个数据实时热度来排序(理想中)的代价是无法接受的。 因此,我们需要一种轻量级的方法来跟踪热度的变化。
- Concurrent Access. 大量并发请求固有地访问每个热点。 因此,至关重要的是要同时支持两个读/写操作的高并发性,以维持令人满意的性能。(个人认为这是最难的)
对于热点转移问题,我们的设计原则是避免对链中的商品进行重新排序,而是移动头指针,例如指向最热门的商品或全局性更好的位置。 为了确保无论头部指针如何移动,都始终可以访问存储桶中的所有项目,我们将冲突链替换为有序环结构,称为HotRing(第3.1节)。 尽管此设计无法实现§2.2中讨论的最佳热点感知,但我们在实验中观察到它足够有效且快速。 此外,我们应用了两种轻量级策略来检测运行时的热点转移(第3.2节)。
对于并发访问问题,无锁结构是规范的解决方案,可消除昂贵的锁和同步操作(笔者:说不要使用无锁的大哥来看一看,极致的性能不追求无锁可以吗?)。 许多工作表明,无锁设计可以显着提高系统吞吐量[2,5,21]。 示例包括基于原子比较和交换(CAS)的read-copyupdate(RCU)[13]和 Hazard Pointers [40]。 在我们的工作中,无锁设计采用了现有的工作[19,50],巧妙地解决了删除和插入的并发问题(第3.3节)。 我们扩展了此设计,以支持HotRing所需的所有基本操作,包括热点移动检测,头指针移动和重新哈希(第3.4节)。
3 Design of HotRing
在本节中,我们将详细介绍HotRing中采用热点感知的详细设计,包括索引结构,热点移动检测策略和无锁操作(即读/写,插入/删除,头指针移动和rehash)。
3.1 Ordered-Ring Hash Index
图4描述了HotRing的索引结构,它改进了常规哈希索引中冲突链的结构。 在我们的设计中,链中的最后一个项目链接到第一个项目,形成了一个冲突环。 以这种方式,哈希表中的头指针可以指向相应环中的任何项,而不是固定在链中的头项上。 碰撞环的设计使HotRing可以根据数据的热度移动头部指针,并从任何起始位置扫描整个环。 请注意,如果环中只有一个数据项,则其下一个指针仅指向其自身(一个有序的循环链表)。
但是,由于基于环的设计,存在一个严重的问题:如果找不到目标项目,则可能导致环中无限次遍历。 确定何时可以安全终止查找过程非常重要。 注意,将头指针指向的第一项标记为停止信号是不够的,因为它可以被并发请求修改(例如,被标记的项被删除)。 因此,我们提出了有序环结构来帮助确定查找过程的终止。 直观地讲,我们可以按其键对环中的项目进行排序。 在这种情况下,如果我们已经遇到两个分别小于和大于目标项目的连续项目,则可以确定未找到项目的发生。 此外,由于比较两个长键可能会比较昂贵,因此我们首先利用了标记字段(在第2.1节中介绍)。 也就是说,项目 k k k 通过其 tag 和 key 字段对来排序,即 o r d e r k = ( t a g k , k e y k ) orderk =(tag k,key k) orderk=(tagk,keyk)。
在数据项 k k k 的查找过程中,假设正在访问数据项 i i i ,如果满足以下条件之一,我们可以立即终止。
C o n d i t i o n f o r I t e m F o u n d ( H i t ) : Condition f or Item Found (Hit) : ConditionforItemFound(Hit):
o r d e r i = o r d e r k orderi = orderk orderi=orderk
C o n d i t i o n s f o r I t e m N o t F o u n d ( M i s s ) : Conditions f or Item Not Found (Miss) : ConditionsforItemNotFound(Miss):
{ o r d e r i − 1 < o r d e r k < o r d e r i o r o r d e r k < o r d e r i < o r d e r i − 1 o r o r d e r i < o r d e r i − 1 < o r d e r k \begin{cases} order_{i-1} < order_k < order_i \\ or order_k < order_i < order_{i-1} \\ or order_i < order_{i-1} < order_k \end{cases} ⎩⎪⎨⎪⎧orderi−1<orderk<orderiororderk<orderi<orderi−1ororderi<orderi−1<orderk
图5说明了在HotRing中查找项目的所有可能情况。 我们在图中显示每个项目的字典顺序 ( t a g , k e y ) (tag,key) (tag,key)。 例如,由于 t a g A < t a g C tag_A <tag_C tagA<tagC,数据项C在数据项A后面; 由于 k e y C < k e y D key_C <key_D keyC<keyD,数据项D在数据项C之后(具有相同tag)。 由于 t a g A < t a g B < t a g C tag_A <tag_B <tag_C tagA<tagB<tagC,因此与数据项C相比,数据项B被确认为未命中。 与标签I相比,标签G和标签H缺失,分别是因为 t a g G < t a g I < t a g F tag_G <tag_I <tag_F tagG<tagI<tagF 和 t a g I < t a g F < t a g H tag_I <tag_F <tag_H tagI<tagF<tagH。 与传统的基于链的哈希不同,在结束未命中之前,并非必须访问环中的所有项目。 假设一个环包含n个项,则平均只需要与(n / 2)+1个项进行比较。
3.2 Hotspot Shift Identification
在有序环哈希索引中,查找过程可以轻松确定是否有命中或未命中。 剩下的问题是,当热点发生偏移时,如何识别热点并调整头指针。
由于哈希值的分布非常均匀,因此热点项目平均分布在所有存储桶中。在这里,我们专注于每个存储桶中的热点识别。实际上,每个桶中的冲突数据数量相对较少(例如5到10个物品),因此每个碰撞环中通常有一个热点(热点比率低于10%-20%)(笔者:当然前面提到一般热点为百分之一的数据,但缓存中本来已经是热点数据了)。我们可以通过将头指针指向唯一的热点来改善热点访问,这避免了重新组织数据并减少了内存开销。 为了获得良好的性能,必须考虑两个指标,即识别精度和反应延迟。 热点识别的准确性是通过已识别热点的比例来衡量的。 反应延迟是新热点发生到我们成功检测到它之间的时间跨度。 考虑到这两个指标,我们首先介绍一种随机运动策略,该策略可识别反应延迟极低的热点。 然后,我们提出了一种统计采样策略,该策略提供了更高的识别精度和相对较高的反应延迟。
首先,我们定义了本节中使用的几个术语。 头指针指向的第一项称为热数据项(hot item),其余项为冷数据项(cold item)。 他们对它们的访问分别定义为热访问(hot access)和冷访问(cold access)。
3.2.1 Random Movement Strategy
在这里,我们介绍了一种简单的随机运动策略,该策略保留了较少的反应延迟,但实现了相对较低的准确性。基本思想是将头指针从即时决策中定期移至潜在热点,而无需记录任何历史元数据。特别是,为每个线程分配了一个线程局部参数,以记录其执行的请求数。在每个R请求之后,线程确定是否执行头指针移动操作。如果第R个访问是热访问,则头部指针的位置保持不变。否则,指针将移动到该冷访问所访问的项目,这将成为新的热项目。参数R影响反应延迟和识别精度。如果使用小的R,则达到稳定性能的反应延迟将很低。但是,这也可能不利地导致频繁且无效的头指针移动。在我们的方案中,数据访问高度不对称,因此头指针移动很少发生。默认情况下,根据经验将参数R设置为5,这已证明可提供低的反应延迟和可忽略的性能影响(如图15(b)所示)。
请注意,如果工作负载的偏斜度不是那么明显,则随机移动策略将变得效率低下。 更重要的是,该策略无法处理碰撞环中的多个热点。 在这种情况下,头部指针往往会频繁移动,这无助于加快热点访问速度,但会对正常操作产生不利影响。
(笔者:这里其实可以看出来因为冲突环中以tag
排序,所以如果存在两个热点,它们的距离没办法变化,第二个热点的访问仍然非常昂贵,所以看起来rehash才是最好的出路)
3.2.2 Statistical Sampling Strategy
为了获得更高的性能,我们设计了一种统计采样策略,旨在提供更准确的热点识别以及稍高的反应延迟。 我们首先介绍HotRing中项和指针的详细格式,并展示如何利用现有格式来维护统计信息而又不增加空间开销(笔者:所以为什么还要用随机方案呢)。 然后,我们详细阐述了估计访问频率的采样策略。 最后,考虑到一个环中可能存在多个热点,我们提出了一种在热点转移时导出最佳头部指针运动的方法。
索引格式。我们计划记录每个冲突环中所有数据项的进入频率。由于现代机器的物理地址仅占用48位(但可以使用64位原子比较和交换操作进行更新),因此我们可以利用剩余的16位来记录元数据。在HotRing中,每个头指针都由三部分组成(如图6(a)所示):一个 Active 位,一个总计数器(15位)和地址(48位)。Active 位是用于控制统计采样以识别热点的标志。总计数器记录对相应环的访问次数。此外,一般数据项的结构如图6(b)所示。rehash是控制rehash过程的标志(在§3.4中讨论)。Occupied 用于确保并发正确性(在本节的后面讨论)。 HotRing使用下一个数据项中的其余14位记录每个数据项的访问计数(笔者:因为是一个循环链表,所以每一个数据项势必都有一个指针指向它自己)。根据在环级别和项级别维护的统计信息,访问频率的计算非常简单。
统计抽样。如何以低开销动态地识别热点是一个具有挑战性的问题。哈希表通常很大,例如包含 2 27 2^{27} 227 ~ 2 30 2^{30} 230 个存储桶。大量环的统计信息的同时和连续更新将导致严重的性能下降。在保持精度的同时,最大程度地减少开销是至关重要的,这可以通过HotRing中的定期采样来实现。特别是,每个线程维护一个用于处理请求的线程本地计数器。在每R个请求完成之后,我们确定是否启动新一轮采样(通过打开图6(a)中的“Active”标志)。如果第R次访问是热访问,则意味着当前热点标识仍然准确,并且无需触发采样。否则,这意味着热点已经转移,我们开始采样。根据与第3.2.1节类似的考虑,将参数R设置为5。当激活位被置位时,对环的后续访问将被记录在总计数器和相应项目的计数器中。此采样过程需要额外的CAS操作,并导致临时访问不足。为了缩短这一时间段,我们将样本数量设置为与每个环中的项目数量相同,我们相信已经提供了足够的信息来推导新的热点。
热点调整。 基于收集的统计数据,我们能够确定新的热门商品,并根据商品的访问频率移动头部指针。 采样过程完成后,最后一个访问线程负责频率计算和热点调整。 首先,线程使用CAS原语以原子方式重置“Active”位,从而确保仅一个线程将执行后续任务。 然后,该线程计算环中每个项目的访问频率。 数据项 k k k 的访问频率是 n k / N n_k / N nk/N,其中N是环的总计数器,而 n k n_k nk是第k个项目的计数器。 接下来,我们计算指向每个项目的头部指针的收入(income)。 当头指针指向某个数据项 t ( 0 < t < k ) t(0 <t <k) t(0<t<k)时,相应的收入(corresponding income) W t W_t Wt通过以下公式计算:
W t = ∑ i = 1 k n i N ∗ [ ( i − t ) m o d ( k ) ] W_t = \sum_{i=1}^{k} \frac {n_i}{N} * [(i-t) mod(k)] Wt=i=1∑kNni∗[(i−t)mod(k)]
W t W_t Wt被衡量为环中某个数据项被头指针指向时的平均数据访问次数。因此,选择具有 m i n ( W t ) min(W_t) min(Wt)作为热点的项目可确保可以更快地访问热点。 如果计算出的位置与前一个头部不同,则应使用CAS原语移动头部指针。 请注意,该策略不仅处理单个热点,而且还适用于多个热点。 这有助于找出避免热点之间频繁移动的最佳位置(例如,不一定是最热的数据)(笔者:没太看懂这里的意思,多个热点如果不扩容的话不管head放在哪里都会跳啊)。 热点调整完成后,负责的线程将重置所有计数器,以为将来的下一轮采样做准备(笔者:是否有重置的必要呢,多个R内的数据不是更加具有普适性)。
使用RCU优化写密集的热点(Write-Intensive Hotspot with RCU)。对于更新操作,HotRing为不到8个字节的值提供了就地更新方法(即,现代计算机最多支持8个字节的原子操作(笔者:寄存器大小为64位,CMPXCHG参数为寄存器))。在这种情况下,read/write对数据来说热度是相同的。但是,对于较大的值,情况完全不同,如图7所示。必须应用读-复制-更新(RCU)协议才能实现高性能。在这种情况下,需要在更新过程中修改前一项的指针以指向新项。如果修改了头部中的写入密集型热点,则必须遍历整个碰撞环才能到达其先前的项。就是说,写密集型的热项也使其前一个项热。以此为基础,我们对统计抽样策略进行了一些修改。对于RCU更新,其前一项的计数器将增加。这有助于将头对准写密集型热点的先例,从而使整个RCU更新操作变得更快。(笔者:个人认为这里阐述的是写操作实际操作的是热点的前一个数据项,所以在写操作时对统计抽样策略进行一点点修改,当然双向链表也可,不过每一个数据项一个八字节的消耗对内存数据库来说是否值得付出也需要测试)。
3.2.3 Hotspot Inheritance
当RCU操作更新或者删除头指针指向的数据项,头指针随机移动时,头指针将很有可能指向一个非热点的数据项。这将导致热点识别策略频繁触发。 此外,由于频繁触发识别策略,系统的性能将严重下降。
首先,如果冲突环只有一个项(即,下一个项地址与头指针的位置相同),则CAS会修改头指针以完成更新或删除。 如果有多个项目,HotRing将使用现有的热点信息(即头指针位置)来继承热点。 我们为RCU更新和删除操作设计了不同的头部指针移动策略,以确保热点调整的有效性:对于头部项目的RCU更新,由于以下内容的时间局部性,最近更新的项目极有可能立即被访问访问。 因此,头指针将移动到该已更改热点数据项的新版本。 对于删除数据项,只需将标题指针移动到下一个项,这是一种直接有效的解决方案。(笔者:不要问为什么这样做,问就是测试所得)。
3.3 Concurrent Operations
头部指针的移动使无锁设计更加复杂。这主要体现在以下几个方面:一方面,头指针的移动可能与其他线程并发。因此,我们需要考虑头部指针移动和其他修改操作的并发性,以防止指针指向无效项。另一方面,当我们删除或更新项目时,我们需要检查头部指针是否指向该项目。如果是这样,我们需要正确地( correctly and smartly)移动头部指针。在本节中,我们主要介绍并发访问的控制方法,以解决HotRing中的并发问题。为了实现高访问并发性并确保高吞吐量,我们已经实现了一套完整的无锁设计,这是先前的工作严格证明的(which has been rigorously introduced by previous work)。原子CAS操作用于确保两个线程不会同时修改相同的“下一个数据项地址”。如果多个线程试图更新相同的“下一个项目地址”,则只有一个线程成功,而其他线程则失败。失败的线程必须重新执行其操作。
读操作:SotRing扫描冲突环,使用3.1节中描述的key搜索目标数据项,无需其他操作即可确保读取操作的正确性。 因此,读取操作是完全无锁的。
插入操作。 插入操作会创建一个新数据项(例如,图8(a)中的C项),并修改前一个数据项的“下一个数据项地址”。 两个同时插入的内容可能会争夺相同的下一个项目地址。 CAS确保只有一个成功,而另一个必须重试。
更新操作。我们针对不同的值大小设计了两种更新策略。就地更新操作(对于8字节值)不会影响其他操作,这是通过CAS保证的(前面提到了CAS最大值为8字节)。但是,RCU操作(用于较大的值(笔者:起码大于8字节,因为CAS没办法保证原子了))需要创建一个新项目,这对其他操作的并发性提出了挑战。以RCU更新和插入为例,在图8(a)中:一个线程正试图通过修改项目B的下一个项目地址来插入项目C,而另一个线程正试图同时用 B ‘ B^` B‘ 更新 B B B。这两个线程的操作将成功,因为它们使用CAS修改了不同的指针。但是,由于数据项B对于环是不可见的,因此即使对项C的插入操作成功完成,也无法随后对其进行访问并导致错误。图8(b)中存在相同的问题。为了解决该问题,HotRing使用“Occupied”位(如图6(b)所示)以确保正确性。我们分两步执行更新操作。例如,在“更新并插入”的情况下:首先,原子地将要更新的数据项B的“下一个数据项地址”设置为已占用。一旦设置了“占用”位,项C的插入将失败,必须重试。其次,将项目A的下一个项目地址自动更改为项目 B , B^, B,,并重置 B , B^, B, 的已占用位。
删除操作。 删除是通过将指向已删除数据项的指针修改为其下一个数据项来实现的。 因此,必须确保在操作过程中不更改已删除项目的“下一个数据项地址”。 同样,我们利用“已占用”位来确保并发操作的正确性。 对于RCU更新和删除(如图8(c)所示)的情况,项目D的更新是通过更新前向项目B的指针来处理的,而项目B当前正在被删除。 无法正确遍历更新的项目 D ‘ D` D‘,从而导致数据丢失。 如果将项目B的已占用位设置为删除,则项目D的更新将无法修改项目B的下一个项目地址,而必须重试。 一旦完成项目B的删除,就可以成功执行更新操作。
(笔者:通过上面的描述我们可以看到会出问题的地方在于某个结点指向它的next指针和它指向的next指针都修改的时候会有问题,这里的做法就是使用“Occupied”位,使得指针的修改只能有一个线程先执行,“Occupied”位被重置后另一个操作才可以执行)。
头指针的移动。头指针的移动是HotRing中的特殊动作。 为了确保头部指针移动与其他操作(尤其是更新和删除)的并发正确性,我们需要其他管理措施。 有两个主要问题需要解决:(1)如何处理正常操作的并发性和由识别策略引起的头指针移动? (2)如何处理由于更新或删除头指针所指数据项引起的头指针移动?
对于由识别策略引起的头部指针移动,我们还使用了“Occupied”位以确保正确性。 将头指针移动到新数据项时,我们将其“Occupied”位设置为确保该项目在移动过程中不会被更新或删除。 对于head指向的数据项更新,HotRing将头指针移动到该数据项的新版本。 在移动头指针之前,我们需要确保其他线程不会更改(即更新或删除)新版本项。 因此,在更新数据项时,HotRing会首先设置新版本项目的“Occupied”位,直到移动完成。 对于删除head指向的数据项,HotRing不仅需要占用准备删除的数据项,还需要占用其下一个数据项。 因为如果在删除操作期间未占用下一个项目,则可能已更改了下一个节点,这使头指针指向无效的项目(笔者:3.2.3中提到删除一个热点数据后head指向热点的下一个数据项,这个数据项可能在head指向时被删除或者修改)。
3.4 Lock-free Rehash
随着新数据的插入,环中冲突项的数量继续增加,导致每次访问遍历更多的项。 在这种情况下,KVS的性能将严重下降。 我们在HotRing中提出了一种无锁的rehash策略,该策略允许随着数据量的增加而灵活地rehash。 常规的重新哈希策略由哈希表的负载因子(即平均链长)触发。 但是,这没有考虑热点的影响,因此不适用于HotRing。 为了使索引适应热点数据的增长,HotRing使用访问开销(即检索项目的平均内存访问次数)来触发rehash。 我们的无锁重新哈希策略包括三个步骤:
初始化阶段。首先,HotRing创建一个后端rehash线程。线程通过共享标签的最高位来初始化新哈希表,该哈希表的大小是旧哈希表的两倍。如图9(a)所示,“旧表”的存储桶中有一个旧的头指针,而“新表”中相应地有两个新的头指针。散列所需的位数从k扩展到k +1。 HotRing根据标签范围对数据进行划分。假设哈希值具有n位,并且标记范围为 [ 0 , T ) [0,T) [0,T)( T = 2 ( n − k ) T = 2^{(n-k)} T=2(n−k)),则两个新的头指针分别管理 [ 0 , T / 2 ) [0,T / 2) [0,T/2) 和 [ T / 2 , T ) [T / 2,T) [T/2,T) 中的项。同时,rehash线程创建一个rehash节点,其包含两个字rehash项,这两个子rehash项分别对应于两个新的头指针。除了没有存储有效的KV对之外,每个rehash项都与数据项具有相同的格式。 HotRing通过每个项目中的Rehash位标识rehash项。在初始化阶段,两个子哈希项的tag设置不同。如图9(b)所示,相应的重新哈希项分别将标签设置为0和 T / 2 T/2 T/2。
分裂阶段。 在分裂阶段,rehash线程通过将两个rehash项插入环来拆分该环。 如图9(c)所示,重新哈希项分别插入到项B和项E之前,成为划分环的标签范围的边界。 完成两个插入操作后,新表将变为活动状态。 之后,后续访问(来自新表)需要通过比较标记来选择相应的头指针,而先前访问(来自旧表)则通过识别rehash节点来进行。 可以正确访问所有数据,而不会影响并发读取和写入。 到目前为止,逻辑上将对项目的访问分为两个路径。 当我们查找目标数据项时,最多需要扫描一半的环。 例如,用于访问数据项F的遍历路径为 Head1 -> E -> F。(笔者:这里有一点很有意思,不过在新表中虽然new head的出现把环从逻辑上划分开了,但是其实还是一个环,所以用原来的不减那一位的tag值进行比较仍然可以在这一个大环内访问正确,因为tag超过自己本身那半圈后依然判断错误。这里不明白为什么旧表和新表可以同时处理请求)
删除阶段。 在此阶段,rehash线程删除rehash节点(如图9(d)所示)。 在此之前,rehash处理线程必须维持一个过渡期,以确保从旧表发起的所有访问均已完成,例如读-复制-更新同步原语的宽限期[13](笔者:我并不知道这玩意是干什么的)。 当所有访问结束时,rehash线程可以安全地删除旧表,然后重新哈希节点。 请注意,过渡期仅阻塞重新哈希线程,而不阻塞访问线程。(就图中来看new head的指定是计算出来的,可能是 W t W_t Wt)
(笔者:对于这个rehash过程我只能说巧妙。)
4 Evaluation
在本节中,我们使用基于实际工作负载的基准评估HotRing的性能。 特别是,我们将HotRing的吞吐量和可伸缩性与基于无锁链的哈希和其他基准系统进行了比较。 我们还提供详细的评估,以证明HotRing采用的主要设计的有效性。
4.1 Experimental Setup
(笔者:百分之多少的数据拥有多少的数据访问量,越高的热点的访问率拥有越高的参数 θ \theta θ)
我们在由两个带有 2.50GHz处理器的Intel®Xeon®CPU E5-2682 v4 组成的机器上进行实验。 它们有2个插槽,每个插槽具有16个内核(总共64个超线程)。 该机器具有256GB RAM 容量,并运行带有Linux 3.10内核的CentOS 7.4 OS。 为了获得更好的性能,我们将每个线程绑定到相应的内核(笔者:绑核可以带来CPU高阶cache的高命中率和NUMA架构下更快的内存访问速度)。 表1总结了机器的详细硬件配置。
Workloads。 我们使用YCSB核心工作负载[10]进行实验,但涉及扫描操作的工作负载 E 除外。 对于每个数据项(即键值对),对于in-place-update and read-copy-update (RCU) ,我们分别将键大小设置为8个字节,将值大小设置为8个字节和100个字节。 在每个测试中,已存储(loaded)的key的数量固定为2.5亿,并且key/存储桶的比率(即,密钥的数量除以存储桶的数量)会变化,以控制冲突链的平均长度。 另外,我们在YCSB中调整zipfian分布参数 θ \theta θ 以生成模拟每日和极端热点方案的工作负载。 表2显示了具有不同热点定义和偏斜度的热点访问率。 回想一下,图1显示了我们生产环境中的工作负载分布。 我们观察到,在日常情况下, θ \theta θ 分别为[0.9,0.99],在极端情况下为[1,1.22]。 因此,我们选择 θ \theta θ 的0.99和1.22作为代表。
Baselines:为了更好地展示HotRing中的热点感知设计的优势,我们实现了基于无锁链的哈希索引作为Baselines(Chaining Hash)。 它是根据Memcached中的哈希结构进行修改的,并使用CAS原语将新数据项插入冲突链的头部。 我们还与其他KVS系统进行了比较:FASTER 的C ++版本,其中我们确保所有数据都驻留在内存中; Masstree,一种高性能的内存范围索引,是具有非哈希索引的代表性KVS。 另外,基于锁的Memcached 也作为参考。
请注意,索引结构的内存占用量会极大地影响系统性能。 为了进行公平的比较,我们严格使所有方法的索引的内存消耗相同。 在每个测试中,如果没有另外指定,我们将使用以下默认设置:64个线程,8字节值的有效负载,YCSB的工作负载B, θ \theta θ 设置为1.22,以及key-bucket比率设置为8(对于HotRing)。
4.2 Comparison to Existing Systems
我们根据上面介绍的四个 Baselines 对HotRing进行了评估,即Chaining Hash,FASTER,Masstree和Memcached,它们都是高性能KVS实现。
Overall performance。图10显示了不同YCSB工作负载上所有方法的整体系统吞吐量。我们运行两种具有不同热点识别策略的HotRing变体:HotRing-r采用随机移动策略,HotRing-s采用抽样统计策略。与其他系统相比,HotRing在所有工作负载下均具有更高的吞吐量性能,尤其是对于工作负载B和C。HotRing-s的性能比其他方法高出2.10 - 7.75倍。单线程可达到12.90M ops / sec;64个线程可达到565.36M ops / sec,这意味着可观的可扩展性。此外,HotRing还保留了插入操作的优势。对于YCSB的工作负载D和F(带有大量插入),HotRing-s的性能优于其他方法1.81 - 6.46倍。这是因为有序环结构通过提前终止加快了商品放置速度,而tag字段则降低了排序成本。尽管HotRing-r的热点识别不如HotRing-s准确(差了约7%),但与其他系统相比,其总体性能仍得到显著改善。
Collision chaining length。图11(a)显示了当我们改变冲突链的长度时不同方法的吞吐量。我们将键-桶比率从2调整为16,这意味着哈希表中的冲突变得更加激烈。我们可以看到,当键-桶比率为2时,Chaining Hash和FASTER具有良好的性能。这是因为,当冲突链较短时,热点项目的内存访问开销相对较小,从而使缓存的影响更为显着(尤其是FASTER)。但是,随着碰撞链长度的增加,频繁访问热点项会严重降低Chaining Hash和FASTER的性能。相反,即使是长链,HotRing仍可保持令人满意的性能。特别是,当键-桶比率为2时,与Chaining Hash和FASTER相比,其读取吞吐量为1.93和1.31倍。当比率变为16时,性能差距将分别增加到4.02和3.91倍。这是因为HotRing将热点放在头部附近,因此需要较少的内存访问。这种设计对缓存更友好,其中仅头部指针和热点项目需要缓存,从而提供更高的性能。因此,我们得出结论,由于HotRing具有热点感知设计,因此它具有更好的性能和可伸缩性。
Access skewness。图11(b)显示了zipfian参数 θ \theta θ 变化时不同方法的吞吐量。 我们将 θ \theta θ 从0.5调整为1.22,这意味着工作负载中的热点问题变得更加严重。 可以看出,随着 θ \theta θ 的增加,Chaining Hash和FASTER的性能改进并不明显,因为它们缺乏热感知因素。 相反,随着 θ \theta θ 的增加,HotRing的性能显着提高,尤其是当 θ \theta θ 大于0.8时。 即使当 θ \theta θ 在[0.5,0.8]范围内(热点问题较小)时,HotRing-s仍比其他产品具有更好的性能。 这是因为HotRing-s能够处理碰撞环中多个热点的情况(笔者:个人认为是rehash的方法)。 当有多个具有相似访问频率的项目时,HotRing-s可以找到最佳的头部指针位置以获得最佳性能(第3.2.2节)。 但是,在这种情况下,HotRing-r无法选择最佳的头部指针位置,从而导致频繁触发指针移动。
RCU operation。为了更显著地展示RCU的性能,我们使用YCSB生成具有100字节值有效负载的写入密集型工作负载(仅50%的写入和只写)。图12显示了涉及RCU操作时不同方法的吞吐量。在此测试中,我们证明了在HotRing(第3.2.2节)中需要对RCU操作进行特殊处理。特别是,HotRing-s表示抽样统计策略将在RCU更新项目时增加前向项目计数器。 HotRings(w / o)表示该策略没有区分RCU操作。首先,HotRing-s(w / o)在所有情况下的性能都较差,甚至比Chaining Hash和FASTER更差。这是因为HotRing-s(w / o)需要遍历整个冲突环才能完成对热点的的RCU操作。但是,在HotRing-s中,优化的热点计数策略可显着提高RCU性能。请注意,当键-桶比率等于2时,HotRings的性能会比FASTER的性能稍慢。这是因为热点项需要在标头点所指向的第二个插槽中进行RCU操作,在该位置需要进行一次额外的内存访问。此外,它还涉及一项额外的CAS操作(在被占用的位上)以完成RCU操作。随着碰撞项目的数量不断增加,上述问题将得到极大缓解。例如,当密钥桶比率达到8时,HotRing-s的吞吐量要比Chaining Hash和FASTER好1.32倍。
4.3 Investigation of Detailed Designs
在本节中,我们将HotRing与传统的链式哈希进行比较,以研究可识别热点设计的优势所在。
Break-down cost:我们收集工作负载执行过程中涉及的不同功能的分解成本。图13显示了HotRing和Chaining Hash中单个读取访问的平均分解成本(其中,key-bucket比率为8)。在此图中,HeadPointer是定位相应冲突环或冲突链的头部指针的成本; HeadItem是访问头指针的成本; Non-HeadItem是访问其他数据项的成本; HashValue是由于进行哈希计算而产生的成本;Benchmark 是读取和解释工作负载命令的成本( Benchmark is the cost to read and interpret the workload command);其他是系统内核的成本。可以看出,Chaining Hash中的开销主要由Non-HeadItem访问控制,当q = 1.22 / 0.99时,约为193ns / 660ns。这表明基于链的哈希中的热点项目倾向于在链中均匀分布,从而显着增加了访问成本。相反,HotRing-r和HotRing-s大大降低了Non-HeadItem的成本。特别是对于HotRing-s,当q = 1.22 / 0.99时,NonHeadItem成本约为10ns / 136ns。由于Non-HeadItem的比例与热点识别准确度负相关,这意味着HotRing-s具有更高的热点识别准确度,可以检测到更多热点项目并将其放置在头部。
Reaction delay。反应延迟是衡量热点识别策略的重要指标之一。 图14显示了热点转移后的吞吐量趋势(工作量C)。 我们可以观察到,HotRing-r的反应比HotRing-s更快,后者仅需不到2秒即可达到稳定状态。 但是,由于热点检测不准确,其峰值吞吐量要低得多。 链散列的吞吐量不受影响,因为它缺乏热点感知能力。
Read miss。我们评估了两种方法的吞吐量,如图15(a)所示。 可以看出,HotRing和Chaining Hash之间的性能差距随着链长的增加而扩大。 特别是,当key-bucket比率为2时,HotRing改进了1.17倍;当比率为16时,HotRing改进了1.80倍。这是因为HotRing只需要与平均一半项目进行比较(如图5所示),而“链接哈希”则访问链中的所有数据项。
Parameter R(笔者:热点转移时用到了这个参数,即头指针移动的最小单位)。回想一下,参数R的选择会影响头指针移动的频率(第3.2节)。 当R较小时,热点标识的反应延迟较小,但会导致头指针移动更加频繁(且无效)。 图15(b)显示了R在不同情况下对总体吞吐量的影响。 可以观察到,当R太小(由于热点识别的开销)或太大(由于热点转移的处理延迟)时,性能会稍微变差。 实际上,为了平衡考虑和提高吞吐量,我们将R设置为5。
Tail latency。HotRing-s需要统计采样,并且采样过程中的最后一个线程需要计算访问频率以找到头部指针的最佳位置。 因此,由于这种额外的计算,可能存在长尾访问。 图16显示了10万次访问的延迟分布。 当q = 1.22时,99%的响应时间约为2µs,但长尾访问需要8.8µs。 当q = 0.99(99%的响应时间为3µs,长尾访问时间为9.6µs)时,情况与此相似。 请注意,长尾访问部分与简化我们的实现选择有关,并且可以通过将其他计算移至专用后端线程来进一步缓解。
Lock-free rehash。rehash是确保增长的哈希表稳定性能的重要机制。 我们构造以下方案来评估我们的无锁重新哈希操作:在初始状态下,已加载key的数量为2.5亿,密钥/存储桶的比率为8; 然后,我们使用YCSB工作负载读取50%(q = 1.22),插入50%来模拟哈希表的连续增长。 图17显示了进行修补后,HotRing随时间的性能。 特别地,I,T和S分别表示重新哈希的初始化,过渡和拆分阶段。 可以观察到,随着数据量的不断增长,两个连续的哈希操作有助于保持吞吐量。 rehash期间的短期的性能下降是由于新哈希表开始工作时暂时缺乏热点感知而引起的(笔者:那为什么不能更具rehash之前的数据进行new head的指向呢)。
5 Related Work
现有的许多工作都集中在键值存储的索引结构设计上。 Memcached是一种广泛使用的分布式键值存储,许多公司都在使用它。 但是,由于锁的频繁竞争,其多线程性能并不令人满意。 在Memcached的基础上,有大量的工作在文献中做出了杰出的贡献。 通过实现无锁设计和对缓存友好的优化,它们实现了更高的并发性和吞吐量。 特别是,FASTER是具有无锁设计的最新实现之一。 对于热点感知,Splay树是一项鼓舞人心的工作,它对其结构进行了调整,以针对最近访问的项目进行优化。 但是,其基于锁的设计使其不适用于高度并发的情况。
此外,还有许多关于将系统设计与新兴硬件更好地集成的工作,例如FPGA,启用RDMA的NIC,GPU,用于TCP的低开销用户级实现。以及具有硬件级可靠数据报的InfiniBand。 同时,为了提供快速的内存分配(用于插入和删除),许多协议还利用了无锁内存管理方法,可以用来防止ABA问题。 请注意,这些针对硬件和内存管理的优化与索引结构的设计正交,我们也可以采用这些思想来进一步提高HotRing的性能。
6 Conclusion and Future Work
在KVS的实际部署中,热点问题很常见,并且在最近变得更加严重。例如,为了提供高可用性的服务,阿里巴巴的NoSQL产品Tair必须分配比必要数量更多的计算机,以处理突然出现的热点。因此,我们探索了设计热点感知内存式KVS的机遇和挑战。基于已经发现的见解,我们提出了一种称为HotRing的哈希索引,该索引已针对大量并发访问一小部分项目进行了优化。通过将head指针指向经常访问的数据项,它可以动态地适应热点的变化。在大多数情况下,可以在两次内存访问中检索热门数据项。 HotRing在设计中全面采用了无锁结构,用于常见的哈希操作和特定于HotRing的操作。广泛的实验表明,与其他在高度倾斜的工作负载中使用内存的KVS相比,我们的方法能够实现2.58的吞吐量提高。现在,HotRing已成为Tair的子组件,已在阿里巴巴集团中广泛使用。
目前,HotRing-r是为每个链上的单个热点设计的,而HotRing-s还可以处理多个热点。 在大多数情况下,我们可以通过重新哈希来减少链接长度,从而缓解多热点问题。 对于某些无法解决的极端情况,我们将继续探索合适的解决方案。
(笔者:个人认为这里比较极端的情况其实指的就是在某一个冲突环内部多个热点互相邻接,且扩容也无法分开其中的结点,这样会使得head指针不停的迁移,个人认为这里有一个空间换时间的优化就是可以记录一段时间内部每个环的热点迁移情况,如果在扩容后仍然比较多的话有理由相信可能热点相邻,下一次热点迁移的时候没必要计算全部的数据结点,只需要从head开始计算部分环上数据就可以。不过需要数学计算一下概率。
我其实还有一个疑问,就是在rehash的时候为什么新旧哈希表需要一起提供服务,猜测可能是在进入分裂状态以后同时提供服务不影响正确性且提升吞吐量,但是删除阶段的那个过渡期我是真的不太懂为什么,下去再补补RCU去。
最后就是为什么HotRing-s可以处理多热点的情况,理论来说head指向的热点还是基于前R次访问,这样总有一个更热的热点被选出来,如果每次热点的访问分段交替执行的话性能可预料到的会很差。)