Redis主从同步浅析

太累了就写这么多吧,有空补补

概述

《数据密集型应用》 一书中写道:

数据分布在多个节点上有两种常见的方式:

1.复制(Replication)在几个不同的节点上保存数据的相同副本,可能放在不同的位置。 复制提供了冗余:如果一些节点不可用,剩余的节点仍然可以提供数据服务,复制也有助于改善性能

2.将一个大型数据库拆分成较小的子集(称为分区,即 partitions),从而不同的分区可以指派给不同的节点

虽然Redis可能更像是计算密集型的应用,但是书中的观点依然十分适用。复制意味着在通过网络连接的多台机器上保留相同数据的副本我们希望能复制数据,可能出于各种各样的原因:

  • 使得数据与用户在地理上接近(从而减少延迟)
  • 即使系统的一部分出现故障,系统也能继续工作(从而提高可用性)
  • 伸缩可以接受读请求的机器数量(从而提高读取吞吐量) 我们今天着重想要讲述的就是第二种情况,尝试分析Redis在高可用方面到底做出了什么努力。

数据复制类型

Redis中主从之间的数据复制,包含三种类型以及常见使用场景如下:

  • 全量复制
    第一次建立主从关系,初次同步数据
    从节点与主节点断开时间比较久,再次同步数据
    从节点断开时间并不长久,但是期间主节点有大量写命令执行
  • 增量复制
    从节点断开时间较短,且期间主节点并没有大量写命令执行
  • 同步复制
    主从节点之间通讯正常,主节点执行写命令,同步数据至主节点

如何构建主从

假设我们想要搭建一个最简陋的主从集群,仅仅只需要启动A,B两个Redis实例,如果我们想要A主B从,通常我们有三种方式来指定主从关系,

  • B实例正常启动之后上执行replicaof masterip masterport的主从复制命令,指明A实例的IP(masterip)和端口号(masterport)。
  • B实例启动时,设置启动参数--replicaof masterip masterport。该实例会解析启动参数,获得主库的IP和端口号。
  • B实例启动时制定配置文件,在配置文件中添加replicaof masterip masterport配置项,该实例解析配置文件时会自动找到主节点。

可以看到笔者启动了两个Redis实例:分别在本机的6379、6400端口 然后我们指定6400的主节点是6379,很简单只需要像我一样执行一下replicaof masterip masterport命令。 可以清晰的看到控制台已经指示我们指定主节点成功,此时从节点已经可以正常同步数据了。比如我们可以在主节点上执行set a a命令,按道理应该已经可以将数据同步到从节点可事实并非如此,不信你看,从节点根本就没有数据。难道是延迟?恐怕也不是,因为我前后执行了两次get命令,都是没有数据的。 绝大多数文章都默认大家已经熟知了所有的细节,启动主从集群毫不费事,可事实上一不小心就踩到坑了。我们看看究竟怎么操作,才能挽救这个从节点。

首先我们可以使用config get masterauth命令查看Redis从节点的配置信息,默认情况下是一个""空串,通过config set masterauth修改该配置信息。可以看到修改之后,从节点立马就可以获取到主节点的数据了。 为什么需要修改如下配置呢,我们看一下Redis的官方说明。 也就是说想要打通主从之间的数据通道,我们还需要在配置文件中指定masterauth信息,且必须与主节点的密码一致。

当然你也可以不修改配置文件,像笔者那样启动Redis实例之后,执行config set命令,修改配置也是可以的,只不过修改文件一劳永逸。

主从连接过程

原本笔者认为主从建立连接应该是一个比较简单的过程,况且Redis对网络操作也做了简单的封装,不太需要我们关注细节,但是定睛一看,Redis设计了复杂的精准的状态描述,状态的定义起源可以追溯到Redis Server启动之初。

众所周知,即使程序再怎么复杂,但是入口函数总是确定的。我们看看server.c中的定义的主函数,笔者只挑选跟状态相关的逻辑。

c 复制代码
int main(int argc, char **argv) {
    initServerConfig();
    initServer();
}

initServerConfig

不难看出initServerConfig函数应该是要初始化一些配置信息。在5.+版本的源码中描述主从信息的属性高达30多个,其中用来描述主从复制状态的就是repl_state。

