(五)REDIS-哨兵与集群

(一)概念介绍:

Sentinel(哨兵)是Redis的高可用性解决方案,主要是通过一个或多个Sentinel实例组成的Sentinel系统对任意多个主服务器以及这些主服务器的所有从服务器进行监视,当某个主服务器下线后,Sentinel系统自动将该主服务器下的某个从服务器升级为新的主服务器,然后由新的主服务器继续处理来自客户端的命令请求。Sentinel系统工作状况如下图所示:

 当server1的下线时长超过用户设定的下线时长上限时,Sentinel系统就会对server1执行故障转义操作:

1 首先,Sentinel系统会挑选server1属下的一个从服务器,并将它升级为主服务器。

2 随后,Sentinel系统会向server1的其它所有从服务器发送新的复制命令,让它们成为新的主服务器的从服务器,当所有从服务器开始复制新的主服务器时,故障转义操作执行完毕。

3 另外,Sentinel系统还会继续监视已经下线的server1,当它重新上线时,将他设置为新的主服务器的从服务器。

 

 Sentinel系统进行故障转移的过程比较复杂,我们接下来一一介绍:

(一) 启动并初始化Sentinel

  • 1.初始化服务器:

Sentinel本质上是一个运行在特殊模式下的redis服务器,它的特殊之处在于与普通的Redis服务器执行的工作是不同的,如下表所示列出了两者的功能点不同:

功能 使用情况
数据库和键值对方面额命令,比如Set、DEL、FLUSHDB 不适用
事务命令,比如MULTI、WATCH 不适用
脚本命令,比如EVAL 不适用
RDB持久化命令,比如SAVE、BGSAVE 不适用
AOF持久化命令,比如BGREWRITEOF 不适用
复制命令,比如SLAVEOF Sentinel内部可以使用,但是客户端不可以使用
发布与订阅命令,比如PUBLISH、SUBSCRIBE

SUBSCRIBE、PSUBSCRIBE、UNSUBSCRIBE、PUNSUBSCRIBE四个命令

在Sentinel内部和客户端可以使用,但PUBLISH命令不可以在内部使用

文件事件处理器(负责发送命令请求、处理命令回复) 在Sentinel内部使用,但关联的文件事件处理器和普通Redis服务器不同
时间事件处理器(负责执行serverCron函数)

Sentinel内部使用,时间事件的处理器仍然是serverCron函数,serverCron函数

会调用sentinel.c/sentinelTimer函数,后者包含了sentinel要执行的所有操作

从上面表格可以看出Sentinel是没有使用到数据库相关的功能的,因此在初始化的时候,不用载入RDB文件和AOF文件。

  • 2.使用Sentinel专用代码:

普通服务器使用redis.h/REDIS_SERVERPORT常量的值作为服务器端口,而普通Redis服务器使用sentinel.c/REDIS_SENTINEL_PORT常量的值作为服务器端口。

此外普通服务器使用redis.c/redisCommandTable作为服务器的命令表,而Sentinel使用sentinel.c/sentinelcmds作为服务器的命令列表。

  • 3 初始化sentinel状态

服务器初始化sentinel状态要做的工作就是初始化一个sentinel.c/sentinelState结构(简称“sentinel状态”),该结构保存了所有与sentinel功能有关的状态。sentinelState结构如下图所示:

 

  •  4 初始化Sentinel状态的master属性

Sentinel状态的masters字典记录了所有被监视的主服务器相关的信息,其中:字典的键是被监视主服务器的名字,字典键对应的值是被监视主服务器对应的sentinel.c/sentinelRedisInstance的结构,sentinelRedisInstance可以对应主服务器,从服务器,或者另外一个sentinel。sentinelRedisInstance的结构如下图所示:

 

 Sentinel状态的初始化将引发对masters字典的初始化,而masters字典的初始化是根据被载入的Sentinel配置文件进行的。

  • 5 创建连向主服务器的网络连接:

初始化sentinel的最后一步是创建连向被监视主服务器的网络连接,用以发送命令与从命令回复中获取信息。sentinel会创建两个连向主服务器的连接:

