Hot Ring

395 阅读12分钟

论文原址:

www.usenix.org/conference/…

发现问题->证明问题严重性->提出可能的解决方案->实现解决方案->证明解决方案的有效性

开始

简介

一种热点可感知的内存 KV 存储结构。

场景

  • 在某些时刻,缓存系统迎来巨大的访问量 (双11秒杀)
  • 可能存在访问倾斜,大多数访问集中在极少数数据上(微博热点事件)

背景问题与挑战

  • 对于集群级别的热点监测倍受重视

  • 对于单机存储的热点问题也有一些优化

对于内存KV 存储的热点问题被忽视,针对已有的的散列数据结构的问题,说明解决热点问题的挑战和设计准则。

内存KV存储数据结构中,散列表是最常用的。散列表的主要作用是分离链表

1.png

2.png

如图,在最坏的情况下,热点数据在链表尾部,访问时需要读取更多的内存,造成大量开销。

已有的解决方法:

  • 利用CPU cache,但是cache容量太小
  • rehash,但会成倍增加内存使用
  • Java HashMap,链表转红黑树

挑战

  • 热点是动态变化的,如何检测,如何转移
  • 内存 KVS 对时延要求是很高的( 无锁的数据结构尤为重要)

设计

  • 基于一个有序的环哈希索引结构,通过移动头部指针快速访问热点。
  • 内部全面采用无锁结构设计(CAS),支持大并发下的读、更新等操作(RCU、 Hazard Pointers等 )。针对其的特定操作:热点移位检测、头部指针移动、有序环重新哈希。
  • 提供一个轻量级策略来检测运行时的热点转移。

设计实现

有序环

1.jpg

冲突链表被有序环代替:

  • 原有链表的尾部会指向原有的头部
  • 环的头指针可以改变
  • 环的项是有序排列的

使用环的优势

  • 单向链表

    • 必须从头节点开始,到尾节点终止。移动头指针的情况下,会导致一些节点无法被访问
    • 移动热点不方便,需要把中间节点移动到头节点,移动链表中的节点很复杂且难以做到无锁并发
    • Head 可以指向任意节点,即从任意节点开始遍历,可以遍历完整个链表。

有序的优势

  • 无序

    • 环形链表,没有终结点,如果查找值不存在,不知何时停止
  • 有序

    • 可以根据前后项的关系判断是否终结本次查询

有序的前后项关系:

  • 前驱节点 < 待查找节点 <后驱节点 miss
  • 前驱节点 > 后驱节点 && 待查找节点 < 后驱节点 miss
  • 前驱节点 > 后驱节点 && 待查找节点 > 前驱节点 miss
  • 待查找节点 == 节点K hit

3.jpg

热点识别转移

对于热点数据,可以将头节点移到热点项,以降低热点数据访问的性能开销。

问题:如何检测热点项。

论文中名称: 即头指针是Hot Item, 其它都是Cold Item,它们的访问分别是Hot Access和Cold Access。

为何需要转移

  • 避免多次遍历,希望把热点数据放在冲突链前面
  • 热点经常变化,需要不断移动节点

随机移动策略:头指针周期移动,指向一个的热点项,这个决定不依赖任何历史元数据。

  • 实现

    • 每个线程维护一个变量,记录执行了多少次请求

    • 每隔R个请求,线程决定是否要移动头指针

      • 若第R个请求是hot access,头指针不移动
      • 若第R个请求是cold access,头指针移动到第R个请求所在的项(clod access)上
  • 优缺点:效率高,不需要采样、计算,响应速度快,效果可能差

    • 在热点集中时非常有效,头指针会趋向于指向热点数据,否则可能会频繁摇摆

