面试题:Redis要点总结(复制、哨兵、集群)
1.Redis使用sync命令实现的复制功能
(1)Redis复制功能的两个核心操作
(2)从服务器对主服务器发起同步操作
(3)主服务器对从服务器进行命令传播操作
(4)旧版使用sync命令进行同步操作的问题
(1)Redis复制功能的两个核心操作
Redis的复制功能分为同步和命令传播两个操作。
同步操作用于将从服务器的数据库状态更新至主服务器当前所处的数据库状态。
命令传播操作用于在主服务器的数据库状态被修改,导致主从服务器的数据库状态出现不一致时,让主从服务器的数据库重新回到一致状态。
(2)从服务器对主服务器发起同步操作
从服务器对主服务器的同步操作,需要通过向主服务器发送sync命令来完成,以下是具体的处理步骤。
步骤一:从服务器向主服务器发送sync命令。
步骤二:主服务器收到sync命令后,便会执行bgsave命令在后台生成一个RDB快照文件,并使用一个客户端输出缓冲区(复制缓冲区)记录从现在开始执行的所有写命令。
步骤三:当主服务器的bgsave命令执行完毕时,主服务器会将bgsave命令生成的RDB快照文件发送给从服务器。从服务器接收并载入这个RDB快照文件,将自己的数据库状态更新至主服务器执行bgsave命令时的数据库状态。
步骤四:主服务器将记录在复制缓冲区里面的所有写命令发送给从服务器,从服务器执行这些写命令将数据库状态更新至主服务器数据库当前所处的状态。
(3)主服务器对从服务器进行命令传播操作
在同步操作执行完毕后,主从服务器两者的数据库将达到一致状态。此后每当主服务器执行客户端发送的写命令时,为了让主从服务器再次回到一致状态,主服务器需要对从服务器执行命令传播操作,将自己执行的写命令发送给从服务器执行。
(4)旧版使用sync命令进行同步操作的问题
对于初次复制来说,旧版使用sync命令实现的复制功能能很好完成任务。但对于断线后重复制来说,旧版使用sync命令实现的复制功能的效率却非常低。因为无论主从服务器断开的时间有多短,主从服务器都要重新执行一次sync命令。
sync命令是一个非常耗费资源的操作,原因有三:包括生成RDB、发送RDB、加载RDB。
原因一:主服务器需要执行bgsave命令来生成RDB快照文件,这个生成操作会耗费主服务器大量CPU、内存和磁盘IO
原因二:主服务器需要将生成的RDB快照文件发送给从服务器,会耗费主从服务器大量的带宽和流量
原因三:接收到RDB快照文件的从服务器需要载入RDB快照文件,在载入期间会阻塞主进程而导致无法处理命令请求
(5)为什么主从库间的复制不使用AOF文件
原因一:AOF文件记录的是文本协议格式的内容,RDB文件是经过LZF算法紧凑压缩的二进制文件。无论是把RDB加载到磁盘还是通过网络传输,IO效率都比加载和传输AOF高
原因二:在从库进行恢复时,使用RDB文件进行恢复的效率也高于AOF文件
2.Redis使用psync命令实现的复制功能
(1)新版使用psync命令进行同步操作
(2)实现部分复制功能需要三类变量
(3)主从服务器的复制偏移量
(4)主服务器的复制积压缓冲区
(5)服务器的运行ID
(6)psync命令的整体运行流程
(7)psync命令的全量复制流程
(8)psync命令的部分复制流程
(9)复制缓冲区和复制积压缓冲区对比
(1)新版使用psync命令进行同步操作
从Redis2.8开始,便使用psync命令代替sync命令来执行复制时的同步操作,psync命令具有全量复制和部分复制两种模式。
一.全量复制用于处理初次复制的情况
全量复制的执行步骤和sync命令的执行步骤基本一样,都是通过让主服务器创建并发送RDB快照文件以及向从服务器发送保存在复制缓冲区里面的写命令来进行同步。
二.部分复制用于处理断线后重复制情况
当从服务器在断线后重新连接主服务器时,如果条件允许,主服务器可以将主从服务器连接断开期间执行的写命令发送给从服务器,从服务器只要接收并处理这些命令即可。
(2)实现部分复制功能需要三类变量
一.主服务器的复制偏移量和从服务器的复制偏移量
二.主服务器的复制积压缓冲区
三.服务器的运行ID
(3)主从服务器的复制偏移量
参与复制的主从节点都会维护自身的复制偏移量。主服务器每次向从服务器传播N个字节时,就将自己的复制偏移量加上N。从服务器每次收到主服务器传播来的N个字节时,就将自己的复制偏移量加上N。从节点会每秒上报自身的复制偏移量给主节点,因此主节点也会保存从节点的复制偏移量。
(4)主服务器的复制积压缓冲区
复制积压缓冲区是由主服务器维护的一个固定长度的先进先出队列,默认大小为1MB。当主服务器进行命令传播时,它不仅会将写命令发送给所有从服务器,还会将写命令入队列复制积压缓冲区。主服务器的复制积压缓冲区里,会保存最近传播的写命令,且会为队列中的每个字节记录相应的复制偏移量。
当从服务器重新连上主服务器时,从服务器会通过psync命令将自己的复制偏移量发送给主服务器。如果偏移量之后的数据仍然存在于复制积压缓冲区里,那么主服务器将对从服务器执行部分复制操作,否则主服务器对从服务器执行全量复制操作。
(5)服务器的运行ID
服务器启动时会生成一个运行ID。当从服务器对主服务器进行初次复制时,主服务器会将自己的运行ID发送给从服务器,从服务器会保存主服务器的运行ID。当从服务器断线并重新连上一个主服务器时,从服务器将会向当前的主服务器发送之前保存的运行ID。如果从服务器之前保存的运行ID和当前连接的主服务器的运行ID相同,则可以尝试执行部分复制,否则主服务器对重服务器执行全量复制。
如果只使用IP + 端口方式识别主服务器,而不使用服务器的运行ID进行识别,那么主服务器重启后变更的数据库(替换RDB或AOF文件),此时从服务器基于偏移量复制是不安全的。
Redis关闭然后再重启,它的服务器运行ID会随之改变,可以使用debug reload命令重新加载RDB并保持运行ID不变。
(6)psync命令的整体运行流程
步骤一:从服务器接到客户端发来的replicaof命令
步骤二:从服务器判断这是否是第一次执行复制
如果是,则向主服务器发送"psync ? -1",主服务器会返回"+fullresync"执行全量复制。如果不是,则向主服务器发送"psync"。
步骤三:从服务器判断主服务器是否返回"+continue"
如果是 ,则表示主服务器执行部分复制。如果不是,则主服务器返回"+fullresync"执行全量复制。
(7)psync命令的全量复制流程
步骤一:从服务器发送psync命令进行数据同步。由于是第一次复制,从服务器没有复制偏移量和主服务器的运行ID,所以发送"psync ? -1"
步骤二:主服务器根据"psync ? -1"解析出当前为全量复制,于是回复从服务器服务"+fullresync"响应
步骤三:从服务器接收主服务器的响应数据,保存主服务器的运行ID和复制偏移量
步骤四:主服务器执行bgsave命令保存RDB快照文件到本地
步骤五:主服务器发送RDB快照文件给从服务器,从服务器把接收到的RDB快照文件保存在本地并作为自己的数据文件
步骤六:主服务器生成RDB快照文件到从服务器收到RDB快照文件期间,主服务器的写命令会保存在复制缓冲区(客户端输出缓冲区),当从服务器加载完RDB快照文件后,主服务器再把复制缓冲区的数据发给从服务器,从而保证主从服务器之间的数据一致性
步骤七:从服务器收到RDB快照文件后会进行加载,如果从服务器开启了AOF持久化,则会执行bgrewriteaof命令
(8)psync命令的部分复制流程
步骤一:当主从服务器出现网络中断并超过repl-timeout时间,主节点会认为从节点故障并中断复制连接
步骤二:主节点继续响应命令,并会保存一段时间的写命令数据到复制积压缓冲区
步骤三:主从节点重连后,从节点会将之前保存的主服务器运行ID和复制偏移量作为psync命令的参数,发给主节点请求继续复制
步骤四:主节点收到从节点的请求后,会先核对服务器运行ID是否一致,复制偏移量对应的数据是否还在复制积压缓冲区里,然后再决定是执行部分复制还是执行全量复制
(9)复制缓冲区和复制积压缓冲区对比
主节点会把收到的写操作命令,既写到复制缓冲区replication buffer,也写到复制积压缓冲区repl_backlog_buffer。
一.使用AOF缓冲区的原因
由于Redis使用单线程响应命令,如果每次写AOF文件命令都直接追加到硬盘,那么性能则取决于硬盘。先写入AOF缓冲区,Redis可以提供多种AOF缓冲区同步硬盘策略,在性能和安全性上作出平衡。
二.使用AOF重写缓冲区的原因
由于Redis服务器使用单线程来处理命令请求,所以Redis将AOF重写程序放到子进程里执行。子进程在AOF重写完成后,可能会导致数据不一致问题的,因此需要解决数据不一致的问题。
三.使用复制缓冲区的原因
每个从节点都有一个,保证复制期间,主从服务器的数据是一致的。每个客户端连上Redis服务器后,Redis服务器都会分配一个客户端输出缓冲区。主服务器先把数据写到客户端输出缓冲区中,然后再把客户端输出缓冲区里的数据写到客户端套接字中,最后通过网络连接发送给客户端。
所以主从同步和命令传播时,从库作为一个客户端会通过客户端输出缓冲区,保证主从节点数据一致。参数client-output-buffer-limit限制其大小,超过其大小会断开客户端连接,可能引发复制风暴。复制风暴就是多个主服服务器或者一个主服务器被多个从服务器在短时间内进行全量复制。
四.使用复制积压缓冲区的原因
从节点共享,保证主从服务器断连恢复后,可以尽量使用部分复制来提升性能。复制积压缓冲区是为了从节点断连后能找到主从差异数据而设计的环形缓冲区,可以避免全量复制。
3.Redis主从服务器之间的心跳检测
第一:在命令传播阶段,从服务器默认会以每秒一次向主服务器发送"replconf ack"命令,实现实时监测主从节点网络状态以及上报自身偏移量。
第二:主服务器默认每隔10秒对从服务器发送ping请求,判断从服务器的存活性和连接状态。
总结就是:从发主偏移量每秒一次,主发从ping请求每十秒一次。
4.从服务器如何实现复制主服务器的(复制的实现)
步骤一:设置主服务器的地址和端口
从服务器收到命令"replicaof ip port"后,设置其服务器状态。
步骤二:建立套接字连接
从服务器根据设置的IP和端口,创建连向主服务器的套接字连接。如果从服务器创建的套接字能连向主服务器,那么从服务器将为该套接字关联复制处理器。
步骤三:发送ping命令
从服务器成为主服务器的客户端后,做的第一件事就是向主服务器发送一个ping请求,以检查套接字的读写是否正常,确认主服务器能否正常处理命令请求。
步骤四:身份验证
从服务器收到主服务器返回的pong回复后,表明一切正常,开始身份验证。
步骤五:发送端口信息
身份验证通过后,从服务器向主服务器发送从服务器的监听端口号。
步骤六:同步(psync的全量复制和部分复制流程)
从服务器向主服务器发送psync命令执行同步操作。主服务器执行同步操作后,主服务器也会成为从服务器的客户端。如果是全量复制,那么主服务器需要成为从服务器的客户端,才能将复制缓冲区里的写命令发送给从服务器。如果是部分复制,那么主服务器需要成为从服务器的客户端,才能将复制积压缓冲区的写命令发送给从服务器。
步骤七:命令传播(基于长连接的命令传播)
完成同步后,主从服务器会进入命令传播阶段,保证主从服务器的数据一致性。
5.Redis的复制拓扑介绍
(1)一主一从结构(简单)
当应用写命令并发量较高,且需要持久化时,可以只在从节点开启AOF。这样既保证数据安全,又避免持久化对主节点的影响。但要注意主节点重启时,由于主节点是没持久化的,需要从节点先断开与主节点的复制,然后再重启主节点,避免从节点数据清空。
(2)一主多从结构(适合读多不适合写多)
利用多个从节点实现读写分离。对于读占比较大的场景,可以把读请求发送到从节点来分担主节点压力,日常的慢查询命令如keys、sort也可以在其中一台从节点上执行。但对于写并发量较高(写多)的场景,多个从节点会导致主节点的写命令的多次发送,从而过度消耗带宽影响主节点。
注意:复制风暴就是多个主节点或一个主节点被多个从节点短时间全量复制,解决方法是使用树状结构。
(3)树状主从结构(适合写多读多)
从节点不但可以复制主节点的数据,也可以作为其他从节点的主节点继续向下复制。通过引入复制中间层,可以有效降低主节点负载和需要传给从节点的数据量。所以当主节点需要挂载多个从节点时,可以采用树状主从结构降低主节点压力。
6.Redis主从复制数据延迟的处理
Redis复制数据的延迟是由异步复制导致的,无法避免。延迟时间取决于网络带宽和命令阻塞的情况。可以通过监听主从节点的复制偏移量,当延迟较大时触发报警或通知客户端避免读取延迟高的从节点。
说明一:监控程序定期检查主从节点的偏移量,它们的差值就是主从节点延迟的字节量
说明二:当延迟字节量过高时,可以采用ZooKeeper的监听回调机制实现客户端通知
说明三:客户端接到具体的从节点高延迟通知后,修改读命令路由到其他从节点或主节点上。延迟恢复后再次通知客户端,恢复从节点的读命令请求
注意:为了保证复制的数据一致性,从节点自身不会主动删除超时数据,但会检查键的过期时间来决定是否返回数据。
7.Redis主从复制的优缺点
(1)主从复制的好处
(2)主从复制的问题
(3)Redis主从复制模式下的高可用问题
(1)主从复制的好处
好处一:主挂了从可以顶上
好处二:从可以分担主的读压力
(2)主从复制的问题
问题一:一旦主节点出现故障,需要手动将一个从节点晋升为主节点,需要修改应用的主节点地址,需要通过命令修改其他从节点复制新的主节点,整个过程需要人工干预
问题二:主节点的写能力受单机限制
问题三:主节点存储能力受单机限制
问题一是Redis的高可用问题,问题二和问题三是Redis的分布式问题。
(3)Redis主从复制模式下的高可用问题
一旦主节点挂了,需要人工干预,除了带来运维的不方便,应用方也无法实时感知主节点的变化,必然造成一定的写数据丢失和读数据错误,甚至造成应用服务不可用。而且整个故障转移过程人工介入,实时性和准确性无法保障。
即便自动化整个故障转移过程,也存在问题。
问题一:判断主节点不可达的机制是否健全和标准(主节点是否真的挂了)
问题二:如果有多个从节点,怎么保证只有一个被晋升为主节点(选择哪个从节点作为主节点)
问题三:通知客户端新的主节点机制是否足够健壮(怎么把新主节点的信息通知给从节点和客户端)
8.Redis Sentinel和高可用
Redis哨兵主要负责三个任务:监控、选主、通知。
Redis Sentinel是一个分布式架构,其中包含若干个Sentinel节点和若干个Redis数据节点。每个Sentinel节点会对Redis数据节点和其余Sentinel节点进行监控,当它发现节点不可达时,会对不可达节点做下线标识。如果被标识的是主节点,它还会和其他Sentinel节点进行协商。当大多数节点都认为主节点不可达时,这些Sentinel节点会选举出一个Sentinel节点来完成自动故障转移的工作,同时会将这个变化实时通知给Redis的应用方,整个过程是自动的,实现了真正的高可用。
Redis Sentinel比Redis主从复制模式只是多了若干个Sentinel节点,Redis Sentinel并没有针对Redis节点做特殊处理。
9.Redis如何保存更多的数据
(1)纵向扩展
升级单个Redis实例的配置,简单直接,如果不要求持久化,这是不错的选择,会受硬件成本限制。但如果需要持久化,则面临内存实例过大时,fork子进程阻塞的问题。
(2)横向扩展
使用Redis集群进行数据切分,可扩展性好。
切片集群的方案有:
一.基于客户端的分区:ShardedJedis
二.基于代理的Codis、Twemproxy
三.基于服务端的Redis Cluster
10.一个普通Redis服务器的初始化过程
步骤一:初始化服务器状态结构
步骤二:载入服务器配置
步骤三:初始化服务器数据结构
步骤四:还原数据库状态
步骤五:执行事件循环
11.一个Sentinel服务器的初始化过程
步骤一:初始化服务器状态结构
Redis Sentinel本质上还是一个运行在特殊模式下的Redis服务器。
步骤二:使用Sentinel专用代码
比如普通Redis使用6379端口,Sentinel服务器使用26379端口。
步骤三:初始化Sentinel状态
SentinelState代表着一个Sentinel服务器。
步骤四:初始化Sentinel状态的masters属性
masters属性是一个字典。字典的键是被监视主服务器的名字,字典的值是被监视主服务器对应的SentinelRedisInstance结构。masters字典的初始化是根据被载入的Sentinel配置文件来进行的。
步骤五:创建连向主服务器的网络连接
网络连接包括命令连接和订阅连接。命令连接专门用于向主服务器发送命令和接收命令回复,此时Sentinel服务器成为主服务器的客户端。订阅连接专门用于订阅主服务器的__sentinel__:hello频道。
每个SentinelRedisInstance结构代表一个被Sentinel监控的Redis服务器实例,可以是主服务器、从服务器或者另一个Sentinel服务器。
//SentinelState(Sentinel服务器)
dict *masters; //保存了所有被这个Sentinel服务器监视的主服务器,masters字典的值指向一个SentinelRedisInstance//SentinelRedisInstance
char *name, *runid;
int quorum;
//判断自己下线需要的票数
int flags;
//实例类型(主、从、Sentinel)
dict *sentinels;
//主的Sentinel名单
12.Sentinel如何向主从服务器获取信息和发送信息
(1)Sentinel获取主服务器信息
(2)Sentinel获取从服务器信息
(3)Sentinel向主服务器和从服务器发送频道信息
(4)Sentinel接收来自主和从服务器的频道信息
(5)Sentinel通过订阅频道更新的认知
(1)Sentinel获取主服务器信息
Sentinel默认会以每10秒一次的频率,通过命令连接向被监视的主服务器发送info命令。通过分析info命令的回复,来获取主服务器的当前信息(服务器ID、角色、所有从服务器信息)。这样,Sentinel就无须用户提供从服务器的地址信息,也可以自动发现从服务器。
主服务器返回的从服务器信息,会被用于更新对应的主服务器SentinelRedisInstance的slaves字典。这个字典记录了主服务器下所有从服务器的名单,字典的值是从服务器对应的SentinelRedisInstance。
主服务器实例结构的flags属性的值为SRI_MASTER,从服务器实例结构的flags属性的值为SRI_SLAVE。主服务器实例结构的name值是用户使用Sentinel配置文件设置的,而从的name值则是自动设置的ip:port。
(2)Sentinel获取从服务器信息
当Sentinel发现主服务器有新的从服务器出现时,Sentinel会:
一.创建新的从服务器相应的实例结构
二.创建连接到新的从服务器的命令连接和订阅连接,然后订阅从服务器的__sentinel__:hello频道
Sentinel默认会以每10秒一次的频率通过命令连接向从服务器发送info命令,通过分析info命令的回复来对从服务器的实例结构进行更新。
(3)Sentinel向主服务器和从服务器发送频道信息
Sentinel默认会以每2秒一次的频率,通过命令连接向所有被监视的主从服务器发送广播命令,即向这些主从服务器的__sentinel__:hello频道发送Sentinel本身的信息和主服务器的信息。
publish sentinel:hello
(4)Sentinel接收来自主和从服务器的频道信息
当Sentinel与一个主服务器或者从服务器建立起订阅连接后,Sentinel就会通过订阅连接向服务器发送订阅命令。
subscribe sentinel:hello
这样每个Sentinel都会订阅每一个服务器__sentinel__:hello频道,Sentinel对每个服务器的__sentinel__:hello频道的订阅会一直持续到Sentinel与该服务器的连接断开为止。
对于每个与Sentinel连接的服务器,Sentinel既通过命令连接向服务器的__sentinel__:hello频道发送信息,又通过订阅连接从服务器的__sentinel__:hello频道接收信息。
对于监视同一个服务器的多个Sentinel来说,一个Sentinel向__sentinel__:hello频道发送的消息,会被其他Sentinel接收到,其他Sentinel接收到这些信息可以更新对发送信息的Sentinel的认知,也可以更新对被监视服务器的认知。
(5)Sentinel通过订阅频道更新的认知
这些认知包括:更新sentinels字典和创建连向新Sentinel的命令连接。
在Sentinel为主服务器创建的实例结构SentinelRedisInstance中的sentinels字典保存了监视该主服务器的所有Sentinel。当Sentinel通过频道信息发现一个新的Sentinel时,它不仅会为新Sentinel在sentinels字典中创建相应的实例结构,还会创建一个连向新Sentinel的命令连接,而新Sentinel也同样会创建连向这个Sentinel的命令连接。使用命令连接相连的各个Sentinel可以通过向其他Sentinel发送命令请求来实现主观下线检测和客观下线检测。
因为一个Sentinel可以通过分析接收到的某服务器的频道信息来获知其他Sentinel的存在,并通过向某服务器发送频道信息来让其他Sentinel知道自己的存在,所以用户在使用Sentinel的时候并不需要提供各个Sentinel的地址信息,监视同一个主服务器的多个Sentinel可以自动发现对方。
13.Sentinel如何检测主客观下线并实现故障转移
(1)检测主观下线状态
(2)检查客观下线状态
(3)选举领头Sentinel
(4)领头Sentinel执行故障转移
(5)Redis Sentinel架构具有的功能
(6)Redis Sentinel架构包含多个Sentinel的好处
(1)检测主观下线状态
Sentinel默认会以每秒一次的频率向所有与它创建了命令连接的实例(主服务器、从服务器、其他Sentinel服务器)发送ping命令,通过实例返回的ping命令回复来判断实例是否在线(将服务器实例结构的flag设为SRI_S_DOWN)。
(2)检查客观下线状态
当Sentinel将一个主服务器判断为主观下线之后,为了确认这个主服务器是否真的下线了,它会向同样监视这一主服务器的其他Sentinel进行询问。
当Sentinel从其他Sentinel那里接收到足够数量的已下线判断后,Sentinel就会将这个主服务器判断为客观下线(将主服务器实例结构的flag设为SRI_O_DOWN),并对主服务器执行故障转移。
客观下线的判断条件:认为主服务器已进入下线状态的Sentinel数量,超过Sentinel配置中设置的quorum参数的值。
(3)选举领头Sentinel
当一个主服务器被判断为客观下线时,监视这个下线主服务器的各个Sentinel会进行协商,选举出一个领头Sentinel,并由领头Sentinel对下线主服务器执行故障转移操作。
(4)领头Sentinel执行故障转移
步骤一:在已下线主服务器下的所有从服务器里挑选出一个从服务器,并将其转换为新的主服务器。从服务器被选中的规则是:先看优先级配置最高的,再看复制偏移量最大的,最后看服务器运行ID最小的。
步骤二:让已下线主服务器下的所有从服务器改为复制新的主服务器。
步骤三:将旧的主服务器设为从服务器。
简单来说就是:首先选新主,然后所有从复制新主,最后监视旧主设旧主为新主的从。
(5)Redis Sentinel架构具有的功能
一.监控功能
Sentinel节点会定期检测Redis数据节点、其余Sentinel节点是否可达。
二.通知功能
Sentinel会将故障转移的结果通知给应用方。
三.故障转移功能
实现从节点晋升为主节点,并维护正确的主从关系。
四.客户端可获取配置的功能
客户端在初始化和切换主节点时,需要和Sentinel节点集合进行交互来获取主节点信息。
(6)Redis Sentinel架构包含多个Sentinel的好处
好处一:节点故障判断由多个Sentinel节点共同完成,可以有效防止误判
好处二:若干个Sentinel节点组成的Sentinel集合,即便个别Sentinel节点不可用,整个Sentinel节点集合仍然健壮
14.Sentinel配置优化和部署技巧
(1)sentinel.conf配置优化说明
(2)Sentinel的部署技巧
(1)sentinel.conf配置优化说明
1.sentinel monitor <m-name> <ip> <port> <quorum>
<quorum>代表判定主观下线所需票数,一般设为Sentinel节点的一半加1。
<quorum>也与Sentinel节点的领导者选举有关,至少要max(quorum, num(sentinels)/2 + 1)个Sentinel选举领导者。
2.sentinel down-after-milliseconds <m-name> <times>每个Sentinel节点都要定期发送ping命令来判断Redis数据节点和其余Sentinel节点是否可达,<times>为超时时间。
3.sentinel parallel-syncs <m-name> <nums>
用来限制在一次故障转移之后,每次向新的主节点发起复制操作的从节点个数。
尽管复制操作通常不会阻塞主节点,但多个从节点同时向主节点发起复制,会对主节点造成一定网络和磁盘IO。
4.sentinel failover-timeout <m-name> <times>故障转移超时时间。
5.sentinel notification-script <m-name> <script-path>故障转移期间,告警级别的Sentinel事件发生时触发的脚本。
6.sentinel client-reconfig-script <m-name> <script-path>故障转移结束后,触发对应的脚本。
(2)Sentinel的部署技巧
技巧一:Sentinel节点不应都部署在同一物理机上
技巧二:部署至少三个且奇数个的Sentinel节点
因为领导者选举至少一半加一个节点,奇数个节点可以在满足该条件的基础上节省一个节点。而且节点越多,整个节点集合存在节点故障的概率越大。
技巧三:一般每个主节点配置一套Sentinel
但如果多个主节点是同一个业务的,也可以用一套Sentinel监控多个主节点。
15.Sentinel客户端的基本实现原理
各个语言的客户端需要对Redis Sentinel进行显示的支持。虽然Redis Sentinel可以完成故障转移,但是如果客户端无法获知主节点已经转移了,那么使用Redis Sentinel的意义就不大。
无论哪种编程语言的客户端,如果需要正确连接Redis Sentinel,必须要有Sentinel节点集合和masterName。而在此基础上,实现一个Redis Sentinel客户端的具体步骤如下。
步骤一:遍历Sentinel节点集合获取一个可用的Sentinel节点,Sentinel节点之间可以共享数据,任意一个Sentinel节点都可以获取主节点信息
步骤二:通过sentinel get-master-addr-by-name masterName获取主节点相关信息
步骤三:通过role命令或info replication命令获取当前主节点是否是真正的主节点,防止故障转移期间主节点变化
步骤四:保持和Sentinel节点集合的联系,时刻获取关于主节点的信息
Jedis会为每一个Sentinel节点单独启动一个线程,然后利用Redis的发布订阅功能,每个线程都会去订阅Sentinel节点的"+switch-master"频道(也就是切换主节点频道)。
领头Sentinel节点在结束对主节点故障转移后会发布消息到"+switch-master"频道。此外,Sentinel节点会将故障转移的各个阶段发生的行为都通过这种发布订阅的形式对外提供。
16.Sentinel的基本实现原理(哨兵机制的基本流程)
(1)三个定时监控任务
(2)主观下线和客观下线
(3)领导者Sentinel节点选举
(4)执行故障转移
总结来说包括:三个定时监控任务、主观下线和客观下线、Sentinel领导者选举、故障转移。
(1)三个定时监控任务
第一个定时任务:每隔10秒,每个Sentinel节点会向主节点和从节点发送info命令获取最新拓扑结构。
第二个定时任务:每隔2秒,每个Sentinel节点会向主节点和从节点的__sentinel__:hello频道上,发送该Sentinel节点对主节点的判断以及当前Sentinel节点的信息。同时每个Sentinel节点也会订阅该频道,来了解其他Sentinel节点以及它们对主节点的判断。
第三个定时任务:每隔1秒,每个Sentinel节点会向主节点、从节点、其余Sentinel节点,发送ping命令做一次心跳检测确认可达。
其中,定时任务二可以完成以下两项工作:
工作一:发现新的Sentinel节点,对新的Sentinel保存起来,并与该Sentinel节点创建连接。
工作二:Sentinel节点之间交换主节点的状态,作为后面客观下线和领导者选举的依据。
(2)主观下线和客观下线
一.主观下线
要保证所有Sentinel实例的配置是一致的,尤其是主观下线的判断值down-after-milliseconds。每个Sentinel节点会每隔1秒对主节点、从节点、其他Sentinel节点发送ping命令做心跳检测,当这些节点超过down-after-milliseconds没有进行回复,Sentinel节点就会对该节点做失败判定。
二.客观下线
当Sentinel判定主观下线的节点是主节点时,该Sentinel节点会通过命令sentinel is-master-down-by-addr,向其他Sentinel节点询问对主节点的判断来减少误判,当超过个数则Sentinel节点作出客观下线决定。
(3)领导者Sentinel节点选举
Redis使用了Raft算法实现Sentinel领导者选举,思路说明如下。
说明一:每个在线的Sentinel节点都有资格成为领导者。当它确认主节点客观下线的时候,会向其他Sentinel节点再次发送命令sentinel is-master-down-by-addr,请求将自己设置为领导者。和检测客观下线时发送的命令sentinel is-master-down-by-addr不同,这次发送会带上运行ID。
说明二:收到命令的Sentinel节点,就会根据先到先得的规则,如果没有同意过其他Sentinel节点的命令sentinel is-master-down-by-addr,那么就将同意该请求,否则拒绝。
说明三:如果该Sentinel节点发现自己的票数已经大于等于max(quorum, num(sentinels)/2 + 1),则将会成为领导者,而选举出的Sentinel领导者节点,则需要负责故障转移。如果此过程没有选举出领导者,则会进入下一轮选举。
(4)执行故障转移
首先,在从节点列表中选出一个节点作为新的主节点,选举方法如下:
一.过滤主观下线或断线或断线次数多的节点
二.选择从节点优先级slave-priority最高的节点
三.选择复制平移量最大的节点
四.选择运行ID最小的节点
然后,Sentinel领导者节点会对选出来的从节点执行replicaof no one命令,让其成为主节点。
接着,Sentinel领导者节点会向剩余的从节点发送命令,让它们成为新节点的从节点。
最后,将原来的主节点更新为从节点,并当其恢复后让它去复制新的主节点。
17.Sentinel的高可用读写分离
Redis Sentinel需要客户端实现的有:故障转移通知以及高可用的从节点。
首先,一般的读写分离模型,客户端是直接连接固定的某个从节点,而Redis Sentinel的故障转移只是针对主节点的,并没有实现从节点的高可用。
然后,Redis Sentinel要想实现读写分离的高可用,可以依赖Sentinel节点的消息通知,获取Redis数据节点的状态变化。把所有从节点看作一个资源池,无论下线还是上线从节点,通过感知和从节点变动相关的频道消息,将其从资源池中添加或删除,从而实现从节点的高可用。
常见的Sentinel节点的发布订阅频道:
+switch-master:切换主节点
+sdown:节点主观下线
+covert-to-slave:切换从节点
+reboot:重启了某个节点
18.关于Sentinel的一些问题
(1)问题一
Sentinel(哨兵)实例是否越多越好,如何调整down-after-milliseconds的值?
一.哨兵实例越多,误判率越低,但在判定主库下线和选举领导者时,实例需要拿到的票数也就越多。等待所有哨兵投票完的时间可能也相应增加,主从库切换的时间、故障转移的时间也会变长。从而导致客户端堆积较多的请求,甚至可能会导致客户端请求溢出,造成请求丢失。
二.调大down-after-milliseconds可降低误判,但也意味着主从切换时间变长。
所以实例越多,那么通信次数越多、出现机器故障的风险越大,从而影响选举时间、影响主从切换时间。
(2)问题二
主从切换过程中,客户端能否进行正常的请求操作?如何才能让应用程序不感知服务的中断?
一.如果客户端使用了读写分离,那么读请求可正常在从库执行,但写请求会失败。
二.如果不想让业务感知异常,客户端只能把失败的写请求缓存起来,或写入消息队列中间件中,等哨兵切换完主从后,再把这些写请求发给新的主库,但这种方案只适合对写入请求返回值不敏感的业务。
三.等哨兵完成主从切换后,客户端需及时感知主库发生变更,做法如下:哨兵提升一个从库为新主库后,会把新主库的地址写入自己实例的+switch-master频道中,客户端需要订阅哨兵的这个频道,这样才能感知主库发生变更时拿到最新的主库地址,这属于哨兵主动通知客户端。如果客户端因为某些原因错过了哨兵的通知,客户端也需要主动获取最新的主从地址。所以客户端访问主从库时,不能写死主从库地址,需要能从哨兵集群中获取最新的主从地址。
19.Redis Cluster集群的简介
(1)Redis的分布式方案
(2)常见的分区规则
(3)常见的希分区规划
(4)Redis Cluster采用虚拟槽分区
(5)Redis Cluster存在的功能限制
(1)Redis的分布式方案
Redis Cluster是Redis的分布式解决方案,除了Redis Cluster之外,Redis的分布式方案还有:
一.客户端分区方案,优点是分区逻辑可控,缺点是需要自己处理数据路由、高可用、故障转移等问题
二.代理方案,优点是简化客户端分布式逻辑和升级维护便利,缺点是加重架构部署复杂度和性能损耗
(2)常见的分区规则
分布式数据库首先要解决把整个数据集按照分区规划映射到多个节点的问题,常见的分区规则有哈希分区和顺序分区两种:
一.哈希分区,特点是离散度好、数据分布与业务无关、无法顺序访问,如Redis Cluster
二.顺序分区,特点是离散度易倾斜、数据分布与业务相关、可顺序访问,如HBase
(3)常见的希分区规划
一.节点取余分区
用数据的键或ID,根据节点数量,使用公式计算出哈希值:hash(key) % N,哈希值决定数据映射节点。
优点是:简单,常用于数据的分库分表规则。一般采用预分区方式提前根据数据量规划好分区数,如512或1024张表。扩容时通常采用翻倍扩容,避免数据映射全部被打乱导致全量迁移的情况。
缺点是:扩容或收缩节点时,数据节点映射关系要重新计算,会导致大量数据的重新迁移或缓存雪崩。
二.一致性哈希分区(多用于缓存场景)
为系统中每个节点分配一个token,token数量在0~2^32之间,这些token构成一个哈希环。节点查找时,首先根据key计算哈希值,然后顺时针找到第一个大于该哈希值的token节点。
优点是:相比于节点取余,加入和删除节点只影响哈希环中相邻的节点,对其他节点无影响。
缺点是:加减节点会造成部分数据无法命中,需手动处理或忽略这部分数据,因此一致性哈希常用于缓存场景。而且不适合少量数据节点的分布式方案,因为节点变化将大范围影响哈希环中的数据映射。普通的一致性哈希分区在增减节点时需要增加一倍或减去一半节点才能保证数据负载均衡,容易数据倾斜。
三.虚拟槽分区(解决一致性哈希的缺点)
槽是集群内数据管理和迁移的基本单位,采用大范围槽的目的是方便数据拆分和集群拓展。
(4)Redis Cluster采用虚拟槽分区
Redis所有的键会根据哈希函数映射到0~16383整数槽内,计算公式是:slot = CRC16(key) & 16383。每个节点负责维护一部分槽及槽所映射的键值数据。
Redis Cluster虚拟槽分区的特点:
一.解耦数据和节点间的关系,简化节点扩容和缩容
二.节点自身维护槽的映射关系,不需要客户端或者代理服务维护槽分区的元数据
三.支持节点、槽、键之间的映射查询,用于数据路由、在线伸缩等场景
(5)Redis Cluster存在的功能限制
一.key批量操作支持有限,如mset、mget目前只支持具有相同slot值的key执行批量操作
二.key事务操作支持有限,只支持具有相同slot值的key的事务操作
三.不能将一个大的键值对象如hash、list等映射到不同的节点
四.不支持多数据库空间,单机Redis默认支持16个数据库,集群模式只能使用1个数据库
五.从节点只能复制主节点,不支持树状复制结构
20.Redis Cluster集群搭建的步骤
可以使用redis-trib.rb create快速搭建集群。
步骤一:准备节点
Redis集群节点数量至少为6个,才能保证组成完整的高可用集群。每个节点需要开启配置cluster-enabled yes,让Redis运行在集群模式下。
步骤二:节点握手
节点握手是指集群模式下的各节点,通过Gossip协议彼此通信来感知对方。节点握手是集群彼此通信的第一步,由客户端发起命令:cluster meet。
cluster meet命令是一个异步命令,执行之后立刻返回。只要一个节点向其他所有节点发起cluster meet命令即可,其他节点会相互自动发现并完成握手。
步骤三:分配槽
Redis集群把所有数据映射到16384个槽中,每个key会映射为一个固定的槽。通过命令cluster addslots可以为节点分配槽,通过命令cluster replicate可以让一个节点成为从节点。
节点握手完成后,集群处于下线状态。槽分配完成后,集群才进入在线状态。
21.Redis Cluster集群执行命令的实现原理
(1)集群的数据结构
(2)节点的3次握手和cluster meet命令的原理
(3)Redis Cluster的槽指派
(4)在Redis Cluster中执行命令的部分流程
(5)在Redis Cluster中执行命令的部分源码
(6)Redis Cluster返回客户端的MOVED错误
(7)Redis Cluster集群节点的数据库
(8)重新分片 + 迁移槽数据 + 集群伸缩
(9)Redis Cluster返回客户端的ASK错误
(10)在Redis Cluster中执行命令的完整流程
(1)集群的数据结构
每个节点都会保存着一个ClusterState结构,这个结构记录了当前节点视角下集群目前所处的状态。每个节点都会使用一个ClusterNode结构来记录自己的状态,并为集群中的所有节点都创建出一个相对应的ClusterNode结构。
一.ClusterState结构
ClusterNode *myself; //指向当前节点的指针
dict *nodes; //集群节点名单,键为节点名字,值为节点对应的
clusterNode结构ClusterNode *slots[16384]; //每个槽指派的节点信息,每个数组项是一个指向clusterNode结构的指针
zskiplist *slots_to_keys; //保存槽和键关系的跳跃表
ClusterNode *migrating_slots_to[16384]; //当前节点正在迁移至其他节点的槽
ClusterNode *importing_slots_to[16384]; //当前节点正在从其他节点导入的槽
二.ClusterNode结构
char name[]; //节点名字
int flags; //节点标记(主节点还是从节点,在线还是下线)
char ip[]; //节点的IP地址
int port; //节点的端口号
char slots[]; //二进制位数组,记录了节点负责处理哪些槽,若节点处理了槽i,则slots[]数组在索引i的值为1
int numslots; //节点处理的槽数
ClusterNode *slaveof; //如果当前节点是从节点,那么这是一个指向主节点的指针
ClusterNode **slaves; //如果当前节点是主节点,那么每个数组项都指向其从节点的clusterNode结构
int numslaves; //正在复制这个主节点的从节点数量
list *fail_reports; //一个链表,记录了所有其他节点对该节点的下线报告
ClusterLink *link; //保存连接节点所需的有关信息
(2)节点的3次握手和cluster meet命令的原理
客户端向节点A发送命令cluster meet,请求节点A与节点B进行握手,收到命令的节点A将会与节点B进行握手,以此来确认彼此的存在,为将来的进一步通信打好基础。
首先,节点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消息,此时节点A可知节点B已成功收到自己发送的meet消息。然后,节点A将向节点B返回一条ping消息。
节点B收到节点A返回的ping消息,此时节点B可知节点A已成功收到自己返回的pong消息,握手完成。
(3)Redis Cluster集群的槽指派
一.数据库分为16384个槽
Redis集群的整个数据库被分为16384个槽(slot)。当数据库中的16384个槽都有节点在处理时,集群处于上线状态,否则处于下线状态。
二.记录节点的槽指派信息
ClusterNode结构的slots属性和numslots属性,记录了节点负责处理哪些槽。slots属性是一个二进制位数组,拥有16384个元素。若节点处理了槽i,则slots数组在索引i的值为1。检查节点是否处理某个槽或者将某个槽指派给节点负责,复杂度都是O(1)。
三.传播节点的槽指派信息
一个节点除了会将自己负责的槽记录在ClusterNode结构的slots属性和numslots属性之外,还会将自己的slots数组通过消息发送给集群中的其他节点,以此来告知其他节点自己目前处理哪些槽。所以集群中的每个节点都会知道数据库中的16384个槽分别被指派给了哪些节点。
四.记录集群所有槽的指派信息
ClusterState结构中的slots数组记录了集群中所有16384个槽的指派信息。虽然ClusterNode.slots数组记录了单节点的槽指派信息,但ClusterState.slots数组记录集群所有槽指派信息依然很有必要。因为通过这两个slots数组,可以让查找槽i被指派到哪个节点以及查找节点的所有槽的时间复杂度变为O(1)。
(4)在Redis Cluster中执行命令的部分流程
当客户端向节点发送与数据库键有关的命令时,接收命令的节点会计算出键属于哪个槽。如果当前节点就是负责处理键所在槽的节点,那么节点就会执行命令。否则,节点会向客户端返回一个MOVED错误,客户端根据MOVED错误提供的信息转向正确节点。
请求路由一:节点对于不是它的键命令只回复MOVED错误进行重定向响应,并不负责转发。
请求路由二:节点对于是它的键命令找不到时会检查槽是否迁移,如果是槽正在迁移,则回复ASK错误进行重定向响应。
(5)在Redis Cluster中执行命令的部分源码
首先计算键属于哪个槽:i = CRC16(key) & 16384,然后判断槽i是否由当前节点负责处理。
若ClusterState.slots[i] = ClusterState.myself,说明槽i由当前节点负责,那么当前节点可以执行客户端的命令。
若ClusterState.slots[i] != ClusterState.myself,说明槽i并非由当前节点负责,那么节点会根据ClusterState.slots[i]指向的ClusterNode结构记录的IP和端口,向客户端返回MOVED错误,指引客户端转向正确处理槽i的节点。
(6)Redis Cluster返回客户端的MOVED错误
当节点发现键所在的槽并非由自己负责处理的时候,节点就会向客户端返回一个MOVED错误,指引客户端根据MOVED错误中提供的IP和端口,转向负责处理这个槽的节点重新发送命令。
(7)Redis Cluster集群节点的数据库
集群节点保存键值对以及键值对过期时间的处理方式,和单机完全相同。集群节点只能使用0号数据库,但会用ClusterState结构中的slots_to_keys跳跃表保存槽和键的关系。slots_to_keys跳跃表的每个节点的分值是槽号,成员是键,因此可以通过slots_to_keys快速获取某个槽的所有数据库键。
(8)重新分片 + 迁移槽数据 + 集群伸缩
可以将任意数量已经指派给某个节点(源节点)的槽改为指派给另一节点(目标节点)。重新分片操作可以在线进行,进行过程中集群不需要下线,且源节点和目标节点继续正常提供服务。
Redis Cluster集群的重新分配操作由集群管理软件redis-trib负责执行,步骤如下。
步骤一:对目标节点发送cluster setslotimporting命令,让目标节点准备导入槽的数据
步骤二:对源节点发送cluster setslotmigrating命令,让源节点准备迁出槽的数据
步骤三:向源节点发送cluster getkeyinslot命令,获取count个属于槽的键
步骤四:通过pipeline批量迁移获取的键到目标节点
步骤五:重复三四直到槽下所有键值迁移到目标节点
步骤六:向集群中任意一个节点发送cluster setslotnode命令,通知槽已分配给目标节点
注意:下线节点的情况
由于集群的节点不停地通过Gossip消息彼此交换节点状态,因此需要通过一种健壮的机制让集群内所有节点忘记下线的节点,让其他节点不再与下线节点进行Goosip消息交换。当节点收到cluster forget命令后,会把down_id指定的节点加入到禁用列表中,该列表中的节点不再发消息。
(9)Redis Cluster返回客户端的ASK错误
在重新分片期间,源节点向目标节点还没完成槽的迁移。这时当客户端向源节点发送关于键key的命令,且key恰好属于正在被迁移的槽时:
一.源节点会先在自己的数据库里查找key,如果找到就直接执行命令
二.如果找不到,则该key有可能已经被迁移到目标节点,源节点将向客户端返回一个ASK错误,指引客户端转向正在导入槽的目标节点,并重新发送命令
(10)在Redis Cluster中执行命令的完整流程
节点收到一个关于键key的命令请求:
一.如果键key所属的槽i正好是指派给自己的,则节点会尝试在自己的数据库里查找键key。
如果可以找到,那么节点就直接执行客户端发送的命令。如果实在找不到,那么节点将检查自己的ClusterState.migrating_slots_to[i],看看槽是否正在迁移。若正在迁移,则节点会向客户端返回一个ASK错误,引导客户端去目标节点找键key。
二.如果键key所属的槽i不是指派给自己的,则节点会向客户端返回一个MOVED错误。
当客户端接收到节点返回的MOVED错误时,客户端会根据错误提供的IP和端口,转向至负责槽的节点,并向该节点重新发送之前想要执行的命令。
当客户端接收到节点返回的ASK错误时,客户端会根据错误提供的IP和端口,转向至正在导入槽的目标节点,然后首先向目标节点发送一个ASKING命令,之后再重新发送要执行的命令。
ASKING命令唯一要做的就是打开发送该命令的客户端的REDIS_ASKING命令(一次性命令)。通常如果客户端向节点发送一个关于槽i的命令,而槽i又没有指派给这个节点的话,那么节点将向客户端返回一个MOVED错误。但是如果节点的ClusterState.importing_slots_from[i]显示节点正在导入槽i,并且发送命令的客户端带有REDIS_ASKING标识,那么节点将破例执行这个关于槽i的命令一次。
22.Redis Cluster集群节点通信的实现原理
(1)采用Gossip协议维护节点元数据
(2)Gossip协议下的Redis集群通信过程
(3)Gossip协议下的ping消息
(4)Gossip协议下的meet消息
(5)Gossip协议下的pong消息
(6)Gossip协议下的fail消息
(7)Gossip协议下的publish消息
(8)meet、ping、pong消息说明
(1)采用Gossip协议维护节点元数据
分布式存储中需要提供维护节点元数据信息的机制,其中元数据是指:节点负责的数据、是否出现故障。常见的元数据维护方式为:集中式和P2P方式。
Redis Cluster集群采用P2P的Gossip协议。Gossip协议的工作原理就是:节点彼此不断通信交换信息,一段时间后所有的节点都会知道集群完整信息。
(2)Gossip协议下的Redis集群通信过程
说明一:集群中的每个节点都会单独开辟一个TCP通道,用于节点间彼此通信,通信端口号在基础端口号上加10000
说明二:每个节点在固定周期内通过特定规则选择几个节点发送ping消息
说明三:接收到ping消息的节点用pong消息响应
集群中每个节点通过一定规则挑选要通信的节点,每个节点可能知道全部节点,也可能仅知道部分节点。只要这些节点可以正常通信,最终会达到一致的状态,实现集群状态的同步。
当节点出现故障、新节点加入、主从角色变化、槽信息变更时,通过不断的ping、pong消息通信,那么经过一段时间后,所有的节点都会知道整个集群全部节点的最新状态。
(3)Gossip协议下的ping消息
集群里每个节点默认每隔一秒就会从已知节点列表中随机选出5个节点,然后对这5个节点中最长时间没有发送过ping消息的节点发送ping消息,以此来检测被选中的节点是否在线。
此外,如果节点A最后一次收到节点B发送的pong消息时间,距离当前时间已超过了cluster_node_timeout选项设置时长的一半,那么节点A也会向节点B发送ping消息,防止节点A长时间没选中节点B。
ping消息不仅会封装自身节点的状态数据,还会封装其他节点的状态数据。
(4)Gossip协议下的meet消息
节点A向节点B发送meet消息,请求节点B加入到节点A当前所处的集群里。通过meet消息完成节点握手后,节点B会加入集群并进行周期性的ping、pong消息交换。
(5)Gossip协议下的pong消息
当节点B接收到节点A的ping、meet消息时,会给节点A发送pong消息确认消息通信正常。
一个节点也可以通过向集群广播自己的pong消息,以此来让集群中的其他节点立即刷新关于这个节点的认知。比如一次故障转移成功后,新的主节点会向集群广播一条pong消息,从而让集群中其他节点立即知道新节点接管槽。
(6)Gossip协议下的fail消息
当节点判定集群内另外一个节点下线时,会向集群广播一个fail消息。其他节点接收到fail消息后,会把对应节点更新为下线状态。
当集群里的主节点A将主节点B标记为已下线时,主节点A将向集群广播一条关于主节点B的fail消息。所有接收到这条fail消息的节点都会将节点B标为已下线,因为单纯使用Gossip协议来传播节点的已下线信息会给节点信息的更新带来延迟。
(7)Gossip协议下的publish消息
当节点收到客户端一个publish命令时,节点会执行这个命令,并向集群广播一条publish消息,然后所有接收到这个publish消息的节点都会执行相同的publish命令。
当客户端向集群中的某个节点发送publish命令publish的时候,接收到publish命令的节点不仅会向自己的channel频道发送消息message,还会向集群广播一条publish消息。所有接收到这条public消息的节点都会向自己的channel频道发送message消息,这样订阅了集群的channel频道的客户端才能确保收到消息。
(8)meet、ping、pong消息说明
每次发送meet、ping、pong消息时,发送者都从自己的已知节点列表中随机选出两个节点,并将这两个被选中的节点的信息保存在消息正文里。
假设在一个包含A、B、C、D、E、F六个节点的集群里,节点A向节点B发送ping消息,并且消息里包含节点C和节点D的信息。当节点B收到这条ping消息时,它将更新对节点C和节点D的认知(创建或更新ClusterNode结构)。之后节点B将向节点A返回一条pong消息,并且消息里包含节点E和节点F的消息。当节点A收到这条pong消息时,它将更新对节点E和节点F的认知(创建或更新ClusterNode结构)。
23.Redis Cluster集群复制与故障转移的实现原理
(1)节点的复制方法
(2)故障发现流程
(3)主观下线
(4)客观下线
(5)尝试客观下线
(6)故障恢复的流程
(7)故障转移的时间
(1)节点的复制方法
步骤一:向节点发送cluster replicate命令,可以让接收该命令的节点成为node_id所指定节点的从节点,并开始对主节点进行复制。
步骤二:接收到该命令的节点会在自己的ClusterState.nodes字典中找到node_id所对应节点的ClusterNode结构,并将自己的ClusterState.myself.slaveof指针指向这个结构,以此来记录这个节点正在复制的主节点。
步骤三:然后,这个节点会修改自己在ClusterState.myself.flags中的属性,关闭原本的REDIS_NODE_MASTER标识,打开REDIS_NODE_SLAVE标识,表示该节点已经由原来的主节点变成了从节点。
步骤四:最后,这个节点会调用复制代码,并根据ClusterState.myself.slaveof指针指向的ClusterNode结构所保存的IP地址和端口号,对主节点进行复制。
一个节点成为从节点并开始复制某个主节点的信息,会通过消息发送给集群中的其他节点。最终集群中的所有节点都会知道某个从节点正在复制某个主节点。
集群中的所有节点都会在代表主节点的ClusterNode结构的slaves属性和numslaves属性中,记录正在复制这个主节点的从节点名单。
(2)故障发现流程
Redis集群内节点通过ping、pong消息实现节点通信,消息可以传播节点槽信息、主从状态、节点故障等。因此故障发现也是通过消息传播机制实现的,主要环节包括主观下线和客观下线。
主观下线是指:某个节点认为另一个节点不可用,即下线状态,这个状态只能代表一个节点的意见,可能存在误判。
客观下线是指:标记一个节点真正的下线,集群内多个节点都认为该节点不可用,从而达成共识的结果。
如果是持有槽的主节点故障,需要为该节点进行故障转移。Redis集群自动故障转移过程分为故障发现和故障恢复。
Redis集群对故障节点的发现流程是:
主观下线 -> Gossip消息传播 -> 保存到下线报告链表 -> 尝试客观下线 -> 广播fail消息 -> 故障转移
(3)主观下线
集群内每个节点都会定期向其他节点发送ping消息,接收到ping消息的节点会回复pong消息作为响应。如果在cluster_node_timeout时间内某节点无法与另一个节点顺利完成ping消息通信,则将该节点标记为主观下线。
其中,主观下线的识别流程如下:
首先,节点A发送ping消息给节点B。
如果节点A与节点B通信正常,节点B将接收到ping消息并返回pong消息给节点A,节点A会更新最近一次与节点B的通信时间。
如果节点A与节点B通信出现问题,则断开连接,下次会进行重试。
如果节点A与节点B一直通信失败,则节点A记录的与节点B的最后通信时间将无法更新。
当节点A内的定时任务检测到与节点B最后的通信时间超过cluster_node_timeout时,将更新本地节点B的状态为主观下线(pfail)。
(4)客观下线
当某个节点判断另一个节点主观下线后,相应的节点状态会跟随消息在集群内传播。当接收节点发现消息中含有主观下线的节点状态时,会在本地找到故障节点的ClusterNode结构并保存到下线报告链表中。
通过Gossip消息传播,集群内的节点会不断收集到故障节点的下线报告。当半数以上持有槽的主节点都标记某个节点是主观下线时,将标记节点客观下线(fail)和广播fail消息。
为什么必须是负责槽的主节点参与故障发现决策?
因为集群模式下只有处理槽的主节点才负责读写请求和集群槽等关键信息维护,从节点只进行复制。
为什么要半数以上持有槽的主节点标记主观下线?
必须半数以上是为了应对网络分区等原因造成的集群分隔情况。
下线报告的有效期限是:cluster_node_timeout * 2,主要针对故障误报的情况。因此不建议将cluster_node_timeout设置得过小,以防下线报告过期而无法收集到一半以上的下线报告。
(5)尝试客观下线
集群中的节点每次接到其他节点的主观下线信息时,都会尝试客观下线,流程如下:
首先统计有效的下线报告数量,如果小于集群内持有槽的主节点数量的一半,则退出。当下线报告数量大于持有槽的主节点数量的一半时,标记对应的故障节点为客观下线。然后向集群广播一条fail消息,通知所有节点将故障节点标记为客观下线(fail)。
当网络出现分区时,可能集群会被分割为一大一小两个独立集群。大的集群内持有槽的主节点超半数,可以完成客观下线并广播fail消息,但小集群无法收到fail消息。所以部署节点时要避免故障节点所有的从节点都在小集群内而无法完成后续的故障转移,降低主从被分区的可能。
(6)故障恢复的流程
Redis Cluster集群的自动故障转移过程整体上分为故障发现和故障恢复两部分。
当一个从节点发现自己正在复制的主节点进入了客观下线状态时,该从节点将开始对下线主节点进行故障转移恢复。
故障恢复的流程是:资格检查 -> 准备选举时间 -> 发起选举 -> 选举投票 -> 替换主节点。
一.资格检查
如果从节点与主节点断线时间超过:cluster_node_time*cluster_slave_validity_factor,则当前从节点不具备故障转移资格。其中cluster_slave_validity_factor是从节点有效因子,默认为10。
二.准备选举时间
从节点具备故障转移资格后,更新发起选举的时间,只有到达该时间后才能发起选举。选举时间采用延迟触发机制,复制偏移量最大的延迟是1秒,复制偏移量越小延迟越长。
三.发起选举
当从节点定时任务检测发起选举的时间到达后,便执行发起选举。首先会更新配置纪元,从节点每次发起选举时都会自增集群的配置纪元。然后会广播选举消息,要求所有收到这条消息、并且具有投票权的主节点向这个从节点投票。
注意:节点消息传播时,为了防止过期消息,会以配置纪元大的一方为准。
四.选举投票
只有持有槽的主节点才会向从节点投票,每个持有槽的主节点在一个配置纪元内只能投一次票。Redis集群没有使用从节点来投票,是因为当从节点只有1个时无法凑够半数以上的票。
五.替换主节点
当从节点收到半数以上持有槽的主节点的投票后,这个从节点便被选为新的主节点。
首先,当前从节点会执行slaveof no one命令成为主节点。然后,新的主节点会执行clusterDelSlot撤销故障主节点负责的槽,执行clusterAddSlot将这些槽委派给自己。接着,新的主节点会向集群广播pong消息,让集群中所有节点立即知道自己已经由从节点变成了主节点,并接管了槽。
(7)故障转移的时间
一.主观下线识别时间 = cluster_node_timeout
二.主观下线状态消息传播时间 <= cluster_node_timeout / 2
三.从节点转移时间 <= 1000毫秒
由于存在延迟发起选举的机制,偏移量最大的从节点会最多延迟1秒发起选举。通常第一次选举就会成功,所以从节点执行转移时间在1秒以内。
故障转移时间 <= cluster_node_timeout + cluster_node_timeout / 2 + 1000
其中cluster_node_timeout的值默认是15s,但又不是越小越好,越小越增加Gossip消息的频率,增加带宽消耗。
24.通过Smart客户端支持Redis Cluster集群
(1)MOVED错误重定向的优缺点
(2)Smart客户端的简介
(3)Smart客户端操作集群的流程
(4)Smart客户端可能存在的问题
(5)Smart客户端针对批量操作、Lua和事务的处理
(1)MOVED错误重定向的优缺点
优点:客户端能随机连接集群内任意一个Redis获取键所在节点,代码简单,对客户端协议影响小。
缺点:每次执行键命令前都要到Redis上进行重定向才能找到要执行命令的节点,额外增加了IO开销。
因此,集群客户端通常采用Smart客户端。
(2)Smart客户端的简介
大多数语言的Redis客户端都采用了Smart客户端来支持集群协议。Smart客户端通过在内部维护槽和节点的映射关系,本地就可以实现键到节点的查找,从而保证IO最大化。而MOVED错误重定向会协助Smart客户端更新槽和节点的映射的缓存。
(3)Smart客户端操作集群的流程
首先,Smart客户端在初始化时会随机选择一个运行节点,使用cluster slots命令来初始化槽和节点的映射关系。
然后,Smart客户端会解析cluster slots命令的返回结果并缓存slots在本地,然后为每一个节点创建唯一的连接(池)。
之后,Smart客户端执行键命令时,首先会计算键的slot并根据本地slots缓存获取目标节点连接,然后发送命令。
如果出现连接错误,则随机找出活跃节点发送命令,且最多重试5次,否则执行异常。
如果收到MOVED错误,则使用cluster slots命令更新本地slots缓存,然后向正确的节点发送命令。
(4)Smart客户端可能存在的问题
问题一:客户端内部维护本地slots缓存表,并且针对每个节点维护连接池,当集群规模非常大时,客户端要维护非常多的连接并消耗更多的内存。
问题二:可能在并发量高的场景下出现cluster slots风暴和写锁阻塞问题。客户端是要获得写锁才能执行cluster slots命令的,而计算槽对应的节点需要使用读锁去访问本地slots缓存。读写锁是读锁共享、读锁和写锁互斥,并发量大且故障正在转移(宕机的节点还没恢复)时会导致请求阻塞,影响集群吞吐。
改进措施:
一.降低cluster slots命令的调用次数
二.降低写锁持有时间,如对执行cluster slots命令不加锁,只对修改本地slots缓存时才加锁,而且修改前对比新获取的slots映射是否变化才加写锁进行更新
(5)Smart客户端针对批量操作、Lua和事务的处理
一.批量操作
可以利用Smart客户端保存的槽和节点的映射关系,对要批量处理的key进行归档,然后对每个节点对应的子key列表执行mget或者pipeline操作。
二.Lua和事务
Lua和事务所操作的key必须在一个节点上,Smart客户端提供了hashtag。首先将事务中所有的key添加hashtag,然后使用CRC16计算hashtag对应的slot,获取指定slot对应的节点连接池并执行事务。
使用Smart客户端操作集群可以实现通信效率最大化,客户端内部负责计算和维护键->槽->节点的映射,用于快速定位键命令到目标节点。
集群获取Smart客户端全面高效的支持需要一个过程,用户在选择Smart客户端时建议review集群交互代码,比如异常判定、重试逻辑、更新槽的并发控制等。
对于ASK错误重定向,客户端不会更新本地槽节点的缓存。对于MOVED错误重定向,客户端需要更新本地槽节点的缓存。
25.Redis Cluster集群的补充说明
(1)集群的完整性
(2)集群的带宽消耗
(3)集群的发布订阅功能
(4)集群的读写分离
(1)集群的完整性
默认情况下,当集群16384个槽任何一个没有指派到节点时,那么整个集群都不可用。当持有槽的主节点下线时,从故障发现到自动完成转移期间,整个集群也是不可用的。
建议将参数cluster_require_full_coverage配置为no,这样当主节点故障时只影响它负责槽的相关命令,不影响其他主节点的可用性。
(2)集群的带宽消耗
集群内Gossip消息通信会消耗带宽,官方建议集群最大规模在1000以内,因此:
一.尽量避免大集群,同一系统可针对不同业务场景拆分使用多套集群
二.适度提高cluster_node_timeout降低消息发送频率,但cluster_node_timeout会影响故障转移速度
三.避免集中部署,如60个节点的集群,3台机器每台部署20个节点,带宽消耗将异常严重
(3)集群的发布订阅功能
集群模式下单节点收到的客户端publish命令都会向所有的节点进行广播,加重带宽负担。所以需要频繁使用发布订阅功能时,应避免在大量节点的集群使用,否则严重消耗网络带宽。建议使用Sentinel结构专门用于发布订阅功能。
(4)集群的读写分离
集群模式下从节点不接收任何读写请求,发给从节点的键命令会MOVED重定向到负责槽的主节点上。但当从节点需要分担主节点的读压力时,可以使用readonly命令打开只读状态。
集群模式下的读写分离,同样和哨兵主从复制的读写分离一样遇到:数据延迟、读到过期数据、从节点故障等问题。
集群模式下的读写分离成本比较高,可以直接扩展主节点数量来扩展集群性能,一般不建议集群模式下做读写分离。
26.Redis Cluster集群的倾斜问题
(1)数据倾斜
(2)请求倾斜
集群倾斜指不同节点之间数据量和请求量出现明显差异,加大负载均衡和开发运维难度。
(1)数据倾斜
一.节点和槽分配严重不均
使用redis-trib.rb info可以列举出每个节点负责的槽和键总量以及每个槽平均键数量。当节点和槽分配不均时,可用redis-trib.rb rebalance进行平衡。
二.不同槽对应键数量差异过大
键通过CRC16哈希函数映射到槽上,正常情况下槽内键数量会相对均匀。但当大量使用hash_tag时,会产生不同的键映射到同一个槽的情况。
cluster countkeysinslot <slot> 可以获取槽对应的键数量
cluster getkeysinslot <slot> <count> 循环迭代出槽所有的键,从而发现使用hash_tag的键
三.集合对象包含大量元素
通过命令redis-cli–bigkeys找出大集合对象,然后根据业务场景进行拆分。
四.节点内存倾斜
当集群大量使用hash、set等数据结构时,若hash-max-ziplist-value等压缩列表使用条件在各节点不一致时,那么就可能出现节点内存倾斜。
(2)请求倾斜
若热点键对应的是小对象的get、set等,那么即使请全量各节点差异较大一般也不会产生负载严重不均。
若热点键对应的是大对象的hgetall、smembers等复杂度高的命令,则可能会导致对应节点负载过高。
解决方法如下:
一.合理设计键,热点大集合对象做拆分或使用hmget替代hgetall避免整体读取
二.不要对热点键使用hash_tag,避免热点键都在同一槽同一节点
三.若一致性要求不高,可以使用本地缓存减少热点键的调用
27.Redis Cluster集群的核心问题
(1)请求路由
(2)数据迁移
Redis Cluster方案就是为了解决单个节点数据量大、写入量大时产生的性能瓶颈问题。多个节点组成一个集群,可以提高集群的性能和可靠性,但随之而来的是集群的管理问题。最核心的问题就两个:请求路由和数据迁移,当然其他问题也包括:扩容缩容和数据平衡。
(1)请求路由
Redis Cluster在每个节点记录完整的槽和节点的映射关系,以便于纠正客户端的错误路由请求。同时也发给客户端让客户端也缓存一份,便于客户端直接找到指定节点。客户端与服务端配合完成数据的路由,这需要业务在使用Redis Cluster时,必须升级为集群版的SDK才支持这种配合。
其他Redis集群方案如Twemproxy、Codis都是增加代理层,客户端通过代理层对整个集群进行操作。代理层后面可以挂很多实例,由代理层维护请求路由。客户端不需要更换SDK,但代理层会带来性能损耗。
(2)数据迁移
判断一个集群方案是否成熟的标准是:数据迁移过程中是否影响业务。
Twemproxy不支持在线扩容,它只解决了请求路由的问题,它扩容时需要停机,对业务影响大。Redis Cluster和Codis都支持在线扩容,但数据迁移过程中会存在key的访问重定向,这时访问延迟会变大。数据迁移完成后,Redis Cluster需要客户端更新路由缓存,而Codis会在代理层更新路由表让客户端整个过程无感知。
Redis Cluster的数据迁移是同步的,迁移一个key会同时阻塞源节点和目标节点,迁移过程有性能问题。Codis的数据迁移是异步的,迁移速度更快,对性能影响最小,实现也比较复杂。