LSM 优化系列(三)-- 【ATC‘19】9SILK- Preventing Latency Spikes in Log-Structured Merge Key-Value Stores

本文主要是介绍LSM 优化系列(三)-- 【ATC‘19】9SILK- Preventing Latency Spikes in Log-Structured Merge Key-Value Stores,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

    • 1. Latency Spike in LSM
      • 1.1 LSM三种internal操作
      • 1.2 长尾延时的原因
    • 2. 长尾延时的业界解决方案
      • 2.1 Rocksdb
      • 2.2 Rate-Limited Rocksdb
      • 2.3 TRIAD
      • 2.4 Pebblesdb
      • 2.5 Lessons Learned
    • 3. SILK 实现
      • 3.1 SILK 设计原则
      • 3.2 SILK 详细实现
        • 3.2.1 按照概率分配I/O带宽
        • 3.2.2 优先级调度 和 可抢占的 Internal ops设计
    • 4. 性能数据
    • 5. 业界相关LSM优化的展望
      • 5.1 降低Compaction开销方向
      • 5.2 Compaction变种 或者 实现相关的代替方案
      • 5.3 在LSM 的数据结构和相关算法上的一些优化

1. Latency Spike in LSM

论文原地址 可以参考

在 USENIX Annual Technical Conference2019的定会上,该篇论文提出了解决LSM 架构在update 或者有部分update参与的场景下出现的长尾延时问题。

关于LSM架构下的长尾延时问题,之前在介绍rocksdb的Rate Limiter 实现原理 时提到过,这里简单描述一下。

1.1 LSM三种internal操作

  • Flush 由write-buffer 写入 L0
  • L0-L1 Compaction 因为L0 层文件之间允许有key的重叠(LSM 为了追求写性能,使用append only方式写入key,write-buffer一般是skiplist的结构),所以只允许单线程将L0的文件通过compaction写入L1
  • Higher Level Compactions 大于L0层 的文件严格有序,所以可以通过多线程进行compaction.
    在这里插入图片描述

Flush 操作如下:

  • 请求写入到(write-buffer)memtable之中,当达到write_buffer_size大小 进行memtable switch
  • 旧的memtable变成只读的用来 Flush,同时生成一个新的memtable用来接收客户端请求
  • Flush的过程就是在L0 生成一个sst文件描述符,将immutable 中的数据通过系统调用写入该文件描述符代表的文件中。
    在这里插入图片描述

L0–> L1 Compaction 操作如下:

  • 将一个L0的sst文件和多个L1 的文件进行合并
  • 目的是节省足够的空间来让write-buffer持续向L0 Flush
    在这里插入图片描述

Higher Level Compactions操作如下:
对整个LSM进行GC,主要丢弃一些多key的副本 和 删除对应的key的values

这个过程并不如L0–>L1的compaction 紧急,但是会产生巨量的IO操作,这个过程可以后台并发进行。
在这里插入图片描述

1.2 长尾延时的原因

  • L0满, 无法接收 write-buff不能及时Flush,阻塞客户端
    在这里插入图片描述
    因果链如下:
    没有协调好internal ops – 》 Higher Level Compactions 占用了过多的IO --》L0–>L1 compaction 过慢 --》L0没有足够的空间 --》Write-buffer无法继续刷新。
  • Flush 太慢,客户端阻塞
    在这里插入图片描述
    因果链如下:
    没有协调好internal ops --》 Higher Level Comapction占用了过多的I/O --》 Flushing 过程中没有足够的IO资源 --》Flushing 过慢 --》Write-buffer提早写满而无法切换成immutable memtable,阻塞客户端请求。

综上我们知道了在LSM 下客户端长尾延时主要是由于三种 内部操作的IO资源未合理得协调好导致 最终的客户端操作发生了阻塞。

针对长尾延时的优化我们需要通过协调内部的internal 操作之间的关联,保证Flushing 优先级最高,能够占用最多的IO资源;同时也需要在合理的时机完成L0–L1的Compaction 以及 优先级最低但是又十分必要的Higher Level Compaction。

