Tendis存储版 Binlog和复制优化

原来的binlog格式

  1. binlog格式是物理格式,保证幂等,记录rocksdb的绝对内容
  2. 一个事务多个rocksdb操作,会有多个记录
  3. 每个事务有唯一的txnid,每个操作rocksdb操作记录一个localid
  4. 记录格式

    primary_key: txnid | local_id| flag| timestamp second_key: “” value: rocksdb_key| rocksdb_value

问题:

  1. 一个事务多条rocksdb的binlog记录,存在存储空间的浪费
  2. binlog的key无法精确定位,只能通过前缀搜索然后通过rocksdb游标进行遍历,会消耗大量的CPU资源
  3. txnid是事务开启时候确定,由于复制的高低水位导致,导致tendis无法支持过大的事务

解决思路:

  1. 一个事务一条binlog
  2. binlog的key需要能精确定位
  3. 事务提交时候确定binlog id

新binlog方案

术语

Repllog : 表示一个事务记录在rocksdb的日志,对应RepllogRawV2/RepllogV2

Binlog:表示将多个repllog打包发送到slave的一条日志,多个repllog对应一个Binlog文件。

RepllogV2记录格式

key,只包含binlogid,可唯一构造,便于游标的实现,可以不用基于rocksdb的游标

primary_key: binlogid(8字节),大端存储

second_key: “”

value:

slotid : 4字节:表示该操作涉及的slot,涉及多个slots使用-1表示

flag: 2字节,目前总是 ReplFlag::REPL_GROUP_START | ReplFlag::REPL_GROUP_END,以后对于超大事务会利用这个flag将一个事务的repllog拆成多个,目前不会拆分。

txnid: 8字节,master执行该操作的事务id

timestamp: 8字节,事务提交时候的时间戳

versionEp: 8字节,这个是计算存储分离方案的重点,以后再详述

comStr: LenStr,记录当前binlog的command name n * ReplLogValueEntryV2

ReplLogValueEntryV2

Replop : 1字节,

timestamp :变长uint_64, 表示这个rocksdb操作的时间戳

key : rocksdb的key长度和内容

value : rocksdb的value长度和内容

关键数据结构

  1. class RocksKVStore: public KVStore {
  2. ...
  3. Status assignBinlogIdIfNeeded(Transaction* txn) final;
  4. void setNextBinlogSeq(uint64_t binlogId, Transaction* txn) final;
  5. uint64_t getNextBinlogSeq() const final;
  6. Expected<TruncateBinlogResult> truncateBinlogV2(uint64_t start, uint64_t end,
  7. Transaction *txn, std::ofstream *fs) final;
  8. uint64_t saveBinlogV2(std::ofstream* fs, const ReplLogRawV2& log);
  9. Expected<uint64_t> getBinlogCnt(Transaction* txn) const final;
  10. Expected<bool> validateAllBinlog(Transaction* txn) const final;
  11. uint64_t _nextBinlogSeq;
  12. // <txnId, <commit_or_not, binlogId>>
  13. std::unordered_map<uint64_t, std::pair<bool, uint64_t>> _aliveTxns;
  14. // As things run parallel, there will be false-holes in _aliveBinlogs.
  15. // Fortunely, when _aliveBinlogs.begin() changes from uncommitted to
  16. // committed, we have a chance to remove all the continuous committed
  17. // binlogIds follows it, and push _highestVisible forward.
  18. // <binlogId, <commit_or_not, txnId>>
  19. std::map<uint64_t, std::pair<bool, uint64_t>> _aliveBinlogs;
  20. ...
  21. }
  22. class RocksTxn: public Transaction {
  23. std::unique_ptr<RepllogCursorV2> createRepllogCursorV2(
  24. uint64_t begin,
  25. bool ignoreReadBarrier) final;
  26. Status applyBinlog(const ReplLogValueEntryV2& logEntry) final;
  27. Status setBinlogKV(uint64_t binlogId,
  28. const std::string& logKey,
  29. const std::string& logValue) final;
  30. Status delBinlog(const ReplLogRawV2& log) final;
  31. uint64_t getBinlogId() const final;
  32. void setBinlogId(uint64_t binlogId) final;
  33. uint32_t getChunkId() const final { return _chunkId; }
  34. void setChunkId(uint32_t chunkId) final;
  35. std::vector<ReplLogValueEntryV2> _replLogValues;
  36. }

关键流程处理

  1. 在事务过程中,事务记录repllog, 并加入到_replLogValues,seeRocksTxn::setKV()/RocksTxn::delKV()
  2. 事务提交时候,分配binlogid(KVStore::assignBinlogIdIfNeeded()),并将一个或多个ReplLogValueEntryV2,encode为RepllogRawV2之后,插入到rocksdb中,详见RocksTxn::commit();
  3. 同时维护KVStore::_highestVisible(RocksKVStore::markCommittedInLock());

RepllogCursorV2

由于repllog的key由唯一的递增整数组成,因此,遍历replllog可以直接增大这个id,调用rocksdb的getKV即可,详见RepllogCursorV2::next()RepllogCursorV2::nextV2();

复制

master向slave发送binlog

applybinlogsv2 storeId binlogs count flag

flag详细作用查看[[flushall在复制中的实现]]

binlog格式

header: 1字节,总为2,表示版本为2

(replog_key + repllog_value) * n 详见Binlog::writeHeaderBinlog::writeRepllogRaw

测试用例

repl_test是一个集成测试用例,未来需要不断扩展(repl_test.cpp)

小优化

  • catalog作为元数据库,setkv/delkv不能写repllog,否则repllog永远不能删除