Post

Redis 多机数据库的实现

Redis 官网:https://redis.io/docs/management/replication

Redis 翻译站:https://www.redis.cn/topics/replication.html

复制

概念:

  1. 在 Redis 中,用户可以通过执行 SLAVEOF命令或者设置 slaveof 选项,让一个服务器去复制(replicate)另一个服务器,其中被复制的服务器为主服务器 master,对主服务器进行复制的服务器称为从服务器(slave)。
  2. 数据库状态一致,简称一致:进行复制过程中的主从服务器双方的数据库保存相同的数据。

旧版复制功能的实现

Redis 的复制功能分为同步(sync)和命令传播(command propagate)两个操作:

  • 同步:将从服务器的数据库状态更新至主服务器当前所处的数据库状态。
  • 命令传播:在 master 的数据库状态被修改时主从数据库状态不一致时,让主从重新回到一致状态。

同步:客户端向从服务器发送SLAVEOF命令,要求从服务器复制主服务器时,从服务器首先需要执行同步操作,就是将服务器的数据库状态更新至主服务器当前所处的数据库状态。执行执行步骤如下:

  1. 从服务器向主服务器发送SYNC命令
  2. 收到SYNC命令的主服务器执行BGSAVE命令,在后台生成一个RDB文件,并使用一个缓冲区记录从现在开始执行的所有写命令。
  3. 当主服务器的BGSAVE命令执行完毕时,主服务器会将生成的RDB文件发送给从服务器,从服务器接收并载入将自己的数据库状态更新至主服务器执行BGSAVE命令时的数据库状态。
  4. 主服务器将记录在缓冲区里面的所有写命令发送给从服务器,从服务器执行这些写命令,将自己的数据库状态更新至主服务器数据库状前所处的状态。

命令传播:同步操作执行完毕之后,主从服务器两者的数据库达到一致状态,为了后续保持一致,主服务器会将自己执行的写命令发送给从服务器。

旧版复制功能的缺陷

服务器对主服务器的复制分两种情况:

  1. 初次复制:从服务器之前没复制过任何主服务器,或者从服务器当前要复制的主服务器和上次的不一样。
  2. 断线后重复制:处于命令传播阶段的主从服务器因为网络中断了复制,从服务器通过自动重连又连上了,然后再次开始复制【从服务器发现不一致,会再发SYNC命令给主服务器,重新回到一致】。

这里的断线后复制效率非常低,SYNC命令是一个非常耗费资源的操作,每次执行SYNC命令,主从服务器会有以下操作:

  1. 主服务器需要执行BGSAVE命令来生成 RDB 文件,【消耗大量 CPU、内存和磁盘 I/O 资源】
  2. 主服务器需要将自己生成的 RDB 文件发送给从服务器,【消耗从服务器大量的网络资源(带宽和流量),主服务器响应命令请求的时间也有影响】
  3. 从服务器载入 RDB 文件,【载入期间从服务器因为阻塞而没办法处理命令请求】

新版复制功能的实现

为了解决断线重复制低效:使用PSYNC命令代替SYNC来执行复制时的同步操作。

PSYNC命令两种模式1完整重同步(full resynchronization)和2部分重同步(partial resynchronization)

  1. 完整重同步:与SYNC初次复制步骤一样
  2. 部分重同步:断线后再连上主服务器时,如果条件允许就只同步断开期间的写命令。

部分重同步的实现

三个部分构成:

  1. 主服务器的复制偏移量(replication offset)和从服务器的复制偏移量
  2. 主服务器的复制积压缓冲区(replication backlog)
  3. 服务器的运行ID(run ID)

复制偏移量:执行复制的双方会分别维护一个复制偏移量,主服务器每次向从服务器传播N个字节的数据时,就将自己的offset的值加上N,从服务器每次收到主服务器传播来的N个字节的数据时,把自己的offset加上N。所以offset值相同,则主从服务器处于一致状态。

复制积压缓冲区:由主服务器维护的一个固定长度(fixed-size)先进先出(FIFO)队列,默认 1MB。

服务器的运行ID:执行复制时,互相用ID标识自己。

PSYNC命令的实现

  1. 如果服务器以前没有复制过任何主服务器,或者之前执行过SLAVEOF no one命令,那么从服务器在开始一次新的复制将向主服务器发送PSYNC ? -1命令,主动请求主服务器进行完整重同步(因为这时不可能执行部分重同步)
  2. 相反地,如果服务器已经复制过某个主服务器,那么从服务器在开始一次新的复制时将向主服务器发送PSYNC <runid> <offset>命令:其中 runid 是上一次复制的主服务器的运行ID,而 offset 则是从服务器当前的复制偏移量,接收到这个命令的主服务器会通过这两个参数来判断应该对从服务器执行哪种同步操作。

