调研比较了三个Redis集群的解决方案:
系统 | 贡献者 | 是否官方Redis实现 | 编程语言 |
Twemproxy | 是 | C | |
Redis Cluster | Redis官方 | 是 | C |
Codis | 豌豆荚 | 否 | 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 Id : SlotId = crc32(key) % 1024
- 每一个 slot 都会有一个特定的 server group id 来表示这个 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 ID比对方小(字母顺序)
- 则把自己的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<lastVoteEpoch的选举请求都不予投票
- currentEpoch<MASTER currentEpoch字段的选举请求都不予投票
4.2.3.5 选举优先权
当Slave节点发现Master节点处于FAIL状态时,不会立刻试图进行选举,而是会延迟一段时间,延迟时常用以下公式进行计算:
Txt代码
- DELAY = 500 milliseconds + random delay between 0 and 500 milliseconds + SLAVE_RANK * 1000 milliseconds
其中,SLAVE_RANK由Slave收到Master数据复制的更新程度来衡量。在发起选举之前,Slave之间交换各自获得Master数据复制的更新排名,最新更新的SLAVE_RANK = 0, 其次更新的SLAVE_RANK = 1,以此类推...
4.2.3.6 故障转移耗时评估
- 假设配置NODE_TIMEOUT=2s,FAIL_REPORT_VALIDITY_MULT=3s
- 标记Master为PFAIL状态耗时NODE_TIMEOUT=2s
- 升级PFAIL状态为FAIL状态,耗时:NODE_TIMEOUT * FAIL_REPORT_VALIDITY_MULT = 6s
- 选举前随机延时期望:1s
- 收集足够多Master投票:MAX((2*NODE_TIMEOUT),2)=4s
- 总计耗时约:13s
4.3.3 主备平衡功能
Redis Cluster能够自动的迁移Slave节点,从Slave节点有冗余的Master节点到完全没有Slave节点的Master节点。
具体算法:
- 首先定义Good Slave:对于某一节点来说,如果另一个Slave节点没有处于FAIL状态,则认为该Slave节点为Good Slave节点。
- 当有Slave节点发现有Master节点没有Good Slave时开始触发主备平衡迁移。
- 所有发现有主备平衡需求之后,拥有最多Good Slave节点的Master节点的所有Slave中,Node ID最小的Slave节点真正开始迁移。成为没有没有Good Slave Master新Master。
- 可以配置cluster-migration-barrier参数,控制主备平衡迁移的时候,迁出Master最少需要拥有的Good Slave数
4.4 Codis
- 支持故障检测并报警
- codis-redis-group中的Slave节点无法自动提升为Master节点
- 由管理员通过Web界面/命令行来手动操作
5.功能限制
Twemproxy:
- 不支持多key操作
- 不支持MULTI/EXEC
- 不支持EVAL
Redis Cluster:
- 当Client连接到集群的主体部分时可能有少量的写丢失,当Client连接到集群的小部分时可能有显著的写丢失
- 复杂的多Key操作(Set求并/求交)不能跨节点操作,可以通过使用Hash Tag使相关Key强制哈希到同一Server,但是在数据重新分片期间,还是可能有时间不可用
- 不支持MULTI/EXEC
- Redis 3.0 正式版时间:2015年2月上旬
Codis:
不支持命令:KEYS, MOVE, OBJECT, RENAME, RENAMENX, SORT, SCAN, BITOP,MSETNX, BLPOP, BRPOP, BRPOPLPUSH, PSUBSCRIBE,PUBLISH, PUNSUBSCRIBE, SUBSCRIBE, UNSUBSCRIBE, DISCARD, EXEC, MULTI, UNWATCH, WATCH, SCRIPT EXISTS, SCRIPT FLUSH, SCRIPT KILL, SCRIPT LOAD, AUTH, ECHO, SELECT, BGREWRITEAOF, BGSAVE, CLIENT KILL, CLIENT LIST, CONFIG GET, CONFIG SET, CONFIG RESETSTAT, DBSIZE, DEBUG OBJECT, DEBUG SEGFAULT, FLUSHALL, FLUSHDB, INFO, LASTSAVE, MONITOR, SAVE, SHUTDOWN, SLAVEOF, SLOWLOG, SYNC, TIME
6. 性能
Twemproxy:[来源:http://antirez.com/news/44]
- 通常操作Proxy与直接操作Redis实例性能一样
- 最坏情况下有20%的性能下降
Redis Cluster:[来源: http://redis.io/topics/cluster-spec]
1000个节点内拥有线性的伸缩性:通常情况下与直接操作Redis实例性能相同。
Codis:[来源:http://0xffff.me/blog/2014/11/11/codis-de-she-ji-yu-shi-xian-part-3/]
- 相对于单Redis实例40%性能损失
- 支持多核
7. 总结
Twemprosy:
- 轻量级
- 在Proxy层实现一致性哈希
- 快速的故障节点移除
- 可借助Sentinel和重启工具降低故障节点移除时的Cache失配
Redis Cluster:
- 无中心自组织结构
- 更强的功能:主备平衡
- 故障转移响应时间长
- 暂时未达到正式版
Codis:
- 基于Zookeeper的Proxy高可用
- 非官方Redis实现
- 侧重于动态水平扩容
- 手动故障转移