tendis搬迁实现关键技术点

底层数据设计支持

为了实现底层数据能够按照slot进行搬迁,数据按slot划分,slot信息放入key的成员变量集合的第一位,这样slot相同的数据在rocksdbkvstore中对应的chunkid也相同,数据相邻,如下图所示:

tendis_migrate

tendis存储版 支持key 级别的并发控制,之前版本的tendis存储版只实现key访问级别访问锁 和 DB级别的锁(DB是基于rockdb的kvstore来设定)

这里将原来的二级锁结构,key锁->DB锁 改成三级锁结构key锁->slot锁->DB锁 。 这样实现有两个好处:

  • 在集群扩容搬迁数据时,能够基于 slot级别来进行数据搬迁控制
  • 实现了slot cursor 方便遍历一个slot中所有的数据,数据搬迁时发送snapshot也需要

基于增量加全量数据的扩缩容方式

社区版redis的搬迁是以key为单位以同步方式进行搬迁,如果搬迁是遇到大key , 社区版比较难解决,同步搬迁容易卡非常久,超过15秒,甚至自动触发切换,把Master判死,Redis会重新选择新的Master,由于migrating状态是不会同步给slave的,所以slave切换成master后,它身上是没有migrating状态的。 一旦migrating状态消除, ASK协议就不能正常工作,导致访问出错。

这种问题是单线程的redis 很难避免的, 但tendis存储版 是多线程模式, 因此这里设计tendis存储版 使用独立的线程池进行搬迁,同时tendis存储版 的搬迁是以slot为维度,不同slot之间可以并发搬迁, 由于之前实现了slot锁级别控制 因此很容易做到slot 级别数据统一搬迁。

基于这种设计,搬迁任务发送方sender的类实现成员变量如下:

  1. std::bitset<CLUSTER_SLOTS> _slots;
  2. std::atomic<bool> _isRunning;
  3. std::unique_ptr<DbWithLock> _dbWithLock;
  4. std::string _taskid;
  5. std::shared_ptr<ClusterNode> _dstNode;
  6. std::list<std::unique_ptr<ChunkLock>> _slotsLockList;

_slots 是这次搬迁任务负责的slots集合,用一个bitmap表示,默认配置是10个,takid 表示 这次搬迁任务的任务id, 是由接收方的clusternodeid +uuid生成,发送方和接收方有一个相同的父taskid. _dstNode记录了接收方的cluster node信息,_slotsLockList 存放了当前搬迁任务持有的chunk锁集合。

搬迁基于的技术是快照+增量binlog的技术, snapshot快照是rockdb底层引擎天然支持的特性,能够生成某个时间点的全量数据,而binglog是类似于 mysql主从同步使用的binlog , 是用来增量同步主从数据的,tendis存储版 支持这两个特性。

  1. 初始化时,集群会初始化一个搬迁线程池和搬迁线程定期调度线程有以及搬迁任务的类列表,调度线程会根据任务的状态机来进行不同任务状态机 进行不同操作。

  2. receiver 会接收到搬迁命令,它会做一系列元数据检查,然后发送一个内部命令给sender,当sender 检查ok时会进入sender的搬迁任务进入WAITING状态,

  3. receiver 接收到回包后,会进入REIVESNAPSHOT状态(表示进入ready状态),然后会根据建立的dstNode信息给对方发一个内部命令(加上自己的taskid)

  4. sender 接受到后会再次检查,然后找到匹配的taskid的任务,将这个taskid的sender的搬迁任务进入START状态,进入START状态;

  5. 调度线程检查到START状态后,会依次进入全量数据同步阶段 和增量数据同步阶段 在全量数据同步阶段 就使用上面提到的slot cursor扫描这个task 的slots 列表里面涉及的的所有slot数据,发送全量数据给接收方(这个过程会通过不断小部分迭代和通信来确保中间不丢数据,具体过程看下面的时序图) tendis_migrate
    全量数据发送完后,receiver会将client链接变成 接受命令session进入RECEIVEBINLOG状态,开始接受binlog命令

  6. 增量同步阶段是为了完成在全量数据发送过程中新产生的数据,设计了如下追加binlog的算法实现 首先,在发送snapshot的开始瞬间,任务会记录下当前db最新数据的offset (记录为起始位置begin),当snapshot完成后,这里再取一次db 最新offset得到maxid
    这里实现一个Sendbinlog接口,让binlog扫描maxid和begin之间的数据发送到receiver,当发送完成时再取一次db 最新数据maxid,由于这个时间段一直有流量写入,因此maxid会一直大于上面一次的end位置 但考虑到写入的速度一定是小于binlog发送的速度,因此这里设计一个迭代收敛的算法,让这个过程循环迭代,没迭代完一次求一个maxidbegin的差值,直到这个差值小于10000以内 算法描述如下

    1. send_binlogs(bitmap) {
    2. begin = _snapshotStart; end = _highest+1;
    3. retry = 10;
    4. while (retry-- == 1) {
    5. send_binlog_low(bitmap ,begin ,end);
    6. begin = end;
    7. end = _highestID;
    8. if (maxid - begin < 10000) {
    9. finished = true;
    10. break;
    11. }
    12. }
    13. lock_chunks(bitmap);
    14. end = maxid;
    15. send_binlog_low(bitmap ,begin ,end);
    16. unlock_chunks(bitmap);
    17. }

    当while循环完成后 这里会给这次任务的slot 上锁(阻塞了这部分slot相关的请求),这样offset就不会增加,这个时候再发送最后一个binlog序列,将这10000条发送过去后 再解锁 这样设计的目的是保证最后上锁的时间很短,通常在ms级别,做到用户无感知

  7. 在sender在完成所有binlog发送后 会发送一个命令给receiver , receiver找到对应的takid 后需要修改cluster 元数据 (即将slots的归属者设置为自己,然后发送一个gossip广播通知所有其他节点)并将task状态设置其状态为 success,

  8. sender收到receiver回包后解锁, 将slots归属改为对方,然后清理自身存储的这部分脏数据