rocksdb --基本操作

RocksDb的基本操作:Basic Operation

读流程

读流程

写流程

写流程

Opening A Database

Writes

Atomic Updates

  • 多个更新操作通过WriteBatch可以保证原子性。WriteBatch中操作会按照顺序应用到db中。 把多个单独的修改放入WriteBatch也可以提高bulk updates速度。
  #include "rocksdb/write_batch.h"
  ...
  std::string value;
  rocksdb::Status s = db->Get(rocksdb::ReadOptions(), key1, &value);
  if (s.ok()) {
    rocksdb::WriteBatch batch;
    batch.Delete(key1);
    batch.Put(key2, value);
    s = db->Write(rocksdb::WriteOptions(), &batch);
  }

Synchronous Writes

  • 默认rocksdb的每个write都是异步:把进程中write push到操作系统内存后就返回。而从操作系统内存到底层存储的传输过程是异步的。如果打开sync选项,则write操作直到写数据都已经被push到存储层后才返回。(在posix 系统中,这种方式是通过在write操作之前调用fsync(...) or fdatasync(...) or msync(..., MS_SYNC)方法实现的)
  rocksdb::WriteOptions write_options;
  write_options.sync = true;
  db->Put(write_options, ...);

Non-sync Writes

  • 如果是非同步写(异步),rocksdb只是在操作系统 buffer或内部buffer(如果设置了options.manual_wal_flush = true)缓存WAL write。比同步写快。
  • 非同步写坏处: 机器crash后,会导致最新的一些updates操作会丢失。注意,如果只是rocksdb进程的crash不会导致数据丢失,即使sync为false,因为一个udpate被认为执行完成时,其进程中的内存数据已经被push到操作系统内存中了。

Comparators

  • rocksdb的key默认按照字典序排序bytes。注意:cpu的字节序时采用小端存储:低地址存储低字节,高地址存储高字节,所以int64的变量内容存入key,如果不进行大端转换,存储到roksdb的key中也是小端存储,这样排序并不满足数值大小规则。
  • 可以通过实现rocksdb::Comparator接口来自定义排序规则。如果此时也提供了Filter,则需要保证Filter规则和比较规则兼容
class TwoPartComparator : public rocksdb::Comparator {
   public:
    // Three-way comparison function:
    // if a < b: negative result
    // if a > b: positive result
    // else: zero result
    int Compare(const rocksdb::Slice& a, const rocksdb::Slice& b) const {
     // key中包含两个部分,只按照第一部分进行排序
      int a1, a2, b1, b2;
      ParseKey(a, &a1, &a2);
      ParseKey(b, &b1, &b2);
      if (a1 < b1) return -1;
      if (a1 > b1) return +1;
      return 0;
    }

    // Ignore the following methods for now:
    const char* Name() const { return "TwoPartComparator"; }
    void FindShortestSeparator(std::string*, const rocksdb::Slice&) const { }
    void FindShortSuccessor(std::string*) const { }
  };

  //  use
  TwoPartComparator cmp;
  rocksdb::DB* db;
  rocksdb::Options options;
  options.create_if_missing = true;
  options.comparator = &cmp;
  rocksdb::Status status = rocksdb::DB::Open(options, "/tmp/testdb", &db);

Backwards compatibility

  • 如果comparator的name确定了,那么以后再次打开db时必须使用相同的name的comparator,否则打开失败。
  • 如果key的format后期可能会变化,那么为了兼容,在设计之初,可以给每个key预留一个字节作为version,这样如果有新的key fromat,那么新key format的version加一,那么可以不用改变comparator的name,同时comparator中根据version采用不同的比较规则。

Block size(读写传输单元)

  • rocksdb在和持久化存储(应该是sstfile)间传输数据时(读写操作),rocksdb会将key排序相近的数据组成一个block为单位进行传输,默认block size是4096个未压缩字节。
  • db的内容存储在sstfiles中,每个sstfile包含多个有序的压缩的blocks.
  • 如果应用的bulk scan操作多的话,调大该值,这样可以使一个block中都是scan需要的值,如果大多数都是单次的value读操作,就调小该值。但是不要低于1k, 也不要大于几M,否则效果不明显。
  • 有更大的block size时,使用compression(压缩)效率更高。
  • Options::block_size选项来设置该参数。

