本文作者:百度基础架构部工程师,王钰
Redis 的主从复制经验了屡次演进,本文将从最根本的原理和实现讲起,并层层递进,逐渐出现 Redis 主从复制的演进历史。大家将理解到 Redis 主从复制的原理,以及各个改良版本解决了什么问题,并最终看清 Redis 7.0 主从复制原理的全貌。
什么是主从复制?
在数据库语境下,复制(replication)就是将数据从一个数据库复制到另一个数据库中。主从复制,是将数据库分为主节点和从节点,主节点源源不断地将数据复制给从节点,保障主从节点中存有雷同的数据。
有了主从复制,数据能够有多份正本,这带来了多种益处:
第一,晋升数据库系统的申请解决能力。单个节点可能撑持的读流量无限,部署多个节点,并形成主从关系,用主从复制放弃主从节点数据统一,如此主从节点能够一起提供服务。
第二,晋升整个零碎的可用性。因为从节点中有主节点数据的正本,当主节点宕机后,能够立即晋升其中一个从节点为主节点,持续提供服务。
Redis 主从复制原理
实现主从复制,直观的思路是产生一份主节点数据的快照发送给从节点,并以此做为基准,随后将快照时刻之后的增量数据发送给从节点,如此就能保障主从数据的统一。总体来看,主从复制个别蕴含全量数据同步、增量同步两个阶段。
在 Redis 的主从复制实现中,蕴含两个相似阶段:全量数据同步和命令流传。
全量数据同步:主节点产生一份全量数据的快照,即 RDB 文件,并将此快照发送给从节点。且从产生快照时刻起,记录新接管到的写命令。当快照发送实现后,将累积的写命令发送给从节点,从节点执行这些写命令。此时基准曾经建设实现,主从节点间数据曾经大体一致。
命令流传:全量数据同步实现后,主节点将执行过的写命令源源不断地发送给从节点,从节点执行这些命令,保障主从节点中数据有雷同的变更,如此保障主从数据继续统一。
下图中给出了 Redis 主从复制的整个过程:
主从关系建设后,从节点向主节点发送一个 SYNC 命令申请进行主从同步。
主节点收到 SYNC 命令后,执行 fork 创立一个子过程,子过程中将所有的数据按特定编码存储到 RDB(Redis Database)文件中,这就产生了数据库的快照。
主节点将此快照发送给从节点,从节点接管并载入快照。
主节点接着将生成快照、发送快照期间积压的写命令发送给从节点,从节点接管这些命令并执行,命令执行后,从节点中的数据也就有了同样的变更。
尔后,主节点源源不断地新执行的写命令同步到从节点,从节点执行流传来的命令。如此,主从数据保持一致。须要阐明的是,命令流传存在时延的,所以任意时刻,不能保障主从节点间数据完全一致。
以上就是 Redis 主从复制的基本原理,很简略很容易了解,Redis 最后就采纳这种计划,但这种计划存在一些问题:
fork 耗时过长,阻塞主过程
执行 fork 时,须要拷贝大量的内存页表,这是一个耗时较多的操作,尤其当内存使用量较大的时候。组内同学曾做过测试,内存占用 10GB 时,fork 须要耗费 100 多毫秒。fork 的时候主过程阻塞 100 多毫秒,这对 Redis 而言,切实太长了。另外 fork 之后,如果主库中有不少的写入,那么因为写时复制机制,会额定耗费不少的内存,还会增大响应工夫。
主从间网络闪断会触发全量同步
如果主从之间的网络呈现了故障,连贯意外断开,主节点无奈持续流传命令至该从节点。之后网络复原,从节点从新连贯上主节点后,主节点不能再持续流传新接管到的命令了,因为从节点曾经漏掉了一些命令。此时,从节点须要从头再来,再次执行全副的同步过程,而这要付出很高的代价。
网络闪断是常产生的事件,闪断期间主节点中可能只写入了比拟少的数据,但就因为这很少的一部分数据,须要让从节点进行一次代价昂扬的全量同步。这种做法是十分低效的,该如何解决这问题呢?下一节 Redis 局部重同步给你答案。
Redis 局部重同步
网络短暂断开后,从节点须要从新同步,这很浪费资源,很不环保。从节点为什么须要从新同步呢?因为主从断开期间有局部命令没有同步到从节点下来。如果疏忽这些命令持续流传后续的命令,则会导致数据的错乱,因为失落掉的命令是不能疏忽的。为什么不将那些命令保留下来呢?这样当从节点从新连贯后,就能够将断连期间的命令补充给它了,这样就不须要从新全量同步了。
Redis 2.8 版本后,引入了局部同步。它在主节点中保护了一个复制积压缓冲区,命令一方面会流传到从节点,另外还会记录在这个缓冲区中。保留所有的命令是不必要的,Redis 中应用了一个环形的缓冲区,这样就能够只保留最近的一些命令了。
命令是保留下来了,但从节点从新连贯后,主节点该从什么中央开始给从节点发送命令呢?如果能给所有命令编一个号,则从节点只须要通知主节点本人最初收到的命令的编号,主节点就晓得该从什么地位发送命令了。Redis 的实现中是对字节进行编号,这个编号在 Redis 的语境中叫做复制偏移量。
有了局部同步后,主从复制的流程变成了上面这样:
主从复制的时候不再应用 SYNC 命令,而是应用 PSYNC,意思的 Partial SYNC,局部同步。PSYNC 的语法如下:
PSYNC <master id> <replication offset>
命令中的两个参数,一个是主节点的编号,一个是复制偏移量。每个 Redis 节点都有一个 40 字节的编号,PSYNC 命令中携带的编号是冀望进行同步的主节点的编号。复制偏移量则示意以后从节点想要从什么中央开始局部同步。
如果是第一次进行主从复制,天然是不晓得主节点的编号,复制偏移量也无意义,此时应用 PSYNC ? -1 来进行全量同步。另外,如果从节点指定的复制偏移量不在主节点的复制积压缓冲区的范畴内,局部同步会失败,会转向全量同步。
有了局部同步,网络闪断后就能够防止全量同步了。然而因为主节点只能保留最近的局部命令,保留多少取决于复制积压缓冲区的大小。如果从节点断开工夫过长,或者断开期间主节点新执行的写命令足够多,漏掉的命令就无奈全副保留到复制积压缓冲区中了。加大复制积压缓冲区能够尽可能多地防止全量同步,但这同时会造成额定的内存耗费。
局部同步耗费了局部内存来保留最近执行的写命令,防止闪断后的全同步,这是很直观、很容易设想的解决方案。这种计划很好,是否还存在其余问题呢?思考以下问题:
如果从节点重启了怎么办?
局部同步依赖主节点的编号和复制偏移量,从节点在首次同步的时候会获取到主节点的编号,并在之后的同步中一直调整复制偏移量,这些信息都存储在内存中。当从节点意外重启后,只管本地存有 RDB 或 AOF 文件,还是须要进行一次全量同步。但实际上齐全能够载入本地数据,并执行局部同步即可。
如果主从切换了怎么办?
如果主节点意外宕机,外围监控组件执行了主从切换。此时其余从节点对应的主节点就变动了,从节点中记录的主节点编号就匹配不上新的主节点了,此时会进行一次全量同步。但实际上所有的从节点在主从切换之前同步进度应该是差不多的,而且新晋升的从节点蕴含的数据应该最全,切主后所有从节点都执行一次全量同步,这切实不合理。
以上问题如何解决,请持续往后看。
同源增量同步
从节点重启后失落了原主节点编号和复制偏移量,这导致重启后须要全量同步,这很好办,把这些信息存下来就能够了。
主从切换后,主节点信息变动了,导致从节点须要全量同步,这也容易解决,只需能确认新主节点上的数据是从原主节点复制来的,那就能够持续从新的主节点上进行复制。
Redis 4.0 当前,对 PSYNC 进行了改良,提出了同源增量复制的解决方案,该计划解决了后面提到的两个问题。
从节点重启后,须要跟主节点全量同步,这实质上是因为从节点失落了主节点的编号信息,在 Redis 4.0 后,主节点的编号信息被写入到 RDB 中长久化保留。
切主后,从节点须要和新主节点全量同步,实质起因是新的主节点不认原主节点的编号。从节点发送 PSYNC < 原主节点编号 > < 复制偏移量 > 给新的主节点,如果新的主节点可能意识 < 原主节点编号 >,并明确本人的数据就是从该节点复制来的。那么新的主节点就应该分明它和该从节点师出同门,应该承受局部同步。如何能力辨认呢,只须要让从节点在切换为主节点时,将本人之前的主节点的编号记录下来即可。
Redis 4.0 当前,主从切换后,新的主节点会将先前的主节点记录下来,察看 info replication 的后果,能够能够看到 master_replid 和 master_replid2 两个编号,前者是以后主节点的编号,后者为先前主节点的编号:
127.0.0.1:6379> slaveof no one
OK
127.0.0.1:6379> info replication
# Replication
role:master
...
master_replid:b34aff08d983991b3feb4567a2ac0308984a892a
master_replid2:a3f2428d31e096a99d87affa6cc787cceb6128a2
master_repl_offset:38599
second_repl_offset:38600
...
repl_backlog_histlen:5180
Redis 中目前值保留了两个主节点编号,但齐全能够实现一个链表,将过往的主节点的编号信息都记录下来,这样就能够追溯的更远了。这样以来,如果一个从节点断开后,执行了屡次主从切换,该从节从新连贯后,仍然能够辨认出它们的数据是同源的。但 Redis 没有这么做,这是因为没有必要,因为就算数据是同源的,但复制积压缓冲区中保留的数据是无限的,屡次主从切换后,复制积压缓冲区中保留的命令曾经无奈满足局部同步了。
有了同源增量复制后,主节点切换后,其余从节点能够基于新的主节点持续增量同步。
此时,主从复制看起来曾经不存在太大的问题了。但做 Redis 的那帮家伙,总处心积虑想着能不能再做些优化。上面我将形容 Redis 主从复制的一些优化策略。
无盘全量同步和无盘加载
Redis 执行全量复制,须要生成以后数据库的一份快照,具体做法是执行 fork 创立子过程,子过程遍历所有数据并编码后写入 RDB 文件中。RDB 生成后,在主过程中,会读取此文件并发送给从节点。
读写磁盘上的 RDB 文件是比拟耗资源的,在主过程中执行势必会导致 Redis 的响应工夫变长。因而一个优化计划是 dump 后间接将数据间接发送数据给从节点,不须要将数据先写入到 RDB。Redis 6.0 中实现了这种无盘全量同步和无盘加载的策略。
采纳无盘全量同步,防止了对磁盘的操作,但也有毛病。个别状况下,在子过程中间接应用网络发送数据,这比在子过程中生成 RDB 要慢,这意味着子过程须要存活的工夫绝对较长。子过程存在的工夫越长,写时复制造成的影响就越大,进而导致耗费的内存会更多。
在全量复制时候,从节点个别是先接管 RDB 将其存在本地,接管实现后再载入 RDB。同样地,从节点也能够间接载入主节点发来的数据,防止将其存入本地的 RDB 文件中,而后再从磁盘加载。
** 共享主从复制缓冲区
**
在主节点的视角中,从节点就是一个客户端,从节点发送了 PSYNC 命令后,主节点就要与它们实现全量同步,并一直地把写命令同步给从节点。Redis 的每个客户端连贯上存在一个发送缓冲区。
主节点执行了写命令后,就会将命令内容写入到各个连贯的发送缓冲区中。发送缓冲区存储的是待流传的命令,这意味着多个发送缓冲区中的内容其实是雷同的。而且,这些命令还在复制积压缓冲区中存了一份呢。这就造成了大量的内存节约,尤其是存在很多从节点的时候。
在 Redis 7.0 中,咱们团队的同学提出并实现了共享主从复制缓冲区的计划解决了这个问题。该计划让发送缓冲区与复制积压缓冲区共享,防止了数据的反复,可无效节俭内存。
总结
本文回顾并总结了 Redis 主从复制的演化过程,并解释了各次演变所解决的问题。最初,形容了对 Redis 主从复制进行优化的一些策略。
上面是对全文的总结:
宏观来看 Redis 的主从复制分为全量同步和命令流传两个阶段。主节点先发送快照给从节点,而后源源不断地将命令流传给从节点,以此保障主从数据的统一。
Redis 2.8 之前的主从复制存在闪断后须要从新全量同步的问题,Redis 2.8 引入了复制积压缓冲区解决了这一问题。
在 Redis 4.0 中,同源增量复制的策略被提出,解决了主从切换后从节点须要全量同步的问题。至此,Redis 的主从复制整体上曾经比较完善了。
Redis 6.0 中,为进一步优化主从复制的性能,无盘同步和加载被提出,防止全量同步时读写磁盘,进步主从同步的速度。
在 Redis 7.0 rc1 中,采纳了共享主从复制缓冲区的策略,升高了主从复制带来的内存开销。
心愿本文能帮忙大家回顾 Redis 主从复制的原理,并对其建设更加粗浅的印象。