Tendis存储版 Binlog和复制优化
原来的binlog格式
- binlog格式是物理格式,保证幂等,记录rocksdb的绝对内容
- 一个事务多个rocksdb操作,会有多个记录
- 每个事务有唯一的txnid,每个操作rocksdb操作记录一个localid
记录格式
primary_key: txnid | local_id| flag| timestamp second_key: “” value: rocksdb_key| rocksdb_value
问题:
- 一个事务多条rocksdb的binlog记录,存在存储空间的浪费
- binlog的key无法精确定位,只能通过前缀搜索然后通过rocksdb游标进行遍历,会消耗大量的CPU资源
- txnid是事务开启时候确定,由于复制的高低水位导致,导致tendis无法支持过大的事务
解决思路:
- 一个事务一条binlog
- binlog的key需要能精确定位
- 事务提交时候确定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长度和内容
关键数据结构
class RocksKVStore: public KVStore {
...
Status assignBinlogIdIfNeeded(Transaction* txn) final;
void setNextBinlogSeq(uint64_t binlogId, Transaction* txn) final;
uint64_t getNextBinlogSeq() const final;
Expected<TruncateBinlogResult> truncateBinlogV2(uint64_t start, uint64_t end,
Transaction *txn, std::ofstream *fs) final;
uint64_t saveBinlogV2(std::ofstream* fs, const ReplLogRawV2& log);
Expected<uint64_t> getBinlogCnt(Transaction* txn) const final;
Expected<bool> validateAllBinlog(Transaction* txn) const final;
uint64_t _nextBinlogSeq;
// <txnId, <commit_or_not, binlogId>>
std::unordered_map<uint64_t, std::pair<bool, uint64_t>> _aliveTxns;
// As things run parallel, there will be false-holes in _aliveBinlogs.
// Fortunely, when _aliveBinlogs.begin() changes from uncommitted to
// committed, we have a chance to remove all the continuous committed
// binlogIds follows it, and push _highestVisible forward.
// <binlogId, <commit_or_not, txnId>>
std::map<uint64_t, std::pair<bool, uint64_t>> _aliveBinlogs;
...
}
class RocksTxn: public Transaction {
std::unique_ptr<RepllogCursorV2> createRepllogCursorV2(
uint64_t begin,
bool ignoreReadBarrier) final;
Status applyBinlog(const ReplLogValueEntryV2& logEntry) final;
Status setBinlogKV(uint64_t binlogId,
const std::string& logKey,
const std::string& logValue) final;
Status delBinlog(const ReplLogRawV2& log) final;
uint64_t getBinlogId() const final;
void setBinlogId(uint64_t binlogId) final;
uint32_t getChunkId() const final { return _chunkId; }
void setChunkId(uint32_t chunkId) final;
std::vector<ReplLogValueEntryV2> _replLogValues;
}
关键流程处理
- 在事务过程中,事务记录repllog, 并加入到
_replLogValues
,seeRocksTxn::setKV()
/RocksTxn::delKV()
- 事务提交时候,分配binlogid(
KVStore::assignBinlogIdIfNeeded()
),并将一个或多个ReplLogValueEntryV2
,encode为RepllogRawV2
之后,插入到rocksdb中,详见RocksTxn::commit()
; - 同时维护
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::writeHeader
和Binlog::writeRepllogRaw
测试用例
repl_test是一个集成测试用例,未来需要不断扩展(repl_test.cpp)
小优化
- catalog作为元数据库,setkv/delkv不能写repllog,否则repllog永远不能删除