统计采样策略:依赖计数历史元数据判断热点项。

  • 优缺点: 效率低、效果可能更好

  • 索引格式

    4.jpg

    • 头指针head包括:

      • active:作为控制统计采样的标识
      • total_counter:当前环总共的访问次数
      • address:环的头地址
    • 环上每一项的next

      • rehash:作为控制rehash的标识
      • occupied:用于并发控制,保证并发访问的正确性
      • counter:该项的访问次数
      • address:下一项的地址
    • 利用x86-64架构和系统特性,地址使用48位节省处理的16位用于统计采样。

  • 统计采样:为了降低开销,且保证识别的准确性,和随机移动一样,周期性地调整。

    • 每个线程维护一个变量,记录执行了多少次请求

    • 每隔R个请求,线程决定是否要移动头指针

      • 若第R个请求是hot access,头指针不移动,且不开启采样

      • 否则,则需要移动头指针,开启统计采样,采样个数也是R

        • 打开head.active(CAS)
        • 后续的请求会被记录到head.total_count和对应的next.count(CAS)
  • 热点调整: 基于上一步的统计采样,就可以决定哪一个是头节点

    • 关闭head.active(CAS)
    • 遍历环
    • 公式:
    Wt=i=1kniN[(it)modk]W_t=\sum_{i=1}^k \frac{n_i}{N} * [(i-t)\mod k]
    • 其中,N:total count 、n:counter、(i-t):从t节点访问到i节点的内存访问次数、n/N:每一项的访问频率。

    公式:当头结点方位t节点的时候,要访问环中所有的节点中的代价,代价最小值为头结点。

    故本质不是直接将头结点设置为访问次数最大的节点,因为在环中的热点不止有一个。

    • 使用CAS设置新的头指针

    • 重置所有的计数器

  • 写入密集型的热点:RCU机制

    • 项的数据 Less than 8 bytes -- CAS,Read读热点、Update被视作一样的操作。
    • 项的数据 More than 8 bytes -- RCU,针对update的优化点,在进行更新操作时使用RCU并且需要修改前一项的next指针,要想获取修改项的前驱项需要遍历整个环,因此修改一个Hot Item也会让其前驱变为Hot Item。在RCU下,更新的是前一项的计数器
    • 在RCU下,更新的是前一项的计数器,头指针会趋向于指向写入项的前一项,在写密集型的热点时,可以直接定位到热点的前一项,更新时就不需要遍历链表。

热点继承

头节点如果被 RCU 更新或者删除,头节点需要指向其它项。但是不能随便指向其它项,因为很可能新的一项是cold item,然后频繁触发热点识别,降低性能。 防止冷启动。

解决方式:

  • 若环只有一项,直接CAS更新头指针即可,指向空指针就OK。

  • 若环有多个项,利用已有的热点信息(即头指针的位置)处理:

    • RCU更新:指向 new update item(在日常的经验中一个数据被更新很可能会被再多次访问,比如读取)
    • 删除: 指向被删除项的下一个节点,point to next item。因为被删除的节点是热节点,则此时指向它的下一个节点,其收益并不会减少太多。

并发操作

HotRing的并发控制的基础是CAS

Read

因为没有对环形链表结构修改,所以不存在并发问题,可以直接不加锁访问

Insert

并发插入时数据丢失问题

当需要并发向B节点与E节点插入C、D时,会导致其中一个节点丢失。

步骤

  • 创建新项
  • 新项的next指针指向前一项next指针所指地址
  • 修改前一项的next指向新项(CAS)

第三步可通过CAS保证线程安全。若前一项next字段发生竞争,CAS会失败,此时操作需要重试(重试后2步)。

Update

上述介绍过,当更新的数据不超过8字节:使用 CAS,不需要其它操作。

当更新的数据超过8字节:使用RCU更新,需要分3种情况。

  • RCU-update & Insert 并发引发的数据丢失

    并发执行更新B和插入C,修改前一项的next需要CAS,两个操作都会成功,但是结果不能成环。

  • RCU-update & RCU-update

    并发执行更新B和D,CAS都会成功,但是最后结果也会导致无法成环,且B’的next是一个丢弃数据指针。

  • RCU-update & Delete 并发引发的数据丢失

    并发执行删除B和更新D,CAS也都会成功,但是最后结果也会导致无法成环,且A的next是一个丢弃数据指针。

由上问题可知,只有CAS设置next指针是不够的。回到索引设计中,需要额外字段来确保并发安全,即next中的 occupy字段

  • RCU-update & Insert

    • RCU-update前,尝试CAS修改B.next值,置B.next.occupy = 1
    • Insert时,使用CAS连接前一个节点,发现B.next.occupy = 1,操作会失败,重试
    • 操作完成后,新版本项的occupy= 0
  • RCU-update & Delete

    • Delete前,尝试CAS修改待删除项B.next值,置B.next.occupy = 1
    • RCU-update时,使用CAS连接前一个节点,发现B.next.occupy = 1,操作会失败,重试
    • 操作完成后,新版本项的occupy为0
  • RCU-update & RCU-update:与RCU-update & Delete类似

头指针移动

需要解决问题:

  • 头指针要指向新热节点,新热节点被RCU 更新 或者 删除,头指针指向旧的新热节点上
  • 头指针指向的头节点需要进行update/delete 引起的转移