Write buffer

  • Options::write_buffer_size 设置内存中write buffer大小,达到大小后,会将buffer写到磁盘上sstfile中。默认64M. 该值越大,内存利用越高,而且再次打开db时,将要需要更久的恢复时间。
  • Options::max_write_buffer_number: 设置内存中最多存在的write buffer数量,默认2两个buffer,当一个write buffer正在flush到L0中时,新的写操作写入另一个write buffer中。flush操作在Thread Pool中被执行。
  • Options::min_write_buffer_number_to_merge:buffers最小合并数量,在把write buffer写入文件前,会对多个buffers合并(merge),以减少写数据量,因为多个buffer中可能会存在重复的key,这样会删除旧的,只保留新的写入。默认为1, 如果是1,那么意味着,不合并buffer,每个buffer都会被在磁盘上写成一个单独的文件。(L0层)

Compression(压缩block的算法)

  • 每个block在被写入持久化存储中前都会被压缩。压缩默认开启,而且对于uncompressible data自动关闭。(文件中的Filter block不会被压缩, data blocks和index blocks会被压缩)
  • Options.compression控制前n-1个level中使用的压缩算法,默认,Snappy,但是LZ4更推荐,都是lightweight压缩算法,在space和cpu使用方面有一个很好的平衡。
  • Options.compression_per_level[index]: 为每个level配置不同压缩算法,覆盖compression的配置,也可以取消压缩
  • options.bottommost_compression: 设置最后一层level压缩算法,因为包含更多数据,所以推荐采用heavy-weight算法ZSTD,或Zlib。压缩空间更小cpu使用更高。
  rocksdb::Options options;
// 关闭压缩
  options.compression = rocksdb::kNoCompression;
  ... rocksdb::DB::Open(options, name, ...) ....

压缩算法,得确保机器上拥有该压缩算法包

rocksdb::kNoCompression , rocksdb::kSnappyCompression , 
rocksdb::kLZ4Compression , rocksdb::kLZ4HCCompression ,
rocksdb::kZSTD , rocksdb::kZlibCompression ,
rocksdb::kBZip2Compression 
  • 更多参数配置:

Cache (缓存频繁访问的uncopressed block)

  • db的内容存储在sstfiles中,每个sstfile包含多个有序的压缩的blocks.
  • options.block_cache不为空,则会被用来cache uncompressed block的数据(比如filter block?或者从压缩block读出来解压完的block?)。
  • 使用OS的file cache(即 Page cache)来缓存compressed data。
#include "rocksdb/cache.h"
  rocksdb::BlockBasedTableOptions table_options;
  table_options.block_cache = rocksdb::NewLRUCache(100 * 1048576); // 100MB uncompressed cache

  rocksdb::Options options;
  options.table_factory.reset(rocksdb::NewBlockBasedTableFactory(table_options));
  rocksdb::DB* db;
  rocksdb::DB::Open(options, name, &db);
  ... use the db ...
  delete db
  • 当执行bulk read时,可以关掉cache,不然bulk read中内容会替换掉cached中大部分内容, 在每个iterator option中可以配置该项。
  rocksdb::ReadOptions options;
  options.fill_cache = false;
  rocksdb::Iterator* it = db->NewIterator(options);
  for (it->SeekToFirst(); it->Valid(); it->Next()) {
    ...
  }

Key Layout

  • 因为磁盘传输和cache的最小单位都是block,所以排序相近的key通常都待在相同的block中,为此,为了提高性能,可以把经常一起访问的key放在一起,不相关的放在不同block中,可以通过给key设置前缀来实现。
  • 比如, 基于rocksdb实现的文件系统,key/value的类型有如下两种,为了提升方便读取,可以为filename和file_block_id两个key加上不同的一个字符前缀"/"和“0”来实现。
filename -> permission-bits, length, list of file_block_ids
file_block_id -> data

Filters (提升读取效率)

  • Filters,See rocksdb/filter_policy.h for detail.
  • 由于rocksdb采用LSMT的数据组织方式,一次Get操作可能会涉及到多次的io操作才能读到需要的数据,为了减少io次数,提供了FilterPolicy机制。FilterPolicy对Range scan(Iterator)操作没有用。
  • BlockBasedTableOptions.filter_policy选项如果不为空,则使用该filter,可以使用内置的Bloom Filter,也可以继承FilterPolicy自定义策略。
 rocksdb::Options options;
   rocksdb::BlockBasedTableOptions bbto;
   bbto.filter_policy.reset(rocksdb::NewBloomFilterPolicy(
       10 /* bits_per_key 为每个key消耗的内存:10个bit,值越大,越占用内存,精度越高*/,
       false /* use_block_based_builder*/));
   options.table_factory.reset(rocksdb::NewBlockBasedTableFactory(bbto));
   rocksdb::DB* db;
   rocksdb::DB::Open(options, "/tmp/testdb", &db);
   ... use the database ...
   delete db;
   delete options.filter_policy;
  • 如果使用了自定义的key comparator,那么你的filter policy需要考虑和你的comparator兼容,此时可能需要自定义FilterPolicy。比如,你的key排序规则只考虑key的前两个数值,对于key后面的内容忽略掉,不参与比较,那么,此时你的FilterPolicy需要定制也忽略key中剩余部分的值。如下:
