YUKIPEDIA's blog

一个普通的XMUER

《Summer Pockets》久島鴎推し


Redis 集群方案

目录

1.主从复制

经典的 一主多从 模式实现方案。就是将原来的一台 redis 服务器,同步数据到多台从 redis 服务器上,主从服务器之间采用的是 读写分离 的方式。

主服务器可以进行 读写操作,当发生写操作时,自动将写操作同步给从服务器,从服务器一般是 只读 的,并接受主服务器同步过来的写操作命令。

image.png

也就是说,所有的数据修改只在主服务器上进行,然后将最新的数据同步给从服务器,这样就达到主从一致。注意,主从服务器之间的命令复制是 异步 进行的。

由于主从同步是 异步 的,这就导致了主从复制方案无法实现 强一致性保证,数据不一致是难以避免的。

同步这两个字说的简单,但是这个同步过程要考虑的事情不是一两个。

第一次同步

多台服务器之间到底要怎么确定谁是主服务器,谁是从服务器?

我们可以使用 replicaof 命令形成主服务器和从服务器的关系。比如,现在有服务器 A 和服务器 B,我们在 B 上执行下面的命令:

replicaof <服务器 A 的 IP 地址> <服务器 A 的 redis 端口号>

接着,服务器 B 就会变成 A 的 从服务器,然后与主服务器进行第一次同步。

主从服务器间的第一次同步分为三个阶段:

  1. 建立链接、协商同步;
  2. 主服务器同步数据给从服务器
  3. 主服务器发送新写操作命令给从服务器

image.png

第一阶段:建立链接,协商同步

执行了 replicaof 命令后,从服务器就会给主服务器发送 psync 命令,表示要进行数据同步。

psync 命令包含两个参数,分别是 主服务器的 runID复制进度 offset

  • runID :每个 redis 服务器在启动时都会自动生成一个随机的 ID 来唯一标识自己。当从服务器和主服务器第一次同步时,因为不知道主服务器的 runID ,所以将其设置为 “?”。
  • offset :表示复制进度,第一次同步时值为 -1.

主服务器收到 psync 命令后,会用 FULLRESYNC 作为相应命令返回给对方。

这个相应命令会带上两个参数:主服务器的 runID 和主服务器目前的复制进度 offset 。从服务器收到响应后,会记录这两个值。

FULLRESYNC 响应命令的意图是采用 全量复制 的方式,也就是主服务器会把所有的数据都同步给从服务器。

所以,第一阶段的工作时为了全量复制做准备。

第二阶段:主服务器同步数据给从服务器

接着,主服务器会执行 bgsave 命令来生成 RDB 文件,然后把文件发送给从服务器。

从服务器接收到 RDB 文件后,会先清空当前数据,然后载入 RDB 文件。

注意,主服务器生成 RDB 的这个过程是不会阻塞主线程的,因为 bgsave 命令是产生了一个子进程来生成 RDB 文件,是 异步 工作的,这样 redis 依然可以正常处理命令。

但是,这期间的写操作命令并没有记录到刚刚生成的 RDB 文件中,这时主从服务器间的数据就不一致了。

那么为了保证主从服务器的数据一致性,主服务器在下面这三个时间间隙中将收到的写操作命令,写入到 replication buffer 缓冲区里

  • 主服务器生成 RDB 文件期间
  • 主服务器发送 RDB 文件给从服务器期间
  • 从服务器加载 RDB 文件期间

第三阶段:主服务器发送新写操作命令给从服务器

在主服务器生成的 RDB 文件发送完,从服务器收到 RDB 文件后,丢弃所有旧数据,将 RDB 数据载入到内存。完成 RDB 的载入后,会回复一个确认消息给主服务器。

接着,主服务器将 replication buffer 缓冲区里所记录的写操作命令发送给从服务器,从服务器执行来自主服务器 replication buffer 缓冲区里发来的命令,这时主从就一致了。

到此为止,主从服务器的第一次同步工作就完成了

命令传播

主从服务器在完成第一次同步后,双方之间会维护一个 TCP 连接。

后续主服务器可以通过这个连接继续将写操作命令传播给从服务器,然后从服务器执行该命令,使得主从一致。

而且这个连接是 长连接,这样可以避免频繁的 TCP 连接和断开带来的性能开销。

增量复制

