Redis源码阅读(五)主从复制与哨兵机制

2022-01-28 17:06:18 浏览数 (1)

Redis 单节点存在单点故障问题,为了解决单点问题,一般都需要对 Redis 配置从节点,然后使用哨兵来监听主节点的存活状态,如果主节点挂掉,从节点能继续提供缓存功能。

一、主从复制

主从复制一般用于实现数据的读写分离,主节点提供写操作,从节点提供读操作,适用于读多写少的场景。

字段解析:

  • runId:每个 Redis 节点启动都会生成唯一的 uuid,每次 Redis 重启后,runId 都会发生变化。
  • offset:主节点和从节点都各自维护自己的主从复制偏移量 offset 当主节点有写入命令时,offset=offset 命令的字节长度; 从节点在收到主节点发送的命令后,也会增加自己的 offset,并把自己的 offset 发送给主节点。 这样,主节点同时保存自己的 offset 和从节点的 offset,通过对比 offset 来判断主从节点数据是否一致。
  • repl_backlog_size:保存在主节点上的一个固定长度的先进先出队列,默认大小是 1MB。

A. 复制过程

(1)从节点执行 slaveof [masterIP] [masterPort],主动连接主服务器请求同步数据。

slaveof命令的处理函数为replicaofCommand:

代码语言:javascript复制
void replicaofCommand(client *c) {
    // slaveof no one命令可以取消复制功能
    if (!strcasecmp(c->argv[1]->ptr,"no") &&
        !strcasecmp(c->argv[2]->ptr,"one")) {
    } else {
        // 记录主服务器的Ip地址和端口
        server.masterhost = sdsnew(ip);
        server.masterport = port;
        server.repl_state = REPL_STATE_CONNECT;
    }
    addReply(c,shared.ok);
}

(2)从节点中的定时任务发现主节点信息,建立和主节点的 Socket 连接。

replicaofCommand函数只是记录主服务器IP地址与端口,并没有向主服务器发起连接请求。

说明连接建立是异步的,主从复制的相关操作是在时间事件处理函数serverCron中进行的:

代码语言:javascript复制
// 以一秒为周期执行主从复制相关操作
run_with_period(1000) replicationCron();

在replicationCron中,从服务器向主服务器发起连接请求:

代码语言:javascript复制
if (server.repl_state == REPL_STATE_CONNECT) {
    // 在连接中,会创建对应的文件事件
    if (connectWithMaster() == C_OK) {
        serverLog(LL_NOTICE,"MASTER <-> REPLICA sync started");
        server.repl_state = REPL_STATE_CONNECTING;
    }
}

(3)从节点发送 Ping 信号,主节点返回 Pong,两边能互相通信。

Ping包:ping包由一个包头 (type字段为CLUSTERMSG_TYPE_PING(0)) 和多个gossip section (记录的关于其他节点的状态信息,包括节点名称、IP地址、状态以及监听地址,等等)组成。Redis集群中每个节点通过心跳包可以知道其他节点的当前状态并且保存到本节点状态中。

Pong包:pong包格式同ping包,只是包头中的type字段写为CLUSTERMSG_TYPE_PONG(1)。

注意pong包除了在接收到ping包和meet包之后会作为回复包发送之外,当进行主从切换之后,新的主节点会向集群中所有节点直接发送一个pong包,通知主从切换后节点角色的转换。

(4)连接建立后,从节点向主节点发送psync命令请求同步数据,主节点将所有数据发送给从节点(数据同步)。

主服务器处理psync命令的入口函数为syncCommand。

【Redis 2.8 之后使用 psync [runId] [offset] 命令;支持全量和部分复制;

Redis 4.0针对主从复制又提出了两点优化,提出了psync2协议】

psync2 他的好处在于 redis 主从切换后,不需要重新进行 重新 fullsync 同步,只需要部分同步,有点类似 binlog 那种

(5)主节点把当前的数据同步给从节点后,便完成了复制的建立过程;然后,主节点就会持续的把写命令发送给从节点,保证主从数据一致性。

