《redis设计与实现》第三部分 (第16章 哨兵)

第三部分 多机数据库的实现

16.0 第16章 Sentinel

哨兵系统可以监视所有主服务器下,以及主服务器下的所有从服务器。如果被监视的主服务器下线,哨兵系统可以讲下线的从服务器升级成新的主服务器

16.1 启动并初始化Sentinel

16.1.1 初始化服务器

  • sentinel本质是运行一个特殊模式下的Redis服务器,并不使用数据库,所以初始化sentinel不需要载入RDB和AOF
  • 可能会用到的主要功能
    • 复制命令SLAVEOF:Sentinel内部使用,客户端不可以使用
    • 发布与订阅命令:SUBSCRIBE、PSUBSCRIBE、UNSUBSCRIBE、PUNSUNSCRIBE在sentinel内部和客户端可以使用,但是PUBLISH只能在sentinel内部使用
    • 文件事件处理器:Sentinel内部使用,关联的文件事件处理器和普通的Redis服务器不同
    • 时间事件处理器:serverCron会调用sentinelTimer(desynchronize every Sentinel,确保可以选出leader)
	//code0 sentinel.c
	void sentinelTimer(void) {
    sentinelCheckTiltCondition();
    sentinelHandleDictOfRedisInstances(sentinel.masters);
    sentinelRunPendingScripts();
    sentinelCollectTerminatedScripts();
    sentinelKillTimedoutScripts();

    /* We continuously change the frequency of the Redis "timer interrupt"
     * in order to desynchronize every Sentinel from every other.
     * This non-determinism avoids that Sentinels started at the same time
     * exactly continue to stay synchronized asking to be voted at the
     * same time again and again (resulting in nobody likely winning the
     * election because of split brain voting). */
    server.hz = CONFIG_DEFAULT_HZ + rand() % CONFIG_DEFAULT_HZ;
}

16.1.2 Sentinel专用代码

  • 端口
    • 普通Redis服务器使用的6379
    • sentinel使用的:#define REDIS_SENTINEL_PORT 26379
  • command
//code1 sentinel.c
struct redisCommand sentinelcmds[] = {
    {"ping",pingCommand,1,"",0,NULL,0,0,0,0,0},
    {"sentinel",sentinelCommand,-2,"",0,NULL,0,0,0,0,0},
    {"subscribe",subscribeCommand,-2,"",0,NULL,0,0,0,0,0},
    {"unsubscribe",unsubscribeCommand,-1,"",0,NULL,0,0,0,0,0},
    {"psubscribe",psubscribeCommand,-2,"",0,NULL,0,0,0,0,0},
    {"punsubscribe",punsubscribeCommand,-1,"",0,NULL,0,0,0,0,0},
    {"publish",sentinelPublishCommand,3,"",0,NULL,0,0,0,0,0},
    {"info",sentinelInfoCommand,-1,"",0,NULL,0,0,0,0,0},
    {"role",sentinelRoleCommand,1,"ok-loading",0,NULL,0,0,0,0,0},
    {"client",clientCommand,-2,"read-only no-script",0,NULL,0,0,0,0,0},
    {"shutdown",shutdownCommand,-1,"",0,NULL,0,0,0,0,0},
    {"auth",authCommand,2,"no-auth no-script ok-loading ok-stale fast",0,NULL,0,0,0,0,0},
    {"hello",helloCommand,-2,"no-auth no-script fast",0,NULL,0,0,0,0,0}
};

16.1.3 Sentinel状态保存

  • 服务器一般状态还使用redisServer,sentinelState状态保存服务器中和sentinel相关的状态
  • 成员变量部分介绍
    • current_epoch:当前纪元,用来实现故障转移
    • masters:所有被sentinel监视的主服务器,字典类型,key是主服务器的名字,value是指向sentinelRedisInstance指针
