去中心化架构

tendis存储版选择的一致性方式是Gossip协议,Gossip是一种去中心化的分布式协议,数据通过节点像病毒一样逐个传播,因为是指数级传播,整体传播速度非常快,该协议能够保证最终一致性,

和官方redis 方案类似,tendis cluster 中共有16384个slot , 每个master节点负责一部分slot , 客户端可以连接集群中任何一个节点 都可以访问到正确的数据, 通过 hash 计算 路由到正确的节点。支持move协议, 但ASK协议不支持,tendis的搬迁方案不是key级别的,不需要ASK协议

数据结构设计

tendis存储版使用current epochconfig epoch 来记录集群变化时间的epoch

原则上:

  • 如果epoch不变, 集群就不应该有变更(包括选举和迁移槽位)
  • 每个节点的node epoch都是独一无二的
  • 拥有越高epoch的节点, 集群信息越新

核心类包括:ClusterNode, 表示单个集群节点的信息, 在gossip实现中, 关注的核心元数据信息如下:

  • 主从关系 这部分逻辑直接影响了数据复制,在集群发生变化时,主从关系的元数据也需要更新传播。另外failover时也需要进行切换
  • 负责的slots 信息 这部分是最核心的元数据,直接决定了数据路由到哪个节点
  • epoch信息 这是集群节点的epoch配置纪元,每个节点在集群中都有独一无二的纪元,当节点在通信时元数据发现有冲突时,总是以epoch高的节点为准,可以认为它反映了节点信息的新旧程度。
  • failreport信息 用于检测集群中疑似发生故障的节点

关注的主要是两个集群的元数据信息,tendis存储版cluster Node其中核心数据结构如下:

  1. std::string _nodeName;
  2. uint64_t _configEpoch;
  3. // TCP/IP session with this node, connect success
  4. std::shared_ptr<ClusterSession> _nodeSession;
  5. // 管理的slot信息
  6. std::bitset<CLUSTER_SLOTS> _mySlots;
  7. // slave 列表
  8. std::vector<std::shared_ptr<ClusterNode>> _slaves;
  9. // master 列表
  10. std::shared_ptr<ClusterNode> _slaveOf;
  11. std::list<std::shared_ptr<ClusterNodeFailReport>> _failReport;

ClusterState 表示集群状态的类 核心成员信息如下:

  1. CNodePtr _myself; // This node
  2. uint64_t _currentEpoch;
  3. uint64_t _lastVoteEpoch; // Epoch of the last vote granted.
  4. std::unordered_map<std::string, CNodePtr> _nodes; // node table
  5. ClusterHealth _state;
  6. uint16_t _size; // cluster size
  7. std::unordered_map<std::string, uint64_t> _nodesBlackList;
  8. mutable myMutex _mutex;
  9. mutable std::mutex _failMutex;
  10. std::condition_variable _cv;
  • _nodes 集群中所有节点的列表 当添加一个节点或者删除一个节点时,只需要将命令发给集群中的任意一个节点,这个节点会修改本地的 node table,并且这个修改会最终复制到所有的节点上去
  • current epoch 表示整个集群中的最大版本号,集群信息每变更一次,该版本号都会自增以保证每个信息的版本号唯一
  • _state 表示集群健康状态 初始化时该状态为fail , cluster会定期检测当前所有slot 对应的master 是不是存活状态,只有16384个slots对应的所有master都存活时,该状态设置为OK
  • _nodesBlackList 集群黑名单,用于实现集群中的cluster forget 功能, 防止遗忘 的node再次发生通信时把它错误添加回去。

ClusterMsg 类,用于表示集群中的消息通信信息。

  1. uint32_t _totlen;
  2. ClusterMsg::Type _type;
  3. uint32_t _mflags;
  4. std::shared_ptr<ClusterMsgHeader> _header;
  5. std::shared_ptr<ClusterMsgData> _msgData;

消息类包括header 包头和data, 这个设计和redis相似, header 中存放的消息发送者自身的ip,slots等信息

消息体有四种类型:

  1. enum class Type {
  2. Gossip = 0,
  3. Update = 1,
  4. FAIL = 2,
  5. PUBLIC = 3,
  6. };

分别处理正常的ping pong信息,update信息和failover信息等, 对于每种gossip的消息类, 实现了完整的encode方法decode方法来实现基于字节的编码。

gossip实现

在redis cluster实现中,心跳发送PING包和检测PONG响应时间都在clusterCron中, 由于redis的单线程,因此需要使用额外的时间片来处理,tendis存储版 是多线程模型,是基于异步网络模块+线程池工作模式工作的。

因此这里cluster manager使用了独立线程 _controller来处理cluster Cron 任务

  1. _controller =
  2. std::make_unique<std::thread>(
  3. std::move([this]() { controlRoutine(); })
  4. );

crontrolRoutine函数实现了核心的cluster cron功能,流程图如下所示 , 在社区版本的基础上,tendis存储版增加了很多并发锁控制的逻辑(防止发生死锁),另外增加了一个cronCheckReplicate来检测 cluster信息和replication信息不一致的情况,这是由于修改这两部分元数据目前在tedis存储版实现上还没做到原子操作,在极端情况下会出现两者不一致的问题从而导致数据错误。

在元数据持久化方面,redis cluster 存放在一个文本文件中,而 tendis存储版直接存放在catalog的(rocksdb)中,读写更加快速方便。

gossip

failover支持

通过gossip的能力,tendis存储版支持了自动故障检测和故障修复

  • 通过心跳检测来 判断心跳达到超时后,将节点标记为pfail , 每个节点会维护一个fail report列表, 记录标记为pfail的节点
  • 当有一半以上的master 将某个节点标记为pfail后, 该节点会标记为fail状态, 并通过gossip消息广播
  • slave通过gossip消息 向其它master索要投票,原则上复制偏移量offset最新的节点 其获选的可能性越大
  • 获得半数以上投票的slave 可以当选为master