主服务器每次接收到写命令请求时,都会将该命令请求广播给所有从服务器,同时记录在复制缓冲区中。向从服务器广播命令请求的实现函数为replicationFeedSlaves,逻辑如下:

代码语言:javascript复制
void replicationFeedSlaves(list *slaves, int dictid, robj **argv, int argc) {
   //如果与上次选择的数据库不相等,需要先同步select命令
   if (server.slaveseldb != dictid) {
     //将select命令添加到复制缓冲区
     if (server.repl_backlog)
        feedReplicationBacklogWithObject(selectcmd);
     //向所有从服务器发送select命令
     while((ln = listNext(&li))) {
       addReply(slave,selectcmd);
     }
   }
   server.slaveseldb = dictid;
   if (server.repl_backlog) {
     //将当前命令请求添加到复制缓冲区
   }
​
   while((ln = listNext(&li))) {
     //向所有从服务器同步命令请求
  }
}

B. 全量复制

1⃣️ 从节点发送 psync ? -1 命令(因为第一次发送,不知道主节点的 runId,所以为?,因为是第一次复制,所以 offset=-1)。

2⃣️ 主节点发现从节点是第一次复制,返回 FULLRESYNC {runId} {offset},runId 是主节点的 runId,offset 是主节点目前的 offset。

3⃣️ 从节点接收主节点信息后,保存到 info 中。

4⃣️ 主节点在发送 FULLRESYNC 后,启动 bgsave 命令,生成 RDB 文件(数据持久化)。

5⃣️ 主节点发送 RDB 文件给从节点。到从节点加载数据完成这段期间主节点的写命令放入缓冲区。

6⃣️ 从节点清理自己的数据库数据。

7⃣️ 从节点加载 RDB 文件,将数据保存到自己的数据库中

8⃣️ 主节点再将缓冲区中的写命令发送给从节点

C. 增量复制

1⃣️ 部分复制主要是 Redis 针对全量复制的过高开销做出的一种优化措施,使用 psync [runId] [offset] 命令实现。

当从节点正在复制主节点时,如果出现网络闪断或者命令丢失等异常情况时,从节点会向主节点要求补发丢失的命令数据,主节点的复制积压缓冲区将这部分数据直接发送给从节点。

这样就可以保持主从节点复制的一致性。补发的这部分数据一般远远小于全量数据。

2⃣️ 主从连接中断期间主节点依然响应命令,但因复制连接中断命令无法发送给从节点,不过主节点内的复制积压缓冲区依然可以保存最近一段时间的写命令数据。

3⃣️ 当主从连接恢复后,由于从节点之前保存了自身已复制的偏移量主节点的运行 ID。因此会把它们当做 psync 参数发送给主节点,要求进行部分复制

4⃣️ 主节点接收到 psync 命令后首先核对参数 runId 是否与自身一致,如果一致,说明之前复制的是当前主节点。

之后根据参数 offset 在复制积压缓冲区中查找,如果 offset 之后的数据存在,则对从节点发送 COUTINUE 命令,表示可以进行部分复制。因为缓冲区大小固定,若发生缓冲溢出,则进行全量复制。

5⃣️ 主节点根据偏移量把复制积压缓冲区里的数据发送给从节点,保证主从复制进入正常状态

代码语言:javascript复制
int masterTryPartialResynchronization(client *c) {
    //判断服务器运行ID是否匹配,复制偏移量是否合法
    if (strcasecmp(master_replid, server.replid) &&
       (strcasecmp(master_replid, server.replid2) ||
        psync_offset > server.second_replid_offset))
    {
        goto need_full_resync;
    }
​
    //判断复制偏移量是否包含在复制缓冲区
    if (!server.repl_backlog ||
        psync_offset < server.repl_backlog_off ||
        psync_offset > (server.repl_backlog_off     
               server.repl_backlog_histlen))
    {
        goto need_full_resync;
    }
    //部分重同步,标识从服务器
    c->flags |= CLIENT_SLAVE;
    c->replstate = SLAVE_STATE_ONLINE;
    c->repl_ack_time = server.unixtime;
    //将该客户端添加到从服务器链表slaves
    listAddNodeTail(server.slaves,c);
​
   //根据从服务器能力返回 CONTINU
    if (c->slave_capa & SLAVE_CAPA_PSYNC2) {
        buflen = snprintf(buf,sizeof(buf)," CONTINUE %srn", server.replid);
    } else {
        buflen = snprintf(buf,sizeof(buf)," CONTINUErn");
    }
    if (write(c->fd,buf,buflen) != buflen) {
    }
    //向客户端发送复制缓冲区中的命令请求
    psync_len = addReplyReplicationBacklog(c,psync_offset);
    //更新有效从服务器数目
    refreshGoodSlavesCount();
    return C_OK; /* The caller can return, no full resync needed. */
​
need_full_resync:
    return C_ERR;
}