//code2 sentinel.c
/* Main state. */
struct sentinelState {
    char myid[CONFIG_RUN_ID_SIZE+1]; /* This sentinel ID. */
    uint64_t current_epoch;         /* Current epoch. */
    dict *masters;      /* Dictionary of master sentinelRedisInstances.
                           Key is the instance name, value is the
                           sentinelRedisInstance structure pointer. */
    int tilt;           /* Are we in TILT mode? */
    int running_scripts;    /* Number of scripts in execution right now. */
    mstime_t tilt_start_time;       /* When TITL started. */
    mstime_t previous_time;         /* Last time we ran the time handler. */
    list *scripts_queue;            /* Queue of user scripts to execute. */
    char *announce_ip;  /* IP addr that is gossiped to other sentinels if
                           not NULL. */
    int announce_port;  /* Port that is gossiped to other sentinels if
                           non zero. */
    unsigned long simfailure_flags; /* Failures simulation. */
    int deny_scripts_reconfig; /* Allow SENTINEL SET ... to change script
                                  paths at runtime? */
} sentinel;

16.1.4 Sentinel状态保存masters属性

  • sentinelRedisInstance代表每一个被sentinel监视的redis服务器实例(主服务器、从服务器、sentinel实例)
  • 成员变量部分介绍
    • name: 主服务器的名字在配置文件配置,从服务器以及sentinel名字是sentinel自动设置(ip:port)
    • down_after_period:实例无响应多少毫秒之后会被判断为主观下线
    • quorum:判断实例下线所需的支持投票数量
    • parallel_syncs:故障转移过程中,可以同时对主服务器进行同步的从服务器数量
    • failover_timeout:刷新故障迁移状态的最大时限
    • sentinelAddr *addr:实例ip和端口号
    • dict slaves; / Slaves for this master instance. */
//code3 sentinel.c
typedef struct sentinelRedisInstance {
    int flags;      /* See SRI_... defines */
    char *name;     /* Master name from the point of view of this sentinel. */
    char *runid;    /* Run ID of this instance, or unique ID if is a Sentinel.*/
    uint64_t config_epoch;  /* Configuration epoch. */
    sentinelAddr *addr; /* Master host. */
    instanceLink *link; /* Link to the instance, may be shared for Sentinels. */
    mstime_t last_pub_time;   /* Last time we sent hello via Pub/Sub. */
    mstime_t last_hello_time; /* Only used if SRI_SENTINEL is set. Last time
                                 we received a hello from this Sentinel
                                 via Pub/Sub. */
    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. */
    mstime_t down_after_period; /* Consider it down after that period. */
    mstime_t info_refresh;  /* Time at which we received INFO output from it. */
    dict *renamed_commands;     /* Commands renamed in this instance:
                                   Sentinel will use the alternative commands
                                   mapped on this table to send things like
                                   SLAVEOF, CONFING, INFO, ... */

    /* 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. */
    dict *sentinels;    /* Other sentinels monitoring the same master. */
    dict *slaves;       /* Slaves for this master instance. */
    unsigned int quorum;/* Number of sentinels that need to agree on failure. */
    int parallel_syncs; /* How many slaves to reconfigure at same time. */
    char *auth_pass;    /* Password to use for AUTH against master & replica. */
    char *auth_user;    /* Username for ACLs AUTH against master & replica. */

    /* Slave specific. */
    mstime_t master_link_down_time; /* Slave replication link down time. */
    int slave_priority; /* Slave priority according to its INFO output. */
    mstime_t slave_reconf_sent_time; /* Time at which we sent SLAVE OF <new> */
    struct sentinelRedisInstance *master; /* Master instance if it's slave. */
    char *slave_master_host;    /* Master host as reported by INFO */
    int slave_master_port;      /* Master port as reported by INFO */
    int slave_master_link_status; /* Master link status as reported by INFO */
    unsigned long long slave_repl_offset; /* Slave replication offset. */
    /* Failover */
    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. */
    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. */
    char *notification_script;
    char *client_reconfig_script;
    sds info; /* cached INFO output */
} sentinelRedisInstance;

