主从模式

主从结构是常用的计算机系统架构,通常被用于分布式系统中,其中一个节点master拥有最新的数据,其他节点slave复制并同步主节点的数据。

主从结构中,主节点负责写入数据,并将这些数据同步到从节点中;从节点只能读取数据。主从节点键通过网络连接,完成数据同步。

主从模式也常被用于数据库系统中,提供高可用能力。当主节点发生故障或者失效时,从节点可以被选举为新的主节点,保证系统的可用性。

主从模式的优点

  1. 负载均衡:读写分离:提高服务器的性能。
  2. 数据冗余:主从复制实现了数据的热备份,是持久化之外的数据冗余手段。
  3. 高可用基石:主从模式是哨兵模式和集群模式的基础。

主从模式的缺点?

  1. 主从模式不具备自动容错和恢复功能,主节点故障,集群无法工作,可用性较低。从节点升为主节点需要人工手动干预。

为什么redis要使用主从模式?

在了解redis主从结构的原理前,先来了解一下分布式系统的理论基石 CAP原理:

  1. C, Consistent, 即一致性;
  2. A, Availability, 即可用性;
  3. P, Partition, 即分区容忍性。

redis复制过程主从节点之间网络故障时,不能满足强一致性,故而只能满足AP。

如何使用主从复制?

redis 2.8对主从复制功能进行了优化,早先的版本主从复制功能的实现包含两个步骤:

  1. 同步: 用于将从服务器的服务器状态更新至主服务器当前的数据库状态。
  2. 命令传播:用于将 主从同步过程中发生的主服务器的修改命令同步至从服务器,以防止主从数据库状态不一致的情况。

slave of ip port
该命令的作用?

复制过程

主从服务器建立链接

从服务器作为客户端向主服务器发送slaveof master_ip master_port

旧版本复制功能的实现?

旧版本主从复制包含同步命令传播两个部分,如下是整体流程:

旧版复制的缺陷?

主从复制过程有如下两个场景:

  1. 初次复制: slave未同步过任何master,或者当前待复制的主服务器和上一次复制的主服务器不同
  2. 断线后复制:处于命令传播阶段主从服务器因为网络故障而中断,而后网络恢复后继续同步数据库状态。
    在旧版复制功能中,主从服务器发生网络故障后,主从服务器之间数据库状态不一致,slave会向主服务器发送sync命令,主服务器再次生成包含当前数据库状态的rdb文件,而后发送给从服务器,从服务器载入rbd文件后继续执行主服务器发来的写命令。
    上述过程的缺陷是,已经同步过的主从节点断联之后,再次连接时需要主服务器生成包含其当前数据库状态的rdb文件,更为关键的是,此网络故障期间,主节点可能只执行了少数写命令,而需要付出全量更新的开销。

sync命令的开销

sync命令,主从服务器需要执行以下动作:

  1. 主服务器需要执行bgsave命令生成rdb文件,此操作会耗费主服务器的大量cpu、内存和磁盘io资源。
  2. 主服务器将生成的RDB文件发送给从服务器,此操作耗费主从服务器的大量网络资源,并对主服务器的命令响应事件产生影响。
  3. 从服务器载入收到的rdb文件前,无法处理读请求。

全量复制过程?

redis 2.8之前?

sync

旧版本复制的缺陷?

为了解决旧版本复制功能在处理断线重复复制时的低效问题,redis 2.8之后使用psync命令代替sync来执行复制时的同步操作。
psync命令提供两种模式:

  1. 完整重同步: 主服务器创建并发送rdb文件,向从服务器发送保存在缓冲区中的写命令进行同步。
  2. 部分重同步:用于处理断线后的复制情况。断线重连后,如果情况允许,主服务器将主从断联期间执行的写命令发送给从服务器,从服务器接收并执行写命令,完成数据库状态同步。

那么,psync的改进即是感知主从是断联场景,这是如何实现的呢?

部分重同步的实现?

部分重同步功能的实现依赖三个结构:

  1. 主从服务器的复制偏移量 和从服务器的 复制偏移量。
  2. 主服务器的复制积压缓冲区。
  3. 服务器的运行id。
    相应的定义在server.h,注意到,在server中,与主从复制相关的结构定义包含两部分,分别是主服务器配置和从服务器配置。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/* Replication (master) */