复制的实现

  1. 设置主服务器的地址和端口SLAVEOF 127.0.0.1 6379
  2. 建立套接字连接
  3. 发送PING命令
  4. 身份验证AUTH 10086
  5. 发送端口信息REPLCONF listening-port <port-number>
  6. 同步
  7. 命令传播

心跳检测

在命令传播阶段,从服务器默认会以每秒一次的频率,向主服务器发送命令:REPLCONF ACK <replication_offset>;replication_offset 是从服务器当前的复制偏移量。 命令的对于主从服务器有三个作用:

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

Sentinel

Sentinel(哨岗、哨兵)是 Redis 的高可用性(high availability)解决方案:

由一个或多个 Sentinel 实例(instance)组成的 Sentinel System 可以监视任意多个主服务器,以及这些主服务器属下的所有从服务器,并在被监视的主服务器进入下线状态时,自动将主服务器属下的某个从服务器升级为新的主服务器,然后由新的主服务器代替已下线的主服务器继续处理命令请求。

哨兵机制的基本流程

哨兵其实就是一个运行在特殊模式下的 Redis 进程,主从库实例运行的同时,它也在运行。哨兵主要负责的就是三个任务:1 监控、2 选主(选择主库)和 3 通知。

我们先看监控。监控是指哨兵进程在运行时,周期性地给所有的主从库发送PING命令,检测它们是否仍然在线运行。如果从库没有在规定时间内响应哨兵的PING命令,哨兵就会把它标记为“下线状态”;同样,如果主库也没有在规定时间内响应哨兵的PING命令,哨兵就会判定主库下线,然后开始自动切换主库的流程。

这个流程首先是执行哨兵的第二个任务,选主。主库挂了以后,哨兵就需要从很多个从库里,按照一定的规则选择一个从库实例,把它作为新的主库。这一步完成后,现在的集群里就有了新主库。

然后,哨兵会执行最后一个任务:通知。在执行通知任务时,哨兵会把新主库的连接信息发给其他从库,让它们执行replicaof命令,和新主库建立连接,并进行数据复制。同时,哨兵会把新主库的连接信息通知给客户端,让它们把请求操作发到新主库上。

在这三个任务中,通知任务相对来说比较简单,哨兵只需要把新主库信息发给从库和客户端,让它们和新主库建立连接就行,并不涉及决策的逻辑。但是,在监控和选主这两个任务中,哨兵需要做出两个决策:

  • 在监控任务中,哨兵需要判断主库是否处于下线状态;
  • 在选主任务中,哨兵也要决定选择哪个从库实例作为主库。

主观下线和客观下线

哨兵进程会使用PING命令检测它自己和主、从库的网络连接情况,用来判断实例的状态

  1. 如果哨兵发现主库或从库对PING命令的响应超时了,那么,哨兵就会先把它标记为“主观下线”。
  2. 如果检测的是从库,那么,哨兵简单地把它标记为“主观下线”就行了,因为从库的下线影响一般不太大,集群的对外服务不会间断。

但是,如果检测的是主库,那么,哨兵还不能简单地把它标记为“主观下线”,开启主从切换。因为很有可能存在这么一个情况:那就是哨兵误判了,其实主库并没有故障。可是,一旦启动了主从切换,后续的选主和通知操作都会带来额外的计算和通信开销。

为了避免这些不必要的开销,我们要特别注意误判的情况。

首先,我们要知道啥叫误判。很简单,就是主库实际并没有下线,但是哨兵误以为它下线了。误判一般会发生在集群网络压力较大、网络拥塞,或者是主库本身压力较大的情况下。一旦哨兵判断主库下线了,就会开始选择新主库,并让从库和新主库进行数据同步,这个过程本身就会有开销,例如,哨兵要花时间选出新主库,从库也需要花时间和新主库同步。而在误判的情况下,主库本身根本就不需要进行切换的,所以这个过程的开销是没有价值的。正因为这样,我们需要判断是否有误判,以及减少误判。

那怎么减少误判呢?在日常生活中,当我们要对一些重要的事情做判断的时候,经常会和家人或朋友一起商量一下,然后再做决定。