c 复制代码
void initServerConfig(void) {
    server.repl_state = REPL_STATE_NONE;
}

通过代码得知,配置信息的初始化之后,当前服务的repl_state被置为REPL_STATE_NONE状态。上文中的server是redisServer结构体实例,该数据结构定义在server.h文件中,里面详细记录了当前服务所有的global属性。

initServer

initServer函数是Redis启动的重中之重包含但不仅限于忽略SIGHUP、SIGPIPE这两个信号、重新定义SIGTERM、 SIGINT这两个信号的处理、继续为上述server属性赋值、创建epoll对象......超级多的任务。

c 复制代码
void initServer(void) {
    /* 忽略SIGHUP、SIGPIPE这两个信号 */
    signal(SIGHUP, SIG_IGN);
    signal(SIGPIPE, SIG_IGN);
    /* 重新定义SIGTERM、 SIGINT这两个信号的处理 */
    setupSignalHandlers();
    
    /* 继续为server中的若干属性进行赋值 */
    server.hz = server.config_hz;
    ...
    server.system_memory_size = zmalloc_get_memory_size();
    
    ...
    
    /* 创建epoll */
    server.el = aeCreateEventLoop(server.maxclients+CONFIG_FDSET_INCR);
    
    ...
    
    /* 创建事件 */
    if (aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) {
        serverPanic("Can't create event loop timers.");
        exit(1);
    }
    
    ...
}

忽略了超级多其他细节,着重看最后aeCreateTimeEvent函数,这里的作用是创建定时任务,这是Redis增量处理许多后台操作的方式,例如客户端超时、未访问的过期键的驱逐等。serverCron会周期性的执行replicationCron函数。

replicationCron

replicationCron函数先获取repl_state状态,然后根据状态作出对应的超时判定。当且仅当repl_state == REPL_STATE_CONNECT条件成立的时候我们才会与主库建立连接。

c 复制代码
void replicationCron(void) {
    /* 判定是否连接超时 */
    if (server.masterhost &&
        (server.repl_state == REPL_STATE_CONNECTING || slaveIsInHandshakeState()) &&
        (time(NULL)-server.repl_transfer_lastio) > server.repl_timeout)
    {
        serverLog(LL_WARNING,"Timeout connecting to the MASTER...");
        cancelReplicationHandshake();
    }
    
    /* 判定批量传输是否超时 */
    if (server.masterhost && server.repl_state == REPL_STATE_TRANSFER &&
        (time(NULL)-server.repl_transfer_lastio) > server.repl_timeout)
    {
        serverLog(LL_WARNING,"Timeout receiving bulk data from MASTER... If the problem persists try to set the 'repl-timeout' parameter in redis.conf to a larger value.");
        cancelReplicationHandshake();
    }
    
    /* 主从建立连接,判定主节点是不是超时了 */
    if (server.masterhost && server.repl_state == REPL_STATE_CONNECTED &&
        (time(NULL)-server.master->lastinteraction) > server.repl_timeout)
    {
        serverLog(LL_WARNING,"MASTER timeout: no data nor PING received...");
        freeClient(server.master);
    }
    
    /* 判定状态是不是REPL_STATE_CONNECT */
    if (server.repl_state == REPL_STATE_CONNECT) {
        serverLog(LL_NOTICE,"Connecting to MASTER %s:%d",server.masterhost, server.masterport);
        /* 建立主从之间的网络连接 */
        if (connectWithMaster() == C_OK) {
            serverLog(LL_NOTICE,"MASTER <-> REPLICA sync started");
        }
    }
    
    ...
}

masterhost表示当前节点的master的ip信息。有这个判定也很合理,当masterhost都不存在,根本就不用继续后续的逻辑。

replicaofCommand

进行主从连接的前提:repl_state必须等于REPL_STATE_CONNECT,如果没有别的操作,根据前文可以得知repl_state的状态应该是REPL_STATE_NONE。如果redis.conf文件中没有配置replicaof信息,我们需要手动执行replicaof命令才能完成主从连接,不难联想到这个命令应该会导致repl_state状态的修改。 replicaofCommand函数负责处理replicaof命令,该函数的实现在replication.c文件中。