char replid[CONFIG_RUN_ID_SIZE+1]; /* My current replication ID. */
char replid2[CONFIG_RUN_ID_SIZE+1]; /* replid inherited from master*/
long long master_repl_offset; /* My current replication offset */
long long second_replid_offset; /* Accept offsets up to this for replid2. */
redisAtomic long long fsynced_reploff_pending;/* Largest replication offset to
* potentially have been fsynced, applied to
fsynced_reploff only when AOF state is AOF_ON
(not during the initial rewrite) */
long long fsynced_reploff; /* Largest replication offset that has been confirmed to be fsynced */
int slaveseldb; /* Last SELECTed DB in replication output */
int repl_ping_slave_period; /* Master pings the slave every N seconds */
replBacklog *repl_backlog; /* Replication backlog for partial syncs */
long long repl_backlog_size; /* Backlog circular buffer size */
time_t repl_backlog_time_limit; /* Time without slaves after the backlog
gets released. */
time_t repl_no_slaves_since; /* We have no slaves since that time.
Only valid if server.slaves len is 0. */
int repl_min_slaves_to_write; /* Min number of slaves to write. */
int repl_min_slaves_max_lag; /* Max lag of <count> slaves to write. */
int repl_good_slaves_count; /* Number of slaves with lag <= max_lag. */
int repl_diskless_sync; /* Master send RDB to slaves sockets directly. */
int repl_diskless_load; /* Slave parse RDB directly from the socket.
* see REPL_DISKLESS_LOAD_* enum */
int repl_diskless_sync_delay; /* Delay to start a diskless repl BGSAVE. */
int repl_diskless_sync_max_replicas;/* Max replicas for diskless repl BGSAVE
* delay (start sooner if they all connect). */
size_t repl_buffer_mem; /* The memory of replication buffer. */
list *repl_buffer_blocks; /* Replication buffers blocks list
* (serving replica clients and repl backlog) */

从服务器配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/* Replication (slave) */
char *masteruser; /* AUTH with this user and masterauth with master */
sds masterauth; /* AUTH with this password with master */
char *masterhost; /* Hostname of master */
int masterport; /* Port of master */
int repl_timeout; /* Timeout after N seconds of master idle */
client *master; /* Client that is master for this slave */
client *cached_master; /* Cached master to be reused for PSYNC. */
int repl_syncio_timeout; /* Timeout for synchronous I/O calls */
int repl_state; /* Replication status if the instance is a slave */
off_t repl_transfer_size; /* Size of RDB to read from master during sync. */
off_t repl_transfer_read; /* Amount of RDB read from master during sync. */
off_t repl_transfer_last_fsync_off; /* Offset when we fsync-ed last time. */
connection *repl_transfer_s; /* Slave -> Master SYNC connection */
int repl_transfer_fd; /* Slave -> Master SYNC temp file descriptor */
char *repl_transfer_tmpfile; /* Slave-> master SYNC temp file name */
time_t repl_transfer_lastio; /* Unix time of the latest read, for timeout */
int repl_serve_stale_data; /* Serve stale data when link is down? */
int repl_slave_ro; /* Slave is read only? */
int repl_slave_ignore_maxmemory; /* If true slaves do not evict. */
time_t repl_down_since; /* Unix time at which link with master went down */
int repl_disable_tcp_nodelay; /* Disable TCP_NODELAY after SYNC? */
int slave_priority; /* Reported in INFO and used by Sentinel. */
int replica_announced; /* If true, replica is announced by Sentinel */
int slave_announce_port; /* Give the master this listening port. */
char *slave_announce_ip; /* Give the master this ip address. */
int propagation_error_behavior; /* Configures the behavior of the replica
* when it receives an error on the replication stream */
int repl_ignore_disk_write_error; /* Configures whether replicas panic when unable to
* persist writes to AOF. */
/* The following two fields is where we store master PSYNC replid/offset
* while the PSYNC is in progress. At the end we'll copy the fields into
* the server->master client structure. */
char master_replid[CONFIG_RUN_ID_SIZE+1]; /* Master PSYNC runid. */
long long master_initial_offset; /* Master PSYNC offset. */
int repl_slave_lazy_flush; /* Lazy FLUSHALL before loading DB? */
/* Synchronous replication. */
list *clients_waiting_acks; /* Clients waiting in WAIT or WAITAOF. */
int get_ack_from_slaves; /* If true we send REPLCONF GETACK. */

复制偏移量

执行复制的双方分别维护一个复制偏移量。[主服务器应当维护多个?]

  • 主服务器每次向从服务器传播N个字节的数据时,就将自己的复制偏移量+N
  • 从服务器每次收到主服务器的N个字节的数据时,将自身的复制偏移量+N
    通过对比主从服务器的复制偏移量,可以得出主从节点是否处于一致状态。
    如果偏移量一致,则主从服务器处于抑制状态;否则处于非一致状态。
    注意,主服务器配置中复制偏移量为master_repl_offset,而从服务器中,