哨兵机制也是类似的,它通常会采用多实例组成的集群模式进行部署,这也被称为哨兵集群。引入多个哨兵实例一起来判断,就可以避免单个哨兵因为自身网络状况不好,而误判主库下线的情况。同时,多个哨兵的网络同时不稳定的概率较小,由它们一起做决策,误判率也能降低。

在判断主库是否下线时,不能由一个哨兵说了算,只有大多数的哨兵实例,都判断主库已经“主观下线”了,主库才会被标记为“客观下线”,这个叫法也是表明主库下线成为一个客观事实了。这个判断原则就是:少数服从多数。同时,这会进一步触发哨兵开始主从切换流程。

如何选定新主库?

一般来说,我把哨兵选择新主库的过程称为“筛选 + 打分”。简单来说,我们在多个从库中,先按照一定的筛选条件,把不符合条件的从库去掉。然后,我们再按照一定的规则,给剩下的从库逐个打分,将得分最高的从库选为新主库。

一般情况下,我们肯定要先保证所选的从库仍然在线运行。不过,在选主时从库正常在线,这只能表示从库的现状良好,并不代表它就是最适合做主库的。

设想一下,如果在选主时,一个从库正常运行,我们把它选为新主库开始使用了。可是,很快它的网络出了故障,此时,我们就得重新选主了。这显然不是我们期望的结果。

所以,在选主时,除了要检查从库的当前在线状态,还要判断它之前的网络连接状态。如果从库总是和主库断连,而且断连次数超出了一定的阈值,我们就有理由相信,这个从库的网络状况并不是太好,就可以把这个从库筛掉了。

具体怎么判断呢?你使用配置项 down-after-milliseconds * 10。其中,down-after-milliseconds 是我们认定主从库断连的最大连接超时时间。如果在 down-after-milliseconds 毫秒内,主从节点都没有通过网络联系上,我们就可以认为主从节点断连了。如果发生断连的次数超过了 10 次,就说明这个从库的网络状况不好,不适合作为新主库。

好了,这样我们就过滤掉了不适合做主库的从库,完成了筛选工作。

接下来就要给剩余的从库打分了。我们可以分别按照三个规则依次进行三轮打分,这三个规则分别是从库优先级、从库复制进度以及从库ID号。只要在某一轮中,有从库得分最高,那么它就是主库了,选主过程到此结束。如果没有出现得分最高的从库,那么就继续进行下一轮。

第一轮:优先级最高的从库得分高。

用户可以通过 slave-priority 配置项,给不同的从库设置不同优先级。比如,你有两个从库,它们的内存大小不一样,你可以手动给内存大的实例设置一个高优先级。在选主时,哨兵会给优先级高的从库打高分,如果有一个从库优先级最高,那么它就是新主库了。如果从库的优先级都一样,那么哨兵开始第二轮打分。

第二轮:和旧主库同步程度最接近的从库得分高。

这个规则的依据是,如果选择和旧主库同步最接近的那个从库作为主库,那么,这个新主库上就有最新的数据。

如何判断从库和旧主库间的同步进度呢?

主从库同步时有个命令传播的过程。在这个过程中,主库会用 master_repl_offset 记录当前的最新写操作在 repl_backlog_buffer 中的位置,而从库会用 slave_repl_offset 这个值记录当前的复制进度。

此时,我们想要找的从库,它的 slave_repl_offset 需要最接近 master_repl_offset。如果在所有从库中,有从库的 slave_repl_offset 最接近 master_repl_offset,那么它的得分就最高,可以作为新主库。

当然,如果有两个从库的 slave_repl_offset 值大小是一样的(例如,从库 1 和从库 2 的 slave_repl_offset 值都是 990),我们就需要给它们进行第三轮打分了。

第三轮:ID 号小的从库得分高。

每个实例都会有一个 ID,这个 ID 就类似于这里的从库的编号。目前,Redis 在选主库时,有一个默认的规定:在优先级和复制进度都相同的情况下,ID号最小的从库得分最高,会被选为新主库。 到这里,新主库就被选出来了,“选主”这个过程就完成了。

我们再回顾下这个流程。首先,哨兵会按照在线状态、网络状态,筛选过滤掉一部分不符合要求的从库,然后,依次按照优先级、复制进度、ID

Sentinel 系统选举领头 Sentinel 的方法是对 Raft 算法的领头选举方法的实现,关于这方法的详细信息可以观看 Raft 算法的作者录制的“Raft教程”视频http://v.youku.com/vshow/id XNiQxOTk5MTk2.html,或者 Raft 算法的论文。

集群

节点

