关于后端:ToplingDB-Zero-Copy

51次阅读

共计 5189 个字符,预计需要花费 13 分钟才能阅读完成。

  1. 背景 ToplingDB 是 topling 开发的 KV 存储引擎,fork 自 RocksDB,进行了很多革新,其中最重要的部件是 ToplingZipTable, 是 BlockBasedTable 的代替品,性能更高而内存占用更低。ToplingZipTable 应用 CO-Index 与 PA-Zip 实现索引和数据的存储。CO-Index 指 Compressed Ordered Index, 是一类内存压缩的索引,无需解压,在压缩的状态下能够对索引间接搜寻,并且搜寻速度极快。从 String 类型的 Key,搜寻出一个密实 的 整数 ID。PA-Zip 指 Point Accessible Zip, 无需 BlockCache,能够十分疾速地 按 ID 定点拜访单条数据。PA-Zip 也有非压缩的实现,例如 MyTopling(MySQL) 中长度固定且单条数据较短的表,用一个定长数组就能够十分高效地实现。在这种状况下,是能够实现 zero copy 的,但 RocksDB 中有一些问题导致无奈 zero copy。ToplingZipTable 间接应用 topling-zip 中的 BlobStore 类体系来实现 PA-Zip。2. RocksDB 为什么不能 zero copy2.1. SuperVersion 保活传统上,个别通过援用计数来标记对象的存活,然而多线程援用计数会导致 CPU 核间的频繁通信,导致多核不 Scale。所以 RocksDB 在 DB::Get 中,应用了一些技巧,防止了频繁的增减援用计数:对象实例级的 ThreadLocalPtr,每个线程援用一个 SuperVersion 对象,短暂放弃援用计数对于一个 DB 的只读操作:在操作开始处,取出该线程 TLS 的 SuperVersion 指针并将 TLS 指针设为 InUse,取出的 TLS 指针放到该函数的局部变量中,其它线程就察看不到这个 SuperVersion 了在操作完结处,将保留在函数局部变量中的 SuperVersion 指针放回线程 TLS,其它线程就能够察看到这个 SuperVersion 了任何线程创立新的 SuperVersion 对象时(例如 Flush/Compact 完结时),查看所有线程的 TLS,回收不在 InUse 状态的 SuperVersion,回收后相应 TLS 设为 null,这样当那个 TLS 所属的线程应用它时,发现是 null,就从全局获取最新的 SuperVersion 对 TLS 的操作均应用 atomic 以这样的形式,SuperVersion 的保活就不须要每个操作都增减援用计数了,从而实现多核线性 Scale。2.2. PinnableSlice 最高效的 DB::Get 重载版本是 value 为 PinnableSlice 的那个,对于 BlockBasedTable,PinnableSlice 会 Pin 住 BlockCache 中相应的 Block,防止了从 BlockCache 中把 value memcpy 到 std::string 中。PinnableSlice 反对可插拔的 Cleanable 接口,BlockCache 内存的回收就是通过这个 Cleanable 接口实现的。BlockCache 的生存期和 SuperVersion 是互相独立的,所以 PinnableSlice 在这里能够失常工作。2.3. mmap 对于应用 mmap 的 SST,因为有 PinnableSlice,所以实践上,咱们能够把 mmap 中存储 value 的内存间接通过指针返回给用户代码。然而问题就出在这里,SST 是 (间接) 绑定到 SuperVersion 的,要让用户代码拜访 SST 的 mmap,那在用户代码拜访 value 期间就必须保活相应的 SuperVersion。从 DB::Get 返回时,SuperVersion 就放回 TLS 了,从而可能被其它线程回收,用户代码就不能平安地拜访从属于那个 SuperVersion 的 SST 的 mmapRocksDB 本身的 PlainTable 就应用了 mmap,面对这个问题,PlainTable 偷了个懒,只有在 immortal 的状况下,才把 mmap 内存间接返回给用户代码。什么状况下是 immortal 呢?应用了 DB::OpenForReadOnly 的状况下。这个限度就过于刻薄,实际中很难满足!3. ToplingDB 的解决方案首先,咱们绝不能为了 Zero Copy 而毁坏 API 兼容性,必须利用现有的 API 实现 Zero Copy。咱们在 ReadOptions 下面做文章,给 ReadOptions 减少两个办法 StartPin/FinishPin,pin 的对象是 super version:如果想要获取 zero copy 的收益,就在调用 Get 前 StartPin,应用完 Get 回来的 value 之后 FinishPin 如果不想改代码,就只是没有 zero copy,所有跟从前一样用户须要关怀的扭转:diff –git a/include/rocksdb/options.h b/include/rocksdb/options.h
    — a/include/rocksdb/options.h
    +++ b/include/rocksdb/options.h
    @@ -1735,6 +1735,13 @@ struct ReadOptions {
  2. std::shared_ptr<struct ReadOptionsTLS> pinning_tls = nullptr;
    +
  3. // pin SuperVersion to enable zero copy on mmap SST
  4. void StartPin();
  5. void FinishPin();
    +
  6. ~ReadOptions();
    ReadOptions();
    ReadOptions(bool cksum, bool cache);
    };
    实现细节都在 ReadOptionsTLS 中,求知欲强的用户能够看一下具体实现。当然,相应地,SST 的实现局部也要为此做一点适配。咱们给 PlainTable 做了相应的适配,批改少到不堪设想。pinner 设为一个默认结构的 Cleanable,就是指不须要对 value 深拷贝,value 指向的内存也不须要做任何清理工作;pinner 设为 null 时,会对 value 做深拷贝这个批改还带来一个额定收益:当要一一 Get 多条数据时,只须要一次 StartPin/FinishPin,而 StartPin 时会将 SuperVersion 指针放到 ReadOptionsTLS 中,通过 pinning_tls 获取 SuperVersion 防止了绝对低廉的 ThreadLocalPtr 拜访,每次 Get 节俭了大概 30 纳秒。4. db_bench 适配咱们给 db_bench 减少了一个选项 -enable_zero_copy,开启这个选项,Get 就会应用 StartPin/FinishPin 以应用 zero copy。4.1. ToplingZipTable Zero Copy 在阿里云 Xeon 8369HB 的云主机上,咱们测进去这样的性能(-key_size=8 -value_size=20):readrandom:
    0.234 micros/op 4279978 ops/sec 23.365 seconds 100000000 operations;
    114.3 MB/s (100000000 of 100000000 found)Compact 之后,单个 Get 操作 234 纳秒,这其中,DB::Get 占了 83%,约合 194 纳秒,db_bench 驱动代码占 17%,真正干活的 ToplingZipTable::Get 只占了 18%,合 42 纳秒:

    仅 Flush 之后,不 Compact,单个 Get 操作 254 纳秒,比 234 略微慢一点 readrandom:
    0.254 micros/op 3939416 ops/sec 25.384 seconds 100000000 operations;
    105.2 MB/s (100000000 of 100000000 found)测试过程参考 这里,记得增加命令行参数 -enable_zero_copy=true,同时,对 db_bench_enterprise.yaml 做小幅批改。— a/sample-conf/db_bench_enterprise.yaml
    +++ b/sample-conf/db_bench_enterprise.yaml
    @@ -119,16 +119,16 @@ CFOptions:
    default:
    max_write_buffer_number: 4
    memtable_factory: “${cspp}”

  7. write_buffer_size: 8M
  8. write_buffer_size: 128M
    # set target_file_size_base as small as 512K is to make many SST files,
    # thus key prefix cache can present efficiency
    # 把 target_file_size_base 设得很小是为了产生很多文件,从而体现 key prefix cache 的成果
  9. target_file_size_base: 512K
  10. target_file_size_base: 64M
    target_file_size_multiplier: 1
    table_factory: dispatch
    compaction_options_level:

    L1_score_boost: 1
  11. max_bytes_for_level_base: 4M
  12. max_bytes_for_level_base: 400M
    max_bytes_for_level_multiplier: 4
    #level_compaction_dynamic_level_bytes: true
    level0_slowdown_writes_trigger: 20
    @@ -144,7 +144,7 @@ DBOptions:
    max_level1_subcompactions: 7
    inplace_update_support: false
    WAL_size_limit_MB: 0
  13. statistics: “${stat}”
  14. statistics: “${stat}”

    allow_mmap_reads: true
    databases:
    db_bench_enterprise:
    4.2. 小插曲这两头有段小插曲,开始在用 db_bench 测试验证时,发现在 Version::Get 中有意想不到的 ReadOptions 析构函数调用,占比还挺高:

    对照了一下代码,原来在 Version::Get 中,有一行代码:BlobFetcher blob_fetcher(this, read_options);
    ReadOptions 的拷贝就在 BlobFetcher 中,这个很好修,Version::Get 中 read_options 的生存期笼罩了 blob_fetcher, 把拷贝改成援用即可,然而万一 BlobFetcher 在其它中央的生存期没有被 read_options 笼罩,不就出问题了,所以 grep 一下代码,还真找到了这样的中央,一起修掉。4.3. Read 采样 RocksDB 会对 Get 操作进行采样,采样过程中须要计算随机数,随机数发生器是个 Thread Local,这个过程的耗时占比原本很小,然而 Zero Copy 之后,整体耗时也就 200 纳秒,它的绝对占比就比拟大了,所以咱们对此减少了一个环境变量配置:TOPLINGDB_GetContext_sampling,可配置为 {kAlways,kNone,kRandom},其中 kRandom 是默认行为,与上游 RocksDB 保持一致。为了升高采样对耗时的扰动,测试中咱们设置环境变量 TOPLINGDB_GetContext_sampling=kNone4.4. BlockBasedTable, Cache 管够,但无 Zero Copy 换用 BlockBasedTable 进行雷同的测试(Compact 之后):readrandom:
    2.652 micros/op 377083 ops/sec 265.194 seconds 100000000 operations;
    10.1 MB/s (100000000 of 100000000 found)尽管 BlockBasedTable 没有 Zero Copy,但后面咱们提到,它会用 PinnableSlice pin 住 value 援用的 Block,也不须要 memcpy;然而即便如此,性能依然差 10 倍,当然,必须再次强调 测试过程 中的阐明:该测试条件均是单方的最优条件。ToplingZipTable 应用本人的通用索引 NestLoudsTrie 时,搜寻 Key 的速度会慢一些,启用压缩时,获取 Value 的速度会慢一些。外加一条:ToplingZipTable 启用压缩时就没有 zero copy 了。

正文完
 0