16.1.5 创建连接主服务器的网络连接

  • 被sentinel监视的主服务器,会有两个异步连接
    • 向主服务器发送命令,并接收命令回复
    • 订阅连接,订阅主服务器的__sentinel__:hello

16.2 获取主服务器信息

  • sentinel每十秒一次给主服务器发送INFO,获取以下信息:
    • 主服务器本身信息:run_id、role服务器角色
    • 主服务器下的从服务器信息:ip地址、port端口号等
      • 分析INFO命令中的从服务器,更新或者新增sentinelRedisInstance中的slaves字典
      • 主服务器实例结构的flags是SRI_MASTER,从服务器是SRI_SLAVE
//code4 sentinel.c
/* Return the name of the type of the instance as a string. */
const char *sentinelRedisInstanceTypeStr(sentinelRedisInstance *ri) {
    if (ri->flags & SRI_MASTER) return "master";
    else if (ri->flags & SRI_SLAVE) return "slave";
    else if (ri->flags & SRI_SENTINEL) return "sentinel";
    else return "unknown";
}

16.3 获取从服务器信息

  • sentinel发现主服务器有从服务器,会创建对应的实例结构,还会创建连接到从服务器的命令连接和订阅连接
  • sentinel默认情况下,会每十秒通过命令连接向从服务器发送INFO命令,获取以下信息:
    • 从服务器的run_id
    • 从服务器的role
    • 主服务器的ip地址和端口
    • 主从服务器的连接状态
    • 主从服务器的优先级
    • 从服务器的复制偏移量

16.4 向主服务器和从服务器发送信息

  • 默认情况下,sentinel会每两秒一次,向__sentinel__:hello频道发送消息
    • s_开头记录sentinel本身的信息
    • m_记录的是主服务器的信息(如果监视的是从服务器,参数记录的是从服务器正在复制的主服务器的信息)

16.5 接收来自主服务器和从服务器的频道信息

  • 作用
    • 监视一个服务器可能有多个sentinel,一个sentinel发送的信息其他sentinel会接收到(通过订阅)
  • 如何实施?
    • sentinel收到一条信息,会对这个信息进行分析,提取端口号和ip
      + 如果发现是自己,不作处理
      + 如果发现不是自己,根据参数对主服务器的实例结构进行更新

16.5.1 更新sentinels字典

  • sentinel可以分析接收到的频道信息知道其他sentinel的存在(每两秒sentinel会发给服务器)

16.5.2 创建连接到其他sentinel的命令连接

  • sentinel通过频道发现监视同一服务器的其他sentinel,两个sentinel之间会形成连接(只有命令连接用来通信,没有订阅连接),最后监视同一主服务器的多个sentinel将形成相互连接的网络

16.6 检测主观下线状态

  • 默认每秒一次频率向所有创建命令连接的实例(主服务器、从服务器、其他sentinel)发送PING命令,并通过实例返回的PING命令来判断
  • 对PING的回复
    • 有效回复:PONG、LOADING、MASTERDOWN
    • 无效回复:不是有效回复中的三种回复;在指定时间内没有返回任何回复
      • sentinel配置文件中down_after_period指定了sentinel判断实例主观下线所需的时间长度,如果down_after_period毫秒内,连续向sentinel返回无效回复,sentinel会修改这个实例所对应的实例结构,在结构中的flags属性中打开SRI_S_DOWN标识(表明实例主观下线)
      • down_after_period:
        • 作用范围,不仅可以用来判断主服务器的主观下线状态,还可以判断master属下所有从服务器,以及所有监视master的sentinel进入主观下线的标准
        • 多个sentinel设置的主观下线时长可能不同,sentinel1认为master主观下线了,但是sentinel2认为master仍然在线

16.7 检测客观下线状态

  • sentinel从其他sentinel接收到足够数量的已下线判断后,sentinel会认为服务器为客观下线,并对其执行故障转移操作