关于下笔时的问题,主服务器中是否应当维护多个复制偏移量,答案是否定的,在源码定义中未找到对应的定义。可以理解为,一主多从时,服务器会将当前数据库状态传递给多个从服务器,并同步复制偏移量。
redis 4.0之后,所有的从服务器都会从主服务器接收完全相同的复制量,但是由每队主从之间的网络不一定相同,所以需要从服务器自身维护其当前的复制偏移量。那么问题来了,如何保障下一次同步呢?
这里从节点之间的不一致会影响集群同步状态,主节点向从节点发送主从差别状态时就会变得繁琐。

复制积压缓冲区

复制积压缓冲区是由主服务器维护的一个固定长度的FIFO队列,默认大小为1MB
当主服务器进行命令传播时,不仅仅会将写命令发送给从服务器,还会讲命令写入到复制积压缓冲区中。复制积压缓冲区会记录每个字节记记录相对应的复制偏移量。

如果主从断联一段时间后,从服务器重新连上主服务器,则从服务器会通过PSYNC命令将自己的复制偏移量发送给主服务器,主服务器根据从服务器的复制偏移量决定接下来该如何同步。

  1. 如果offset偏移量之后的数据依然存在于复制积压缓冲区中,那么主服务器将对从服务器执行部分重同步操作,即主服务器向从节点发送断联期间执行的命令。 CONTINUE
  2. 如果offset之后逇数据不存在复制积压缓冲区中,则主服务器对当前的从服务器执行完整的重同步操作。
复制积压缓冲区大小?

默认为1MB,如果主服务器需要执行大量写命令或者主从断联时间较长,则可能需要考虑设置合理的复制积压缓冲区大小。
有这样的计算公式: second * write_size_per_second
即对每秒写入数据量和主从断联恢复时长有一定的预估。
为了安全期间,还需要将复制积压缓冲区的大小进行double处理。

复制积压缓冲区实现

复制积压缓冲区由一个环形队列实现。的相关定义包含两部分:

1
2
replBacklog *repl_backlog;
long long repl_backlog_size;
  1. repl_backlog_size: 指定复制积压缓冲区的大小;
  2. replBacklog *repl_backlog: 所有从服务器共享的复制积压缓冲区。

服务器运行ID

实现部分重同步的过程需要用到服务器运行ID

  • 每个服务器(主从)都有自己的运行ID
  • 运行ID在服务器启动时生成,由40位随机十六进制的字符串组成。

主从服务器初次进行同步时,主服务器将自身的运行ID传递给从服务器,从服务器将其保存至
master_replid中,同时保存初次同步的offset

1
2
3
主服务器配置
char replid[CONFIG_RUN_ID_SIZE+1]; /* My current replication ID. */
char replid2[CONFIG_RUN_ID_SIZE+1]; /* replid inherited from master*/
1
2
3
从服务器配置
char master_replid[CONFIG_RUN_ID_SIZE+1]; /* Master PSYNC runid. */
long long master_initial_offset; /* Master PSYNC offset. */

主从重连时,从服务器向当前连接的主服务器发送之前保存的运行ID,如果匹配且offset任保存在主服务器的复制积压缓冲区中,则执行部分重同步。若不再则执行完整重同步;否则执行完整重同步操作。

PSYNC实现

PSYNC的调用方式

  1. 从服务器未复制过任何主服务器,或者执行过slave no one,则从服务器再执行第一次复制时将向主服务器发送PSYNC ? -1,主动向主服务器请求完整重同步。
  2. 已复制过,则开始新的复制时向主服务器发送PSYNC <runid> <offset>, 主服务器判断进行何种操作。

主服务器有三种响应:

  1. 返回+FULLSYNC <runid> <offset> 回复,则表示主从服务器之间将进行完整重同步过程;
  2. 返回+CONTINUE,则主从服务器之间将执行部分重同步操作,从服务器将等待主服务器发送复制积压缓冲区中堆积的写命令,接收后执行即可。
  3. 返回-ERR,则表示主服务器版本低于redis 2.8无法识别PSYNC命令。从服务器将向主服务器发送SYNC命令,执行完整同步过程。

完整流程

心跳检测