使用occupy字段

  • 当要移动头指针时,CAS设置新的头节点的occupy为1,保证其不被更新/删除

  • 当头节点要被更新时:

    • 移动前,更新时会设置新版本的头节点occupy为1
    • 移动完成,重置occupy为0
  • 当头节点要被删除时:

    • 除了设置当前被删除的头节点occupy为1,还得设置下一项的occupy为1,因为下一项是新的头节点,需要保证其不被更新/删除

无锁rehash

rehash原因:同一个链上热点数量过多,导致性能降低

HotRing支持无锁的rehash操作。HotRing使用访问开销(即item操作平均内存访问次数)来触发rehash。

3步:

  • 初始化
  • 分割
  • 删除

初始化

首先创建rehash线程,初始化一个是原2倍大小的散列表。

1.png

11.png

如图所示,在old Table中的一个旧头指针,相应地在new Table 中两个新的头指针。1个环会被拆成2个环。回到上述索引格式Metadata中,自然散列值k的bit数拓展到k+1(在计算机语言中乘2就是左移一位),则tag的bit数减少一位变为tag-1。根据这一位的值(即rehash bit),决定原有环中的项在哪一个新环上(和JDK HashMap类似)

HotRing根据标记范围划分数据。假设哈希值有n位,标签范围为

[0,T)(T=2nk)[0,T)(T=2^{n-k})

两个新的头指针分别管理来自

[0,T/2)[T/2,T)[0,T/2) 、[T/2,T)

的条目。每个重散列项具有与数据项相同的格式,但没有存储有效的KV对

12.png

在初始化阶段,两个子重hash项的标记设置不同。如图所示,相应的rehash项将标签设置为0和T/2,分别为。

分割

13.jpg

在拆分阶段, 后台线程创建一个rehashNode,包含两个子Node,作为2个新环的头(作为假的头)。它的格式和data item一样,但是tag值分别是0和T/2,代表不同的rehash bit。

线程遍历原有的环,根据rehash bit,将项插入到不同的新环上,插入完毕后就可用了(可访问,可写且可读)。

在rehash中,rehash结束前的请求(来自Old Table)通过识别索引格式的rehash节点。正确地访问到new table的数据,而不会影响并发读写。rehash结束后的请求直接访问new table。

删除

线程将创建的rehash item删除。

在此之前,需要保证旧表上的访问要都完成(类似于RCU的grace period同步原语)。所有旧表访问结束后,线程会删除旧表和rehash node。

只有rehash线程会阻塞,其它线程是不会阻塞的

例子

在一个head中,其hash值为hash(key)=137=10001001

故n=10001001,其k=1000,tag=1001。此项为hash表的head8环中

进行rehash

则 k+1=10001,tag-1=001,rehash bit=1,此项在new table的head17环中

总结

背景:论文中通过YCSB对HotRing方案进行了压力测试,并对比了行业有名的Memcached等。可以看到HotRing方案的吞吐量远超其他方案:

14.png

15.png

配置

  • HotRing-r (random movement strategy)
  • HotRing-s (sampling statistics strategy)

左图压测了链表长度对性能的影响,可以发现当链表长度从2一直递增到16的过程中。HotRing方案的性能几乎是恒定的,可以保持一个很好的性能。、右图表明hotring在数据访问呈现严重倾斜的情况下,也能保持非常好的性能。

16.png

θ是齐夫分布的参数,YCSB生成工作负载时, θ的值越高,表明测试的所使用负载的倾斜程度就越严重。

左图表明Hot Ring在read miss的情况下的性能比chaining hash的性能要好。这是因为Hot Ring中每一个桶的环是有序的,判断元素不存在不需要遍历桶中的所有元素。右图热点切换时,不同的热点选择策略稳定下来需要的时间,hotring-r在两秒内就可以达到一种稳定的状态了。

17.png

在分裂前,为了保证从旧哈希表进入的访问均已经返回,rehash的过程被阻塞一段时间,随着数据容量的不断增长,rehash操作可以维持这个稳定的性能。如下图所示:

18.png

知识拓展

RCU

read-copy update。是 Linux 中比较重要的一种同步机制。“读,拷贝更新”,更新数据的时候,需要先复制一份副本,在副本上完成修改,再一次性地替换旧数据。这是 Linux 内核实现的一种针对读多写少的共享数据的同步机制。

适合场景:

  • 高频读低频写
  • 对数据没有强一致性要求

cloud.tencent.com/developer/a…

无锁链表

论文地址:citeseerx.ist.psu.edu/viewdoc/dow…

巨人的肩膀

论文

keys961.github.io/2020/02/28/…

硬核课堂

www.cnblogs.com/helloworldc…