在之前的文章当中,我们了解可Redis的两种持久化机制RDB与AOF。在单机的环境下,一旦这个服务宕机,即便他可以恢复数据,但也仅仅只能恢复宕机之前的数据。宕机之后,便无法再提供缓存服务了。
很明显,为了解决单点故障问题,我们可以采用Redis集群的方式,来确保单个服务宕机,整个服务依旧存在。 但是,仅仅是添加机器就可以了吗?看下面这张图,如果所有的节点都可以接收写请求,那么造成的后果是数据在集群中的不一致。一份数据在集群中存在不同的值。这不是我们想要看到的。
那么,最好采用什么方式?在Redis中,采用的是读写分离模式。一个主节点负责客户端的写操作,其余节点负责客户端的读操作
同步虽然只有简单的两个字,但是实现起来,要麻烦的多,因为在进行同步的时候,要区分各种各样的状况。
如何将节点改为从节点
在Redis 5.0 之前,采用的是slaveOf命令 在之后,采用的是replicaOf命令
bash
# 在从节点中执行该命令
replicaof <主节点ip> <主节点port>
第一次同步:全量同步
在第一次进行同步的时候,采用的是全量同步机制。下面有一张图,你看了会更加清晰
第一阶段:主从节点间协商、建立连接。 第二阶段:主节点将所有数据同步给从节点 第三阶段:第二阶段执行过程中新收到的写命令,再次发送给从节点
下面详细分析每一个阶段流程。
第一阶段是主从节点间建立连接、协商同步的过程,主要是为全量复制做准备。在这一步,从节点和主节点建立起连接,并告诉主节点即将进行同步,主节点确认回复后,主从节点间就可以开始同步了。
具体来说,从节点给主节点发送 psync 命令,表示要进行数据同步,主节点根据这个命令的参数来启动复制。psync 命令包含了主节点的 runID 和复制进度 offset 两个参数。
- runID ,是每个 Redis 实例启动时都会自动生成的一个随机 ID,用来唯一标记这个实例。当从节点和主节点第一次复制时,因为不知道主节点的 runID,所以将 runID 设为 " ? "。
- offset,此时设为 -1,表示第一次复制。
主节点收到 psync 命令后,会用 FULLRESYNC 响应命令带上两个参数:主节点runID 和主节点目前的复制进度 offset,返回给从节点。从节点收到响应后,会记录下这两个参数。
这里有个地方需要注意,FULLRESYNC 响应表示第一次复制采用的全量复制,也就是说,主节点会把当前所有的数据都复制给从节点。
在第二阶段,主节点将所有数据同步给从节点。从节点收到数据后,在本地完成数据加载。这个过程依赖于内存快照生成的 RDB 文件。
具体来说,主节点执行 bgsave 命令,生成 RDB 文件,接着将文件发给从节点。从节点接收到 RDB 文件后,会先清空当前数据库,然后加载 RDB 文件。这是因为从节点在通过 replicaof 命令开始和主节点同步前,可能保存了其他数据。为了避免之前数据的影响,从节点需要先把当前数据库清空。
在主节点将数据同步给从节点的过程中,主节点不会被阻塞,仍然可以正常接收请求。否则,Redis 的服务就被中断了。但是,这些请求中的写操作并没有记录到刚刚生成的 RDB 文件中。为了保证主从节点的数据一致性,主节点会在内存中用专门的 replication buffer缓存池,记录 RDB 文件生成后收到的所有写操作。
最后,也就是第三个阶段,主节点会把第二阶段执行过程中新收到的写命令,再发送给从节点。具体的操作是,当主节点完成 RDB 文件发送后,就会把此时 replication buffer 中的修改操作发给从节点,从节点再重新执行这些操作。这样一来,主从节点就实现同步了。
主从级联模式分担全量复制时的主节点压力
通过分析主从节点间第一次数据同步的过程,你可以看到,一次全量复制中,对于主节点来说,需要完成两个耗时的操作:生成 RDB 文件和传输 RDB 文件。
如果从节点数量很多,而且都要和主节点进行全量复制的话,就会导致主节点忙于 fork 子进程生成 RDB 文件,进行数据全量同步。fork 这个操作会阻塞主线程处理正常请求,从而导致主节点响应应用程序的请求速度变慢。此外,传输 RDB 文件也会占用主节点的网络带宽,同样会给主节点的资源使用带来压力。那么,有没有好的解决方法可以分担主节点压力呢?
其实是有的,这就是"主 - 从 - 从"模式。
在刚才介绍的主从节点模式中,所有的从节点都是和主节点连接,所有的全量复制也都是和主节点进行的。现在,我们可以通过"主 - 从 - 从"模式将主节点生成 RDB 和传输 RDB 的压力,以级联的方式分散到从节点上。
简单来说,我们在部署主从集群的时候,可以手动选择一个从节点(比如选择内存资源配置较高的从库),用于级联其他的从节点。然后,我们可以再选择一些从节点(例如三分之一的从节点),在这些从节点上执行如下命令,让它们和刚才所选的从节点,建立起主从关系。
replicaof 所选从节点的IP 6379 这样一来,这些从节点就会知道,在进行同步时,不用再和主节点进行交互了,只要和级联的从节点进行写操作同步就行了,这就可以减轻主节点上的压力,如下图所示:
好了,到这里,我们了解了主从节点间通过全量复制实现数据同步的过程,以及通过"主 - 从 - 从"模式分担主节点压力的方式。
长连接实现广播同步
一旦主从节点完成了全量复制,它们之间就会一直维护一个网络连接,主节点会通过这个连接将后续陆续收到的命令操作再同步给从节点,这个过程也称为基于长连接的命令传播,可以避免频繁建立连接的开销。
听上去好像很简单,但不可忽视的是,这个过程中存在着风险点,最常见的就是网络断连或阻塞。如果网络断连,主从节点之间就无法进行命令传播了,从节点的数据自然也就没办法和主节点保持一致了,客户端就可能从从节点读到旧数据。
接下来,我们就来聊聊网络断连后的解决办法。
主从节点间网络断了怎么办?
在 Redis 2.8 之前,如果主从节点在命令传播时出现了网络闪断,那么,从节点就会和主节点重新进行一次全量复制,开销非常大。
从 Redis 2.8 开始,网络断了之后,主从节点会采用增量复制的方式继续同步。听名字大概就可以猜到它和全量复制的不同:全量复制是同步所有数据,而增量复制只会把主从节点网络断连期间主节点收到的命令,同步给从节点。
那么,增量复制时,主从节点之间具体是怎么保持同步的呢?这里的奥妙就在于 repl_backlog_buffer 这个缓冲区。我们先来看下它是如何用于增量命令的同步的。
当主从节点断连后,主节点会把断连期间收到的写操作命令,写入 replication buffer,同时也会把这些操作命令也写入 repl_backlog_buffer 这个缓冲区。
repl_backlog_buffer 是一个环形缓冲区,主节点会记录自己写到的位置,从节点则会记录自己已经读到的位置。
刚开始的时候,主节点和从节点的写读位置在一起,这算是它们的起始位置。随着主节点不断接收新的写操作,它在缓冲区中的写位置会逐步偏离起始位置,我们通常用偏移量来衡量这个偏移距离的大小,对主节点来说,对应的偏移量就是 master_repl_offset。主节点接收的新写操作越多,这个值就会越大。
同样,从节点在复制完写操作命令后,它在缓冲区中的读位置也开始逐步偏移刚才的起始位置,此时,从节点已复制的偏移量 slave_repl_offset 也在不断增加。正常情况下,这两个偏移量基本相等。
Redis repl_backlog_buffer的使用
主从节点的连接恢复之后,从节点首先会给主节点发送 psync 命令,并把自己当前的 slave_repl_offset 发给主节点,主节点会判断自己的 master_repl_offset 和 slave_repl_offset 之间的差距。
在网络断连阶段,主节点可能会收到新的写操作命令,所以,一般来说,master_repl_offset 会大于 slave_repl_offset。此时,主节点只用把 master_repl_offset 和 slave_repl_offset 之间的命令操作同步给从节点就行。
就像刚刚示意图的中间部分,主节点和从节点之间相差了 put d e 和 put d f 两个操作,在增量复制时,主节点只需要把它们同步给从节点,就行了。
说到这里,我们再借助一张图,回顾下增量复制的流程。
Redis增量复制流程
不过,有一个地方我要强调一下,因为 repl_backlog_buffer 是一个环形缓冲区,所以在缓冲区写满后,主节点会继续写入,此时,就会覆盖掉之前写入的操作。如果从库的读取速度比较慢,就有可能导致从节点还未读取的操作被主库新写的操作覆盖了,这会导致主从节点间的数据不一致。
因此,我们要想办法避免这一情况,一般而言,我们可以调整 repl_backlog_size 这个参数。这个参数和所需的缓冲空间大小有关。
缓冲空间的计算公式是:缓冲空间大小 = 主库写入命令速度 * 操作大小 - 主从库间网络传输命令速度 * 操作大小。在实际应用中,考虑到可能存在一些突发的请求压力,我们通常需要把这个缓冲空间扩大一倍,即 repl_backlog_size = 缓冲空间大小 * 2,这也就是 repl_backlog_size 的最终值。
举个例子,如果主节点每秒写入 2000 个操作,每个操作的大小为 2KB,网络每秒能传输 1000 个操作,那么,有 1000 个操作需要缓冲起来,这就至少需要 2MB 的缓冲空间。否则,新写的命令就会覆盖掉旧操作了。为了应对可能的突发压力,我们最终把 repl_backlog_size 设为 4MB。
这样一来,增量复制时主从节点的数据不一致风险就降低了。不过,如果并发请求量非常大,连两倍的缓冲空间都存不下新操作请求的话,此时,主从节点数据仍然可能不一致。
可以根据 Redis 所在服务器的内存资源再适当增加 repl_backlog_size 值来确保在写入时不会因为数据过大导致数据被覆盖。
总结
1、 Redis中的集群采用的是主从-读写分离架构。并且在主从复制的时候,采用级联更新(主 -> 从 -> 从)的方式减轻主节点的压力。
2、Redis在进行同步的时候,会fork子进程进行数据同步而不是阻塞主进程。但是从节点较多导致主进程阻塞在了fork子进程方面也会阻塞主进程。所以才会有级联更新(主 -> 从 -> 从)
3、Redis中存在三种不同的主从同步方式。 - 全量同步(用于第一次同步) - 广播同步(在主节点运行的过程中,采用广播的形式将写命令发送给从节点) - 增量同步(在长连接断开亦或者是网络阻塞超时时,采用的同步方式)两个缓冲池:replica buffer 与 repl_backlog_buffer
参考:小林coding、极客时间