主从服务器在完成第一次同步后,就会基于长连接进行命令传播。

但如果主从服务器之间的网络连接断开了,那么就无法进行命令传播,这时就无法保证主从一致了,客户端就可能从 从服务器 读到旧的数据。

假如此时断开的网络,又恢复正常了,要怎么继续保证主从一致呢?

在 redis 2.8 之前,如果主从服务器在命令同步时出现了网络断开又恢复的情况,从服务器就会和主服务器重新进行一次 全量复制,很明显这样的开销太大了。

从 redis 2.8 开始,网络断开又恢复后,主从服务器会采用 增量复制 的方式继续同步,也就是 只会把网络断开期间主服务器接收到的写操作命令同步给从服务器

主要有三个步骤:

  • 从服务器在恢复网络后,会发送 psync 命令给主服务器,此时的 psync 命令里的 offset 参数不是 -1;
  • 主服务器收到该命令后,然后用 CONTINUE 响应命令告诉从服务器接下来采用增量复制的方式同步数据;
  • 然后主服务将主从服务器断线期间,所执行的写命令发送给从服务器,然后从服务器执行这些命令。

那么关键问题来了,主服务器怎么知道要将哪些增量数据发给从服务器呢

答案藏在两个东西里:

  • repl_backlog_buffer:一个 环形 缓冲区,用于主从服务器断连后,从中找到差异的数据;
  • replication offset:标记上面那个缓冲区的同步进度,主从服务器都有各自的偏移量,主服务器使用 master_repl_offset 来记录自己「写」到的位置,从服务器使用 slave_repl_offset 来记录自己「读」到的位置。

那 repl_backlog_buffer 缓冲区在什么时候写入的呢?

在主服务器进行命令传播时,不仅会将写命令发送给从服务器,还会将写命令写入到 repl_backlog_buffer 缓冲区里,因此这个缓冲区里会保存着最近传播的写命令。

网络断开后,当从服务器重新连上主服务器时,从服务器会通过 psync 命令将自己的复制偏移量 slave_repl_offset 发送给主服务器,主服务器根据自己的 master_repl_offset 和 slave_repl_offset 之间的差距,然后来决定对从服务器执行哪种同步操作:

  • 如果判断出从服务器要读取的数据还在 repl_backlog_buffer 缓冲区中,那么主服务器将采用 增量同步 的方式;
  • 相反,如果判断出从服务器要读取的数据已经不存在 repl_backlog_buffer 缓冲区里,那么主服务器将采用 全量同步 的方式。

当主服务器在 repl_backlog_buffer 中找到主从服务器差异(增量)的数据后,就会将增量的数据写入到 replication buffer 缓冲区,这个缓冲区是前面介绍的要传播给从服务器的命令。

image.png

repl_backlog_buffer 环行缓冲区的默认大小是 1M,并且由于它是一个环形缓冲区,所以当缓冲区写满后,主服务器继续写入的话,就会 覆盖 之前的数据。因此,当主服务器的写入速度远超于从服务器的读取速度,缓冲区的数据一下就会被覆盖。

那么在网络恢复时,如果从服务器想读的数据已经被覆盖了,主服务器就会采用全量同步,这个方式比增量同步的性能损耗要大很多。

因此,为了避免在网络恢复时,主服务器频繁地使用 全量同步 的方式,我们应该调整下 repl_backlog_buffer 缓冲区大小,尽可能地大一些,减少出现从服务器要读取的数据被覆盖的概率,从而使得主服务器采用增量同步的方式。

常见问题

怎么判断 redis 某个节点是否正常工作?

通过互相的 ping-pong 心跳检测机制,如果有 一半以上 的节点去 ping 一个节点的时候没有 pong 回应,集群就会认为这个节点挂了,会断开这个节点的连接。

  • redis 主节点默认 每 10 秒 对节点发送 ping 命令,判断从节点的连接状态,可以通过参数 repl-ping-slave-period 控制发送频率
  • redis 从节点 每 1 秒 发送 replconf ack{offset} 命令,给主节点上报自身当前的复制偏移量,目的是:
    • 实时监测主从节点网络状态
    • 上报自身复制偏移量,检查复制数据是否丢失,如果出现数据丢失,再从主节点复制缓冲区中拉取丢失数据

主从复制架构中,过期 key 如何处理?