class CustomFilterPolicy : public rocksdb::FilterPolicy {
   private:
    FilterPolicy* builtin_policy_;
   public:
    CustomFilterPolicy() : builtin_policy_(NewBloomFilter(10, false)) { }
    ~CustomFilterPolicy() { delete builtin_policy_; }

    const char* Name() const { return "IgnoreTrailingSpacesFilter"; }

    void CreateFilter(const Slice* keys, int n, std::string* dst) const {
      // Use builtin bloom filter code after removing trailing spaces
      std::vector<Slice> trimmed(n);
      for (int i = 0; i < n; i++) {
        trimmed[i] = RemoveTrailingSpaces(keys[i]);
      }
      return builtin_policy_->CreateFilter(&trimmed[i], n, dst);
    }

    bool KeyMayMatch(const Slice& key, const Slice& filter) const {
      // Use builtin bloom filter code after removing trailing spaces
      return builtin_policy_->KeyMayMatch(RemoveTrailingSpaces(key), filter);
    }
  };

Checksums(在多副本中通过验证可以做到提前备份)

  • rocksdb中的数据(metadata和blocks)都存有checksum(校验和),提供了两个方法来验证checksum是否正确:如果数据不完成,得到的checksum和已有的checksum就不一致。

    • ReadOptions::verify_checksums, 默认打开,每次读操作都会对当前读的数据所在metat或blocks进行checksums验证。
    • Options::paranoid_checks, 默认打开, 如果发现database的某个部分已经corrupted,那么在open db时或者后续的其它database 操作时就会产生一个error。比上面选项覆盖数据量要大,因为即使你读取的数据blocks是完整的,但是发现其它blocks事不完整的,那么此时也会报错。
  • DB::VerifyChecksum(): 手动触发校验和验证,对所有的数据进行(metadata, data blocks),耗时长,目前只支持BlockBasedTable format格式组织的数据。如果事分布式多副本数据库,可以通过该方法来验证某个副本是否完整,如果不完整,则提前创建要给副本。

Compaction

  • 目的:删除过期数据,提高read效率
  • Compaction.

Approximate Sizes(得到key占用文件存储空间)

  • GetApproximateSizes方法可以得到一个key或多个key在文件系统中占用得存储空间(单位bytes)。
   rocksdb::Range ranges[2];
   ranges[0] = rocksdb::Range("a", "c");
   ranges[1] = rocksdb::Range("x", "z");
   uint64_t sizes[2];
   db->GetApproximateSizes(ranges, 2, sizes);
  • sizes[0]代表key range[a...c)占用文件系统得存储空间,sizes[1] 代表key range[x...z)。

Environment(高级,自定义该接口可改变rocksdb对文件操作或者系统调用方式)

  • rocksdb的实现中对所有文件操作(file operations)或者其它系统调用(system calls)都是通过rocksdb::Env对象来完成的。
  • 复杂的客户端如果想提供自己的Env实现来获得更灵活的控制,那么可以通过继承Env来实现。比如:应用想在file IO paths上引入人工干预的延迟,一次来限制rocksdb对系统上其它应用的影响。
  class SlowEnv : public rocksdb::Env {
    .. implementation of the Env interface ...
  };

  SlowEnv env;
  rocksdb::Options options;
  options.env = &env;
  Status s = rocksdb::DB::Open(options, ...);

Porting

  • rocksdb可以移动到新的平台用户通过实现不同接口,更多看文档

Managebility(通过收集统计信息来更好的tune应用)

  • Options::table_properties_collectors or Options::statistics用来收集使用信息。建议把统计信息输出到其它监控系统中来降低自身应用的负担。

  • 也可以对单次请求做统计:Perf Context and IO Stats Context.

  • refer to rocksdb/table_properties.h and rocksdb/statistics.h

  • Statistics

Purging WAL files(WAL日志删除规则)

  • 默认,当wal日志不在需要时就会被删除。但是用户可以通过TTL和大小限制配置项来archive(归档)日志,然后延迟删除他们。
  • Options::WAL_ttl_seconds and Options::WAL_size_limit_MB.
  • 如果都为0,那么不在需要时立即删除。
  • ttl = 0, size != 0,那么每隔10分钟检查wal files大小,如果超过 size limit,则从最开始位置删除直到size limit大小。
  • ttl!=0, size = 0, 那么每隔 ttl/ 2秒检查wal files,大于ttl的files将被删除。
  • 如果都不为0,每隔10分钟检查,两者都检查,ttl优先,满足任何一个都删除。