以下简单介绍一下Rocksdb 内部原生的Rate Limiter对这个过程的优化。

综上可以看到传统LSM的长尾延时问题 主要是由于LSM 三个internal ops(Flush, L0–>L1 compaction ,Higher Level Compactions)的调度策略导致的。

2. 长尾延时的业界解决方案

2.1 Rocksdb

首先对比了Rocksdb 实现的LSM,开启和关闭Compaction时的长尾延时情况如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vD0ujYIh-1605277760775)(/Users/zhanghuigui/Library/Application Support/typora-user-images/image-20201113144152817.png)]

可以看到开启Internal ops(橘黄色部分) 之后 整个长尾延时相比于未开 高了接近4个量级。

2.2 Rate-Limited Rocksdb

通过限制 internal ops的I/O带宽 是一个业界比较认可的限制长尾延时问题的方法,SILK开发人员也对Rocksdb自实现的Rate Limiter做了测试。关于Rate Limter的实现可以参考Rocklsdb Rate Limiter实现原理。

通过设置不同的Limit Rate来对长尾延时的情况进行统计,得到如下测试结果:

在这里插入图片描述

可以看出随着 限制internal ops带宽越来越高,那么长尾延时 维持在较低的时间 越来越长。

但是这并不能无限制得增加internal ops的限制带宽,因为随着compaction 累积的量越多,后期会 较高概率有一次累积 更多的compaction 与上层吞吐来争抢IO带宽。

2.3 TRIAD

ATC’17 TRIAD TRIAD: Creating Synergies Between Memory,
Disk and Log in Log Structured Key-Value Stores 是一个非常有代表性的先进系统,来降低internal ops对长尾延时的影响,SILK对该系统也做了对应的测试。

在这里插入图片描述

TRIAD 通过调度 减少 L0–》L1 的compaction 降低 compaction 对client 的吞吐影响,但是这个问题会导致Higher Level Compactions的增加。所以上图中可以看出在1000s以后较长的一段时间内,整体的长尾延时还是会增加。

2.4 Pebblesdb

关于Pebblesdb 的整体介绍可以参考Pebblesdb: Building Key-Value Stores Using FLSM

SILK也对pebblesdb的长尾延时做了整体的测试,在读敏感的workload下(95:5)workload下,长尾延时保持在一个非常好的效果,但是在运行了8个小时之后,compaction大量接入,整体的系统效率被严重阻塞。
在这里插入图片描述

因为Pebblesdb运行guards内部存在重叠key,所以到后期大量的Higher level compactions抢占了系统的资源,导致整个client的IO被阻塞,整个stall一直持续直到compaction完成终止。

2.5 Lessons Learned

  1. 对于较高的长尾延时出现的主要原因是 写被存在于内存的write buffer 填满阻塞了。
    Write buffer满主要有如下两个原因
    a. L0满,造成write-buffer向L0 flush被终止。从而导致客户端无法持续向新的write-buffer写入。这里L0满的原因是,L0–》L1 Compaction的过程中没有IO被更高层compaction 产生的I/O抢占,导致L0 提前满。
    b. Flush slow,由于更高层的并发compaction导致Flush的IO被抢占,从而产生Flush的速度没有write-buffer的写入速度快。
  2. 仅仅限制 internal ops的IO带宽并无法解决Flush或者L0 ->L1 compaction等优先级较高的任务被 Higher Level Compactions抢占的问题。所以Rocksdb的 Rate Limiter 以及 Rocksdb的Auto tune等都会导致后续Higher Level compactions抢占优先级较高的任务的IO,从而间接导致write-buffer full.
  3. 近些年的一些LSM优化的方法 提出为了提升吞吐,将Compaction下沉到更高层来做,这在短期内能够收获较为稳定的延时以及较高的吞吐,但是在跑了很长一段时间之后就会出现大量的Compaction抢占系统资源的问题,从而导致Client qps stall。