节点通过握手来将其他节点添加到自己所处的集群当中。 CLUSTER MEET <ip> <port>像一个节点node发送CLUSTER MEET命令,可以让 node 节点与 ip 和 port 所指定的节点握手(handshake),当握手成功,node 节点就会将 ip 和 port 所指定的节点添加到 node 节点当前所在的集群中。

槽指派

集群中的 16384 个槽可以分别指派给集群中的各个节点,每个节点都会记录哪些槽指派给了自己,而哪些槽又被指派给了其他节点。

集群的整个数据库被分为 16384 个槽(slot),数据库中的每个键都属于这 16384 个槽的其中一个。

当数据库中的 16384 个槽都有节点在处理时,集群处于上线状态(ok);相反地,如果数据库中有任何一个槽没有得到处理,那么集群处于下线状态(fail)。

在集群中执行命令

节点在接到一个命令请求时,会先检查这个命令请求要处理的键所在的槽是否由自己负责,如果不是的话,节点将向客户端返回一个MOVED的错误,MOVED错误携带的信息可以指引客户端转向(redirect)至正在负责相关槽的节点。

在对数据库中的16384个槽都进行了指派之后,集群就会进入上线状态,这时客户端就可以向集群中的节点发送数据命令了。

MOVED错误格式:MOVED <slot> <ip>:<port>slot为键所在的槽,而ip和port则是负责处理槽slot的节点的ip地址和端口号。

重新分片

对 Redis 集群的重新分片工作是由 redis-trib 负责执行的,重新分片的关键是将属于某个槽的所有键值对从一个节点转移至另一个节点。

重新分片操作可以在线进行,在重新分片的过程中,集群不需要下线,并且源节点和目标节点都可以继续处理命令请求。

ASK错误

MOVED错误表示槽的负责权已经从一个节点转移到了另一个节点,而ASK错误只是两个节点在迁移槽的过程中使用的一种临时措施。

属于被迁移槽的一部分键值对保存在源节点里面,而另一部分键值对则保存在目标节点里面。

当客户端向源节点发送一个数据库键有关的命令,并且命令要处理的数据库键恰好就属于正在被迁移的槽时:

  1. 源节点会现在自己的数据库里面查找指定的键,如果找到的话,就直接执行客户端发送的命令。
  2. 相反地,如果源节点没能在自己的数据库里面找到指定的键,那么这个键可能已经被迁移到了目标节点,源节点将向客户端返回一个ASK错误,指引客户端转向正在导入槽的目标节点,并再次发送之前想要执行的命令。

复制与故障转移

精简描述:集群里的从节点用于复制主节点,并在主节点下线时,代替主节点继续处理命令请求。 故障转移:

  1. 复制下线主节点的所有从节点里面,会有一个从节点被选中。
  2. 被选中的从节点会执行SLAVEOF no one命令,成为新的主节点。
  3. 新的主节点会撤销所有对已下线主节点的槽指派,并将这些槽全部指派给自己。
  4. 新的主节点向集群广播一条PONG消息,这个消息可以让集群的其他节点立刻知道它成为了主节点,并且这个主节点已经接管了原本由已下线节点负责的槽。
  5. 新的主节点开始接收和自己负责处理的槽有关的命令请求,故障转移完成!

选取新的主节点:https://blog.csdn.net/sz85850597/article/details/86751215

消息

集群中的节点通过发送和接收消息来进行通信,常见的消息包括MEETPINGPONGPUBLISHFAIL五种。

发布与订阅

PUBLISHSUBSCRIBEPSUBSCRIBE等命令组成

执行SUBSCRIBE命令,客户端可以订阅一个或多个频道,从而成为这些频道的订阅者(subscriber):每当有其他客户端向被订阅的频道发送消息(message)时,频道的所有订阅者都会收到这条消息。

频道的订阅与退订

服务器状态在pubsub_channels字典保存了所有频道的订阅关系:SUBSCRIBE命令负责将客户端和被订阅的频道关联到这个字典里面,而UNSUBSCRIBE命令则负责解除客户端和被退订频道之间的关联。

模式的订阅与退订

服务器状态在pubsub_patterns链表保存了所有模式的订阅关系:PSUBSCRIBE命令负责将客户端和被订阅的模式记录到这个链表中,而PUNSUBSCRIBE命令则负责移除客户端和被退订模式在链表中的记录。

发送消息

PUBLISH命令通过访问pubsub_channels字典来向频道的所有订阅者发送消息,通过访问pubsub_patterns链表来向所有匹配频道的模式的订阅者发送消息。

This post is licensed under CC BY 4.0 by the author.