一个是命令链接,该连接专门用于向主服务器发送命令,并接受命令回复。

另一个是订阅连接,该连接专门用于订阅主服务器的_sentinel_:hello频道。

(二) 获取主服务器信息

Sentinel默认以每十秒一次的频率,通过命令连接向主服务器发送INFO命令,并通过分析命令回复获取主服务器器的信息。获取的信息主要有如下两个方面:

1 关于服务器本身的信息,比如run_id域记录的服务器运行id,以及role域记录的服务器角色。

另一方面,获取关于主服务器下所有的从服务器信息,每个从服务器都有一个slave字符串开头的行记录,每行的ip=域记录了从服务器的的IP地址,port==域记录了从服务器的端口号。根据IP地址和port端口,sentinel无须用户提供从服务器的地址信息,就可以自动发现从服务器的。

sentinel根据主服务器的run_id和role域的信息,将对主服务器的实力结构进行更新,如果run_id不同,比如主服务器重启,则会对实例结构更新。而主服务器返回的从服务器信息,如果已经存在,则进行更新,会在slaves字典中为这个主服务器创建一个新的实例结构。

(三)获取从服务器信息

当sentinel发现主服务器有新的从服务器时,除了在为这个主服务器创建一个新的从服务器实例结构之外。还会创建连接到从服务器的命令连接以及订阅连接。之后会默认每十秒通过命令连接向从服务器发送Info命令,并通过命令回复提取一下信息,并根据这些信息对从服务器的实力结构进行更新。

1 从服务器的运行ID run_id 

2 从服务器的角色role

3 主服务器的IP地址master_host,以及master_port

4 主从服务器的连接状态master_link_status

5 从服务器的优先级slave_priority

6 从服务器的复制偏移量slave_repl_offset

(四)向主服务器和从服务器发送信息

默认情况下,sentinel会每两秒,通过命令连接向所有被监视的主服务器和从服务器发送一下格式的命令:

PUBLISH __sentinel__:hello "<s_ip>,<s_port>,<s_runid>,<s_epoch>,<m_name>,<m_ip>,<m_port>,<m_epoch>"

 

(二)集群的工作原理:

Redis哨兵:
哨兵其实一个运行在特殊模式下的redis进程,它有三个作用:监控、自主选举与切换、通知
哨兵进程运行期间,监视所有Redis主节点与从节点,通过周期性地给主从发送PING命令,检测主从库是否挂了。如果从库没有在规定时间内相应,哨兵就会把这个从库标记为下线状态。如果主库没有在规定时间内响应哨兵的PING命令,则会被标记为下线状态,然后开始切换到选举任务。所谓选主,就是从多个从库中,按照一定规则,选出一个当主库。至于通知,则是在选出主库之后,哨兵把新主库的连接信息发给其它从库,让他们与新主库建立主从关系。同时哨兵会把主库的连接信息通知给客户端,让他们把请求发到新主库上。

哨兵模式:
因为哨兵也是一个Redis进程,如果自己挂了,那就起不到监控的作用了,在redis中,通过一个或者多个哨兵实例组成哨兵系统。通过多个哨兵来监控redis节点,并且各个哨兵之间还会进行监控。
其实哨兵之间是通过发布订阅机制组成集群,同时又通过INFO命令,获得从库连接信息,也能和从库建立连接,进行监控。

哨兵如何判定主库下线:
主观下线与客观下线:哨兵进程向主库进程与从库进程发送PING命令,如果主库或者从库没有在规定的时间内响应PING命令,哨兵就把它标记为主观下线。
针对于主库被标记为主观下线,则正在监视这个主库的所有哨兵就以每秒一次的频率进行PING,以确认主库是否真的进入了主观下线。当有多数哨兵在指定时间范围内确认主库的确进入了主观下线状态,则
主库会被标记为客观下线,这样做的目的是避免对主库的误判,以减少没必要的主从切换以及开销。

