Redis系列之Redis Cluster
概述
Redis 2.8版本发布稳定版Redis Sentinel,不过Sentinel集群版存在一些问题:
- 高可用性:Sentinel集群对Redis既有的主从集群提供有限的高可用保障;
- 在线扩容:节点下线,触发选举,选举涉及两个阶段;新增节点,数据迁移过程麻烦。
对比Redis Sentinel
Redis Sentinel,即Redis 哨兵集群,区别如下:
- 集群架构
Redis Sentinel是一主多从, Redis Cluster是多主多从 - 数据一致性
- Redis Sentinel:由于没有分片,Sentinel对数据一致性的保障会更好一些,结构相对简单,脑裂问题会相对少一些,主从之间的数据一致性问题相对简单。主从切换后,存在数据一致性的小概率问题(例如主节点故障前的未同步数据丢失),但整体上保持一致性较好;
- Redis Cluster:由于数据分片,Redis Cluster在发生主从切换和网络分区时,可能存在数据不一致的风险。
- 扩展性
- Redis Sentinel:扩展性差,受限于单机性能和存储能力。尽管可以通过添加从节点分担读请求,但所有写操作仍然只能在主节点进行;
- Redis Cluster:扩展性好,可通过加节点来扩展存储容量和处理能力。每个节点只负责一部分数据,可实现负载均衡。
- 适用场景
- Redis Sentinel:适用于中小型应用,数据量和请求量相对较小的场景,或者不需要水平扩展的情况;
- Redis Cluster:适用于大规模应用,需要处理大数据量和高并发的场景,且需要水平扩展能力。
- 运维
- Redis Sentinel:运维复杂度较低,适合中小规模的Redis部署。需要维护Sentinel实例,并且在节点增加或减少时,配置相对简单;
- Redis Cluster:运维复杂度较高,适合大规模的Redis集群。节点的增删、分片的重新分配、集群状态的维护等都需要较高的运维能力。
简介
从Redis3.0开始,提供Redis Cluster集群支持,用于在多个Redis节点间共享数据,提高服务可用性。Redis Cluster采用去中心化结构,无需proxy代理,应用可以直接访问集群中的数据节点。
Redis Cluster要求至少需要3个Master才能组成一个集群,同时每个Master至少需要有一个Slave节点。各个节点之间保持TCP通信。当Master发生宕机,Redis Cluster自动会将对应的Slave节点提拔为Master,来重新对外提供服务。
Redis Cluster功能:负载均衡,故障切换,主从复制。
Redis Cluster集群,内置数据自动分片机制,集群内部将所有的key映射到16384个Slot中,集群中的每个Redis Instance负责其中的一部分的Slot的读写。集群客户端连接集群中任一Redis Instance即可发送命令,当Redis Instance收到自己不负责的Slot的请求时,会将负责请求Key所在Slot的Redis Instance地址返回给客户端,客户端收到后自动将原请求重新发往这个地址,对外部透明。一个Key到底属于哪个Slot由crc16(key) % 16384
决定。
通信
集群机器等数据信息通常有两种方式:
- 集中式:把集群信息保存在配置中心。好处:元数据的更新和读取,时效性非常好,一旦元数据出现变更,立即就更新到集中式的存储中,其他节点读取时立即就可以感知到。缺点:所有的元数据的跟新压力全部集中在一个地方,可能会导致元数据的存储有压力
- Gossip:好处:元数据的更新比较分散,不是集中在一个地方,更新请求会陆陆续续,打到所有节点上去更新,降低压力。缺点,元数据更新有延时,可能导致部分操作有滞后。
通信的端口就是本身Redis监听端口+10000。假如监听端口6379,通信端口就是16379。
Gossip
Gossip协议的主要职责就是信息交换,常用的Gossip消息有:ping、pong、meet、fail等。
- meet:用于通知新节点加入。消息发送者通知接收者加入到当前集群,meet消息通信正常完成后,接收节点会加入到集群中并进行周期性的ping、pong消息交换
- ping:集群内交换最频繁的消息,集群内每个节点每秒向多个其他节点发送ping消息,用于检测节点是否在线和交换彼此状态信息。ping消息发送封装自身节点和部分其他节点的状态数据
- pong:当接收到ping、meet消息时,作为响应消息回复给发送方确认消息正常通信。pong消息内部封装了自身状态数据。节点也可以向集群内广播自身的pong消息来通知整个集群对自身状态进行更新
- fail:当节点判定集群内另一个节点下线时,会向集群内广播一个fail消息,其他节点接收到fail消息之后把对应节点更新为下线状态。
当新增一个节点,也就是meet消息过程:
- 节点A会为节点B创建一个clusterNode结构,并将该结构添加到自己的
clusterState.nodes
字典里。节点A根据cluster meet
命令给定的IP地址和端口号,向节点B发送一条meet消息。 - 节点B接收到节点A发送的meet消息,节点B会为节点A创建一个clusterNode结构,并将该结构添加到自己的
clusterState.nodes
字典里面。节点B向节点A返回一条pong消息。 - 节点A将受到节点B返回的pong消息,通过这条pong消息节点A可知节点B已成功接收到自己发送的meet消息。随后节点A将向节点B返回一条ping消息。
- 节点B将接收到节点A返回的ping消息,通过这条ping消息节点B可知节点A已经成功接收到自己返回的pong消息,握手完成。
- 之后,节点A会将节点B的信息通过Gossip协议传播给集群中的其他节点,让其他节点也与节点B进行握手。最终经过一段时间后,节点B会被集群中的所有节点认识。
当一个节点故障,怎么判断下线?
集群中的每个节点都会定期向其他节点发送ping命令,如果接受ping消息的节点在指定时间内没有回复pong,则发送ping的节点就把接受ping的节点标记为主观下线。
如果集群半数以上的主节点都将主节点A标记为主观下线,则节点A将被标记为客观下线(通过节点的广播)即下线。
故障切换
当一个从节点发现自己正在复制的主节点进入已下线状态时,从节点将开始对下线主节点进行故障转移,步骤如下:
- 从节点会执行
SLAVEOF no one
命令,成为新的主节点; - 新的主节点会撤销所有对已下线主节点的槽指派,并将这些槽全部指派给自己;
- 新的主节点向集群广播一条pong消息,这条pong消息可以让集群中的其他节点立即知道这个节点已经由从节点变成主节点,并且这个主节点已经接管原本由已下线节点负责处理的槽;
- 新的主节点开始接收和自己负责处理的槽有关的命令请求,故障转移完成。
一致性Hash算法
参考一致性Hash算法。
一致性哈希算法在节点太少时,容易因为节点分布不均匀而造成缓存热点的问题。为了解决此问题,可考虑引入虚拟节点机制,即对每一个节点计算多个Hash,每个计算结果位置都放置一个虚拟节点,以实现数据的均匀分布和负载均衡。
Hash Slot
译为哈希槽,也叫Hash槽。Redis Cluster有16384个哈希槽,对每个Key计算CRC16
值后对16384取模,可获取Key对应的哈希槽。
Redis Cluster中每个Master都会持有部分槽。假如有3个Master,则每个Master持有5000多个哈希槽。哈希槽让节点的增加和移除变得简单,增加一个Master,就将其他Master的哈希槽移动部分过去;减少一个Master,就将它的哈希槽移动到其他Master上去。移动哈希槽的成本非常低。客户端API,可以对指定的数据,让他们走同一个哈希槽,通过hash tag
来实现。
任何一台机器节点宕机,另外几个节点都不受影响。因为Key查找的是哈希槽,而不是机器。
一致性
CAP一致性模型中,Redis Cluster是AP系统,它在网络分区时会优先保证可用性。
参数
提供如下配置项参数:
cluster-enabled
:cluster-config-file
:cluster-node-timeout
:表示当某个节点持续timeout的时间失联时,才可以认定该节点出现故障,需要进行主从切换。如果没有这个选项,网络抖动会导致主从频繁切换,即数据的重新复制;cluster-slave-validity-factor
:cluster-require-full-coverage
:推荐设置为no,表示当负责一个插槽的主库下线且没有相应的从库进行故障恢复时,集群仍然可用,如果为yes则集群不可用;cluster-migration-barrier
:数据迁移的副本临界数,这个参数表示的是,一个主节点在拥有多少个好的从节点的时候就要割让一个从节点出来给另一个没有任何从节点的主节点;cluster-allow-reads-when-down
:
问题
Redis Sentinel集群存在的问题(一致性和脑裂),Redis Cluster集群依旧存在。Redis Cluster集群模式的问题如下:
- 一致性:Redis Cluster是AP系统,它在网络分区时会优先保证可用性。在发生网络分区或某个主节点故障的情况下,如果刚提升的主节点还未完全同步旧主节点的数据,可能会出现数据丢失或数据不一致的情况;
- 脑裂问题:如果出现网络分区或通信故障,多个节点可能会认为自己是主节点,导致多个主节点同时接受写请求。这种情况下,不同的主节点可能会保存不同的数据,从而导致数据不一致。当网络恢复后,可能会有一些数据被丢弃或覆盖;
- 运维管理复杂:Redis Cluster需要管理多个节点,包括主从节点的状态、节点的增加和减少、哈希槽的重新分配等;随着集群规模的扩大,网络延迟和管理复杂性也会随之增加;
- 数据迁移:
- 哈希槽迁移:Redis Cluster使用哈希槽来分片和存储数据,但在节点新增或删除时,数据的重新分布可能导致负载不均衡或部分节点的存储压力过大;
- 热点问题:由于数据的分片机制,如果某些键特别热门,可能会导致对应的节点负载过高,出现性能瓶颈;
- 在进行数据迁移或重新分配哈希槽时,可能会对集群性能产生影响,尤其是在大规模数据迁移时,可能导致延迟增加和吞吐量下降;
- 客户端复杂:Redis Cluster需要支持客户端处理集群模式,包括重新连接、节点切换、哈希槽定位等功能。
优化措施
- 最小复制偏移量机制:Redis Cluster通过判断从节点的复制偏移量,确保提升为主节点的从节点具有最新的数据,以此减少数据丢失的风险;
- Quorum机制:Redis Cluster在进行主从切换时会要求多数派投票,这样可以降低脑裂的可能性;
客户端
JedisCluster集群寻址
JedisCluster配置只用指定集群中某一个节点的IP,端口信息即可。JedisCluster初始化时,会基于配置节点获取整个集群的信息(cluster nodes
命令)。拿到集群中所有Master信息,遍历每个Master节点,通过IP端口构建Jedis实例,然后put到一个全局nodes变量里面(Map类型) , key为IP端口,值为Jedis实例,nodes值如下:
nodes={172.19.93.120:6380=redis.clients.jedis.JedisPool@74ad1f1f,.....}
在上面遍历Master过程中,还做一件事,遍历此台Master负责的槽索引,然后又put到一个全局map slots里面。值为上面的Jedis实例, slots值如下:
slots={0=redis.clients.jedis.JedisPool@74ad1f1f,
1=redis.clients.jedis.JedisPool@74ad1f1f,
2=redis.clients.jedis.JedisPool@74ad1f1f,
....
5461 = redis.clients.jedis.JedisPool@65aa1f2f,
..其他Master机器
16383=redis.clients.jedis.JedisPool@756d1afd}
基于上面的slots变量,当有值set时,会先算出slot = get CRC16(key)&(16383-1)
,假如是12182,然后调用slots.get(12182)
得到Jedis实例,然后去操作Redis。如果发生MovedDataException,说明初始化得到的槽位与节点的对应关系有问题,(节点新增或者宕机)就会重置slots。
实战
安装
Redis 5.0之前的版本提供基于Ruby的集群管理工具redis-trib.rb
,需Ruby 2.3+版本。
安装Ruby
wget https://cache.ruby-lang.org/pub/ruby/2.6/ruby-2.6.2.tar.gz
tar -zxf ruby-2.6.2.tar.gz
cd ruby-2.6.2/
./configure --prefix=/usr/local/ruby
./configure --prefix=/opt/ruby/
make && make install
为redis-trib.rb
安装Redis驱动:gem install redis
Redis 5.X 版本已经将集群管理功能集成到redis-cli里面,不再推荐使用redis-trib.rb
。
命令
创建集群
cluster meet <ip> <port> # 将ip和port指定的redis实例添加到当前集群中,在同一个节点操作添加其他所有的节点
cluster replicate <node_id> # 将当前节点设置为指定节点的从节点,搭建集群时使用
查看集群信息
cluster info # 查看集群信息,包含几个节点,槽位分配和集群状态
cluster nodes # 展示redis集群中所有节点的ip端口,node id,主从关系等
cluster slaves <node_id> # 列出指定node_id从节点信息,node_id必须是Master角色,否则报错
运维集群节点
cluster failover # 手动进行故障转移
cluster forget <node_id> # 从集群中移除指定节点,这样就无法完成握手,过期时为60s,60s后两节点又会继续完成握手
cluster reset [HARD|SOFT] # 重置集群信息,soft是清空其他节点信息,但不修改自己的id,hard会修改自己的id,默认soft
cluster count-failure-reports <node_id> # 列出某个节点的故障报告
cluster SET-CONFIG-EPOCH # 设置节点epoch,只有在节点加入集群前才能设置
槽位相关操作
cluster slots # 列出节点和槽位的关系映射信息
cluster keyslot # 列出key被放置在哪个槽上
cluster countkeysinslot #
cluster getkeysinslot #
cluster setslot <slot> node <node_id> # 将槽指派给指定的节点,如果槽已经指派给另一个节点,那么先让另一个节点删除该槽,然后再进行指派
cluster setslot <slot> migrating <node_id> # 将本节点的槽迁移到指定的节点中
cluster setslot <slot> importing <node_id> # 从 node_id 指定的节点中导入槽 slot 到本节点
cluster setslot <slot> stable # 取消对槽 slot 的导入(import)或者迁移(migrate)
cluster flushslots # 移除指派给当前节点的所有槽,使当前节点变成一个没有指派任何槽的节点
拓展
16384个槽
为啥设计为16384个槽?
可能有些面试官真的会问出这个面试题。参考:GitHub issue 2576。
翻译:
- 如果槽位为65536,发送心跳信息的消息头达8k,心跳包过于庞大。
在消息头中,最占空间的是myslots[CLUSTER_SLOTS/8]
。Redis节点每秒需要发送一定数量的ping消息作为心跳包。当槽位为65536时,大小为65536÷8=8kb,ping消息头太大,浪费带宽。 - Redis的集群主节点数量基本不可能超过1000个。
集群节点越多,心跳包的消息体内携带的数据越多。如果节点过1000个,也会导致网络拥堵。不建议Redis Cluster节点数量超过1000个。节点数在1000以内的集群,16384个槽位够用 - 槽位越小,节点少的情况下,压缩率高。
Redis主节点的配置信息中,负责的哈希槽是通过bitmap保存。在传输过程中,会对bitmap进行压缩。如果bitmap的填充率,即slots / N
很高的话(N表示节点数),bitmap的压缩率就很低。如果节点数很少,而哈希槽数量很多的话,bitmap的压缩率就很低。而16384÷8=2kb