c 复制代码
void replicaofCommand(client *c) {
    /* 本身就是cluster集群模式中的分片节点,不执行replicaof命令 */
    if (server.cluster_enabled) {
        addReplyError(c,"REPLICAOF not allowed in cluster mode.");
        return;
    }
    
    /* 处理特殊情况,replicaof命令后追加的参数是no one*/
    if (!strcasecmp(c->argv[1]->ptr,"no") &&
        !strcasecmp(c->argv[2]->ptr,"one")) {
        /* 有主节点的相关信息 */
        if (server.masterhost) {
            /* 取消主从复制,自己切换回主模式 */
            replicationUnsetMaster();
            sds client = catClientInfoString(sdsempty(),c);
            serverLog(LL_NOTICE,"MASTER MODE enabled (user request from '%s')",client);
            sdsfree(client);
        }
    } else {
        long port;
        /* 这个判定没太懂是什么情景 */
        if (c->flags & CLIENT_SLAVE) {
            addReplyError(c, "Command is not valid when client is a replica.");
            return;
        }
        /* 解析端口,对port赋值 */
        if ((getLongFromObjectOrReply(c, c->argv[2], &port, NULL) != C_OK))
            return;
        
        /* 判定是否已经连接到相同ip port的主节点,如果是则提前返回,并回复OK Already connected to specified master信息 */
        if (server.masterhost && !strcasecmp(server.masterhost,c->argv[1]->ptr)
            && server.masterport == port) {
            serverLog(LL_NOTICE,"REPLICAOF would result into synchronization with the master we are already connected with. No operation performed.");
            addReplySds(c,sdsnew("+OK Already connected to specified master\r\n"));
            return;
        }
    }
    
    replicationSetMaster(c->argv[1]->ptr, port);
    ...
}

repl_state的状态就是在replicationSetMaster函数中被设置为了REPL_STATE_CONNECT,修改完毕后,下一个周期执行replicationCron的时就会触发connectWithMaster函数建立主从连接。从Tcp连接建立开始,到主从之间传输数据,中间还有一个详细的握手过程。其中涉及到11个状态的流转,都定义在server.h文件中。 为了让过程更加直观,笔者还tcp dump了一个pcap文件。 Redis原代码注释中写道:"Handshake states, must be ordered",我们千万不要误以为所有的阶段都需要,比如REPL_STATE_RECEIVE_IP就可以被跳过,从上述抓取的信息也能看得出来。

c 复制代码
void syncWithMaster(aeEventLoop *el, int fd, void *privdata, int mask) {
    /* Skip REPLCONF ip-address if there is no slave-announce-ip option set. */
    if (server.repl_state == REPL_STATE_SEND_IP &&
        server.slave_announce_ip == NULL) {
            server.repl_state = REPL_STATE_SEND_CAPA;
    }
}

详细的状态流转可以查看syncWithMaster函数,replication.c文件中有详细实现。为了更好的感官体验,更容易体会从库在复制类型判断和执行阶段的状态变迁,这里有张图将主从复制各阶段的状态变迁整合在一起。(出自极客时间《Redis源码剖析与实战》)

相关推荐
小信啊啊14 小时前
Go语言切片slice
开发语言·后端·golang
原来是好奇心16 小时前
深入Spring Boot源码(六):Actuator端点与监控机制深度解析
java·开发语言·源码·springboot
Victor35616 小时前
Netty(20)如何实现基于Netty的WebSocket服务器?
后端
缘不易16 小时前
Springboot 整合JustAuth实现gitee授权登录
spring boot·后端·gitee
Kiri霧16 小时前
Range循环和切片
前端·后端·学习·golang
WizLC16 小时前
【Java】各种IO流知识详解
java·开发语言·后端·spring·intellij idea
Victor35616 小时前
Netty(19)Netty的性能优化手段有哪些?
后端
爬山算法16 小时前
Netty(15)Netty的线程模型是什么?它有哪些线程池类型?
java·后端
小鸡吃米…17 小时前
Python - XML 处理
xml·开发语言·python·开源
白宇横流学长17 小时前
基于SpringBoot实现的冬奥会科普平台设计与实现【源码+文档】
java·spring boot·后端