哨兵如何选举:
1如果明确主库已经下线了,哨兵就开始了选举模式:
哨兵选举过程包括:过滤与打分。先在多个从库中,先按照一定的筛选条件,把不符合条件的从库过滤掉。然后按照一定的规则,给剩下的从库逐个打分,将得分最高的从库选举为新主库。
如果已经下线的从库,直接过滤掉;如果从库网络不好,老是超时,也会被过滤掉。参数down-after-milliseconds,它表示我们认定主从库断连的最大连接超时时间。
过滤了不适合做主库的从库之后,就给剩下的从库打分,按照如下三个规则打分:从库优先级,从库复制进度,从库ID号。
从库优先级最高的话,打分最高,优先级可以通过slave-priority配置。如果优先级一样,就选与旧主库复制进入最快的从库。如果优先级与复制进度都相同,从库ID号小的打分最高。

由哪个哨兵执行主从切换?
在标记主库为客观下线后,这个哨兵紧接着向其它哨兵发送命令,再发起投票,希望由它可以来执行主从的切换,这个投票过程称为leader选举。因为最终执行主从切换的哨兵称为leader.
一个哨想成为leader需要满足两个条件:
1 需要拿到num(sentinels)/2 + 1的赞成票
2 并且拿到的票数需要大于等于哨兵配置文件的quorum值。
假如网络故障原因,投票不满足上面的条件,这轮投票就不会产生leader. 哨兵集群会等待一段时间(一般是哨兵故障转移超时时间的2倍),再进行重新选举。

故障转移:
假设哨兵模式架构下,有如下三个哨兵(哨兵1,哨兵2, 哨兵3),一个主库M,两个从库S1和S2, 当哨兵检测到Redis主库M1出现故障,那么哨兵需要对集群进行故障转移,假设选出了哨兵3座位leader,
故障转移流程如下:
1 解除从库S1的从节点身份,升级为新主库
2 从库S2称为新主库的从库
3 原主库节点回复后也变为新主库的从节点。
4 通知客户端应用程序新主节点的地址。

 

Redis cluster集群:
哨兵模式基于主从模式,实现读写分离,还可以自由切换,系统可用性更高,但是每个节点存储的数据是一样的,浪费内存,并且不好在线扩容。因此Redis Cluster集群(切片集群)应运而生。再Redis3.0加入了切片集群,实现了Redis的分布式存储。对数据进行切片,每台Redis节点存储不同的内容,来解决在线扩容的问题。并且,它可以保存大量数据,即分散数据到各个Redis实例,还提供复制与故障转移的功能。

哈希槽:
redis cluster方案采用哈希槽(Hash slot)来处理数据与实例之间的映射关系,一个切片集群被分为16384个slot槽,每个进入redis的键值对,根据key进行散列,分配到哲16384插槽中的一个。使用的hash映射也比较简单,用crc16算法计算粗一个16Bit的值,在对16384取模。数据库中每个键都属于16384个槽中的一个,集群的每个节点都可以处理这16384个槽。集群中的每个节点负责一部分哈希槽。
假设有三个节点A B C,每个节点负责的槽数=16384/3, 那么可能存在这样一种分配:
节点A负责0-5460号哈希槽
节点B负责5461-10922号哈希槽
节点C负责10923-16383号哈希槽
加入客户端给一个redis实例发送数据读写操作时,这个实例上并没有数据,就会进行moved重定向与ASK重定向。

MOVED重定向与ASK重定向
在Redis cluster模式下,节点请求的处理过程如下:
1 通过哈希槽映射,检查当前redis key 是否存在当前节点
2 若哈希槽不是由自身节点负责,就返回MOVED重定向
3 若哈希槽确实由自身负责,且key在slot中,则返回该key对应结果。
4 若Redis key不存在此哈希槽中,检查该哈希槽是否正在迁出
5 若redis key正在迁出,返回ASK错误重定向客户端到迁移的目标服务器上。
6 如哈希槽未迁出,检查哈希槽是否导入中。
7 若哈希槽导入中且有ASKING标记,则直接操作,否则返回MOVED重定向。
MOVED重定向:
客户端给一个redis实例发送数据读写操作时,如果计算出来的槽不存在该节点上,它会返回MOVED重定向错误,MOVED重定向错误中,会将哈希槽所在的新实例的IP和port端口带回去。
ASK重定向:
ASK重定向一般发生于集群伸缩的时候,集群伸缩会导致槽迁移,当我们去源节点访问时,此时数据可能已经迁移到了目标节点,使用ASK重定向可以解决此种情况。

