欢迎访问优讯网!
您当前的位置:首页 > 爱编程

Redis事件模型/主从复制/哨兵模型

时间:2020-01-09 08:02:51  来源:优讯网  作者:小卡司  浏览次数:

1. Redis的事件模型

Redis服务器需要处理两类事件:文件事件和时间事件。

1.1 文件事件

Redis服务器通过套接字与客户端进行连接,而文件事件就是服务器对套接字操作的抽象

Redis基于Reactor模式开发了网络事件处理器,由四部分组成:套接字、I/O多路复用程序、文件事件分派器以及事件处理器

  • 套接字:当有一个套接字准备好执行连接应答、写入、读取、关闭等操作时,就会产生一个文件事件(多个套接字就会有多个文件事件产生;

    • 事件类型有AE_READABLE和AE_WRITABLE
      • 如果客户端对套接字执行write或close操作,或者客户端对服务端的监听套接字执行connect操作,那么产生一个AE_READABLE事件。
      • 如果客户端对套接字执行read操作,那么产生一个AE_WRITABLE事件。
      • 如果一个事件既可读又可写,则先处理AE_READABLE事件,再处理AE_WRITABLE事件。
  • I/O多路复用程序:负责监听多个套接字,并向文件事件分派器传送产生事件的套接字。

    • I/O多路复用程序会将产生的所有事件的套接字放在一个队列中,以有序(sequentially)、同步(synchronously)、每次一个的方式向文件事件分派器传送套接字,只有一个套接字的事件处理完成后才会再发下一个:
    • I/O多路复用的功能是evport、epoll、kqueue和select这些常见的I/O多路复用函数的包装。Redis会在编译时自动选择系统中性能最高的I/O多路复用函数。
  • 文件事件分派器

    • 接收I/O多路复用程序传来的套接字,根据套接字产生的事件类型,调用相应事件处理器
  • 文件事件处理器

    • 连接应答处理器:acceptTCPhandler
      • 对连接服务器的各个客户端进行应答
      • Redis初始化时,将监听套接字的AE_READABLE事件与该处理器关联
      • 客户端连接服务器时,监听套接字产生AE_READABLE事件,触发该处理器执行。
      • 服务器将会创建一个redisClient结构的实例,并添加进自身的RedisServer结构的clients链表中。
      • 处理器对客户端请求进行应答,并创建客户端套接字,将客户端套接字的AE_READABLE事件与命令请求处理器关联。
    • 命令请求处理器:readQueryFromClient
      • 接收客户端传来的命令请求
      • 客户端成功连接服务器后,连接应答处理器将该客户端套接字的AE_READABLE事件与命令请求处理器关联
      • 客户端向服务器发送命令请求时,客户端套接字产生AE_READABLE事件
      • 命令请求处理器读取命令内容,传给相关程序执行。
    • 命令回复处理器:sendReplyToClient
      • 向客户端返回命令执行结果
      • 服务器有命令执行结果要传送给客户端时,将客户端套接字的AE_WRITABLE事件始终与该处理器关联
      • 客户端准备好接收命令执行结果时,客户端套接字产生AE_WRITABLE事件,触发命令回复处理器执行。
      • 将全部回复写入套接字后,关联解除

1.2 时间事件

一个时间事件包括三要素:

  • id
    • 时间时间的全局唯一表示,新事件id大于旧事件。
  • when
    • 毫秒精度的unix时间戳,记录了时间事件的到达时间。
  • timeproc(时间事件处理器)
    • 时间事件处理器,一个函数,时间事件到达时,服务器调用对应处理器来执行。

时间事件分为两类:

  • 定时事件:让一段程序在指定时间后执行一次。

    • 定时事件的处理器返回值是固定的数值,存在ae.h/AE_NOMORE中,如果一个事件的返回为该值,那么该事件在到达一次后,就会被删除
  • 周期事件:让一段程序每隔一段指定时间就执行一次。

    • 周期事件的处理器返回值是非ae.h/AE_NOMORE的值,这时,返回值会覆写when值。让这个时间过一段时间再次到达,以此类推。

服务器将时间事件都放在一个无序链表中(不是按时间顺序排序,而是按照ID排序,新产生的时间事件放在链表的表头),每次时间事件执行器运行时,processTimeEvents函数遍历整个链表,查找所有已经到达的时间事件,并调用相应的事件处理器。

1.2.1 serverCron函数

时间时间最典型的实例就是serverCron函数,它平均100毫秒执行一次,负责Redis定期对自身资源和状态的调整,包括:

  • 更新服务器的统计信息:时间,内存,数据库占用情况
  • 清理过期键值对
  • 关闭失效的连接
  • 尝试进行aof\rdb持久化操作
  • 对从服务器进行定期同步
  • 集群模式下的定期同步,连接测试

1.3 事件调度与执行

aeProcessEvents函数负责何时处理文件事件、何时处理时间事件,以及花费多久的时间 

将aeProcessEvents函数放置在循环中,加上初始化、清理函数,就构成了redis服务器的主函数

文件事件和时间事件是合作关系,服务器会轮流处理这两种事件,并且处理过程中也不会抢占线程。因此时间事件的实际处理时间要比设定的时间晚一些。

2. Redis主从复制

关系数据库通常会使用一个主服务器向多个从服务器发送更新,并使用从服务器来处理所有的读请求,Redis采用了同样方法来实现自己的复制特性。

2.1 旧版复制功能

Redis 2.8以前采用的复制都为旧版复制,主要使用SYNC命令同步复制,SYNC存在很大的缺陷严重消耗主服务器的资源以及大量的网络连接资源。Redis 2.8之后采用PSYNC命令替代SYNC,解决完善这些缺陷,但在介绍新版复制功能之前,必须先介绍旧版复制过程,这样才能更好地形成对比。

复制功能有两种模式,分为同步sync命令传播(command propagate),两个过程配合执行才能实现Redis复制。

  • SYNC命令同步操作
    • 通过从服务器发送到SYNC命令给 主服务器
    • 主服务器 执行BGSAVE命令,在后台生成RDB文件,并从现在开始将所有写命令记录进缓冲区
    • 并发送给 从服务器,同时发送缓冲区保存的所有写命令给 从服务器
    • 从服务器 清空之前数据并执行解释RDB文件,然后执行缓冲区的写命令。
    • 保持数据基本一致(还需要命令传播过程才能保持一致)
  • 命令传播操作:
    • 在同步之后,主服务器 仍然在不断的接受写命令,这会导致好不容易一致的主从状态再次不一致。
    • 通过发送让主从服务器不一致的命令(主服务器接收到的新写命令)给从服务器并执行,让主从服务器的数据库重新回到一致状态。

SYNC命令的缺陷:如果因为网络问题,导致主从断开链接一段时间,那么重新同步的时候, SYNC无法做到断点继续,而是仍要清空之前数据,并重新开始复制操作。

SYNC命令非常消耗资源,原因有三点:

  1. 主服务器执行BGSAVE命令生成RDB文件,这个生成过程会大量消耗主服务器资源(CPU、内存和磁盘I/O资源)

  2. 主服务器需要将自己生成的RBD文件发送给从从服务器,这个发送操作会消耗主从服务器大量的网络资源(带宽与流量)

  3. 接收到RDB文件你的从服务器需要载入RDB文件,载入期间从服务器会因为阻塞而导致没办法处理命令请求。

2.2 新版复制功能

为了解决旧版本中断线情况下SYNC低效问题,在Redis 2.8之后使用PSYNC命令代替SYNC命令执行复制同步操作,自然PSYNC具备完整重同步和部分重同步模式

  • 完整重同步:跟旧版复制基本是一致的,可以理解为“全量”复制。
  • 部分重同步:在命令传播阶段,断线重复制只需要发送主服务器在断开期间执行的写命令给从服务器即可,可以理解为“增量”复制。

2.3 复制的实现

Redis不管是旧版还是新版,复制的实现都可以分为七个步骤,流程图如下:

  1. 设置主服务的地址与端口
    • 当客户端向从服务器发送一下命令时或者在配置文件中配置slaveof选项
    • 127.0.0.1:12345> SLAVEOF 127.0.0.1 6379
  2. 建立套接字连接
    • 从服务器根据设置的套接字创建连向主服务器的套接字连接
    • 主服务器接收从服务器的套接字连接之后,为该套接字创建响应的客户端状态,并将此时的从服务器看做是主服务器的客户端,也就是该从服务器同时具备服务器与客户端两个身份。
  3. 发送PING命令
    • 从服务器成为主服务器的客户端之后,做的第一件事就是向主服务器发送PING命令。PING命令主要有两种作用:
      1. 虽然建立了套接字连接,但是还未使用过,通过发送PING命令检查套接字的读写状态是否正常
      2. 通过发送PING命令检查主服务器能否正常处理命令请求
    • 从服务器在发送PING命令之后将遇到以下三种情况的其中一种:
  4. 身份验证
    • 从服务器接收到主服务器返回的“PONG”回复,接下来就需要考虑身份验证的事。
  5. 发送端口信息
    • 在身份验证步骤之后,从服务器将执行命令REPLCONF listening-port <port>,向主服务器发送从服务器的监听端口号。
  6. 同步
    • 就是上述所指的同步操作,从服务器向主服务器发送PSYNC命令,执行同步操作,值得注意的是开始只有从服务器是主服务器的客户端,但是执行同步操作之后,主服务器也会成为从服务器的客户端。
  7. 命令传播
    • 主从服务器就会进入命令传播阶段,主服务器只要将自己执行的写命令发送给从服务器,而从服务器只要一直执行并接收主服务器发来的写命令(上述已经介绍过,这里不过多介绍)

3. Redis哨兵模型

Sentinel(哨兵、哨岗)是Redis 的高可用性的解决方案:有一个或多个Sentinel实例组成的Sentinel系统可以监视任意多个主服务器,以及这些主服务器属下的所有从服务器,并在被监视的主服务器进入下线状态时,自动将下线主服务器属下的某个从服务器升级为主服务器,然后由新的主服务器代替已下线的主服务器继续处理命令请求。

在替换了新的主服务器之后,如果之前下线的主服务器上线了,就会被降为新的主服务器的从服务器。

3.1 Sentinel的启动

$ redis-sentinel /path/to/your/sentinel.conf
或者
$ redis-sentinel /path/to/your/sentinel.conf --sentinel

这两个命令都能启动Sentinel,效果都是一样的。

Sentinel启动后,会有五个步骤:

  1. 初始化服务器

    • Sentinel的本质是一个运行在特殊模式下的Redis服务器,因此启动时必须对其进行初始化,但是由于Sentinel与普通的服务器不同,它的初始化需要执行的操作也不同
    • 下表是Sentinel 模式下Redis服务器的主要功能的使用情况
  2. 使用Sentinel专用代码

    • 启动Sentinel的第二步,就是将普通Redis服务器使用的代码替换成Sentinel专用的代码。
    • 比如 普通Redis服务器使用 redis.h/REDIS_SERVERPORT常量作为服务端口(#define REDIS_SERVERPORT 6379),使用 redis.h/redisCommandTable 作为服务器的命令表。
    • 而Sentinel使用 reids.h/REDIS_SENTINEL_PORT 常量作为服务器端口,默认26379,使用 redis.h/sentinelcmds 作为服务器的命令表
  3. 初始化Sentinel状态

    • 接下来,服务器会初始化一个 sentinel.c/sentinelState 结构(简称“Sentinel状态”),这个结构保存了服务器所有和Sentinel功能有关的状态,服务器的一般状态仍然由 redis.h/redisServer 结构保存:
    	typedef struct sentinelState{
    		// 当前纪元,用于实现故障转移
    		uint64_t current_epoch;
    		// 保存了所有被这个 Sentinel监视的主服务器
    		// 字典的键是主服务器的名字
    		// 字典的值是一个指向 sentinelRedisInstance 结构的指针
    		dict *masters;
    		// 是否进入了 TILT 模式
    		int tilt;
    		// 目前正在执行的脚本数量
    		int running_scripts;
    		// 进入 TILT 模式的时间
    		mstime_t tilt_start_time;
    		// 最后一次执行事件处理器的时间
    		mstime_t previous_time;
    		// 一个 FIFO 队列,包含了所有需要执行的用户脚本
    		list *scripts_queue;
    	}sentinel;
    
  4. 初始化Sentinel状态的 masters 属性

    • 接下来要做的是将sentinel状态的 masters 属性进行初始化,上面已经说过了,masters 里面保存的是所有被监视的主服务器的信息。master属性是字典,键是主服务器的名字,值是一个指向 sentinelRedisInstance 结构的指针。

    • 我们先介绍一下 sentinelRedisInstance 结构(简称“实例结构”),这个结构代表着一个被Sentinel监视的Redis服务器实例,可以是主服务器、从服务器或者另外一个Sentinel.

    	ypedef struct sentinelRedisInstance {
    		// 标识值,记录了当前Redis实例的类型和状态
    		int flags;      /* See SRI_... defines */
    		// 实例的名字
    		// 主节点的名字由用户在配置文件中设置
    		// 从节点以及Sentinel节点的名字由Sentinel自动设置,格式为:ip:port
    		char *name;     /* Master name from the point of view of this sentinel. */
    		//实例的运行 ID
    		char *runid;    /* Run ID of this instance, or unique ID if is a Sentinel.*/
    		//配置纪元,用于实现故障转移
    		uint64_t config_epoch;  /* Configuration epoch. */
    		//实例的地址:ip和port
    		sentinelAddr *addr; /* Master host. */
    		//实例的连接,有可能是被Sentinel共享的
    		instanceLink *link; /* Link to the instance, may be shared for Sentinels. */
    		 // 最近一次通过 Pub/Sub 发送信息的时间
    		mstime_t last_pub_time;   /* Last time we sent hello via Pub/Sub. */
    		// 只有被Sentinel实例使用
    		// 最近一次接收到从Sentinel发送来hello的时间
    		mstime_t last_hello_time; /* Only used if SRI_SENTINEL is set. Last time
    									 we received a hello from this Sentinel
    									 via Pub/Sub. */
    		// 最近一次回复SENTINEL is-master-down的时间                             
    		mstime_t last_master_down_reply_time; /* Time of last reply to
    												 SENTINEL is-master-down command. */
    		// 实例被判断为主观下线的时间                                         
    		mstime_t s_down_since_time; /* Subjectively down since time. */
    		// 实例被判断为客观下线的时间
    		mstime_t o_down_since_time; /* Objectively down since time. */
    		 // 实例无响应多少毫秒之后才会被判断为主观下线(subjectively down)
    		mstime_t down_after_period; /* Consider it down after that period. */
    		 // 从实例获取INFO命令回复的时间
    		mstime_t info_refresh;  /* Time at which we received INFO output from it. */
    
    		/* Role and the first time we observed it.
    		 * This is useful in order to delay replacing what the instance reports
    		 * with our own configuration. We need to always wait some time in order
    		 * to give a chance to the leader to report the new configuration before
    		 * we do silly things. */
    		// 实例的角色 
    		int role_reported;
    		// 角色更新的时间
    		mstime_t role_reported_time;
    		// 最近一次从节点的主节点地址变更的时间
    		mstime_t slave_conf_change_time; 
    /* Last time slave master addr changed. */
    
    		/* Master specific. */
    		 /*----------------------------------主节点特有的属性------------*/
    		 // 其他监控相同主节点的Sentinel
    		dict *sentinels;    /* Other sentinels monitoring the same master. */
    		// 如果当前实例是主节点,那么slaves保存着该主节点的所有从节点实例
    		// 键是从节点命令,值是从节点服务器对应的sentinelRedisInstance
    		dict *slaves;       /* Slaves for this master instance. */
    		  // 判定该主节点客观下线(objectively down)的投票数
    		// 由SENTINEL monitor <master-name> <ip> <port> <quorum>配置
    unsigned int quorum;/* Number of sentinels that need to agree on failure. */
    		 // SENTINEL parallel-syncs <master-name> <number> 选项的值
    		// 在执行故障转移操作时,可以同时对新的主服务器进行同步的从服务器数量
    		int parallel_syncs; /* How many slaves to reconfigure at same time. */
    		  // 连接主节点和从节点的认证密码
    		char *auth_pass;    /* Password to use for AUTH against master & slaves. */
    
    		/* Slave specific. */
    		/*----------------------------------从节点特有的属性--------------------*/
    		 // 从节点复制操作断开时间
    		mstime_t master_link_down_time; /* Slave replication link down time. */
    		 // 按照INFO命令输出的从节点优先级
    		int slave_priority; /* Slave priority according to its INFO output. */
    		 // 故障转移时,从节点发送SLAVEOF <new>命令的时间
    		mstime_t slave_reconf_sent_time; /* Time at which we sent SLAVE OF <new> */
    		 // 如果当前实例是从节点,那么保存该从节点连接的主节点实例
    		struct sentinelRedisInstance *master; /* Master instance if it's slave. */
    		// INFO命令的回复中记录的主节点的IP
    		char *slave_master_host;    /* Master host as reported by INFO */
    		// INFO命令的回复中记录的主节点的port
    		int slave_master_port;      /* Master port as reported by INFO */
    		 // INFO命令的回复中记录的主从服务器连接的状态
    		int slave_master_link_status; /* Master link status as reported by INFO */
    		// 从节点复制偏移量
    		unsigned long long slave_repl_offset; /* Slave replication offset. */
    
    		/* Failover */
     /*----------------------------------故障转移的属性----------------------------------*/
    		// 如果这是一个主节点实例,那么leader保存的是执行故障转移的Sentinel的runid
    // 如果这是一个Sentinel实例,那么leader保存的是当前这个Sentinel实例选举出来的领头的runid
    		char *leader;       /* If this is a master instance, this is the runid of
    							   the Sentinel that should perform the failover. If
    							   this is a Sentinel, this is the runid of the Sentinel
    							   that this Sentinel voted as leader. */
    		// leader字段的纪元                       
    		uint64_t leader_epoch; /* Epoch of the 'leader' field. */
    		// 当前执行故障转移的纪元
    		uint64_t failover_epoch; /* Epoch of the currently started failover. */
    		// 故障转移操作的状态
    		int failover_state; /* See SENTINEL_FAILOVER_STATE_* defines. */
    		// 故障转移操作状态改变的时间
    		mstime_t failover_state_change_time;
    		// 最近一次故障转移尝试开始的时间
    		mstime_t failover_start_time;   /* Last failover attempt start time. */
    		//  更新故障转移状态的最大超时时间
    		mstime_t failover_timeout;      /* Max time to refresh failover state. */
    		// 记录故障转移延迟的时间
    		mstime_t failover_delay_logged; /* For what failover_start_time value we
    										   logged the failover delay. */
    		// 晋升为新主节点的从节点实例                                   
    		struct sentinelRedisInstance *promoted_slave; /* Promoted slave instance. */
    		/* Scripts executed to notify admin or reconfigure clients: when they
    		 * are set to NULL no script is executed. */
    		// 通知admin的可执行脚本的地址,如果设置为空,则没有执行的脚本 
    		char *notification_script;
    		 // 通知配置的client的可执行脚本的地址,如果设置为空,则没有执行的脚本
    		char *client_reconfig_script;
    		// 缓存INFO命令的输出 
    		sds info; /* cached INFO output */
    	} sentinelRedisInstance;
    
    • 其中的 addr 属性是一个指向 sentinel.c/sentinelAddr 结构的指针,这个结构保存实例的IP地址和端口号:
    	typedef struct sentinelAddr{
    		char *p;
    		int port;
    	}sentinelAddr;
    
    • 对Sentinel 状态的初始化将引发对 masters 字典的初始化,而 masters 字典的初始化是根据被载入的Sentinel配置文件来进行的。假设我们有master1和master2,由如下图1的配置文件导入
    • 那么我们得到两个sentinelRedisInstance:
    • 最终sentinelRedisInstance为:
  5. 创建连向主服务器的网络连接

    • 这是最后一步啦,这一步是创建连向被监视主服务器的网络连接,Sentinel将成为主服务器的客户端,可以向主服务器发送命令,并从命令回复中获取相关的信息。
    • 每个被Sentinel监视的主服务器,Sentinel会创建两个连向主服务器的异步网络连接:
      1. 命令连接,用于向主服务器发送命令,并接收命令回复
      2. 订阅连接,用于订阅主服务器的__sentinel__:hello频道

3.2 Sentinel与服务器的交互

Sentinel作为一个监视Redis服务器的监控系统,必然需要有如下的权利或者义务:

  1. 要能掌握被自己监视的主服务器和其从服务器的状态信息

    • INFO命令:每十秒一次,通过命令链接向被监视的主服务和从服务器发送INFO命令。分析主服务器的应答得到主服务器的状态信息。
  2. 要有为“与自己监视了相同服务器的其他Sentinel”感知到自己提供便利的义务。

    • 广播频道消息:Sentinel每两秒一次,通过命令链接向所有被自己监视的主服务器和从服务器发送PUBLISH命令,发布自己的一些状态信息到对应主服务器的__sentinel__:hello频道,以便让其他监视了同一服务器的Sentinel(当然这些Sentinel也订阅了该服务器的__sentinel__:hello频道)感知到自己的存在,宣誓主权。
  3. 要能感知到“与自己监视了同一服务器的其他Sentinel”的状态信息。

    • 接收频道消息:Sentinel在与主服务器创建订阅链接后就会通过订阅命令来订阅主服务器的__sentinel__:hello频道。通过订阅链接,Sentinel能接收到该频道上其他Sentinel发布的他们各自的状态信息。从而感知到他们的存在。
    • 创建Sentinel之间的链接:Sentinel A 感知到另一个Sentinel B 时,如果是第一次感知到,那么A会创建连向B的命令链接。当然B也会有一个发现A的过程,所以对于监视相同服务器的Sentinel来说,他们是这样相互关联的。

归纳完毕,现在我们来一一展开介绍:

3.2.1 INFO命令

假设有个主服务器和三个从服务器。

Sentinel 默认每十秒一次,通过命令连接向被监视的主服务器发送 INFO 命令,得到如下信息:

  • 关于服务器本身的信息
    • 包括 run_id 域记录的服务器运行ID,以及 role 域记录的服务器角色
  • 关于主服务器属下的所有从服务器信息
    • 每个从服务器都由一个“slave”字符串开头的行记录,每行的 ip= 域记录了从服务器的IP地址, port= 域记录了从服务器的端口号。根据这些IP地址和端口号,Sentinel无须用户提供从服务器的地址信息,就可以自动发现从服务器。

根据 run_id 域和 role 域的信息,Sentinel将对主服务器的实例结构(sentinelRedisInstance)进行更新。而主服务器返回的从服务器信息,将会被用于更新主服务器实例结构(sentinelRedisInstance)的 slaves 字典(记录了属下从服务器的名单,key为ip:port格式,值指向从服务器的sentinelRedisInstance实例)。

Sentinel 分析 INFO 命令中包含的从服务器信息时,会检查这个从服务器实例结构(sentinelRedisInstance)是否已经存在于主服务器的 slaves 字典: 如果存在,就对从服务器的实例结构进行更新,如果不存在(表明这个从服务器是新发现的从服务器),Sentinel会在 slaves 字典中为这个从服务器创建一个新的实例结构。

当Sentinel发现主服务器有新的服务器出现时,除了会为这个新从服务器创建相应的实例结构之外,还会创建连接到从服务器的命令连接和订阅连接

创建了命令连接之后,每10秒一次向从服务器发送 INFO 命令,依次来维护从服务器的实例结构(sentinelRedisInstance)的状态。

主服务器实例结构的 flags 值为 SRI_MASTER,从服务器是 SRI_SLAVE

3.2.2 广播频道消息

Sentinel会每两秒一次,通过命令连接向所有被监视的主服务器和从服务器发送以下格式的命令:

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

这条命令就表示向服务器的 __sentinel__:hello 频道发送一条信息,信息由一下部分组成:

  • 以 s_ 开头的参数记录Sentinel本身的信息
  • 以 m_ 开头的参数则是该频道所属的主服务器的信息,当然如果监视的是从服务器,这个信息表示的就是所属的从服务器的信息
  • 具体含义如下图

3.2.3 接收频道消息

在建立起订阅连接之后,Sentinel会通过这个连接,向服务器发送SUBSCRIBE __sentinel__:hello命令,也就是订阅这个频道,这个订阅关系会一直持续到Sentinel与服务器的连接断开之后。

对于监视同一服务器的多个Sentinel来说,一个Sentinel发送的信息会被其他的Sentinel接收到,并用于更新其他Sentinel对发送信息Sentinel的认知,和被用于更新其他Sentinel对被监视服务器的认知。

假如该Sentinel A从其监控的主服务器M的 __sentinel__:hello频道中,接收到其他Sentinel B发来的<s_ip>,<s_port>,<s_runid>,<s_epoch>,<m_name>,<m_ip>,<m_port>,<m_epoch>格式的信息后,Sentinel A会从信息中分析出以下信息:

  • 与Sentinel相关的参数:Sentinel B的IP、port、run_id、配置纪元
  • 与主服务器相关参数:Sentinel B 正在监视的这个主服务器(也就是主服务器M)的名字、IP、port、配置纪元

服务器实例结构(sentinelRedisInstance)中除了slave字典外,还有一个sentinels字典,存放着其他共同监控着这个主服务器的sentinels的状态信息。这个字典的键是Sentinel的名字,格式:ip:port。值是对应Sentinel的实例结构(还是sentinelRedisInstance结构)。

根据之前那些主服务器参数,Sentinel A 会在自己的Sentinel状态(sentinelState)的 masters 字典中查找相应的主服务器实例结构(sentinelRedisInstance),然后根据Sentinel参数,检查主服务器实例结构的 sentinels 字典中,Sentinel B的实例结构是否存在:

  • 存在,就对Sentinel B的实例结构进行更新
  • 存在,说明Sentinel B是才开始监视主服务器的新Sentinel,Sentinel A 会为Sentinel B创建一个新的实例结构,并将这个结构添加到 sentinels 字典里面

3.2.4 创建Sentinel之间的链接

当Sentinel通过频道信息发现了一个新的Sentinel时,它不仅会为新的Sentinel在 sentinels 字典中创建相应的实例结构,还会创建一个连向新Sentinel的命令连接。

新的Sentinel同样会创建连向这个Sentinel的命令连接,最终监视同一主服务器的多个Sentinel将形成相互连接的网络:SentinelA有连向SentinelB的命令连接,SentinelB也有连向SentinelA的命令连接。

Sentinel之间不会创建订阅连接

3.3 监控下线和故障转移

了解了Sentinel与服务器/其他Sentinel的交互方式后,就可以来着手解决实际问题了,Sentinel的使命主要有两点:

  • 监控下线
  • 选举领头sentinel
  • 故障转移

3.3.1 监控下线

3.3.1.1 检测主观下线状态

主观下线状态,即单个sentinel认为某个主服务器/从服务器/sentinel下线了,至于是不是真的已经下线,则不一定。

  • 默认情况下,Sentinel会以每秒一次的频率向所有与它创建了命令连接的实例(包括主服务器、从服务器、其他Sentinel在内)发送 PING 命令,并通过实例返回的 PING 命令回复来判断实例是否在线。
  • 对 PING 命令的回复,Redis只认两种含义:
    • 有效回复:实例返回 +PONG 、 -LOADING 、-MASTERDOWN 三种其中一种
    • 无效回复,除了上面三种之外的其它回复,或者在指定时限内没有返回任何回复
  • Sentinel配置文件中的 down-after-millseconds 选项指定了Sentinel判断实例进入主观下线所需的时间长度:如果一个实例在 down-after-millseconds 毫秒内,连续向Sentinel返回无效回复,那么Sentinel会修改这个实例所对应的实例结构,在结构的 flags 属性中打上 SRI_S_DOWN 标识,用于表示这个实例已经进入主观下线状态。(也就是我认为你已经下线了)

主观下线时长选项,即 down-after-millseconds 的值,不仅会被Sentinel用于判断其监控的主服务器的主观下线状态,还会被用于判断该主服务器属下的所有从服务器,以及所有同样监视这个主服务器的其他Sentinel的主观下线状态。

多个Sentinel设置的主观下线时长可能不同,对于监视同一个主服务器的多个Sentinel来说,这些Sentinel设置的 down-after-milliseconds 选项的值可能不同,因此,当一个Sentinel将主服务器判断为主观下线时,其它Sentinel可能任然会认为主服务器处于在线状态。

3.3.1.2 检测客观下线状态

客观下线状态,即经过确认后,可断定为事实上确实下线了。

当Sentinel将一个主服务器判断为主观下线之后,为确定这个服务器是否真的下线,它会向同样监视这个主服务器的其它Sentinel进行询问,当接收到足够数量的已下线判断之后,Sentinel就会将从服务器判定为客观下线,并对主服务器进行故障转移操作。

  • 发送 SENTINEL is-master-down-by-addr 命令
    • entinel会发送下面的命令询问其它Sentinel是否同意主服务器下线:
    • SENTINEL is-master-down-by-addr <ip> <port> <current_epoch> <runid>
  • 接收 SENTINEL is-master-down-by-addr 命令
    • 当一个Sentinel(目标Sentinel)接收到另外一个Sentinel(源Sentinel)发来的 SENTINEL is-master-by-addr 命令时,目标Sentinel会分析并取出命令请求中包含的各个参数,并根据其中的IP和port,判断主服务器是否已经下线,然后向源Sentinel返回一个包含三个参数的 Multi Bulk 回复作为这个命令的回复。这三个参数分别是:
      1. <down_state>:返回目标Sentinel对主服务器的检查结果,1表示主服务器已下线,0表示主服务器未下线
      2. <leader_runid>:可以是 * 符号或者目标Sentinel的局部领头Sentinel的运行ID,*表示命令仅仅用于检测主服务器的下线状态,而局部领头Sentinel的运行ID则用于选举领头Sentinel
      3. <leader_epoch>:目标Sentinel的局部领头Sentinel的配置纪元,用于选举领头Sentinel。仅在 leader_runid 值不为 * 时有效,如果其值为 * ,这个参数总为0
  • 接收 SENTINEL is-master-down-by-addr 命令之后
    • 根据其他Sentinel发回的 SENTINEL is-master-down-by-addr 回复,Sentinel会统计反馈了“同意这个主服务器已经下线”这个信息的sentinel数量。
    • 当这个值达到配置指定的判断客观下线所需的数量时(即 quorum 属性的值),Sentinel会将主服务器实例结构中(sentinelRedisInstance) flags 属性的 SRI_O_DOWN 标识打开,标识主服务器已经进入客观下线状态。

3.3.2 选举领头sentinel

当一个Master服务器客观下线后,监控这个Master服务器的所有Sentinel将会选举出一个领头Sentinel。并由领头Sentinel对客观下线的Master进行故障转移。

选举领头Sentinel的规则和方法:

  1. 所有监控客观下线Master的Sentinel都有可能成为领头Sentinel。每次进行领头Sentinel选举之后,不论是否选举成功,所有Sentinel的配置纪元(configuration epoch)的值都会自动增加一次

  2. 在一个配置纪元里面,所有Sentinel都有一次将某个Sentinel设置为局部领头Sentinel的机会,并且局部领头Sentinel一旦设置,在这个配置纪元里面将不能再更改

  3. 监视Master客观下线的所有在线Sentinel都有要求其它Sentinel将自己设置为局部领头Sentinel的机会。

  4. 当一个Sentinel(源Sentinel)向另一个Sentinel(目标Sentinel)发送SENTINEL is-master-down-by-addr命令,并且命令中的runid参数不是“*”符号而是当前Sentinel的运行ID时,这表示当前Sentinel要求目标Sentinel将自己设置为领头Sentinel

  5. Sentinel设置局部领头Sentinel的规则是先到先得。即最先向目标Sentinel发送设置要求的Sentinel将会成为局部领头Sentinel,之后接受到的请求都会被拒绝

  6. 目标Sentinel接收到SENTINEL is-master-down-by-addr命令后,将向源Sentinel返回一条命令回复,回复中的leader_runid参数和leader_epoch参数分别记录了目标Sentinel的局部领头Sentinel的运行ID和配置纪元

  7. 源Sentinel在接收到目标Sentinel返回的命令回复之后,会检查回复中leader_epoch参数的值和自己的配置纪元是否相同,如果相同的话,那么源Sentinel继续取出回复中的leader_runid参数,如果leader_runid参数的值和源Sentinel的运行ID一直,那么表示目标Sentinel将源Sentinel设置成了局部领头Sentinel,记录下来

  8. 记录之后,如果有某个Sentinel发现自己已经被半数以上的Sentinel设置成了局部领头Sentinel,那么这个Sentinel就会成为领头Sentinel。

  9. 领头Sentinel的产生需要半数以上的Sentinel支持,并且每个Sentinel在每个配置纪元里面只能设置一次局部Sentinel,所以在一个配置纪元里面,只会出现一个领头Sentinel。

  10. 如果在给定时限内,没有一个Sentinel被选举为领头Sentinel,那么各个Sentinel将在一段时间之后再次进行选举,直到选出领头Sentinel为止,(所以建议哨兵设置奇数个,且数量不小于3)。

3.3.3 故障转移

接收到SENTINEL is-master-down-by-addr命令回复的源Sentinel可以统计出有多少个Sentinel将自己设置成局部领头Sentinel。如果超过半数,则当前Sentinel就会被选为领头Sentinel并进行故障转移。

故障转移包括以下三步:

  1. 在已下线的Master主机下面挑选一个他的Slave服务器,并将其转换为主服务器。
  2. 其余所有Slave服务器复制新的Master服务器。
  3. 让已下线的Master服务器变成新的Master服务器的Slave。当已下线的服务器再次上线后将复新的Master的数据。

3.3.3.1 选举新的主服务器的过程

领头Sentinel会在所有Slave中选出新的Master,发送SLAVEOF no one命令,将这个服务器确定为主服务器。

领头Sentinel会将已下线Master的所有从服务器保存在一个列表中,按照以下规则,一项一项进行过滤

  1. 删除列表中所有处于下线或者短线状态的Slave。(保证剩下都是在线的)

  2. 删除列表中所有最近5s内没有回复过领头Sentinel的INFO命令的Slave。(保证剩下都是近期成功通信过的)

  3. 删除所有与下线Master连接断开超过down-after-milliseconds * 10毫秒的Slave。(过滤掉过早的和下线Master断开连接的,这样可以保证剩下的Slave,数据都比较新)

  4. 领头Sentinel将根据Slave优先级,对列表中剩余的Slave进行排序,并选出其中优先级最高的Slave。

  5. 如果有多个具有相同优先级的Slave,那么领头Sentinel将按照Slave复制偏移量,选出其中偏移量最大的Slave。(复制偏移量的slave就是保存的最新数据的slave)

  6. 如果有多个优先级最高,偏移量最大的Slave,那么根据运行ID最小原则选出新的Master。

确定新的Master之后,领头Sentinel会以每秒一次的频率向新的Master发送INFO命令,当得到确切的回复:role由slave变为master之后,当前服务器顺利升级为Master服务器。

3.3.3.2 修改从服务器的复制目标

选出新的Master服务器后,领头Sentinel会向下线Master的剩余Slave发送SLAVEOF命令,让它们复制新的Master。

3.3.3.3 将旧的Master变成Slave

当已下线的Master重新上线后,领头Sentinel会向此服务器发送SLAVEOF命令,将当前服务器变成新的Master的Slave。

来顶一下
返回首页
返回首页

原文链接:https://my.oschina.net/lscherish/blog/3155066


推荐资讯
如何下载旧版centos iso镜像 如何下载迷你mini版的centos镜像
如何下载旧版centos i
计算机的正确使用姿势 电脑痴如何正确的使用电脑
计算机的正确使用姿势
好用的后台管理的前端框架模版H-ui H-ui框架模版分享
好用的后台管理的前端
微信电脑多开方法 无需辅助电脑版微信双开方法分享
微信电脑多开方法 无
相关文章
栏目更新
栏目热门