|
调研比较了三个Redis集群的解决方案:
系统贡献者是否官方Redis实现编程语言TwemproxyTwitter是CRedis ClusterRedis官方是CCodis豌豆荚否Go+C 1.基本架构
1.1 Twemproxy
- 增加Proxy层,由Proxy实现一致性哈希算法(支持:KETAMA/取模/随机)
数据分片算法:
采用一致性哈希算法,以KETAMA为例:
1.2 Redis Cluster
- 无中心自组织的结构
- 各节点维护Key->Server的映射关系
- Client可以向任意节点发起请求,节点不会转发请求,只是重定向Client
- 如果在Client第一次请求和重定向请求之间,Cluster拓扑发生改变,则第二次重定向请求将被再次重定向,直到找到正确的Server为止
数据分片算法:
Key空间被划分为16384个区间,每个Master节点负责一部分区间。
1.3 Codis
- 客户端可以连接到任意的codis-proxy,就和连接原生的Redis Server
- 由Zookeeper维护数据路由表和 codis-proxy 节点的元信息
数据分片算法:
Key空间被划分为1024个区间, 对于每个key来说, 通过以下公式确定所属的 Slot>
每一个 slot 都会有一个特定的 server group>
2.水平扩容
Twemproxy:
- 不支持运行时水平扩容,需要重启。
- 根据一致性哈希算法进行数据重新分片。
Redis Cluster:
- 支持通过运行时增加Master节点来水平扩容,提升存储容量,尽力降低命中率波动
- 存在节点A,需要迁出其中的部分Key区间。新增节点B,接收由节点A迁出的Key区间。
- 相应Key区间的请求首先还是会发送给A节点:如果请求为新建Key则直接重定向到B节点;如果请求不是新建Key且A节点存储有对应的Key则直接作出响应,否则重定向到B节点
- 同时Cluster会调用实用工具redis-trib向A节点发送MIGRATE命令,把迁移区间内的所有Key原子的迁移到B节点:同时锁住A、B节点=》在A节点删除Key=》在B节点新建Key=》解锁
- 运行时动态迁移大尺寸键值可能造成响应时延
Codis:
- 支持运行时水平扩容
- 底层基于Codis Server特殊实现原子的数据迁移指令
3.主从备份
3.1 主从备份是否必须
Twemproxy:
- 没有数据复制不影响可用节点顶替故障节点
- 故障发生时,没有数据复制的故障节点的Key会全部丢失
Redis Cluster:
没有主从备份的节点一旦故障,将导致整个集群失败:无法写入/读取任何Key;无法进行数据重新分片。
Codis:
- 若出现故障,需要手动配置节点,进行故障转移。
- 如果没有进行故障转移,只故障节点负责的slots 会失败
3.2 主从备份方案
Twemproxy本身不支持出从备份,和Redis Cluster一样,需要引入Redis本身的主备复制功能。
- 可以设置1主1备或者1主多备
- 当Slave节点接入Cluster时,就会向配置的Master节点发送SYNC命令。断开重连时,也会再次发送SYNC命令
- 此后Master将启动后台存盘进程,同时收集所有接收到的用于修改数据集的命令,在后台进程执行完毕后,Master将传送整个数据库文件到Slave,以完成一次完全同步。而Slave服务器在接收到数据库文件数据之后将其存盘并加载到内存中。此后,Master继续将所有已经收集到的修改命令,和新的修改命令依次传送给Slaves,Slave将在本次执行这些数据修改命令,从而达到最终的数据同步。
- Redis的数据复制是异步的,无论在Master端还是Slave端都不会阻塞。
- Slave会周期性确认收到的备份数据
Twemproxy引入主备复制后的架构更新为:
开启主备复制后的Redis Cluster的架构更新为下图,Client可以向任意节点发起请求,无论是Master节点还是Slave节点。
4.故障检测与转移
4.1 Twemproxy
4.1.1 故障检测
Twemproxy本身通过三个配置项实现:
Txt代码
- auto_eject_hosts: true
- timeout: 400
- server_failure_limit: 3
如果Server Pool开启了auto_eject_hosts,则当连续server_failure_limit次访问某Server,都超时timeout无响应,则标记该节点为故障。
4.1.2 故障转移
故障节点将从Hash环上直接取下,之前保存在该Server上的Key将丢失。
4.1.3 故障转移耗时评估
假设配置:timeout=400ms, server_failure_limit=2, 则故障转移需要耗时800ms。
4.2 Twemproxy借助其他工具
使用Twemproxy时可以引入Redis Sentinel来进行故障检测。引入Redis Sentinel后Twemproxy的架构更新为:
- 每个Sentinel节点可以监控一个或多个Master节点,及其所有Slave节点
4.2.1 启动Redis Sentinel
- redis-sentinel /path/to/sentinel.conf,其中的配置文件是必须的,配置文件将会被用来存储运行时状态信息。在配置文件中只需要指明要监视的Master节点列表。
- 无须为运行的每个 Sentinel 分别设监听同一Master的其他 Sentinel 的地址, 因为 Sentinel 可以通过发布与订阅功能来自动发现正在监视相同主服务器的其他 Sentinel
- 不必手动列出主服务器属下的所有从服务器, 因为 Sentinel 可以通过询问主服务器来获得所有从服务器的信息。
4.2.2 故障检测
- 每个 Sentinel 以每秒钟一次的频率向它所知的主服务器、从服务器以及其他 Sentinel 实例发送一个 PING 命令。
- 如果一个实例(instance)距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值, 那么这个实例会被 Sentinel 标记为主观下线。
- 如果一个Master被标记为主观下线, 那么正在监视这个Master的所有 Sentinel 要以每秒一次的频率确认主服务器的确进入了主观下线状态。
- 如果一个主服务器被标记为主观下线, 并且有足够数量的 Sentinel (至少要达到配置文件指定的数量)在指定的时间范围内同意这一判断, 那么这个主服务器被标记为客观下线。
- 当没有足够数量的 Sentinel 同意主服务器已经下线, 主服务器的客观下线状态就会被移除。 当主服务器重新向 Sentinel 的 PING 命令返回有效回复时, 主服务器的主观下线状态就会被移除。
4.2.3 故障转移
Redis Sentinel进行故障转移的过程:
- 某Sentinel节点发现主服务器已经进入客观下线状态。
- 该Sentinel发起选举,试图当选故障转移主持节点
- 如果当选失败, 那么在设定的故障迁移超时时间的两倍之后, 重新尝试当选。 如果当选成功, 那么执行以下步骤。
- 选出一个Slave节点,并将它升级为Master节点
- 向被选中的从服务器发送 SLAVEOF NO ONE 命令,让它转变为Master节点
- 通过发布与订阅功能, 将更新后的配置传播给所有其他 Sentinel , 其他 Sentinel 对它们自己的配置进行更新。
- 向已下线主服务器的Slave节点发送 SLAVEOF 命令, 让它们去复制新的Master节点
Redis Sentinel选择新的Master节点进行故障转移之后,Twemproxy无法找到新的Master节点,此时需要引入第四方工具redis-twemproxy-agent(node.js),更新Twemproxy配置,并重启。
4.2.4 故障转移耗时评估
- 每个 Sentinel 以每秒钟发送一次PING,配置down-after-milliseconds=2s,则主观下线耗时3s
- 由主观下线升:数量的 Sentinel (至少要达到配置文件指定的数量)在指定的时间范围内同意这一判断1s
- Sentinel当选故障转移主持节点:1s
- 选出一个Slave节点,并将它升级为Master节点,向被选中的从服务器发送 SLAVEOF NO ONE 命令,让它转变为Master节点:0.5s
- 通过发布与订阅功能, 将更新后的配置传播给所有其他 Sentinel , 其他 Sentinel 对它们自己的配置进行更新:1s
- 总计耗时:6.5s
4.3 Redis Cluster
4.3.1 故障检测
节点状态的维护:
- 节点的拓扑结构是一张完全图:对于N个节点的Cluster,每个节点都持有N-1个输入TCP连接和N-1个输出TCP连接。
- 节点信息的维护:每秒随机选择节点发送PING包(无论Cluster规模,PING包规模是常量);每个节点保证在NODE_TIMEOUT/2 时间内,对于每个节点都至少发送一个PING包或者收到一个PONG包.
在节点间相互交换的PING/PONG包中有两个字段用来发现故障节点:PFAIL(Possible Fail)和FAIL。
PFAIL状态:
- 当一个节点发现某一节点在长达NODE_TIMEOUT的时间内都无法访问时,将其标记为PFAIL状态。
- 任意节点都可以将其他节点标记为PFAIL状态,无论它是Master节点还是Slave节点。
FAIL状态:
当一个节点发现另一节点被自己标记为PFAIL状态,并且在(NODE_TIMEOUT * FAIL_REPORT_VALIDITY_MULT)的时间范围内,与其他节点交换的PING/PONG包中,大部分Master节点都把该节点标记为PFAIL或者FAIL状态,则把该节点标记为FAIL状态,并且进行广播。
4.3.2 故障转移
4.3.2.1 Slave选举的时机
当某一Slave节点发现它的Master节点处于FAIL状态时,可以发起一次Slave选举,试图将自己晋升为Master。一个Master节点的所有Slave节点都可以发起选举,但最终只有一个Slave节点会赢得选举。Slave发起选举的条件:
- Slave的Master处于FAIL状态
- 该MASTER节点存储的Key数量>0
- Slave与Master节点失去连接的时间小于阀值,以保证参与选举的Slave节点的数据的新鲜度
4.3.2.2 Cluster逻辑时钟
Config epoch:
每个Master节点启动时都会为自己创建并维护configEpoch字段,设置初始值为0。Master会在自己的PING/PONG包中广播自己的configEpoch字段。Redis Cluster尽力保持各个Master节点的configEpoch字段取值都不同。算法:
- 每当一个Master节点发现有别的Master节点的configEpoch字段与自己相同时
并且自己的Node>
- 则把自己的currentEpoch+1
Slave的PING/PONG包中也包含configEpoch字段,Slave的configEpoch字段取值是它的Master的configEpoch字段取值,由最后一次与Master交换PING/PONG包时取得。
Cluster epoch:
每一个节点启动的时候都会创建currentEpoch字段,无论是Master节点还是Slave节点,并设置初始值为0。每当一个节点收到来自其他节点的PING/PONG包时,若其他节点的currentEpoch字段大于当前节点的currentEpoch字段,则当前节点把自己的currentEpoch字段设置为该新观察到的currentEpoch值。
4.3.2.3 Slave选举的过程
- Slave节点递增自己的currentEpoch字段
- 发送FAILOVER_AUTH_REQUEST数据包给每一个MASTER节点
- 若MASTER节点投票晋升该SLAVE节点,则回复FAILOVER_AUTH_ACK。某个MASTER节点投过票之后,在NODE_TIMEOUT * 2时间内不能再给同一MASTER的SLAVE选举投票。
- 若Slave在MAX((2*NODE_TIMEOUT),2)的时间内获得大多数MASTER节点的投票,则赢得选举
- 其间,所有currentEpoch小于选举发起时取值的MASTER投票都会被丢弃
- 若没有任何Slave赢得选举,选举可以在MAX(NODE_TIMEOUT * 4,4)的时间后重新举行
4.3.2.4 Master节点投票逻辑
- 请求选举的Slave的Master必须处于FAIL状态
- Master节点维护lastVoteEpoch字段,每当MASTER给某个选举请求投票时,更新lastVoteEpoch字段为请求的currentEpoch值
- currentEpoch
|
|
|