Cluster集群节点的通讯协议:Gossip
Gossip协议基本思想:一个节点想要分享一些信息给网络中的其它一些节点,它会周期性随机选择一些界定,并把消息传递给这些节点。这些收到信息的节点接下爱会做同样事情。
Redis Cluster集群通过Gossip协议进行通信,节点之前不断交换信息,交换的信息包括节点出现故障,新节点接入,主从节点变更信息,slot信息等。消息类型包括:ping,pong, meet ,fail等。
meet信息:通知新节点加入。消息发送者通知消息接收者加入集群。meet消息通信正常后,接受节点加入到集群之后会周期性地ping pong信息交换。
ping消息:节点每秒会向其它节点发送ping消息,消息中带有自己一直的两个节点的地址、槽、状态信息、最后一次通信时间等。
pong消息:当接收到ping,meet消息时,作为响应消息,回复给发送方确认消息正常通信,消息中同样带有自己已知的两个节点信息。
fail消息:当节点判定集群中另一个节点下线时,会向集群广播一个fail消息,其它节点接收到fail消息之后会把对应节点更新为下线状态。
特别,每个节点通过集群总线(cluster bus)与其它节点通信时,使用特殊的端口号,即对外服务端口号加10000.nodes之间的通信协议是二进制协议。

故障转移:
redis集群实现了高可用,当集群内节点出现故障时,通过故障转移,以保证集群正常对外提供服务。redis集群通过ping pong消息,实现故障发现,这个环境包括主观下线和客观下线。
主观下线:某个节点认为另一个节点不可用,即下线状态,这个状态并不是最终的故障判定,只能代表一个节点的意见,可能存在误判的情况。
客观下线:指标记一个节点真正的下线,集群内多个节点都认为该节点不可用,从而达成共识结果。如果持有槽的主节点故障,需要为该节点进行故障转移。
故障恢复:故障发现后,如果下线节点是主节点,则需要在它的从节点中选一个替换它,以保证集群的高可用。流程分别为:
1 资格检查:检查从节点是否具备替换故障主节点的条件。
2 准备选举时间:资格检查过后,更新触发故障选举时间。
3 发起投票:到了故障选举时间,进行选举。
4 选举投票:只有持有槽的主节点才有票,从节点收集到足够的票(大于一半),触发替换主节点操作。

 

集群脑裂
如果主节点的网络失联,与所有的从节点都失联了,但是客户端并不知情,数据还是在往这个结点传,这些数据都写在了主节点的缓冲区中,此时,哨兵发现了主节点失联了,于是选出了另外一个主节点,此时就出现了所谓的:集群脑裂——此时集群有两个主节点,然后原主节点的网络好了,但是由于哨兵机制,现在原主节点降级为从节点,会与现主节点做一次全量同步,原主节点也就是现在的从节点数据全部清空,这样在主节点网络失联期间的数据就丢失了。【哨兵在选出新的主之前,且原来的主网络恢复之后,数据还是往原来的主写数据,减少丢失数据的关键就是哨兵在选出新的主之前,设置一个值,如果原来的主网络恢复需要的时间大于该值,就不写数据。换句话说就是设置一个值,如果主节点没有响应,就被认定为下线了,后面就不继续往这个节点写数据了

