去中心化架构
tendis存储版选择的一致性方式是Gossip协议,Gossip是一种去中心化的分布式协议,数据通过节点像病毒一样逐个传播,因为是指数级传播,整体传播速度非常快,该协议能够保证最终一致性,
和官方redis 方案类似,tendis cluster 中共有16384个slot , 每个master节点负责一部分slot , 客户端可以连接集群中任何一个节点 都可以访问到正确的数据, 通过 hash 计算 路由到正确的节点。支持move协议, 但ASK协议不支持,tendis的搬迁方案不是key级别的,不需要ASK协议
数据结构设计
tendis存储版使用current epoch
和config epoch
来记录集群变化时间的epoch
原则上:
- 如果epoch不变, 集群就不应该有变更(包括选举和迁移槽位)
- 每个节点的node epoch都是独一无二的
- 拥有越高epoch的节点, 集群信息越新
核心类包括:ClusterNode
, 表示单个集群节点的信息, 在gossip
实现中, 关注的核心元数据信息如下:
- 主从关系 这部分逻辑直接影响了数据复制,在集群发生变化时,主从关系的元数据也需要更新传播。另外failover时也需要进行切换
- 负责的slots 信息 这部分是最核心的元数据,直接决定了数据路由到哪个节点
- epoch信息 这是集群节点的epoch配置纪元,每个节点在集群中都有独一无二的纪元,当节点在通信时元数据发现有冲突时,总是以epoch高的节点为准,可以认为它反映了节点信息的新旧程度。
- failreport信息 用于检测集群中疑似发生故障的节点
关注的主要是两个集群的元数据信息,tendis存储版
中cluster Node
其中核心数据结构如下:
std::string _nodeName;
uint64_t _configEpoch;
// TCP/IP session with this node, connect success
std::shared_ptr<ClusterSession> _nodeSession;
// 管理的slot信息
std::bitset<CLUSTER_SLOTS> _mySlots;
// slave 列表
std::vector<std::shared_ptr<ClusterNode>> _slaves;
// master 列表
std::shared_ptr<ClusterNode> _slaveOf;
std::list<std::shared_ptr<ClusterNodeFailReport>> _failReport;
ClusterState 表示集群状态的类 核心成员信息如下:
CNodePtr _myself; // This node
uint64_t _currentEpoch;
uint64_t _lastVoteEpoch; // Epoch of the last vote granted.
std::unordered_map<std::string, CNodePtr> _nodes; // node table
ClusterHealth _state;
uint16_t _size; // cluster size
std::unordered_map<std::string, uint64_t> _nodesBlackList;
mutable myMutex _mutex;
mutable std::mutex _failMutex;
std::condition_variable _cv;
- _nodes 集群中所有节点的列表 当添加一个节点或者删除一个节点时,只需要将命令发给集群中的任意一个节点,这个节点会修改本地的 node table,并且这个修改会最终复制到所有的节点上去
- current epoch 表示整个集群中的最大版本号,集群信息每变更一次,该版本号都会自增以保证每个信息的版本号唯一
- _state 表示集群健康状态 初始化时该状态为fail , cluster会定期检测当前所有slot 对应的master 是不是存活状态,只有16384个slots对应的所有master都存活时,该状态设置为OK
- _nodesBlackList 集群黑名单,用于实现集群中的cluster forget 功能, 防止遗忘 的node再次发生通信时把它错误添加回去。
ClusterMsg 类,用于表示集群中的消息通信信息。
uint32_t _totlen;
ClusterMsg::Type _type;
uint32_t _mflags;
std::shared_ptr<ClusterMsgHeader> _header;
std::shared_ptr<ClusterMsgData> _msgData;
消息类包括header
包头和data
, 这个设计和redis相似, header 中存放的消息发送者自身的ip,slots等信息
消息体有四种类型:
enum class Type {
Gossip = 0,
Update = 1,
FAIL = 2,
PUBLIC = 3,
};
分别处理正常的ping pong
信息,update
信息和failover
信息等, 对于每种gossip的消息类, 实现了完整的encode方法
和decode方法
来实现基于字节的编码。
gossip实现
在redis cluster实现中,心跳发送PING包和检测PONG响应时间都在clusterCron
中, 由于redis的单线程,因此需要使用额外的时间片来处理,tendis存储版 是多线程模型,是基于异步网络模块+线程池工作模式工作的。
因此这里cluster manager使用了独立线程 _controller来处理cluster Cron 任务
_controller =
std::make_unique<std::thread>(
std::move([this]() { controlRoutine(); })
);
crontrolRoutine
函数实现了核心的cluster cron功能,流程图如下所示 , 在社区版本的基础上,tendis存储版增加了很多并发锁控制的逻辑(防止发生死锁),另外增加了一个cronCheckReplicate
来检测 cluster信息和replication信息不一致的情况,这是由于修改这两部分元数据目前在tedis存储版实现上还没做到原子操作,在极端情况下会出现两者不一致的问题从而导致数据错误。
在元数据持久化方面,redis cluster 存放在一个文本文件中,而 tendis存储版直接存放在catalog的(rocksdb
)中,读写更加快速方便。
failover支持
通过gossip
的能力,tendis存储版支持了自动故障检测和故障修复
- 通过心跳检测来 判断心跳达到超时后,将节点标记为
pfail
, 每个节点会维护一个fail report
列表, 记录标记为pfail
的节点 - 当有一半以上的
master
将某个节点标记为pfail
后, 该节点会标记为fail
状态, 并通过gossip
消息广播 slave
通过gossip
消息 向其它master
索要投票,原则上复制偏移量offset
最新的节点 其获选的可能性越大- 获得半数以上投票的
slave
可以当选为master