Tair ldb(leveldb存储引擎)实现介绍

jopen 13年前
     <div id="news_body">     <p>        <a href="/misc/goto?guid=4958532115685964292" target="_blank">Tair</a> 是淘宝开源的分布式 KV 缓存系统,内部将功能模块化,抽离出底层存储细节,可以接入不同的存储引擎。<a href="/misc/goto?guid=4958183634494907073" target="_blank">leveldb</a> 是 Google 开源的单机存储引擎,目前,已经作为 Tair 的持久化存储引擎 ldb 上线使用,这里对接入 leveldb 所做的处理以及修改进行介绍。</p>     <p>        Tair 首先是一个分布式的框架,有一系列策略满足 CAP(数据备份,迁移复制等)。另外,还有针对应用场景的功能特性(namespace,数据过期时间,原子计数等)。接入 leveldb 时主要对 KV 操作之外的功能做相应的处理。</p>     <p>        <strong>一. 修改配置</strong></p>     <p>        leveldb 中有一系列参数会与读写的效率有关,将相关的配置以及编译常量统一修改成可运行时配置参数,测试选取最佳配置值。</p>     <p>        <strong>二. 实例个数控制</strong></p>     <p>        首先确定在一个 tair server 上要启几个 leveldb 实例?</p>     <p>        Tair 中以桶来组织数据,如果按照一个桶一个 leveldb 实例,在做迁移复制的时候会很方便,但考虑如果在一块磁盘上起多个实例,那么整体看来,多个顺序写变成了随机写,每个实例的 compact 进程会加剧整个磁盘的随机 IO,所以并不采用每个桶一个实例,而是针对磁盘的数量由相关配置控制实例的个数。</p>     <p>        <strong>三. 内部 key 的格式</strong></p>     <p>        为实现 Tair 中的功能逻辑,ldb 传入 leveldb 的 user-key 格式如下:</p>     <p style="text-align:center;"><a><img alt="Tair ldb(leveldb存储引擎)实现介绍" src="https://simg.open-open.com/show/18e06300c23a1c192ab8d095df1af361.jpg" width="300" height="48" /></a></p>     <p>        1)    Tair 中的数据可以设置过期时间,过期时间保存可以保存在在 value 的 meta 中,但考虑能在 leveldb 内部提前检查,省去解析 value 的消耗,将过期时间保存在 key 中,但并不参与排序。</p>     <p>        2)    Tair 中的数据组织以及迁移复制都是以桶(bucket)单位,为获得一个桶的数据,添加桶号前缀,保证一个桶的数据都存储在一起。</p>     <p>        3)    Tair 中的数据区分 namespace,会将 namespace 作为客户端传入 key 的前缀存储。</p>     <p>        <strong>四. 自定义 comparator</strong></p>     <p>        为了实现 Tair 中的功能逻辑,实现自定义的 comparator 传入 leveldb,实现自定义的排序逻辑(传入 leveldb 的 user-key 中表示过期时间的前四个字节不参与排序),并为 comparator 添加两个判断数据是否有效的逻辑接口(ShouldDrop ()/ShouldDropMaybe ()),修改 leveldb 内部做遍历以及 compact 时判断数据是否有效(kTypeValue/kTypeDeletion)的逻辑。</p>     <p>        1)    ShouldDropMaybe (): 用来判断数据是否已经过期。解析 key 中的 expired_time 即可。</p>     <p>        2)    ShouldDrop (): 用来判断数据是否属于已经清理掉的数据(bucket 已经迁移或者 namespace 已经被清理)。</p>     <p>        3)    区分过期数据与清理数据的判断,是因为丢弃过期数据必须保证该 key 是数据的最后出现,否则删除该数据会让该 key 失去最后的更新状态,而清理数据有 gc 信息保证,不需要关心数据的状态。</p>     <p>        <strong>五. 清理数据/自定义的内部 compact 逻辑</strong></p>     <p>        这里把清理数据的操作称为 gc。</p>     <p>        1.     迁移的 bucket 以及清理的 namespace 中的数据</p>     <p>        一旦发生 bucket 迁移或者清理 namespace,会把相应的信息保存下来(GcNode)</p>     <blockquote>      struct GcNode      <p>{</p>      <p>// 迁移走的 bucket 或者清掉的 namespace</p>      <p>int32_t key_;</p>      <p>// 清理发生时的 SequnceNumber,用来判断数据的时间点</p>      <p>uint64_t sequence_;</p>      <p>// 清理发生时的 FileNumber,用来缩小 compact 的范围</p>      <p>uint64_t file_number_;</p>      <p>// 清理发生时的时间</p>      <p>uint32_t when_;</p>      <p>};</p>     </blockquote>     <p>        1)    Tair 中会有桶迁移或者 namespace 清理的操作,废弃的数据并不会立刻清理,依靠后续的 compact 进程清理。但同时桶又可能被迁移回来,namespace 也可能继续使用。leveldb 中的 SequnceNumber 恰好可以标识数据的时间点,所以在做数据清理的时候,记录下当时的 SequnceNumber(sequnce_),在做判断的时候,只有小于 sequnce_的数据才认为可以被丢弃。</p>     <p>        2)    filenumber_是为了缩小主动 compact 的范围。</p>     <p>        leveldb 自身进行 compact 的过程中,加入自实现 comparator 的 ShouldDropMaybe ()/ShouldDrop ()判断逻辑,会将对应的 gc 数据清理。但 leveldb 自身的 compact 进程并不能保证将所有的数据清理掉,所以添加 compact 的定时线程,主动触发 compact 做数据清理。对于清理的 bucket 或者 namespace,根据 key 的格式,可以构造出需要 compact 的 key-range,但直接使用 leveldb 提供的 CompactRange 有以下问题:</p>     <p>        1)    如果某个 sstable 在记录 gc 之后已经被 compact 过,所要清理的数据就已经丢弃掉了,并不需要再做 compact。</p>     <p>        2)    主动触发的 compact 并不是基于均衡 db 状态,所以造成的 level 迁移可能会有反作用。</p>     <p>        对于1)2)的问题,做以下策略:</p>     <p>        1)    FileNumber 全局递增可以用来标识时间点,gc 时,记录下当时的 FileNumber,主动 compact 时,只需要选取符合 key-range 并且 FileNumber 小于记录的 filenumber_的 sstable 即可,缩小需要 compact 的 sstable 范围。</p>     <p>        2)    主动触发的 compact 只选取 level-n中的 sstable,compact 后,也只生成 level-n中的 sstable,level 之间的均衡,仍由内部 compact 进程负责。</p>     <p>        3)    基于上述1) 2),为 leveldb 添加 CompactRangeSelfLevel ()逻辑,实现1) 2)中的策略,主动触发 compact 以清理数据,整个 db 的均衡仍由内部的机制保证。</p>     <p>        gc 信息会同时记 binlog,在 server 重启时 replay。</p>     <p>        2.     过期的数据</p>     <p>        因为过期是一个持续的时间状态,如果要完全回收过期的数据,只能对全 db 做 compact,这样做的性能比不合算,所以目前对过期数据不做特别的主动清理,依靠内部以及外部触发的 compact 进程中回收空间。</p>     <p>        <strong>六. 上层 cache</strong></p>     <p>        leveldb 内部有 sstable 元信息和 block 数据的 cache,优化读的效率,在 leveldb 上层再嵌入 Tair 自带的 mdb 做 KV 层面的 cache,并添加 cache 的信息统计。当前统计看到,热点数据的读基本落在 mdb 中。</p>     <p>        <strong>七. 后续的优化</strong></p>     <p>        1)    leveldb 本身的一些优化(参见 <a href="/misc/goto?guid=4959499514930538864" target="_blank">leveldb 的实现解析</a>)。</p>     <p>        2)    优化网络的使用,使用新的网络框架。</p>     <p>        3)    ssd 使用优化。leveldb 内部做的一些细节优化针对于 sas 盘的 IO 性能,当前使用的 ssd IO 能也未充分利用。后续针对提升 ssd 使用性能做相应的 IO 优化(dio)。</p>     <p>        4)     内存的优化使用。大内存的使用有很大的优化余地,避免 swap 的使用,pagecache 的相应回写策略配置。</p>     <p>        5)    对不存在数据查找的优化,采用 mock value 处理或者添加 bloomilter。</p>     <p>        6)    range 以及数据 dump 功能的实现。</p>     <p>        欢迎大家对 leveldb 的使用优化提出宝贵建议。</p>     <p>        广告时间: Tair 现在已经将 <a href="/misc/goto?guid=4958185538616997143" target="_blank">Redis</a> 的存储部分抽离出来,作为非持久化的存储引擎 rdb,即将上线使用,欢迎大家强势围观。<br /> </p>     <div id="come_from">      来自:      <a id="link_source2" href="/misc/goto?guid=4959499515059418271" target="_blank">rdc.taobao.com</a>     </div>     <p></p>    </div>