主节点通过淘汰算法淘汰了一个 key 后,主节点会模拟一条 del 命令发送给从节点,让从节点进行删除 key 操作。

主从复制中两个 buffer(replication buffer、repl backlog buffer) 有什么区别?

  • 出现阶段不同:
    • repl backlog buffer 在 增量复制 阶段出现,一个主节点只分配一个 repl backlog buffer
    • replication buffer 在全量复制阶段和增量复制阶段都会出现,主节点会给每个新连接的从节点分配一个 replication buffer
  • 两个 buffer 都有大小限制,当缓冲区满了之后,发生的事情不同:
    • 当 repl backlog buffer 满了,因为是环形结构,会直接 覆盖起始位置数据
    • 当 replication buffer 满了,会导致连接断开,删除缓存,从节点重新连接,开始全量复制

主从切换如何减少数据丢失?

主从切换过程中,产生数据丢失的情况有两种:

  • 异步复制同步丢失
  • 集群产生脑裂数据丢失(在 哨兵模式 中存在的问题,此处不赘述)

在实际开发中,不可能保证数据完全不丢失,只能做到尽量少的数据丢失。

对于 异步复制同步丢失

如果主节点还没来得及把新数据同步给从节点就出现了宕机,那么主节点内存中的数据会丢失。如何减少异步复制的数据丢失呢?

redis 配置里有一个参数:min-slaves-max-lag ,表示一旦所有的从节点数据复制和同步的延迟都超过了 min-slaves-max-lag 定义的值,那么主节点就会拒绝接受任何请求

举个例子,假设把 min-slaves-max-lag 配置为 10s 后,根据目前主从复制速度,如果数据同步完成所需时间超过 10s,就会认为 master 未来宕机后损失的数据会很多,master 就拒绝新的写入请求。这样就能将 master 和 slave 数据差控制在 10s 内。

对于客户端,当客户端发现 master 不可写后,可以采取 降级措施,将数据暂时写入本地缓存和磁盘中,一段时间后 master 恢复正常,再重新写入 master 保证数据不丢失。也可以把数据写入消息队列,等 master 恢复后再消费消息队列中的数据。

主从如何做到故障自动切换?

在主从复制方案中,主节点挂了,从节点是无法自动升级为主节点的,这个过程需要人工处理,在此期间 redis 无法对外提供写操作。

要实现故障自动切换,就需要使用到 哨兵机制。哨兵在发现主节点出现故障时,由哨兵自动完成故障发现和故障转移,并通知给应用方,从而实现高可用。

2.哨兵模式

在使用 redis 主从服务时,当主节点宕机,需要手动进行恢复。为了解决这个问题,redis 增加了 哨兵模式,因为哨兵模式做到了可以监控主从服务器,并提供 主从节点故障转移的功能

image.png

哨兵机制是如何工作的?哨兵其实是一个运行在特殊模式下的 redis 进程,所以它也是一个节点。哨兵节点主要负责三件事情:监控、选主、通知

如何判断主节点真的故障了?

哨兵每 1 秒给所有主从节点发送 ping 命令,如果主从节点正常,会发送一个响应命令给哨兵,这样就可以判断节点是否正常运行。

如果主节点或从节点 没有在规定时间内 响应哨兵的 ping 命令,哨兵就会将它们标记为 主观下线。这个 规定时间 是配置项 down-after-milliseconds 参数设定的。

之所以针对主节点设计 主观下线客观下线 两个状态,是因为主节点可能并没有故障,只是因为主节点的系统压力比较大或者网络发生了拥塞,导致主节点没有在规定时间内相应哨兵的 ping 命令。

因此,为了减少误判的情况,哨兵在部署的时候 不会只部署一个节点,而是用多个节点部署成 哨兵集群(至少三台),通过多个哨兵节点一起判断,就可以避免单个哨兵因为自身网络状况不好,而误判主节点下线的情况。

那么如何判断主节点为客观下线呢?当一个哨兵判断主节点主观下线后,就会向其他哨兵发起命令,其他哨兵收到这个命令后,就会根据自身和主节点的网络状况,做出赞成投票或者拒绝投票的响应

image.png

当赞成票数达到哨兵配置文件中的 quorum 配置项设定的值后,这时主节点就会被该哨兵标记为 客观下线。一般来说,quorum 设置为哨兵个数的一半加 1,例如 3 个哨兵就设置 2.

