主从机制
基本原理
CAP原理:
- Consistent:一致性
- Availability:可用性
- Partition tolerance:分区容忍性
网络分区:分布式节点网络断开的场景。
CAP基本原理是:当网络分区发生时,不能同时保证一致性和可用性。
redis支持主从同步和从从同步:
redis的主从数据是异步的,分布式的redis不满足一致性;主从网络断开的时候,主节点仍然可以提供服务,因此满足可用性。redis满足最终一致性,从节点会努力追赶主节点,最终两者状态一致。
同步机制
增量同步:
- 主节点有一个环形队列,用于存储改变内存状态的值
- 主节点会在队列中不断写入改变自己状态的指令
- 指令不断发送给从节点进行同步
注意:如果写入速度超过读取速度,则环形队列值会被新的数据覆盖
快照同步:
- 主节点进行
bgsave
,输出存储到磁盘 - 发送快照到对应的从节点
- 从节点继续经过增量同步来同步数据
这种方式比较常用,新节点加入redis集群时,也是全量加载一次,然后增量加载数据
增量和快照同步时,数据都经过磁盘了
无盘复制:直接遍历内存数据,然后发送给从节点,速度快但是精度太差。
快照同步会影响正在进行AOF的fsync
操作,发生快照同步会推迟fsync
的过程,影响主节点的效率。
wait
指令会让redis的异步复制变成同步复制,但是阻塞服务,而且如果网络分区,则redis节点将永远阻塞。
哨兵机制
问题背景
主从机制的缺陷:
- 主从集群依赖master的活性,如果master崩溃,则从节点无法同步数据
- 集群没有选举机制,master崩溃后,无法确定新的主节点
问题的本质原因:缺少master奔溃后的选举很同步机制
解决方案:基本思路是,引入哨兵观测master健康状态,如果检测到master崩溃,则确认新的master,并使用新的master同步集群数据。
注意,哨兵是配置提供者,而不是代理
引入哨兵后客户端处理方式:
- 遍历哨兵集群,选择一个可用哨兵服务器。哨兵服务器数据是共享同步的
- 客户端从哨兵获取redis集群的master的地址,之后与master直接通信
- 客户端连接不上master后,主动请求哨兵,获取新的地址
- master其它原因出故障,但是客户端可以与master通信时;哨兵会主动通知客户端更换
详细解决方案:
定时监控是解决问题的核心。基本步骤如下:
- 每隔10s,向主节点发送
info
命令,获取集群的拓扑结构。不需要知道从节点的地址,主节点会把信息都发来。主节点故障时,利用之前的info的信息,从从节点中选择一个获取集群信息。 - 哨兵节点订阅
__sentinel__:hello
频道,获取其它哨兵对集群节点的判断,也获取其它哨兵信息;同时也发送自己对集群节点的判断。 - 每隔1s,哨兵向主节点、从节点和其它哨兵节点做心跳检测
- 对某个节点,如果最后一次收到的回复的时间超过配置的时间,则判定为主观下线;如果master在客观下线之前回复了消息,则恢复正常状态。
- 足够多的哨兵认为master下线后,则master为客观下线。
- master客观下线后,哨兵集群选出哨兵的master,让哨兵master在redis的slave节点选出主节点,然后指定其它节点向该节点复制信息。
参考资料:
- https://zhuanlan.zhihu.com/p/44474652
- https://juejin.im/post/5c1b482d6fb9a049dd803f55
- https://hellokangning.github.io/zh/post/redis-sentinel-client-connection/
min-slaves-to-write 1 # 至少有一个从节点在写,否则停止服务
min-slaves-max-lag 10 # 10s内没有收到从节点的反馈,则认为是异常,配合上面那个
Redis Cluster 集群
Redis Cluster把所有的数据分为16384个槽位,槽位的信息存储在每个节点中,不需要其它的分布式存储空间空间来存储节点。
客户端请求redis集群的两种方式:
- 简单方式,直接把请求Key发送到连接的任一个客户端上。如果key命中了则进行读写,否则该服务器会回发MOVE指令和对应的服务器地址x,此时对x服务器操作
- Smart客户端:保留服务器的映射表,然后客户端自己计算对应的服务器地址,并发送。可以定时更新表,也可以当请求失败时更新。
redis的slot和服务的映射关系:
16384个槽有自己对应的映射关系,然后迁移的时候,只需要移动对应槽中的数据即可。使用slot=CRC16(key)/16384
来计算对应的key
属于哪个槽:
redis-trib是redis的迁移工具,迁移单位是槽,迁移的槽处于中间过渡状态。
单个key的迁移过程:
- 对当前key执行dump指令序列化
- 通过“客户端”向目标节点发送数据
- restore指令携带序列化的内容作为参数
- 目标节点反序列化参数
- 目标节点返回“客户端”OK内容
- 当前节点删除key,完成迁移
迁移的过程是同步的,执行restore指令到删除key的时候,主线程是阻塞的。如果迁移过程中出现故障,则下次重新连接工具时,会自动迁移。
集群环境下,避免产生大key,否则迁移会发生阻塞,影响线上服务。
迁移过程中,客户端访问流程:
- 先访问旧节点,数据存在则直接返回
- 旧节点数据不在,两种可能:
- 数据在新节点
- 数据根本不存在
对于第二种情况,旧节点返回一个-ASK targetNoodeAddr
的重定向指令,客户端收到该指令后,去目标节点执行一个不带参数的ASKING
指令,然后再在目标节点执行原来的查询指令。
迁移没完成的时候,新节点无法管理有关的key
,如果此时直接发送查询指令,新节点不认可该指令,会向客户端发送-MOVE
指令,让客户端再去源节点查询,如此会形成重定向循环。ASKING
指令是告诉目标节点,必须执行下面的指令。因此迁移影响效率,原来只需要一个ttl,现在需要3个ttl了。
Redis Cluster提供了cluster-node-timeout
,表示节点持续对应的时间失联时,才认定节点出现故障,并进行主从切换,如果没有该参数,则网络抖动可能引起主从频繁切换。
Redis Cluster是去中心化的,不支持事务,mget
等命令会慢,因为这是拆分到多个集群执行的。rename
等方法也不是原子的,会从源节点转移到目标节点。
槽位变化感知:
MOVED
指令修正槽位,如果客户端请求的数据不在当前节点,则当前节点返回给客户端MOVE
指令,并带有目标节点的地址,此时客户端会刷新自己的映射表。
ASKING
指令临时修正槽位,发生再迁移时候,返回给客户端asking error
。上面提到了,客户端不会刷新映射表。
2次重试:收到MOVED
指令,去新节点时,新节点正在迁移,返回给客户端ASKING
的命令了。一般有多次重试的限制次数,超过抛出异常。
集群变更感知:
- 目标节点挂掉,客户端抛出
ConnectionError
,立刻随机挑选一个节点重试,重试的节点通过MOVED
指令通知槽位被分配到了新的节点地址。 - 运维手动更改集群信息,主节点切换到其它节点,并把旧的节点移除出集群。在旧节点的指令会获取
ClusterDown
错误,并通知不可用。客户端关闭所有的连接,清空映射表,向上层报错。下一条指令来时,会重新初始化信息。
集群的高可用性
集群中的每个节点,增加至少一个slave节点作为备份。当某个节点的master不可用时,添加对应的slave作为master节点即可。
参考文档
- https://www.javazhiyin.com/22957.html
- https://www.infoq.cn/article/sIRPs21lbMvDAJtqQRJE
- https://zhuanlan.zhihu.com/p/72056688
- https://medium.com/@pubuduboteju95/deep-dive-into-redis-clustering-1d71484578a9 (推荐)
总结
- redis主从:主从是为了备份数据,意外情况下可快速恢复
- redis哨兵:为了保证集群的可用性
- redis集群:解决单机容量不够的问题