综上 ,我们能够得出如下结论,并不是所有的internal ops都需要平等共享系统资源,比如 Flush 以及 L0->L1 Compaction,就是优先级比较高的internal ops,这一些操作不能及时完成,就会导致Client ops被stall。

3. SILK 实现

SILK 在总结了之前业界相关的优化方案,为了避免再次出现上文2.5中指出的Lesson learned,核心的设计思想将遵循如下三个原则。

3.1 SILK 设计原则

  1. 让Internal ops都有能够获得IO带宽的机会。SILK 在client 的流量洪峰时会减少Higher level Compaction的IO带宽占用,在client 流量低峰时增加Higher Level的Compactions的带宽。
  2. 对LSM tree的internal ops进行优先级调度。正如SILK将LSM的internal ops分为三种不同的操作,并调度优先级来降低对Clinet的长尾延时的影响。SILK的调度策略如下:
    a. SILK确保Flush足够快,即Flush的优先级最高。从而为内存中的write-buffer接受客户端的请求留下足够的空间。Flush的速度是直接影响客户端的长尾延时问题。
    b. SILK 将L0 -> L1 Compaction的速度放在第二优先级,确保L0不会达到它的容量上限,从而保证Flush能够有足够的空间完成操作。
    c. SILK 将Higher Level Compaction的优先级设置为最低,因为这些Compactions的目的是为了维持LSM的形状,并不会短期内对Client的长尾延时造成影响。

3.2 SILK 详细实现

3.2.1 按照概率分配I/O带宽

SILK会持续监控 Client操作被分配的I/O带宽,并且分配可用的带宽给到internal ops。
根据客户端 qps的波动情况,配置对应的监控粒度,用来决定为internal ops分配带宽的比例。当前SILK的实现配置的监控粒度是10ms。

具体配置方式如下:

T B/s : 表示在LSM 架构的KV存储中 ,总的I/O带宽
C B/s: SILK 监控的Client操作占用的I/O带宽

则internal ops可用的带宽是:I = T - C - µ B/s,其中 µ 可以理解为是一个小的buffer。

为了灵活得调整I/O带宽,SILK使用了Rocksdb标准的Rate Limiter, 同时SILK 为Flushing和 L0->L1 Compaction,也维护了一个最小的可配置的I/O带宽 时间间隔。

3.2.2 优先级调度 和 可抢占的 Internal ops设计

SILK 维护了internal work的线程池。当一个Flush任务或者一个Compaction任务处理完成之后,线程池会根据当前LSM 不同层待Compaction的量以及内存中write-buffer的状态 来判断是否需要调度更多的线程进行对应的internal (Flush 或者 L0 Compaction)任务的调度处理。
接下来主要介绍一下SILK 维护的两个线程池:一个 为flushing的 high-priority 线程池,另一个低优先级的Compactions线程池。

Flushing 在所有的internal ops中优先级最高。所以它有自己专有的线程池,并且有权限抢占其他任何低优先级的I/O带宽。Flushing的最小带宽需要能够满足从immutable memtable flush的速度快于active memtable的更新速度。当前 实现的SILK 允许内存中存在两个memtable(一个是接受更新的active memtable,另一个只读的immutable memtable) 和一个flush线程,可以通过设置memtable的个数来让client ops低时延维持时间更久。

ps : 这里提到的memtable 也是之前描述的write-buffer,都是LSM在内存中使用跳表维护的有序组件。

L0–》L1 Compaction。L0->L1的compaction 主要是为了保证L0有足够的空间来接受Flush的结果。SILK的实现中 这个internal ops并没有专用的线程进行调度,而是在需要进行L0–>L1的Compaction时 从Compaction pool中随机选择一个线程抢占 进行L0->L1的处理。

这里有必要说明一下,L0–>L1 的compaction这么重要,为什么我们不实用多线程调度,从而加快这个过程?
因为L0的sst文件之间存在key的overlap,为了保证大于L0的层内sst文件之间不存在overlap,则需要保证L0的文件处理的原子性,所以这里只能使用一个线程进行调度。