哨兵判断完主节点客观下线后,哨兵就要开始在多个 从节点 中,选出一个从节点来做新主节点。

由哪个哨兵进行主从故障转移?

在哨兵集群中,我们还需要选出一个 leader,让 leader 来执行主从切换。

选举 leader 的过程其实是一个投票的过程,在投票开始前,必须要有 候选者

那么谁来作为候选者呢?哪个哨兵节点判断主节点为客观下线,这个哨兵节点就是候选者。换句话说,所谓的候选者就是想当 leader 的哨兵。

举个例子,如果有三个哨兵,当 B 先判断主观下线后,就会给 A 和 C 发送 is-master-down-by-addr 命令,接着,其他哨兵会进行投票。

当 B 收到赞成票数达到哨兵配置文件中的 quorum 配置项设定的值后,就会将主节点标记为 客观下线,此时的哨兵 B 就是一个 leader 候选者。

现在 B 已经是候选者了,那么候选者如何选举成为 leader 呢?

候选者会向其他哨兵发送命令,表明希望成为 leader 来执行主从切换,并让所有其他哨兵对它进行投票。

每个哨兵只有一票,可以投给自己或投给别人,但是只有候选者才能把票投给自己

在投票过程中,任何一个候选者要满足两个条件:

  • 拿到半数以上的赞成票
  • 拿到的票数要大于等于配置文件中的 quorum

比如 3 个哨兵,quorum 为 2,那么任何一个候选者只要拿到 2 个赞成票,就可以选举成功了。如果没有满足条件,就需要重新选举。

但如果说某个时间点,刚好有两个哨兵判断主节点客观下线,这时不就有两个候选者了?这时又如何决定谁是 leader?

每位候选者都会先给自己投一票,然后请求其他哨兵投票。如果投票者先收到 A 的请求,就会先投票给它,之后收到 B 的请求后,就会拒绝投票,因为它的投票机会已经用完了。这时候,A 先满足了上面的两个条件,A 就会成为 leader。

主从故障转移的过程

主从故障转移包含以下四个步骤:

  1. 在已下线主节点(旧主节点)属下的所有从节点中,挑选出一个从节点,将其切换为主节点
  2. 让其他从节点修改复制目标,修改为复制 新主节点
  3. 将新主节点的 IP 地址和信息,通过 发布者/订阅者机制 通知给客户端
  4. 继续监视旧主节点,当这个旧主节点重新上线时,将它设置为新主节点的从节点

步骤一:选出新主节点

选择一个状态良好、数据完整的从节点后,向这个从节点发送 SLAVEOF no one 命令,将这个从节点转换为主节点。

从节点有很多,到底选择哪个从节点作为新主节点呢?

首先,要把 网络状态不好的 从节点过滤掉,否则将来不久可能又要做一次主从故障迁移。那如何判断从节点的网络状态?

redis 中有个配置项叫 down-after-milliseconds * 10 ,其 down-after-milliseconds 是主从节点断连的最大连接超时时间。如果在这个时间内主从节点都没有通过网络联系上,就认为主从节点断连了。发生断连的次数超过 10 次,就说明这个从节点网络状况不好。

将网络状态不好的从节点过滤掉后,接下来要对所有从节点进行三轮考察:优先级、复制进度、ID 号。在进行每一轮考察的时候,哪个从节点优先胜出,就选择其作为新主节点。

  • 第一轮考察:哨兵首先会根据从节点的优先级来进行排序,优先级越小排名越靠前
  • 第二轮考察:如果优先级相同,则查看复制的下标,哪个从 主节点 接收的复制数据多,哪个就靠前
  • 第三轮考察:如果优先级和下标都相同,就选择从节点 ID 较小的那个

第一轮考察:优先级最高的从节点胜出

redis 有个叫 slave-priority 配置项,可以给从节点设置优先级。

我们可以根据 服务器性能配置 来设置从节点的优先级,比如 A 的物理内存最大,我们就把 A 的优先级设置成最高,这样在第一轮考察的时候,优先级最高的 A 就会胜出,成为新的主节点。

第二轮考察:复制进度最靠前的从节点胜出

如果在第一轮考察中,发现 优先级最高的从节点有两个,那么就会进行第二轮考察,比较两个从节点哪个复制进度。

