当前位置: 首页 > 科技观察

深入研究Redis高可用架构:Sentinel原理与实践

时间:2023-03-22 11:56:50 科技观察

【.com原创稿件】在之前的文章《深入学习 Redis 高可用的基石:主从复制》中提到,Redis主从复制的功能包括数据热备份、负载均衡、故障恢复等;但是主从复制的一个问题是故障恢复不能自动化。本文要介绍的sentinel是基于Redis主从复制的。其主要功能是解决主节点故障恢复的自动化问题,进一步提高系统的高可用性。文章会先介绍sentinel的作用和架构;然后描述哨兵系统的部署方法,以及通过客户端访问哨兵系统的方法;(注:文章内容基于Redis3.0版本)哨兵的作用及架构哨兵的作用在介绍哨兵之前,先从宏观的角度回顾一下Redis高可用相关的技术。它们包括:持久性、复制、哨兵和集群。它们的主要功能和问题是:持久化:持久化是最简单的高可用方式(有时甚至不被归类为高可用方式),其主要作用是数据备份,就是将数据存储在硬盘上,保证数据不会丢失由于进程退出而丢失。复制:复制是Redis高可用的基础。哨兵和集群都是基于复制来实现高可用的。Replication主要实现数据的多机备份,以及读操作的负载均衡和简单的故障恢复。缺陷:故障恢复不能自动化;写操作不能负载均衡;存储容量受限于单机。Sentinel:基于复制,Sentinel实现了故障自动恢复。缺陷:写操作不能负载均衡;存储容量受限于单机。集群:Redis通过集群,解决了写入操作无法负载均衡、存储容量受单机限制的问题,实现了比较完善的高可用解决方案。再说说哨兵,RedisSentinel,Redis哨兵,是在Redis2.8版本中引入的。Sentinel的核心功能是主节点的自动故障转移。以下是Redis官方文档中对哨兵功能的描述:监控(Monitoring):哨兵会不断的检查主节点和从节点是否运行正常。自动故障转移(Automaticfailover):当主节点无法正常工作时,Sentinel会启动自动故障转移操作,将故障主节点的其中一个从节点升级为新的主节点,并让其他从节点复制新的主节点代替。主节点。配置提供者(Configurationprovider):客户端初始化时,通过连接sentinel获取当前Redis服务的主节点地址。通知:哨兵可以将故障转移的结果发送给客户端。其中,监控和自动故障转移功能使Sentinel能够及时检测到主节点的故障并完成转移;而配置提供者和通知功能需要体现在与客户端的交互中。这里对本文中“客户端”一词的用法进行解释:在上一篇文章中,只要是通过API访问Redis服务器的,都会被称为客户端,包括redis-cli、Java客户端Jedis、etc.为了区别起见,本文中的客户端不包括redis-cli,但比redis-cli更复杂。redis-cli使用Redis提供的底层接口,客户端封装这些接口和功能,充分利用Sentinel的配置提供者和通知功能。哨兵架构一个典型的哨兵架构图如下:它由哨兵节点和数据节点两部分组成:哨兵节点:哨兵系统由一个或多个哨兵节点组成,哨兵节点是不存储数据的特殊Redis节点。数据节点:主节点和从节点都是数据节点。哨兵系统部署方法本部分将部署一个简单的哨兵系统,包括1个主节点、2个从节点和3个哨兵节点。为了方便:所有这些节点都部署在一台机器上(局域网IP:192.168.92.128),以端口号区分;节点的配置尽可能简单。部署主从节点哨兵系统中的主从节点与普通主从节点配置相同,无需额外配置。以下是主节点(port=6379)和两个从节点(port=6380/6381)的配置文件。配置比较简单,不再赘述。#redis-6379.confport6379daemonizeyeslogfile"6379.log"dbfilename"dump-6379.rdb"#redis-6380.confport6380daemonizeyeslogfile"6380.log"dbfilename"dump-6380.rdb"slaveof192.168.92.1286379#redis-636"1.logdaem6381.log"dbfilename"dump-6381.rdb"slaveof192.168.92.1286379配置完成后,依次启动主节点和从节点:redis-serverredis-6379.confredis-serverredis-6380.confredis-serverredis-6381.conf节点启动最后连接master节点,查看主从状态是否正常,如下图:部署哨兵节点哨兵节点本质上是特殊的Redis节点。3个Sentinel节点的配置几乎完全一样,主要区别在于端口号(26379/26380/26381)。下面以26379节点为例,介绍节点的配置和启动方法。配置部分尽量简化,更多配置后续会介绍。#sentinel-26379.confport26379daemonizeyeslogfile"26379.log"sentinelmonitormymaster192.168.92.12863792其中sentinelmonitormymaster192.168.92.12863792配置意思:sentinel节点监听master节点192.168.92.1928master节点名称:63是mymaster,2的含义与master节点的故障判断有关:至少需要两个sentinel节点同意才能判断master节点的故障并进行failover。有两种方式启动哨兵节点。两者的功能是完全一样的:redis-sentinelsentinel-26379.confredis-serversentinel-26379.conf--按照上面的方法配置并启动sentinel后,整个sentinel系统就启动了。您可以通过redis-cli连接到sentinel节点来进行验证。如下图所示:可以看出26379哨兵节点一直在监听mymaster主节点(即192.168.92.128:6379),发现了它的2个从节点和另外2个哨兵节点。这个时候再看哨兵节点的配置文件,就会发现有一些变化。以26379为例:其中dir只是显式声明了数据和日志所在的目录(只有sentinel上下文中的日志);known-slave和known-sentinel表示sentinel发现了slave节点和其他sentinel。带epoch的参数与配置epoch有关(配置epoch是一个从0开始的计数器,每进行一次***sentinelelection就会+1;***sentinelelection是failover中的一个操作相,其原理在下一节介绍)。演示故障转移哨兵的四个功能中,配置提供者和通知需要客户端的配合。本文将在下一章详细介绍客户端访问Sentinel系统的方法。本节将演示Sentinel在主节点出现故障时的监控和自动故障转移能力。(1)首先使用kill命令杀掉master节点:(2)如果此时使用infoSentinel命令查看sentinel节点,会发现master节点还没有切换过来,因为sentinel发现主节点故障并进行传输,需要一段时间。(3)过一段时间,再次在sentinel节点上执行infoSentinel,发现master节点已经切换到6380节点。但同时可以发现哨兵节点认为新的主节点还有2个从节点。这是因为哨兵将6379节点设置为从节点,同时将6380切换为主节点。虽然6379从节点挂掉了,但是由于sentinel客观上并没有让从节点下线(原理部分会介绍意义),所以认为从节点一直存在。当6379节点重启后,会自动成为6380节点的从节点。下面我们来验证一下。(4)重启6379节点:可以看到6379节点已经成为6380节点的从节点。(5)在failover阶段,sentinel和master-slave节点的配置文件会被重写。对于主从节点,主要是slaveof配置的变化:新的master节点没有slaveof配置,它的slave节点是新master节点的slaveof。对于哨兵节点,除了主从节点信息发生变化外,epoch也会发生变化。下图中可以看到epoch相关的参数都是+1。总结哨兵系统的构建过程,有几点需要注意:哨兵系统中的主从节点与普通的主从节点没有区别。故障检测和转移由哨兵控制和完成。Sentinel节点本质上是Redis节点。对于每个哨兵节点,只需要配置监控主节点即可自动发现其他哨兵节点和从节点。在哨兵节点启动和故障转移阶段,每个节点的配置文件都会被重写(configrewrite)。在本章的例子中,一个哨兵只监控一个主节点;实际上,一个sentinel可以通过配置多个sentinelmonitor来监控多个master节点。客户端访问Sentinel系统上一节演示了Sentinel的两个主要功能:监控和自动故障转移。本节结合客户端来演示Sentinel的另外两个功能:配置提供者和通知。代码示例在介绍客户端原理之前,先以Java客户端Jedis为例,演示一下如何使用:下面的代码可以连接到我们刚刚搭建的sentinel系统,进行各种读写操作(代码仅演示如何连接哨兵,异常处理,资源关闭等不予考虑)。publicstaticvoidtestSentinel()throwsException{StringmasterName="mymaster";Setsentinels=newHashSet<>();sentinels.add("192.168.92.128:26379");sentinels.add("192.168.92.128:26380");sentinels.add("192.168.92.128:26381");JedisSentinelPoolpool=newJedisSentinelPool(masterName,sentinels);//初始化过程做了很多工作Jedisjedis=pool.getResource();jedis.set("key1","值1");pool.close();}客户端原理Jedis客户端对Sentry提供了很好的支持。如上代码所示,我们只需要为Jedis提供一组sentinel节点和masterName即可构造一个JedisSentinelPool对象。然后就可以像普通的Redis连接池一样使用了:通过pool.getResource()获取连接并执行特定的命令。在整个过程中,我们的代码可以在不显式指定主节点地址的情况下连接到主节点;代码中没有failover的体现,可以在sentinel完成failover后自动切换master节点。之所以能做到这一点,是因为在JedisSentinelPool的构造函数中已经做了相关工作;主要包括以下两点:遍历哨兵节点获取主节点信息:遍历哨兵节点,通过其中一个哨兵节点+masterName获取主节点的信息。该功能是通过调用sentinel节点的sentinelget-master-addr-by-name命令实现的。命令示例如下:一旦获取到master节点的信息,就停止遍历(所以一般来说,向上遍历第一个sentinel节点就停止循环)。增加对哨兵的监控:这样当发生故障转移时,客户端可以收到哨兵的通知,完成主节点的切换。具体方法是:利用Redis提供的发布-订阅功能,为每个sentinel节点开启一个单独的线程,订阅sentinel节点的+switch-master通道,收到消息时重新初始化连接池。小结通过client原理的介绍,可以加深对Sentinel功能的理解。配置提供者:客户端可以通过哨兵节点+masterName获取主节点信息,其中哨兵??的作用是配置提供者。需要注意的是,哨兵只是一个配置提供者,并不是代理。两者的区别在于:如果是配置提供者,客户端通过sentinel获取主节点信息后会直接与主节点建立连接,后续的请求(如set/get)会直接发送到主节点。如果是代理,客户端的每一个请求都会发送给哨兵,哨兵通过主节点处理请求。举个例子就很好理解,Sentinel的作用是配置provider,而不是proxy。在之前部署的Sentinel系统中,修改Sentinel节点的配置文件如下:sentinelmonitormymaster192.168.92.12863792改为sentinelmonitormymaster127.0.0.163792然后,在局域网中的另一台机器上运行上述客户端代码,会发现客户端不能连接到主节点。这是因为sentinel是配置提供者,客户端通过它可以查询master节点的地址为127.0.0.1:6379,客户端会和127.0.0.1:6379建立Redis连接,自然连接不上。如果Sentinel是代理,则不会出现此问题。通知:故障转移完成后,哨兵节点会将新的主节点信息发送给客户端,以便客户端及时切换主节点。Sentinel实现的基本原理前面介绍了部署和使用Sentinel的基本方法。本节介绍Sentinel实现的基本原理。Sentry节点支持的命令作为一个运行在特殊模式下的Redis节点,Sentinel节点支持的命令不同于普通Redis节点。在运维中,我们可以使用这些命令来查询或修改Sentinel系统;但更重要的是,如果没有哨兵节点之间的通信,哨兵系统无法实现故障发现和故障转移等各种功能。并且很大一部分通信是通过Sentinel节点支持的命令实现的。下面介绍Sentinel节点支持的主要命令。基本查询通过这些命令可以查询哨兵系统的拓扑结构、节点信息、配置信息等:infosentinel:获取所有被监控的主节点的基本信息。sentinelmasters:获取所有受监控主节点的详细信息。sentinelmastermymaster:获取被监控主节点mymaster的详细信息。sentinelslavesmymaster:获取被监控主节点mymaster的从节点详情。sentinelsentinelsmymaster:获取被监控主节点mymaster的哨兵节点详情。sentinelget-master-addr-by-namemymaster:获取被监控主节点mymaster的地址信息,上面已经介绍过了。sentinelis-master-down-by-addr:Sentinel节点可以通过这个命令询问master节点是否下线,从而客观判断是否下线。添加/删除sentinelmonitormymaster2192.168.92.12816379监控master节点2:与部署sentinel节点时配置文件中的sentinelmonitor功能完全一致,不再赘述。sentinelremovemymaster2:取消当前sentinel节点对master节点mymaster2的监听。Forcedfailoversentinelfailovermymaster:该命令可以强制对mymaster进行故障转移,即使当前master节点运行良好。比如当前master节点所在的机器即将退役,可以使用failover命令提前进行failover。基本原理关于Sentinel的原理,关键是理解下面几个概念。定时任务每个哨兵节点维护3个定时任务。定时任务的作用如下:通过向主从节点发送info命令获取最新的主从结构。通过发布订阅功能获取其他sentinel节点的信息。向其他节点发送ping命令进行心跳检测,判断是否下线。主观下线在心跳检测的定时任务中,如果其他节点在一定时间内没有回复,哨兵节点会主观下线。顾名思义,主观下线就是哨兵节点“主观”判断下线;主观线下的对应是客观线下。客观下线哨兵节点在主观下线主节点后,会通过sentinelis-master-down-by-addr命令向其他哨兵节点询问主节点状态。如果判断主节点已经下线的哨兵数量达到一定值,则主节点客观下线。需要注意的是,客观下线是主节点特有的概念;如果从节点或哨兵节点发生故障,被哨兵主观下线,则不会有后续的客观下线和故障转移操作。顶端哨兵节点的选举当主节点被客观判断为下线时,各哨兵节点将协商选举出一个顶端哨兵节点,顶端节点对其进行故障转移操作。所有监控master节点的sentinel都可能被选举为leader,选举使用的算法是Raft算法。Raft算法的基本思想是先来先服务:即在一轮选举中,哨兵A向B发出成为leader的申请,如果B还没有同意其他哨兵,他会同意A成为leader。选举的具体过程在此不再详述。一般来说,挑选哨兵的过程是很快的。谁先完成线下目标,谁通常就会成为领导者。failover选出的Sentinel开始failover操作,大致可以分为三个步骤:从slave节点中选出一个新的master节点:选择的原则是先过滤掉不健康的slave节点,然后再选出具有最高优先级(由slave-priority指定)。如果无法区分优先级,则选择复制偏移量为***的slave节点;如果仍然无法区分,则选择runid最小的slave节点。更新主从状态:使用slaveofnoone命令使选中的从节点成为主节点;并使用slaveof命令让其他节点成为它的从节点。将下线的主节点(即6379)设置为新主节点的从节点,当6379再次上线时,将成为新主节点的从节点。通过以上关键概念,你就可以基本了解Sentinel的工作原理了。为了更形象的说明,下图是***sentinel节点的日志,包括从节点启动到故障转移完成。Sentry配置及实践建议Sentry配置下面介绍与Sentinel相关的几个配置。sentinelmonitor{masterName}{masterIp}{masterPort}{quorum}sentinelmonitor是sentinel的核心配置,在上一篇文章部署sentinel节点的时候已经讲解过,其中:masterName指定master节点的名字,masterIpmasterPort指定主节点的地址,quorum是判断主节点客观下线的哨兵数量的阈值。当确定主节点下线的哨兵数量达到法定人数时,主节点将客观下线。推荐值是哨兵数量的一半加1。sentineldown-after-milliseconds{masterName}{time}sentineldown-after-milliseconds与主观下线判断有关:Sentinel使用ping命令对其他节点进行心跳检测。如果其他节点在down-after-milliseconds配置的时间内没有回复,Sentry会主观下线。该配置对主节点、从节点、哨兵节点的主观下线判断有效。down-after-milliseconds默认值为30000,即30s;可根据不同的网络环境和应用需求进行调整。值越大,离线主观判断越宽松。优点是误判的可能性小。缺点是故障发现和故障转移的时间会更长,客户端等待的时间也会更长。例如,如果应用对可用性要求很高,可以适当降低该值,以在出现故障时尽快完成传输;如果网络环境比较差,可以适当提高阈值,避免频繁误判。sentinelparallel-syncs{masterName}{number}sentinelparallel-syncs与故障转移后从节点的复制有关:它指定每次向新的主节点发起复制操作的从节点的数量。例如,假设主节点切换完成后,有3个从节点向新的主节点发起复制;如果parallel-syncs=1,从节点将开始一个一个地复制;如果parallel-syncs=3,那么3个从节点Nodes会一起开始复制。parallel-syncs的值越大,从节点完成复制的速度越快,但对主节点的网络负载和硬盘负载的压力也越大;应根据实际情况设置。比如主节点负载低,从节点对服务可用性要求高,可以适当调大parallel-syncs的值。parallel-syncs默认值为1。sentinelfailover-timeout{masterName}{time}sentinelfailover-timeout与failover超时的判断有关,但是这个参数并不是用来判断整个failover阶段的超时时间,而是几个子阶段的超时。例如,如果主节点提升从节点的时间超过超时,或者从节点向新的主节点发起复制操作的时间(不包括复制数据的时间)超过超时,则故障转移超时将失败。failover-timeout默认值为180000,即180s;如果超时,下次该值将翻倍。除了上述参数外,还有一些其他的参数,比如安全验证相关的参数,这里就不介绍了。实践建议Sentinel节点应该不止一个。一方面,增加了Sentinel节点的冗余度,防止Sentinel本身成为高可用的瓶颈;另一方面减少线下的误判。此外,这些不同的哨兵节点应该部署在不同的物理机器上。Sentinel节点的个数应该是奇数,这样Sentinel可以通过投票来“决定”:选举***的决策,客观下线的决策等,每个sentinel节点的配置要保持一致,包括硬件、参数等;此外,所有节点都应使用ntp或类似服务以确保时间准确一致。Sentinel的配置提供者和通知客户端功能需要客户端的支持,比如上面提到的Jedis;如果开发者使用的库没有提供相应的支持,可能需要开发者自己实现。当Sentry中的节点部署在Docker(或其他可能做端口映射的软件)中时,需要特别注意端口映射可能导致Sentry无法正常工作。因为Sentinel的工作是基于与其他节点的通信,而Docker的端口映射可能会导致Sentinel无法连接到其他节点。例如,Sentinels根据声明的IP和端口相互发现。如果Docker中部署了一个SentinelA,并进行了端口映射,其他Sentinel无法使用A声明的端口连接到A。然后描述哨兵系统的部署方法,以及通过客户端访问哨兵系统的方法;然后简要说明sentinel实现的基本原理;***给出了一些关于Sentinel实践的建议。Sentinel在主从复制的基础上,引入了主节点的自动故障转移,进一步提高了Redis的高可用。但是,Sentinel的缺陷也很明显:Sentinel不能自动故障转移从节点。在读写分离的场景下,从节点故障会导致读服务不可用,我们需要对从节点进行额外的监控和切换操作。此外,Sentinel还没有解决写操作无法负载均衡、存储容量受单机限制的问题;这些问题的解决都需要使用集群,我会在后面的文章中介绍,欢迎关注。参考资料:https://redis.io/topics/sentinelhttp://www.redis.cn/《Redis开发与运维》《Redis设计与实现》【原创稿件,合作网站转载请注明原作者和出处为.com】