二、哨兵机制

一旦主节点宕机,从节点晋升为主节点,同时需要修改应用方的主节点地址,还需要命令所有从节点去复制新的主节点,整个过程需要人工干预。因此,可以使用哨兵机制来管理这个过程。

哨兵是Redis的高可用方案,可以在Redis Master发生故障时自动选择一个Redis Slave切换为Master,继续对外提供服务。

Redis Sentinel 最小配置是一主一从,该系统可以执行以下四个任务:

  • 监控:不断检查主服务器和从服务器是否正常运行。
  • 通知:当被监控的某个 Redis 服务器出现问题,Sentinel 通过 API 脚本向管理员或者其他应用程序发出通知。
  • 自动故障转移:当主节点不能正常工作时,Sentinel 会开始一次自动的故障转移操作,它会将与失效主节点是主从关系的其中一个从节点升级为新的主节点,并且将其他的从节点指向新的主节点。
  • 配置提供者:在 Redis Sentinel 模式下,客户端应用在初始化时连接的是 Sentinel 节点集合,从中获取主节点的信息。

A. 典型配置文件

代码语言:javascript复制
// 监控一个名称为mymaster的Redis Master服务,地址和端口号为127.0.0.1:6379,quorum为2
sentinel monitor mymaster 127.0.0.1 6379 2 
​
// 如果哨兵60s内未收到mymaster的有效ping回复,则认为mymaster处于down的状态
sentinel down-after-milliseconds mymaster 60000
// 执行切换的超时时间为180s
sentinel failover-timeout mymaster 180000
​
// 切换完成后,同时向新的Redis Master发起同步数据请求的Redis Slave个数为1
// 即切换完成后依次让每个Slave去同步数据,前一个Slave同步完成后下一个Slave才发起同步数据的请求
sentinel parallel-syncs mymaster 1
​
// 监控一个名称为resque的Redis Master服务,地址和端口号为127.0.0.1:6380,quorum为4
// quorum:①将master标记客观下线所需的哨兵个数;②选举哨兵执行主从切换所需的票数
sentinel monitor resque 192.168.1.3 6380 4

B. 工作原理

① 每个 Sentinel 节点都需要定期执行以下任务:每个 Sentinel 以每秒一次的频率,向它所知的主服务器、从服务器以及其他的 Sentinel 实例发送一个 PING 命令。

② 如果一个 slave 距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 所指定的值,那么这个slave 会被 Sentinel 标记为主观下线。

③ 如果一个 master 被标记为主观下线,那么正在监视这个 master 的所有 Sentinel 节点,要以每秒一次的频率确认 master 的确进入了主观下线状态。

④ 如果一个 master 被标记为主观下线,并且有足够数量的 Sentinel(至少要达到配置文件指定的数量)在指定的时间范围内同意这一判断,那么这个 master 被标记为客观下线。

⑤ 一般情况下,每个 Sentinel 会以每 10 秒一次的频率向它已知的所有 mater 和 slave 发送 INFO 命令。

当一个 master 被标记为客观下线时,Sentinel 向下线 master 的所属 slave 发送 INFO 命令的频率,由 10 秒一次改为 1 秒一次。

⑥ 所有 Sentinel 协商客观下线的 master 的状态,如果处于 SDOWN 状态,则投票自动选出新的主节点,将剩余从节点指向新的主节点进行数据复制。