心跳检测是指,在命令传播阶段,从服务器会以默认每秒一次的频率向服务器发送命令:
replconf ack <replication_offset>replication_offset是从服务器当前的复制偏移量。
上述命令包含三个作用:

  1. 检测主从服务器之间的网络连接状态。
  2. 辅助实现min-slaves选项。
  3. 检测命令丢失。
检测主从网络连接状态

如果主服务器超过一秒钟未收到从服务器发来的replconf ack命令,那么服务器则知道两者的网络连接出问题了。
在主服务器上执行info replication命令,tag栏能反映出当前从服务器最后一次向主服务器发送replconf ack距此时过了多久:

1
2
3
4
5
6
7
8
9
10
11
12
13
182.168.106.129:6379> info replication
# Replication
role:master
connected_slaves:0
master_failover_state:no-failover
master_replid:637b9e49e28c5c17d2a4d43abd7cea4434cc91ae
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0

注意上述返回中从节点数为0是因为本人所使用的redis的模式为集群模式。在后续内容中将补充redis的架构模式。

辅助实现min-slaves选项

redis中如下两个配置可以防止主服务器在不安全的情况下执行写命令。

1
2
min-slaves-to-write 3
min-slaves-max-lag 10

分别对应server中的配置:

1
2
int repl_min_slaves_to_write;   /* Min number of slaves to write. */
int repl_min_slaves_max_lag;

其含义为:在从服务器少于3个或者三个从服务器的延迟大于等于10秒时,主服务器将拒绝少执行命令。

检测命令丢失

如果因为网络故障,主服务器传播给从服务器的写命令半路丢失,那么当从服务器向主服务器发送replconf ack时,主服务器将识别到主从之间的复制偏移量存在差异,而后主服务器就根据从服务器提交的偏移量,在复制积压缓冲区中找到从服务器缺少的数据,讲这些数据重新发给从服务器。

乍看起来,这个过程和部分重同步非常相似,但是两者还是有些差异:
补发缺失数据操作在主从服务器之间没有断线时执行;而部分重同步发生在断线重连之后。

注意到 redis 2.8之前没有replconf ack复制积压缓冲区,即使命令在传播过程中丢失,主从都不会意识到,主服务器也不会向从服务器补发丢失的数据。

思考

无盘复制是什么?

redis默认是磁盘复制,但是如果使用低速磁盘,复制操作会给主服务器带来较大压力。所以redis 2.8.18后开始支持无盘复制。在这种模式下,子进程直接将rdb文件通过网络发送给从服务器,不适用磁盘作为中间存储。

redis主从复制过程为什么选择rdb,而非aof

  • rdb文件内容是经过压缩的二进制数据,同时redis针对不同的数据类型做了针对性优化,文件较小。而aof文件记录的是每一次写操作的命令,写操作越多文件越大,而且包含对重复key的冗余操作。在主从全量同步时,传输rdb文件可以降低对主从服务器的网络带宽开销。从库在加载RDB文件时,文件小,读取快。同时从库按照rdb协议解析还原数据即可。而aof需要依次重放每个写命令,恢复速度比rdb慢很多。
  • 假设使用AOF做全量复制,则服务器必须打开aof功能,必须选择文件刷盘的策略,选择不当会严重影响redis性能。而RDB只有在需要定时备份和主从全量复制时才会触发,生成快照。在很多丢失数据不敏感的业务场景,其实是不需要开启AOF的。

如何理解主-从-从模式

如果是主从模式,一主多从的情况下,如果多个从服务器向主服务器请求全量复制,在主库中需要完成多次fork子进程生成RDB文件,进行全量复制,fork操作会阻塞主线程处理正常请求。另外传输RDB文件也会占用主库网络带宽。
可以通过主-从-从模式建立多级主从模式,以缓解顶级主服务器的压力。

如何理解redis的高可用

  1. 数据不能丢失或尽量减少丢失。
  2. redis服务不中断。
    相对应的,第一点由持久化机制aof和rdb保障;第二点则要求redis不能单点部署。

主从不一致的原因?

  1. 主从网络时延大/断联
  2. 从库收到主库发来的命令,但从库正在执行阻塞式命令,如hgetall

redis 主从模式如何选主?

在主从模式中,主节点故障后,需要人工干预将从节点设置为主节点,同时还需要通知应用方更新主节点地址。故而有了另一种架构模式:哨兵模式

引用

1. redis复制
2. 详解Redis 主从复制原理
3.
4. Redis主从模式的优缺点
5. CAP 定理的含义
6. 极客时间:主从库如何实现数据一致
7. redis主从