那我们如何去减少集群脑裂丢失的数据呢?
应对脑裂的解决办法应该是去限制原主库接收请求,Redis提供了两个配置项。
min-slaves-to-write:与主节点通信的从节点数量必须大于等于该值主节点,否则主节点拒绝写入。
min-slaves-max-lag:主节点与从节点通信的ACK消息延迟必须小于该值,否则主节点拒绝写入。
这两个配置项必须同时满足,不然主节点拒绝写入。
在假故障期间满足min-slaves-to-write和min-slaves-max-lag的要求,那么主节点就会被禁止写入,脑裂造成的数据丢失情况自然也就解决了。
可以避免的场景:
为了防止脑裂我们将min-slaves-to-write设置为1,min-slaves-max-lag设置为12s,down-after-milliseconds哨兵判断主节点客观下线的限制为10s,
主节点因为某些原因卡住了15s,导致哨兵集群判断主节点为主观下线,主从切换,因为从节点与主节点之间的通信延迟大于min-slaves-max-lag设置的12s,所以就不在往原来的住写数据,这样就规避脑裂的情况。

不可避免的场景:

我们同样将min-slaves-to-write设置为1,min-slaves-max-lag设置为15s,down-after-milliseconds哨兵判断主节点客观下线的限制为10s,
哨兵主从切换需要 5s。主节点因为某些原因卡住了 12s,这时还会发生脑裂吗?
主节点卡住12s这时哨兵集群判断主节点下线,同时哨兵集群做主从切换需要5s,这就意味着主从切换过程中,主节点恢复运行,
而min-slaves-max-lag设置为15s那么主节点还是可写,也就是说在12s~15s这期间如果有客户端写入原主节点,那么这段时间的数据会丢失。

 

Mysql与redis如何保持双写一致性?
1 缓存延时双删:当有写请求时,先删除缓存中的数据,然后更新数据库,待数据库更新完成后,休眠1秒,再次删除缓存。可以将1秒内所造成的缓存脏数据,再次删除
2 删除缓存重试:如果第二次删除缓存失败,那么可能导致数据不一致,因此可以在失败之后把删除失败的key放到消息队列,后面重试删除缓存。
3 读取biglog异步删除缓存:(1)更新数据库数据 (2)数据库会将操作信息写入binlog日志当中 (3)订阅程序提取出所需要的数据以及key (4)另起一段非业务代码,获得该信息
(5)尝试删除缓存操作,发现删除失败 (6)将这些信息发送至消息队列 (7)重新从消息队列中获得该数据,重试操作。
订阅binlog程序在mysql中有现成的中间件叫canal,可以完成订阅binlog日志的功能。

 

主从数据不一致:
主从结点之间的写命令发送是异步的,也就是说:
客户端发送写命令
主服务器执行写命令,并异步把写命令发送给从服务器
主服务器执行完写命令就会把结果返回给客户端
并不会等待从服务器执行完命令再返回结果给客户端
在从服务器还没有执行主服务器发送过来的命令时,去读取就会有主从结点的数据不一致。
也就是说无法实现主从数据时刻保持一致,也就是强一致性,除非进行同步,但是同步会很影响效率。
那我们如何去处理这种主从数据不一致的情况呢?
1 从硬件的角度:保证主从结点间通信状况,保证网络连接状况良好
2 从软件的角度:我们可以自己弄一个检测程序,来检测主从结点的数据差, 通过Redis的INFO replication拿到主从结点的offset,得到他们的差值, 对于差值大于我们指定的差值的时候,就让客户端不与这个结点通信

 

异步复制同步丢失
Redis主从结点之间的数据复制,是异步复制的,也就是说可能存在:
主服务器存在很多写命令没有传给从服务器,数据同步需要比较久的时间
然后客户端发送写命令
主服务器执行完写命令
返回结果给客户端
但是主服务器返回给客户端结果后和同步数据前,发生了Redis宕机,那么数据就会丢失
那我们如何尽可能的去减少数据丢失呢?
Redis配置中有个参数min-slaves-max-lag,Redis会根据当前数据同步的速度,判断出同步完成需要的时间,如果时间超过了min-slaves-max-lag,便不再接受客户端的请求,这样即使丢数据,也只会丢这10s内的数据
那么总不能为了宕机这种特殊情况,一直不处理请求,此时我们可以把数据先丢进本地缓存或者磁盘或者消息队列,这样等待Redis恢复正常,再去读取这些消息即可

posted @ 2023-02-01 15:03  小兵要进步  阅读(91)  评论(0编辑  收藏  举报