⑦ 当没有足够数量的 Sentinel 同意 master 下线时,master 的客观下线状态就会被移除。

当 master 重新向 Sentinel 的 PING 命令返回有效回复时,主服务器的主观下线状态就会被移除。

C. 代码流程

(1)启动:redis-server

代码语言:javascript复制
redis-server /path/to/sentinel.conf --sentinel

执行命令后,执行的主要代码逻辑如下:

代码语言:javascript复制
main(){
    ...
    //检测是否以sentinel模式启动
    server.sentinel_mode = checkForSentinelMode(argc,argv);
    ...
    if (server.sentinel_mode) {
        initSentinelConfig();        // 将监听端口置为26379
        initSentinel();              // 更改哨兵可执行命令。哨兵中只能执行有限的几种服务端命令,如ping,sentinel,subscribe,publish,info等等。该函数还会对哨兵进行一些初始化
    }
    ...
    sentinelHandleConfiguration();        // 解析配置文件,进行初始化
    ...
    sentinelIsRunning();                // 随机生成一个40字节的哨兵ID,打印启动日志
    ...
}

在main函数的主流程中,只是对其进行了一些初始化。真正建立命令连接和消息连接的操作是在定时任务serverCron中:

代码语言:javascript复制
serverCron(){
  // ...
  
    // 哨兵模式下,用于建立连接,并且定时发送心跳包并采集信息
    if (server.sentinel_mode) sentinelTimer();
    
    // ...
}

该函数主要功能如下:

  1. 建立命令连接和消息连接。消息连接建立之后会订阅Redis服务的sentinel:hello频道。
  2. 在命令连接上每10s发送info命令进行信息采集;每1s在命令连接上发送ping命令探测存活性;每2s在命令连接上发布一条信息,信息格式如下: sentinel_ip,sentinel_port,sentinel_runid,current_epoch,master_name,master_ip,master_port,master_config_epoch 上述参数分别代表哨兵的IP、哨兵的端口、哨兵的ID(即上文所述40字节的随机字符串)、当前纪元(用于选举和主从切换)、Redis Master的名称、Redis Master的IP、Redis Master的端口、Redis Master的配置纪元(用于选举和主从切换)。
  3. 检测服务是否处于主观下线状态。
  4. 检测服务是否处于客观下线状态并且需要进行主从切换。

(2)如果判断一个Redis Master处于客观下线状态,则需要开始执行主从切换

Redis中选择规则如下:

  1. 如果该Slave处于主观下线状态,则不能被选中。
  2. 如果该Slave 5s之内没有有效回复ping命令或者与主服务器断开时间过长,则不能被选中。
  3. 如果slave-priority为0,则不能被选中(slave-priority可以在配置文件中指定。正整数,值越小优先级越高,当指定为0时,不能被选为主服务器)。
  4. 在剩余Slave中比较优先级,优先级高的被选中;如果优先级相同,则有较大复制偏移量的被选中;否则按字母序选择排名靠前的Slave。

该状态需要把选择的Redis Slave切换为Redis Master,即哨兵向该Slave发送如下命令:

代码语言:javascript复制
MULTI              //开启一个事务
SLAVEOF NO ONE     //关闭该从服务器的复制功能,将其转换为一个主服务器
CONFIG REWRITE     //将redis.conf文件重写(会根据当前运行中的配置重写原来的配置)
CLIENT KILL TYPE normal // 关闭连接到该服务的客户端(关闭之后客户端会重连,重新获取Redis Master的地址)
EXEC               //执行事务

哨兵会依次向其他从服务器发送切换主服务器的命令,如下:

代码语言:javascript复制
MULTI              //开启一个事务
SLAVEOF IP PORT    //将该服务器设置为向新的主服务器请求数据 
CONFIG REWRITE     //将redis.conf文件重写(会根据当前运行中的配置重写原来的配置)
CLIENT KILL TYPE normal //关闭连接到该服务的客户端(关闭之后客户端会重连,重新获取Redis Master的地址)
EXEC               //执行事务

同时,在此过程中,哨兵遵循相应的状态转换。

0 人点赞