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

相关文章

Python如何使用__slots__实现节省内存和性能优化

《Python如何使用__slots__实现节省内存和性能优化》你有想过,一个小小的__slots__能让你的Python类内存消耗直接减半吗,没错,今天咱们要聊的就是这个让人眼前一亮的技巧,感兴趣的... 目录背景:内存吃得满满的类__slots__:你的内存管理小助手举个大概的例子:看看效果如何?1.

一文详解SpringBoot响应压缩功能的配置与优化

《一文详解SpringBoot响应压缩功能的配置与优化》SpringBoot的响应压缩功能基于智能协商机制,需同时满足很多条件,本文主要为大家详细介绍了SpringBoot响应压缩功能的配置与优化,需... 目录一、核心工作机制1.1 自动协商触发条件1.2 压缩处理流程二、配置方案详解2.1 基础YAML

golang 日志log与logrus示例详解

《golang日志log与logrus示例详解》log是Go语言标准库中一个简单的日志库,本文给大家介绍golang日志log与logrus示例详解,感兴趣的朋友一起看看吧... 目录一、Go 标准库 log 详解1. 功能特点2. 常用函数3. 示例代码4. 优势和局限二、第三方库 logrus 详解1.

MySQL中慢SQL优化的不同方式介绍

《MySQL中慢SQL优化的不同方式介绍》慢SQL的优化,主要从两个方面考虑,SQL语句本身的优化,以及数据库设计的优化,下面小编就来给大家介绍一下有哪些方式可以优化慢SQL吧... 目录避免不必要的列分页优化索引优化JOIN 的优化排序优化UNION 优化慢 SQL 的优化,主要从两个方面考虑,SQL 语

如何解决mysql出现Incorrect string value for column ‘表项‘ at row 1错误问题

《如何解决mysql出现Incorrectstringvalueforcolumn‘表项‘atrow1错误问题》:本文主要介绍如何解决mysql出现Incorrectstringv... 目录mysql出现Incorrect string value for column ‘表项‘ at row 1错误报错

MySQL中慢SQL优化方法的完整指南

《MySQL中慢SQL优化方法的完整指南》当数据库响应时间超过500ms时,系统将面临三大灾难链式反应,所以本文将为大家介绍一下MySQL中慢SQL优化的常用方法,有需要的小伙伴可以了解下... 目录一、慢SQL的致命影响二、精准定位问题SQL1. 启用慢查询日志2. 诊断黄金三件套三、六大核心优化方案方案

Redis中高并发读写性能的深度解析与优化

《Redis中高并发读写性能的深度解析与优化》Redis作为一款高性能的内存数据库,广泛应用于缓存、消息队列、实时统计等场景,本文将深入探讨Redis的读写并发能力,感兴趣的小伙伴可以了解下... 目录引言一、Redis 并发能力概述1.1 Redis 的读写性能1.2 影响 Redis 并发能力的因素二、

使用国内镜像源优化pip install下载的方法步骤

《使用国内镜像源优化pipinstall下载的方法步骤》在Python开发中,pip是一个不可或缺的工具,用于安装和管理Python包,然而,由于默认的PyPI服务器位于国外,国内用户在安装依赖时可... 目录引言1. 为什么需要国内镜像源?2. 常用的国内镜像源3. 临时使用国内镜像源4. 永久配置国内镜

C#原型模式之如何通过克隆对象来优化创建过程

《C#原型模式之如何通过克隆对象来优化创建过程》原型模式是一种创建型设计模式,通过克隆现有对象来创建新对象,避免重复的创建成本和复杂的初始化过程,它适用于对象创建过程复杂、需要大量相似对象或避免重复初... 目录什么是原型模式?原型模式的工作原理C#中如何实现原型模式?1. 定义原型接口2. 实现原型接口3

MySQL中Next-Key Lock底层原理实现

《MySQL中Next-KeyLock底层原理实现》Next-KeyLock是MySQLInnoDB存储引擎中的一种锁机制,结合记录锁和间隙锁,用于高效并发控制并避免幻读,本文主要介绍了MySQL中... 目录一、Next-Key Lock 的定义与作用二、底层原理三、源代码解析四、总结Next-Key L