而Rocksdb原生的实现中为了减少L0的文件重叠度,也会调度L0–>L0的compaction,这里SILK仍然会沿用rocksdb的逻辑(SILK是基于rocksdb 5.7版本进行的改造),只是会将L0–>L0的Compaction 看作L0–>L1 一样的优先级。

Higher Level Compactions。更高层的Compaction 拥有调度的优先级最低。比如L1–>L2进行Compaction的过程中任务被L0–>L1的Compaction需求抢占,这个时候SILK会丢弃掉当前正在做的Compaction任务,不过实际的测试并没有发现这个操作对性能产生非常明显的影响。

SILK控制了总的KV store的I/O带宽,通过在Compaction线程池中调度更低优先级的 Compactions来完成这一过程。比较有趣的是 SILK能够根据 Compactions 任务的紧急程度进行对应I/O带宽的分配,从而能够降低整个LSM tree发生Latency Spike的概率 。

4. 性能数据

在这里插入图片描述
Nutanix 的workload是:write:read:scan的比例 – 57:41:2 ,客户端稳定输出的峰值20k/s
可以看到 SILK 能够在很长的一段时间保证客户端p99维持在1ms以下,且吞吐能够和客户端接近,相比于其他的系统则 比较有优势。

在这里插入图片描述
在Synthetic的workload下,拥有较高的写比例,同时 peek也由原来的20k/s 逐渐下降到 每隔10s 20k, 每隔50s 20k,每隔100s 20k, 更长时间之后来一次洪峰20k

可以看到silk 的延时会随着洪峰持续的时间而逐渐增加,如果洪峰是间断性来临(比如 洪峰维持10s, 50s, 100s),则SILK能够提供非常稳定且较低的延时。而因为洪峰的持续时间越长,待Compaction的量累积越多,Higher level compactions的影响越大(已经无法避免得调度更高层的Compaction)。
不过还是能够看到silk是有能力支撑突然而来的流量洪峰,保证吞吐的前提下并维持在一个较低的延时上。

在这里插入图片描述
50% read 和 50% write 场景下,这里SILK 将自己和前两种不同的 internal限速策略进行对比,比如 :
第一行蓝色的表示 动态分配internal IO的带宽,关闭 支持优先级抢占的调度策略。这个前期能够维持一个平稳的延时,但是随着 Compaction 量的累积,后期仍会降低高优先级的操作。
第二行只开启 优先级抢占的策略,关闭动态分配IO带宽的策略。因为 internal ops能够进行不同优先级之间的调度抢占,但是没有办法统筹整体的IO带宽,从而导致后期 Higher Level 的Compaction量增加,无法增大I/O带宽,使得internal ops的调度和client的 调度出现冲突。

第三行 SILK 是融合了以上两种的策略,可以看到整体能够维持在一个非常平稳的延时和吞吐上。

5. 业界相关LSM优化的展望

在这里 ,SILK介绍了三个方向的LSM相关的优化 以及 近些年业界已有的优化方案,非常有参考价值。

5.1 降低Compaction开销方向

在这里插入图片描述

5.2 Compaction变种 或者 实现相关的代替方案

在这里插入图片描述
在这里插入图片描述

5.3 在LSM 的数据结构和相关算法上的一些优化

在这里插入图片描述

LSM 的作为新生代存储引擎的基础架构,优异的写吞吐,天然支持的冷热分离架构下提供足量的读的优化。
有得必有失,Compaction的 数据回收和 merge sort带来的I/O 调度 挑战让后来者想要不断去征服这座性能高峰,为引擎界引入足以和B树相媲美的经典架构。

学无止境,不过还好有高手指路,能够发现如此精彩的存储世界,叹之有幸!!!

这篇关于LSM 优化系列(三)-- 【ATC‘19】9SILK- Preventing Latency Spikes in Log-Structured Merge Key-Value Stores的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/275595

相关文章