从节点的复制进度是由 slave_repl_offset 记录的,如果某个从节点的 slave_repl_offset 最接近 master_repl_offset ,说明它的复制进度是最靠前的,于是就可以将它选为新主节点。

第三轮考察:ID 号小的从节点胜出

如果在第二轮考察中,发现有两个从节点优先级和复制进度都是一样的,那么就会进行第三轮考察,比较两个从节点的 ID 号,ID 号小的从节点胜出。

什么是 ID 号?在集群中,每个从节点都有一个唯一编号,这个编号就是 ID 号。

image.png

选举出从节点后,哨兵 leader 向该从节点发送 SLAVEOF no one 命令,让这个从节点解除从节点身份,变为新的主节点。

发送 SLAVEOF no one 命令后,哨兵 leader 会以每秒一次的频率向被升级的从节点发送 INFO 命令(没进行故障转移前,INFO 的频率是每十秒一次),并观察命令回复中的角色信息,当被升级节点的角色信息从原来的 slave 变为 master 时,哨兵 leader 就知道被选中的从节点顺利升级为主节点了。

步骤二:将从节点指向新主节点

哨兵 leader 向所有从节点发送 SLAVEOF ,让它们成为新主节点的从节点。

image.png

步骤三:通知客户端主节点已更换

这一步主要通过 redis 的发布者/订阅者机制 来实现。每个哨兵节点提供发布者/订阅者机制,客户端可以从哨兵订阅消息

哨兵提供的消息订阅频道有很多,不同频道包含了主从节点切换过程中的不同关键事件,几个常见的事件如下:

image.png

客户端和哨兵建立连接后,客户端会订阅哨兵提供的频道。主从切换后,哨兵就会向 switch-master 频道发送新主节点的 IP 和端口消息。

步骤四:将旧主节点变为从节点

故障转移操作最后要做的是,继续监视旧主节点,当旧主节点重新上线时,哨兵集群就会向它发送 SLAVEOF 命令,让它成为新主节点的从节点。

哨兵集群是如何组成的?

第一次搭建哨兵集群时,只需要填下面几个参数,设置主节点名字、主节点 IP 和端口号,以及 quorum 值。

sentinel monitor <master-name> <ip> <redis-port> <quorum>

既然构建集群时不需要填其他哨兵节点的信息,那么哨兵节点是如何感知对方的?

其实哨兵节点之间是通过前面提到的 发布者/订阅者机制 来相互发现的。

在主从集群中,主节点上有一个名为 __sentinel__:hello 的频道,不同哨兵就是通过它来相互发现,实现互相通信的。

3.Redis Cluster

redis 的分布式解决方案,用于实现 数据的分片存储和高可用性 。单点 redis 服务器会面临性能和容量限制,redis cluster 可以将数据 分散存储在多个节点上,通过集群方式扩展存储能力和处理能力。

redis cluster 底层采用 哈希槽 来处理数据和节点之间的映射关系。一个 redis cluster 集群中,共有 16384 个哈希槽,这些哈希槽类似于数据分区,每个键值对会根据它的 key,被映射到一个哈希槽中,具体过程分为两大步:

  • 根据键值对的 key,按照 CRC16算法 计算一个 16 bit 的值
  • 再用 16 bit 值对 16384 取模,得到 0 ~ 16383 范围内的模数,每个模数代表一个相应编号的哈希槽

那么这些哈希槽怎么被映射到具体的 redis 节点上?有两种方案:

  • 平均分配:使用 cluster create 创建集群时,redis 会自动把所有哈希槽平均分配到集群节点上
  • 手动分配:可以使用 cluster meet 命令手动建立节点间的连接,组成集群,再使用 cluster addslots 命令,指定每个节点上的哈希槽个数。需要注意的是,手动分配需要把 16384 个槽都分配完,否则 redis 集群无法正常工作

redis cluster 中的节点通过 Gossip 协议 进行通信。Gossip 协议是一种基于谣言传播的通信机制,节点之间会定期互相交换信息,将自己的状态(在线状态、负责的哈希槽等)传播给其他节点。

另外,redis cluster 中也存在主从结构。当哈希槽分配给不同的主节点后,每个主节点可以配置一个或多个从节点,以实现高可用性和数据冗余

参考:https://xiaolincoding.com/redis/cluster/master_slave_replication.html、https://xiaolincoding.com/redis/cluster/sentinel.html