16.7.1 发送命令询问主服务器是否下线

  • sentinel is-master-down-by-addr <current_epoch>
  • 参数含义
    • ip:被sentinel判断主观下线的服务器的ip地址
    • port:被sentinel判断主观下线的服务器的端口号
    • current_epoch:sentinel当前配置纪元,用于选举领头sentinel
    • runid:也可以是*或者sentinel的运行id
      • *:该命令只用于检测主服务器的客观下线状态
      • sentinel的运行id:选举领头sentinel

16.7.2 接收询问主服务器是否下线的命令

  • 目标sentinel收到命令后,会取出参数,根据ip和端口号,检查主服务器是否下线,然后向源sentinel返回包含三个参数的multi bulk作为回复
    • <down_state>:1表示已下线,0表示未下线
    • <leader_runid>:
      • *:该命令只用于检测主服务器的客观下线状态
      • sentinel的运行id:选举领头sentinel
    • <leader_epoch>
      • 0 (<leader_runid> 为*)
      • 目标sentinel的局部领头sentinel的配置纪元,用于选举领头sentinel

16.7.3 判断是否客端下线

  • 根据其他sentinel的回复,sentinel会统计同意下线的数量,如果大于等于quorum,可以判断客端下线,sentinel会将主服务器实例结构的flags的SRI_O_DOWN标识打开
  • 多个sentinel设置的quorum可能不同,所以客观下线可能不同

16.8 选举领头sentinel

  • 当一个主服务器被判断为客观下线,监控下线主服务器的sentinel会进行协商,选举一个领头的sentinel,领头的sentinel对下线主服务器进行故障转移
  • 如何选举
    • 所有的sentinel都有可能成为领头
    • 每次领头选举之后,不论选举是否成功,sentinel的配置纪元都会自增一次
    • 局部领头被设置之后,配置纪元就不能再修改
    • 每个发现主服务器进入客观下线的sentinel,会要求其他sentinel把自己设置成局部领头
    • 源sentinel发给目标sentinel的is-master-down-by-addr表示:源要求目标把源设置成目标的局部领头sentinel
    • 目标sentinel的局部领头,哪个源先发过来就设置成哪个(先到先得)
    • 目标收到sentinel is-master-down-by-addr,会向sentinel返回一条命令回复,回复中leader_runid和leader_epoch分别记录了目标sentinel的局部领头的信息
    • 源收到目标sentinel,如果leader_epoch和自己的配置纪元相同,取出leader_runid。如果leader_runid和自己的runid一致,表明源就是leader
    • 如果某个sentinel被半数以上的sentinel设置成局部领头,它就变成真正的领头sentinel
    • 每个配置纪元只能设置一次局部领头,需要得到半数以上的支持,所以在配置纪元里面只会出现一个领头sentinel
    • 如果给定时间没有选出来,那么sentinel会在一段时间之后再次进行选举,直到选择出来

16.9 故障转移

  • 选出领头sentinel之后,领头sentinel会对已下线的主服务器执行故障转移操作,有三步
    • 从已下线的主服务器中选择一个从服务器,将其转换成主服务器
    • 已下线主服务器下面的从服务器,修改成复制新的主服务器
    • 将已下线的主服务器设置成新服务器的从服务器。如果旧的主服务器重新上线,就会变成新的主服务器的从服务器

16.9.1 选出新的主服务器

  • 选择一个状态良好、数据完整的从服务器,向这个从服务器发送SLAVE no one命令,将它变成主服务器
  • 怎么选择
    • 把主服务器的所有从服务器保存到一个列表中
    • 删除下线或者短线的服务器
    • 删除最近5s内没有回复领头的INFO命令的从服务器
    • 删除所有与已下线主服务器连接断开超过10毫秒的服务器,留下来的数据比较新
    • 根据从服务器的优先级,对列表中的进行排序,选出优先级最高的从服务器(根据复制偏移量选择最大的,运行ID最小的)

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

  • server1下线,server2变成主,server3和server4是从
  • 领头sentinel发送命令给server2和server3:SLAVEOF SERVER2IP SERVER2PORT

16.9.3 将旧的主服务器变成从服务器

  • server1上线,领头sentinel向server1发送SLAVEOF命令,让它变成server2的从服务器
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值