C#使用HttpClient进行Post请求出现超时问题的解决及优化

《C#使用HttpClient进行Post请求出现超时问题的解决及优化》最近我的控制台程序发现有时候总是出现请求超时等问题,通常好几分钟最多只有3-4个请求,在使用apipost发现并发10个5分钟也... 目录优化结论单例HttpClient连接池耗尽和并发并发异步最终优化后优化结论我直接上优化结论吧,

Java内存泄漏问题的排查、优化与最佳实践

《Java内存泄漏问题的排查、优化与最佳实践》在Java开发中,内存泄漏是一个常见且令人头疼的问题,内存泄漏指的是程序在运行过程中,已经不再使用的对象没有被及时释放,从而导致内存占用不断增加,最终... 目录引言1. 什么是内存泄漏?常见的内存泄漏情况2. 如何排查 Java 中的内存泄漏?2.1 使用 J

深入理解Redis大key的危害及解决方案

《深入理解Redis大key的危害及解决方案》本文主要介绍了深入理解Redis大key的危害及解决方案,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着... 目录一、背景二、什么是大key三、大key评价标准四、大key 产生的原因与场景五、大key影响与危

MySQL不使用子查询的原因及优化案例

《MySQL不使用子查询的原因及优化案例》对于mysql,不推荐使用子查询,效率太差,执行子查询时,MYSQL需要创建临时表,查询完毕后再删除这些临时表,所以,子查询的速度会受到一定的影响,本文给大家... 目录不推荐使用子查询和JOIN的原因解决方案优化案例案例1:查询所有有库存的商品信息案例2:使用EX

MySQL中my.ini文件的基础配置和优化配置方式

《MySQL中my.ini文件的基础配置和优化配置方式》文章讨论了数据库异步同步的优化思路,包括三个主要方面:幂等性、时序和延迟,作者还分享了MySQL配置文件的优化经验,并鼓励读者提供支持... 目录mysql my.ini文件的配置和优化配置优化思路MySQL配置文件优化总结MySQL my.ini文件

python 字典d[k]中key不存在的解决方案

《python字典d[k]中key不存在的解决方案》本文主要介绍了在Python中处理字典键不存在时获取默认值的两种方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,... 目录defaultdict:处理找不到的键的一个选择特殊方法__missing__有时候为了方便起见,

详解Spring Boot接收参数的19种方式

《详解SpringBoot接收参数的19种方式》SpringBoot提供了多种注解来接收不同类型的参数,本文给大家介绍SpringBoot接收参数的19种方式,感兴趣的朋友跟随小编一起看看吧... 目录SpringBoot接受参数相关@PathVariable注解@RequestHeader注解@Reque

正则表达式高级应用与性能优化记录

《正则表达式高级应用与性能优化记录》本文介绍了正则表达式的高级应用和性能优化技巧,包括文本拆分、合并、XML/HTML解析、数据分析、以及性能优化方法,通过这些技巧,可以更高效地利用正则表达式进行复杂... 目录第6章:正则表达式的高级应用6.1 模式匹配与文本处理6.1.1 文本拆分6.1.2 文本合并6

使用@Slf4j注解,log.info()无法使用问题

《使用@Slf4j注解,log.info()无法使用问题》在使用Lombok的@Slf4j注解打印日志时遇到问题,通过降低Lombok版本(从1.18.x降至1.16.10)解决了问题... 目录@Slf4androidj注解,log.info()无法使用问题最后解决总结@Slf4j注解,log.info(

Vue3 的 shallowRef 和 shallowReactive:优化性能

大家对 Vue3 的 ref 和 reactive 都很熟悉,那么对 shallowRef 和 shallowReactive 是否了解呢? 在编程和数据结构中,“shallow”(浅层)通常指对数据结构的最外层进行操作,而不递归地处理其内部或嵌套的数据。这种处理方式关注的是数据结构的第一层属性或元素,而忽略更深层次的嵌套内容。 1. 浅层与深层的对比 1.1 浅层(Shallow) 定义