Redis-主从复制、Sentinel、Cluster集群【随笔四】

Redis 集群

Redis 提供了三种集群策略:

  • 主从复制模式:这种模式⽐较简单,主库可以读写,并且会和从库进⾏数据同步,这种模式下,客户端直接连主库或某个从库,但是但主库或从库宕机后,客户端需要⼿动修改 IP,另外,这种模式也⽐较难进⾏扩容,整个集群所能存储的数据受到某台机器的内存容量,所以不可能⽀持特⼤数据量
  • Sentinel(哨兵)模式:这种模式在主从的基础上新增了哨兵节点,但主库节点宕机后,哨兵会发现主库节点宕机,然后在从库中选择⼀个库作为进的主库,另外哨兵也可以做集群,从⽽可以保证但某⼀个哨兵节点宕机后,还有其他哨兵节点可以继续⼯作,这种模式可以⽐较好的保证 Redis 集群的⾼可⽤,但是仍然不能很好的解决 Redis 的容量上限问题。
  • Cluster 模式:Cluster 模式是⽤得⽐较多的模式,它⽀持多主多从,这种模式会按照 key 进⾏槽位的分配,可以使得不同的 key 分散到不同的主节点上,利⽤这种模式可以使得整个集群⽀持更⼤的数据容量,同时每个主节点可以拥有⾃⼰的多个从节点,如果该主节点宕机,会从它的从节点中选举⼀个新的主节点。

对于这三种模式,如果 Redis 要存的数据量不⼤,可以选择哨兵模式,如果 Redis 要存的数据量⼤,并且需要持续的扩容,那么选择Cluster 模式。

主从复制模式

基本概念

通过持久化功能,Redis 保证了即使在服务器重启的情况下也不会丢失(或少量丢失)数据,因为持久化会把内存中数据保存到硬盘上,重启会从硬盘上加载数据。 但是由于数据是存储在一台服务器上的,如果这台服务器出现硬盘故障等问题,也会导致数据丢失。

为了避免单点故障,通常的做法是将数据库复制多个副本以部署在不同的服务器上,这样即使有一台服务器出现故障,其他服务器依然可以继续提供服务。

为此, Redis 提供了复制(replication)功能,能使得从 Redis 服务器(下文称 slave)能精确得复制主 Redis 服务器(下文称 master)的内容。每次当 slave 和 master 之间的连接断开时, slave 会自动重连到 master 上,并且无论这期间 master 发生了什么, slave 都将尝试让自身成为 master 的精确副本。

请添加图片描述

这个系统的运行依靠三个主要的机制:

  • 当一个 master 实例和一个 slave 实例连接正常时, master 会发送一连串的命令流来保持对 slave 的更新,以便于将自身数据集的改变复制给 slave :包括客户端的写入、key 的过期或被删除等等。
  • 当 master 和 slave 之间的连接断开之后,因为网络问题、或者是主从意识到连接超时, slave 重新连接上 master 并会尝试进行部分重同步:这意味着它会尝试只获取在断开连接期间内丢失的命令流。
  • 当无法进行部分重同步时, slave 会请求进行全量重同步。这会涉及到一个更复杂的过程,例如 master 需要创建所有数据的快照,将之发送给 slave,之后在数据集更改时持续发送命令流到 slave 。

Redis 复制的重要特性:

  • Redis 使用异步复制,slave 和 master 之间异步地确认处理的数据量
  • 一个 master 可以拥有多个 slave
  • slave 可以接受其他 slave 的连接。除了多个 slave 可以连接到同一个 master 之外, slave 之间也可以像层叠状的结构(cascading-like structure)连接到其他 slave 。自 Redis 4.0 起,所有的 sub-slave 将会从 master 收到完全一样的复制流。
  • Redis 复制在 master 侧是非阻塞的。这意味着 master 在一个或多个 slave 进行初次同步或者是部分重同步时,可以继续处理查询请求。
  • 复制在 slave 侧大部分也是非阻塞的。当 slave 进行初次同步时,它可以使用旧数据集处理查询请求,假设你在 redis.conf 中配置了让 Redis 这样做的话。否则,你可以配置如果复制流断开, Redis slave 会返回一个 error 给客户端。但是,在初次同步之后,旧数据集必须被删除,同时加载新的数据集。 slave 在这个短暂的时间窗口内(如果数据集很大,会持续较长时间),会阻塞到来的连接请求。自 Redis 4.0 开始,可以配置 Redis 使删除旧数据集的操作在另一个不同的线程中进行,但是,加载新数据集的操作依然需要在主线程中进行并且会阻塞 slave 。
  • 复制既可以被用在可伸缩性,以便只读查询可以有多个 slave 进行(例如 O(N) 复杂度的慢操作可以被下放到 slave ),或者仅用于数据安全。
  • 可以使用复制来避免 master 将全部数据集写入磁盘造成的开销:一种典型的技术是配置你的 master Redis.conf 以避免对磁盘进行持久化,然后连接一个 slave ,其配置为不定期保存或是启用 AOF。但是,这个设置必须小心处理,因为重新启动的 master 程序将从一个空数据集开始:如果一个 slave 试图与它同步,那么这个 slave 也会被清空。
主从复制工作原理

每一个 Redis master 都有一个 replication ID :这是一个较大的伪随机字符串,标记了一个给定的数据集。每个 master 也持有一个偏移量,master 将自己产生的复制流发送给 slave 时,发送多少个字节的数据,自身的偏移量就会增加多少,目的是当有新的操作修改自己的数据集时,它可以以此更新 slave 的状态。复制偏移量即使在没有一个 slave 连接到 master 时,也会自增,所以基本上每一对给定的 Replication ID, offset 都会标识一个 master 数据集的确切版本。

当 slave 连接到 master 时,它们使用 PSYNC 命令来发送它们记录的旧的 master replication ID 和它们至今为止处理的偏移量。通过这种方式, master 能够仅发送 slave 所需的增量部分。但是如果 master 的缓冲区中没有足够的命令积压缓冲记录,或者如果 slave 引用了不知道的历史记录(replication ID),则会转而进行一个全量重同步:在这种情况下, slave 会得到一个完整的数据集副本,从头开始。

在 Redis2.8 之前只能使用 sync 命令来主从同步数据就是全量复制,sync 命令会在不管 slave 是第一次启动还是断线重连都会全量的去复制数据,在 Redis2.8 之后使用 psync 命令来完成主从数据同步,psync 弥补对 sync 只能全量同步数据的问题,psync 的同步过程分为全量复制跟增量复制。

断开重连后,2.8之后的版本会将断线期间的命令传给重数据库,增量复制。

主从刚刚连接的时候,进行全量同步;全同步结束后,进行增量同步。当然,如果有需要,slave 在任何时候都可以发起全量同步。Redis 的策略是,无论如何,首先会尝试进行增量同步,如不成功,要求从机进行全量同步。

SYNC 全量同步

Redis2.8 之前的同步数据是 SYNC 命令的全量复制,初始化和断线重连都是使用的全量复制。

执行过程

请添加图片描述

  • 从数据库启动成功后,连接主数据库,发送 SYNC 命令;
  • 主数据库接收到 SYNC 命令后,开始执行 BGSAVE 命令生成 RDB 文件并使用缓冲区记录此后执行的所有写命令;
  • 主数据库 BGSAVE 执行完后,向所有从数据库发送快照文件,并在发送期间继续记录被执行的写命令;
  • 从数据库收到快照文件后丢弃所有旧数据,将快照保存在磁盘上,然后加载到内存中;
  • 主数据库快照发送完毕后开始向从数据库发送缓冲区中的写命令;
  • 从数据库完成对快照的载入,开始接收命令请求,并执行来自主数据库缓冲区的写命令;(从数据库初始化完成
  • 主数据库每执行一个写命令就会向从数据库发送相同的写命令,从数据库接收并执行收到的写命令(从数据库初始化完成后的操作
PSYNC 增量同步

为了解决 Redis2.8 之前的版本中断线情况下 SYNC 进行全量同步的低效问题,在 Redis 2.8 之后使用 PSYNC 命令代替 SYNC 命令执行同步操作,PSYNC 具备了数据全量重同步 和 部分重同步模式

  • 全量重同步:跟 Redis2.8 之前的版本一样全量复制
  • 部分重同步:salve 断开又重新连时,在命令传播阶段,只需要发送与 master 断开这段时间执行的写命给 slave 即可,可以理解为增量同步
相关概念

PSYNC 同步有几个重要的概念:runid、offset(复制偏移量)以及复制积压缓冲区。

  • runid:每个 Redis 服务器都会有一个表明自己身份的 ID。在 PSYNC 中发送的这个 ID 是指之前连接的 master 的 ID,如果没保存这个 ID,PSYNC 的命令会使用 PSYNC ? -1 这种形式发送给 master,表示需要全量复制。
  • offset(复制偏移量):在主从复制的 master 和 slave 双方都会各自维持一个 offset。master 成功发送 N 个字节的命令后会将 master 里的 offset 加上 N,slave 在接收到 N 个字节命令后同样会将 slave 里的 offset 增加 N。在进行增量复制时,通过 offset 来判断需要增量复制的范围;master 和 slave 如果状态是一致的,那么它们的 offset 也应该是一致的。
  • 复制积压缓冲区:复制积压缓冲区是由 master 维护的一个固定长度环形积压队列(FIFO 队列),它的作用是缓存已经传播出去的命令。当 master 进行命令传播时,不仅将命令发送给所有 slave,还会将命令写入到复制积压缓冲区里面。
执行过程

请添加图片描述

如上图所示 PSYNC 执行过程和 SYNC 的区别在于:salve 连接时,判断是否需要全量同步,全量同步的逻辑过程和 SYNC 一样。

PSYNC 执行步骤如下:

  • 客户端向服务器发送 SLAVEOF 命令,即 salve 向 master 发起连接请求时,slave 根据自己是否保存 master runid 来判断是否是第一次连接。
  • 如果是第一次同步则向 master 发送 PSYNC ? -1 命令来进行全量同步;如果是重连接,会向 master 发送PSYNC runid offset 命令(runid 是 master 的身份ID,offset 是从节点同步命令的全局迁移量)。
  • master 接收到 PSYNC 命令后,首先判断 runid 是否和本机的 id 一致,如果一致则会再次判断 offset 偏移量和本机的偏移量相差有没有超过复制积压缓冲区大小,如果没有那么就给 slave 发送 CONTINUE,此时 slave只需要等待 master 传回失去连接期间丢失的命令。
  • 如果 runid 和本机 id 不一致或者 offset 差距超过了复制积压缓冲区大小,那么就会返回 FULLRESYNC runid offset,slave 将 runid 保存起来,并进行全量同步。

在写操作命令传播时,master 会将每一个写命令传递给 slave,同时也会将写命令存放到积压队列,并记录当前积压队列中存放命令的全局偏移量 offset。当 salve 重连接时,master 会根据从节点传的 offset 在环形积压队列中找到断开这段时间执行的命令,并同步给 salve 节点,达到增量同步结果。

配置复制积压区
# 默认情况下积压队列的大小为1MB,可以通过配置文件设置队列大小;积压队列越大,允许主从数据库断线的时间就越长
repl-backlog-size 1mb
# 当没有salve连接时,默认一小时可以释放环形队列,可以通过配置设置释放时间(单位秒)
repl-backlog-ttl 3600
主从复制优缺点
优点:
  • 支持主从复制,主机会自动将数据同步到从机,可以进行读写分离;
  • 为了分载 Master 的读操作压力,Slave 服务器可以为客户端提供只读操作的服务,写服务仍然必须由 Master 来完成;
  • Slave 同样可以接受其它 Slaves 的连接和同步请求,这样可以有效的分载 Master 的同步压力;
  • Master Server 是以非阻塞的方式为 Slaves 提供服务。所以在 Master-Slave 同步期间,客户端仍然可以提交查询或修改请求;
  • Slave Server 同样是以非阻塞的方式完成数据同步。在同步期间,如果有客户端提交查询请求,Redis 则返回同步之前的数据;
缺点:
  • Redis 不具备自动容错和恢复功能,主机从机的宕机都会导致前端部分读写请求失败,需要等待机器重启或者手动切换前端的 IP 才能恢复(也就是要人工介入);
  • 主机宕机,宕机前有部分数据未能及时同步到从机,切换 IP 后还会引入数据不一致的问题,降低了系统的可用性;
  • 如果多个 slave 断线了,需要重启的时候,尽量不要在同一时间段进行重启。因为只要 slave 启动,就会发送 sync/psync 请求和主机同步,当多个 slave 重启的时候,可能会导致 master IO 剧增从而宕机。
  • Redis 较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂;

Sentinel(哨兵)模式

基本概念

第一种主从同步/复制的模式,当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用。为了解决这一问题,我们可以使用哨兵模式。

Redis 的 Sentinel 系统用于管理多个 Redis 服务器(instance), 该系统执行以下三个任务:

  • 监控(Monitoring): Sentinel 会不断地检查你的主服务器和从服务器是否运作正常。
  • 提醒(Notification): 当被监控的某个 Redis 服务器出现问题时, Sentinel 可以通过 API 向管理员或者其他应用程序发送通知。
  • 自动故障迁移(Automatic failover): 当一个主服务器不能正常工作时, Sentinel 会开始一次自动故障迁移操作, 它会将失效主服务器的其中一个从服务器升级为新的主服务器, 并让失效主服务器的其他从服务器改为复制新的主服务器; 当客户端试图连接失效的主服务器时, 集群也会向客户端返回新主服务器的地址, 使得集群可以使用新主服务器代替失效服务器。

哨兵模式是一种特殊的模式,首先 Redis 提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待 Redis 服务器响应,从而监控运行的多个 Redis 实例
请添加图片描述

具体流程如下:

  • 通过发送命令,让 Redis 服务器返回监控其运行状态,包括主服务器和从服务器;
  • 当哨兵监测到 master 宕机,自动将一台 slave 切换成 master,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换主机;

然而一个哨兵进程对 master 进行监控,也可能会出现问题,比如:如果 master 状态正常,但这个哨兵在询问 master 时,它们之间的网络发生了问题,那这个哨兵可能会误判。为此,我们可以部署多个哨兵,让它们分布在不同的机器上,它们一起监测 master 的状态。各个哨兵之间还会进行监控,这样就形成了多哨兵模式。
请添加图片描述

具体流程如下:

  • 多个哨兵通过发送命令,让 Redis 服务器返回监控其运行状态,包括主服务器和从服务器;
  • 一旦有一个哨兵监测到 master 宕机(不管是不是网络问题),就会询问其它哨兵该 master 节点的状态,如果多个哨兵(设置一个阈值)都认位 master 宕机,则判定为 master 确实宕机了,否则认定为正常。
  • 若认定 master 宕机,则多个哨兵经过选举后,会自动将一台 slave 切换成 master ,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换主机;
哨兵间通信

一个 Sentinel 可以与其他多个 Sentinel 进行连接, 各个 Sentinel 之间可以互相检查对方的可用性, 并进行信息交换。

你无须为运行的每个 Sentinel 分别设置其他 Sentinel 的地址, 因为 Sentinel 可以通过 发布与订阅功 能来自动发现正在监视相同主服务器的其他 Sentinel , 这一功能是通过向频道 sentinel:hello 发送信息来实现的。

与此类似, 你也不必手动列出主服务器属下的所有从服务器, 因为 Sentinel 可以通过询问主服务器来获得所有从服务器的信息。

  • 每个 Sentinel 会以每两秒一次的频率, 通过发布与订阅功能, 向被它监视的所有主服务器和从服务器的 sentinel:hello 频道发送一条信息, 信息中包含了 Sentinel 的 IP 地址、端口号和运行 ID (runid)。
  • 每个 Sentinel 都订阅了被它监视的所有主服务器和从服务器的 sentinel:hello 频道, 查找之前未出现过的 sentinel (looking for unknown sentinels)。 当一个 Sentinel 发现一个新的 Sentinel 时, 它会将新的 Sentinel 添加到一个列表中, 这个列表保存了 Sentinel 已知的, 监视同一个主服务器的所有其他 Sentinel 。
  • Sentinel 发送的信息中还包括完整的主服务器当前配置(configuration)。 如果一个 Sentinel 包含的主服务器配置比另一个 Sentinel 发送的配置要旧, 那么这个 Sentinel 会立即升级到新配置上。
  • 在将一个新 Sentinel 添加到监视主服务器的列表上面之前, Sentinel 会先检查列表中是否已经包含了和要添加的 Sentinel 拥有相同运行 ID 或者相同地址(包括 IP 地址和端口号)的 Sentinel , 如果是的话, Sentinel 会先移除列表中已有的那些拥有相同运行 ID 或者相同地址的 Sentinel , 然后再添加新 Sentinel 。
Sentinel 的工作方式
  • 每个 sentinel 以每秒钟一次的频率向它所知的 master,slave 以及其他 sentinel 实例发送一个PING 命令。
  • 如果一个实例(instance)距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值,则这个实例会被 sentinel 标记为主观下线
  • 如果一个 master 被标记为 主观下线,则正在监视这个 master 的所有 sentinel 要以每秒一次的频率确认master 的确进入了 主观下线 状态。
  • 如果一个 master 被标记为 主观下线,当有足够数量的 sentinel(大于等于配置文件指定的值)在指定的时间范围内确认 master 的确进入了 主观下线 状态,则 master 会被标记为 客观下线
  • 在一般情况下,每个 sentinel 会以每10秒一次的频率向它已知的所有 master ,slave 发送 INFO 命令。当一个 master 被 sentinel 标记为 客观下线 时,sentinel 向下线的 master 的所有 slave 发送 INFO 命令的频率会从10秒一次改为每秒一次。
  • 若没有足够数量的 sentinel 同意 master 已经下线,master 的 客观下线状态就会被移除。 若 master 重新向 sentinel 的 PING 命令返回有效回复,master 的 主观下线 状态就会被移除。
主观下线和客观下线

前面说过, Redis 的 Sentinel 中关于下线(down)有两个不同的概念:

  • 主观下线(Subjectively Down, 简称 SDOWN)指的是单个 Sentinel 实例对服务器做出的下线判断。
  • 客观下线(Objectively Down, 简称 ODOWN)指的是多个 Sentinel 实例在对同一个服务器做出 SDOWN 判断, 并且通过 SENTINEL is-master-down-by-addr 命令互相交流之后, 得出的服务器下线判断。 (一个 Sentinel 可以通过向另一个 Sentinel 发送 SENTINEL is-master-down-by-addr 命令来询问对方是否认为给定的服务器已下线。)

如果一个服务器没有在 down-after-milliseconds 选项所指定的时间内, 对向它发送 PING 命令的 Sentinel 返回一个有效回复(valid reply), 那么 Sentinel 就会将这个服务器标记为主观下线。

服务器对 PING 命令的有效回复可以是以下三种回复的其中一种:

  • 返回 +PONG 。
  • 返回 -LOADING 错误。
  • 返回 -MASTERDOWN 错误。

如果服务器返回除以上三种回复之外的其他回复, 又或者在指定时间内没有回复 PING 命令, 那么 Sentinel 认为服务器返回的回复无效(non-valid)。

注意, 一个服务器必须在 master-down-after-milliseconds 毫秒内, 一直返回无效回复才会被 Sentinel 标记为主观下线。举个例子:如果 down-after-milliseconds 选项的值为 30000 毫秒(30 秒), 那么只要服务器能在每 29 秒之内返回至少一次有效回复, 这个服务器就仍然会被认为是处于正常状态的。

客观下线条件只适用于主服务器: 对于任何其他类型的 Redis 实例, Sentinel 在将它们判断为下线前不需要进行协商, 所以从服务器或者其他 Sentinel 永远不会达到客观下线条件。

只要一个 Sentinel 发现某个主服务器进入了客观下线状态, 这个 Sentinel 就可能会被其他 Sentinel 推选出, 并对失效的主服务器执行自动故障迁移操作

选举领导者哨兵节点

当一个 master 被判断客观下线以后,多个监视该服务的 sentinel 协商,选举一个领头 sentinel,对并由该领导者节点对其进行故障转移操作。

监视该主节点的所有哨兵都有可能被选为领导者,选举使用的算法是 Raft 算法;Raft 算法的基本思路是先到先得:即在一轮选举中,哨兵 A 向 B 发送成为领导者的申请,如果 B 没有同意过其他哨兵,则会同意 A 成为领导者。is-master-down-by-addr 这个命令有两个作用,一是确认下线判定,二是进行领导者选举

一致性算法 Raft

Raft 是一种为了管理复制日志的一致性算法,主要用来解决分布式系统一致性问题。Raft 提供了和 Paxos 算法相同的功能和性能,但是它的算法结构和 Paxos 不同。Raft 算法更加容易理解并且更容易构建实际的系统。

Raft 将一致性算法分解成了3模块:leader选举、日志复制、安全性。

Raft 算法分为两个阶段,首先是选举过程,然后在选举出来的领导人带领进行正常操作,比如日志复制等,这里简单介绍一下 leader 选举。

  • Raft 将系统中的角色分为以下三种:

    • 领导者(Leader):处理客户端交互,日志复制等动作,一般一次只有一个领导者
    • 候选者(Candidate):候选者就是在选举过程中提名自己的实体,一旦选举成功,则成为领导者
    • 跟随者(Follower):类似选民,完全被动的角色,这样的服务器等待被通知投票
  • Term:Raft 协议将时间切分为一个个的 Term(任期),可以认为是一种逻辑时间

leader 选举过程

在 Raft 中,任何时候一个服务器都可以扮演下面的角色之一:Leader、Follower、Candidate;当server启动时,初始状态都是 follower。**每一个 server 都有一个定时器,超时时间为 election timeout(一般为150-300ms),如果某 server 没有超时的情况下收到来自领导者或者候选者的任何消息,定时器重启,如果超时,它就开始一次选举。**如下图:
请添加图片描述

Raft 采用心跳机制触发 Leader 选举,系统启动后:

  • 全部节点初始化为 Follower,term 为0。

  • 节点如果收到了 RequestVote 或者 AppendEntries,就会保持自己的 Follower 身份

    • AppendEntries :用于 leader 向 follower 发送心跳信号与更新同步日志
    • RequestVote:当 follower 转换成 candidate 的时候,就会向其他节点发起请求投票的请求
  • 节点如果一段时间内没收到 AppendEntries 消息,在该节点的超时时间内还没发现 Leader,Follower 就会转换成 Candidate,自己开始竞选 Leader。

  • 一旦转化为 Candidate,该节点立即开始下面几件事情:

    • 增加自己的 term。
    • 启动一个新的定时器。
    • 给自己投一票。
    • 向所有其他节点发送 RequestVote,并等待其他节点的回复。
  • 如果在计时器超时前,节点收到多数节点的同意投票,就转换成 Leader。同时向所有其他节点发送 AppendEntries,告知自己成为了 Leader。

  • 每个节点在一个 term 内只能投一票,采取先到先得的策略,Candidate 前面说到已经投给了自己, Follower 会投给第一个收到 RequestVote 的节点。

  • Raft 协议的定时器采取随机超时时间,这是选举 Leader 的关键。

  • 在同一个 term 内,先转为 Candidate 的节点会先发起投票,从而获得多数票。

Sentinel 选举 leader 流程
  • 某 Sentinel 认定 master 客观下线后,该 Sentinel 会先看看自己有没有投过票,如果自己已经投过票给其他 Sentinel 了,在一定时间(election timeout)内自己就不会成为 Leader。

  • 如果该 Sentinel 还没投过票,那么它就成为 Candidate。

  • Sentinel 需要完成几件事情:

    • 更新故障转移状态为 start
    • 当前 epoch 加1,相当于进入一个新 term,在 Sentinel 中 epoch 就是 Raft 协议中的 term。
    • 向其他节点发送 is-master-down-by-addr 命令请求投票。命令会带上自己的 epoch。
    • 给自己投一票(leader、leader_epoch)
  • 当其它哨兵收到此命令时,可以同意或者拒绝它成为领导者(通过判断 epoch);

  • Candidate 会不断的统计自己的票数,直到发现认同自己成为 Leader 的票数超过一半而且超过它配置的 quorum,这时它就成为了 Leader。

  • 其他 Sentinel 等待 Leader 从 slave 选出 master 后,检测到新的 master 正常工作后,就会去掉客观下线的标识。

选举领导者Sentinel遵循以下规则
  • 所有的 sentinel 都有公平被选举成 leader 的资格,每个做主观下线的 sentinel 节点向其他 sentinel 节点发送上面那条命令,要求将它设置为领导者。
  • 收到命令的 sentinel 节点如果还没有同意过其他的 sentinel 发送的命令(还未投过票),那么就会同意,否则拒绝。
  • 如果该 sentine l节点发现自己的票数已经过半且达到了 quorum(Sentinel节点数/2+1)的值,就会成为领导者
  • 如果在限定时间内,没有选举出 leader,暂定一段时间,再选举。
故障转移

一次故障转移操作由以下步骤组成:

  • 发现主服务器已经进入 客观下线 状态。
  • 对我们的当前纪元进行自增(详情请参考 Raft leader election ), 并尝试在这个纪元中当选。
  • 如果当选失败, 那么在设定的故障迁移超时时间的两倍之后, 重新尝试当选。 如果当选成功, 那么执行以下步骤。
  • 选出一个从服务器,并将它升级为主服务器。
  • 向被选中的从服务器发送 SLAVEOF NO ONE 命令,让它转变为主服务器。
  • 通过发布与订阅功能, 将更新后的配置传播给所有其他 Sentinel , 其他 Sentinel 对它们自己的配置进行更新。
  • 向已下线主服务器的从服务器发送 SLAVEOF 命令, 让它们去复制新的主服务器。
  • 当所有从服务器都已经开始复制新的主服务器时, 领头 Sentinel 终止这次故障迁移操作。

每当一个 Redis 实例被重新配置(reconfigured) —— 无论是被设置成主服务器、从服务器、又或者被设置成其他主服务器的从服务器 —— Sentinel 都会向被重新配置的实例发送一个 CONFIG REWRITE 命令, 从而确保这些配置会持久化在硬盘里。

Sentinel 使用以下规则来选择新的主服务器:

  • 在失效主服务器属下的从服务器当中, 那些被标记为主观下线、已断线、或者最后一次回复 PING 命令的时间大于五秒钟的从服务器都会被淘汰。
  • 在失效主服务器属下的从服务器当中, 那些与失效主服务器连接断开的时长超过 down-after 选项指定的时长十倍的从服务器都会被淘汰。
  • 在经历了以上两轮淘汰之后剩下来的从服务器中,优先选择 slave-priority 最高的节点;
  • 如果 slave-priority 一样,选出复制偏移量(replication offset)最大的那个从服务器作为新的主服务器,因为复制偏移量越大则数据复制的越完整;
  • 如果复制偏移量不可用, 或者从服务器的复制偏移量相同, 那么带有最小运行 ID 的那个从服务器成为新的主服务器,因为 run_id 越小说明重启次数越少。
为什么Sentinel集群至少3节点

Sentinel 节选举成为 Leader 的最低票数为 quorumSentinel节点数/2+1 的最大值,如果 Sentinel 集群只有2个 Sentinel 节点,则

Sentinel节点数/2 + 1
= 2/2 + 1
= 2

即 Leader 最低票数至少为2,当该 Sentinel 集群中由一个 Sentinel 节点故障后,仅剩的一个 Sentinel 节点是永远无法成为 Leader。

也可以由此公式可以推导出,Sentinel 集群允许1个 Sentinel 节点故障则需要3个节点的集群;允许2个节点故障则需要5个节点集群。

哨兵模式的优缺点

优点:

  • 哨兵模式是基于主从模式的,所有主从的优点,哨兵模式都具有。
  • 主从可以自动切换,系统更健壮,可用性更高(可以看作自动版的主从复制)。

缺点:

  • Redis 较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。

Cluster 集群模式

为什么需要 Redis 集群?

在讲 Redis 集群架构之前,我们先简单讲下 Redis 单实例的架构,从最开始的一主N从,到读写分离,再到 Sentinel 哨兵机制,单实例的 Redis 缓存足以应对大多数的使用场景,也能实现主从故障迁移。但是,在某些场景下,单实例存 Redis 缓存会存在的几个问题:

Redis 主从架构+ Sentinel 仍存在问题:

  • 写并发的压力仍在:Redis 单实例读写分离可以解决读操作的负载均衡,但对于写操作,仍然是全部落在了 master 节点上面,在海量数据高并发场景,一个节点写数据容易出现瓶颈,造成 master 节点的压力上升。
  • 海量数据的存储压力:一、内存容量的限制:Redis 的最大缺点和局限性就在于内存存储数据,这样子对容量而言会有相当大的限制。二、持久化和硬盘的限制:Redis 单实例本质上只有一台 Master 作为存储,如果面对海量数据的存储,一台 Redis 的服务器就应付不过来了,而且数据量太大意味着持久化成本高,严重时可能会阻塞服务器,造成服务请求成功率下降,降低服务的稳定性。
基本概念

Redis Cluster 是一种服务器 Sharding 技术,3.0版本开始正式提供。

Redis 的哨兵模式基本已经可以实现高可用,读写分离 ,但是在这种模式下每台 Redis 服务器都存储相同的数据,很浪费内存,所以在 redis3.0 上加入了 Cluster 集群模式,实现了 Redis 的分布式存储,也就是说每台 Redis 节点上存储不同的内容,其结构图如下:
请添加图片描述

Redis Cluster特性:

  • (分片存储)Redis3.0 加入了 Redis 的集群模式,实现了数据的分布式存储,对数据进行分片,将不同的数据存储在不同的 master 节点上面,从而解决了海量数据的存储问题。

  • (指令转换)Redis 集群采用去中心化的思想,没有中心节点的说法,对于客户端来说,整个集群可以看成一个整体,可以连接任意一个节点进行操作,就像操作单一 Redis 实例一样,不需要任何代理中间件,当客户端操作的 key 没有分配到该 node 上时,Redis 会返回转向指令,指向正确的 Redis 节点。

  • (主从和哨兵)Redis 也内置了高可用机制,支持N个 master 节点,每个 master 节点都可以挂载多个 slave 节点,当 master 节点挂掉时,集群会提升它的某个 slave 节点作为新的 master 节点。

集群的数据分片

Redis 集群通过分布式存储的方式解决了单节点的海量数据存储的问题,对于分布式存储,需要考虑的重点就是如何将数据进行拆分到不同的 Redis 服务器上。常见的分区算法有hash算法、一致性hash算法。

hash算法

哈希算法的思想非常简单,与我们所熟知的 HashMap 的 hash 函数类似,通过一个哈希函数得到某一个数字,然后根据数字取模找到相应的服务器。

比如有 N 个 redis 实例,那么如何将 key 映射到 redis 上呢?

通常是采用 hash 算法计算 key 的 hash 值,然后通过取模,均匀的映射到N个 redis 服务器上 hash(key)%N

但是如果增加一个服务器,那么映射公式就变成了 hash(key)%(N+1);如果有一个服务器宕机了,映射公式变成了 hash(key)%(N-1)。这种情况下,几乎所有的缓存都失效了,会导致数据库访问的压力陡增,甚至导致数据库宕机。

hash算法优点就是比较简单,属于静态的分片规则。但是一旦节点数量变化,新增或者减少,由于取模的 N 发生变化, 数据需要重新分布和迁移。

一致性hash算法

一致性哈希算法可以说是哈希算法的升级版,解决了哈希算法扩展性差的问题,一致性哈希算法跟哈希算法不一样,一致性哈希算法会将服务器和数据都通过哈希函数映射到一个首尾相连的虚拟的圆环(哈希环)上,整个空间按顺时针方向组织。因为是环形空间,0 和 2^32-1 是重叠的(总共2^32个)。

具体步骤如下:

  1. 首先根据服务器(节点)的名称或 IP 计算出哈希值,并将其配置到哈希环上。

  2. 然后采用同样的方法求出存储数据的键的哈希值,得到哈希环中相应的位置。

  3. 然后从数据映射到的位置开始顺时针查找,将数据保存到找到的第一个大于等于该哈希值的 Node 上。如果超过 2^32 仍然找不到服务器,就会保存到第一台服务器上。

  4. 新增或删减节点,数据仍然会进行漂移,但是影响范围就很小了,只会影响到相邻的节点。
    请添加图片描述

优点:在加入和删除节点时只影响相邻的两个节点,缺点:加减节点会造成部分数据无法命中。所以一般用于缓存,而且用于节点量大的情况下,扩容一般增加一倍节点保障数据负载均衡。

此外,针对于 hash 节点分散不均匀或者倾倒状态,一致性哈希算法引入了 虚拟节点 的概念。所谓虚拟节点,就是基于原来的物理节点映射出N个子节点,最后把所有的子节点映射到环形空间上。虚拟节点越多,分布越均匀。

请添加图片描述

哈希槽Slot算法

Redis 集群既没有用哈希取模,也没有用一致性哈希,而是用 hash 槽来实现的。Redis 集群创建了 16384 个槽(slot),每个节点负责一定区间的 slot。

具体流程:

  • 根据 Redis 主节点的数量来分配 slot,每个节点负责一定区间的 slot

  • 集群会对使用 CRC16 算法对 key 进行计算并对 16384 取模 (slot = CRC16(key)%16384

  • 得到的结果就是 Key-value 对所放入的 slot,通过这个 slot 去找对应的 Redis 节点

  • 然后直接到这个对应的节点上进行存取操作

举个例子,比如当前集群有3个节点,那么:

  • 节点 A 包含 0 到 5460 号哈希槽
  • 节点 B 包含 5461 到 10922 号哈希槽
  • 节点 C 包含 10923 到 16383 号哈希槽

这种结构很容易添加或者删除节点:

  • 如果我想新添加个节点 D , 我需要从节点 A, B, C 中得部分槽到 D 上。
  • 如果我想移除节点 A ,需要将 A 中的槽移到 B 和 C 节点上,然后将没有任何槽的 A 节点从集群中移除即可。

由于从一个节点将哈希槽移动到另一个节点并不会停止服务,所以无论添加删除或者改变某个节点的哈希槽的数量都不会造成集群不可用的状态。

在 Redis 的每一个节点上,都有这么两个东西,一个是插槽(slot),它的的取值范围是:0-16383。还有一个就是 cluster,可以理解为是一个集群管理的插件。当我们的存取的 Key 到达的时候,Redis 会根据 CRC16 的算法得出一个结果,然后把结果对 16384 求余数,这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,通过这个值,去找到对应的插槽所对应的节点,然后直接自动跳转到这个对应的节点上进行存取操作。

注意:key 与 slot 的关系是永远不会变的,会变的只有 slot 和 Redis 节点的关系

客户端重定向

(1)请求重定向

客户端可能会挑选任意一个 redis 实例去发送命令,每个 redis 实例接收到命令,都会计算 key 对应的 hash slot,如果在本地就在本地处理,否则返回 moved 给客户端,让客户端进行重定向

cluster keyslot mykey,可以查看一个 key 对应的 hash slot 是什么

用 redis-cli 的时候,可以加入 -c 参数,支持自动的请求重定向,redis-cli 接收到 moved 之后,会自动重定向到对应的节点执行命令

(2)计算 hash slot

计算 hash slot 的算法,就是根据 key 计算 CRC16 值,然后对 16384 取模,拿到对应的 hash slot

用 hash tag 可以手动指定 key 对应的 slot,同一个 hash tag 下的 key,都会在一个 hash slot 中,比如:

set mykey1:{100}
set mykey2:{100}

(3)hash slot 查找

节点间通过 gossip 协议进行数据交换,就知道每个 hash slot 在哪个节点上

smart jedis

(1)什么是 smart jedis

基于重定向的客户端,很消耗网络 IO,因为大部分情况下,可能都会出现一次请求重定向,才能找到正确的节点

所以大部分的客户端,比如 java redis 客户端,就是 jedis,都是 smart 的

本地维护一份 hashslot -> node 的映射表,缓存,大部分情况下,直接走本地缓存就可以找到 hashslot -> node,不需要通过节点进行 moved 重定向

(2)JedisCluster 的工作原理

在 JedisCluster 初始化的时候,就会随机选择一个 node,初始化 hashslot -> node 映射表,同时为每个节点创建一个 JedisPool 连接池

每次基于 JedisCluster 执行操作,首先 JedisCluster 都会在本地计算 key 的 hashslot,然后在本地映射表找到对应的节点

如果那个 node 正好还是持有那个 hashslot,那么就 ok;如果说进行了 reshard 这样的操作,可能 hashslot 已经不在那个 node上了,就会返回 moved

如果 JedisCluter API 发现对应的节点返回 moved,那么利用该节点的元数据,更新本地的 hashslot -> node 映射表缓存

重复上面几个步骤,直到找到对应的节点,如果重试超过5次,那么就报错 JedisClusterMaxRedirectionException

jedis 老版本,可能会出现在集群某个节点故障还没完成自动切换恢复时,频繁更新 hash slot,频繁 ping 节点检查活跃,导致大量网络 IO 开销

jedis 最新版本,对于这些过度的 hash slot 更新和 ping,都进行了优化,避免了类似问题

(3)hashslot 迁移和 ask 重定向

如果 hash slot 正在迁移,那么会返回 ask 重定向给 jedis

jedis 接收到 ask 重定向之后,会重新定位到目标节点去执行,但是因为 ask 发生在 hash slot 迁移过程中,所以 JedisCluster API 收到 ask 是不会更新 hashslot 本地缓存

已经可以确定说,hashslot 已经迁移完了,moved 是会更新本地 hashslot->node 映射表缓存的

节点间的通信机制

1.基础通信

(1)redis cluster 节点间采取 gossip 协议进行通信

跟集中式不同,不是将集群元数据(节点信息,故障,等等)集中存储在某个节点上,而是互相之间不断通信,保持整个集群所有节点的数据是完整的

集中式:好处在于,元数据的更新和读取,时效性非常好,一旦元数据出现了变更,立即就更新到集中式的存储中,其他节点读取的时候立即就可以感知到;不好在于,所有的元数据的跟新压力全部集中在一个地方,可能会导致元数据的存储有压力

gossip:好处在于,元数据的更新比较分散,不是集中在一个地方,更新请求会陆陆续续,打到所有节点上去更新,有一定的延时,降低了压力; 缺点,元数据更新有延时,可能导致集群的一些操作会有一些滞后

(2)10000 端口

每个节点都有一个专门用于节点间通信的端口,就是自己提供服务的端口号+10000,比如 7001,那么用于节点间通信的就是 17001 端口

每隔节点每隔一段时间都会往另外几个节点发送 ping 消息,同时其他几点接收到 ping 之后返回 pong

(3)交换的信息

故障信息,节点的增加和移除,hash slot 信息,等等

2. gossip协议

  • meet:某个节点发送 meet 给新加入的节点,让新节点加入集群中,然后新节点就会开始与其他节点进行通信

  • ping:每个节点都会频繁给其他节点发送 ping,其中包含自己的状态还有自己维护的集群元数据,互相通过 ping 交换元数据 每个节点每秒都会频繁发送 ping 给其他的集群,ping 频繁的互相之间交换数据,互相进行元数据的更新

  • pong: 返回 ping 和 meet,包含自己的状态和其他信息,也可以用于信息广播和更新

  • fail:某个节点判断另一个节点 fail 之后,就发送 fail 给其他节点,通知其他节点,指定的节点宕机了

Redis 集群的主从复制模型

为了保证高可用,redis-cluster 集群引入了主从复制模型,一个主节点对应一个或者多个从节点,当主节点宕机的时候,就会启用从节点 。

当其它主节点 ping 一个主节点 A 时,如果半数以上的主节点与 A 通信超时,那么认为主节点 A 宕机了。如果主节点 A 和它的从节点 A1 都宕机了,那么该集群就无法再提供服务了。
默认情况下,redis 集群的读和写都是到 master 上去执行的,不支持 slave 节点读和写,跟 Redis 主从复制下读写分离不一样,因为 redis 集群的核心的理念,主要是使用 slave 做数据的热备,以及 master 故障时的主备切换,实现高可用的。

Redis 的读写分离,是为了横向任意扩展 slave 节点去支撑更大的读吞吐量。而 redis 集群架构下,本身 master 就是可以任意扩展的,如果想要支撑更大的读或写的吞吐量,都可以直接对 master 进行横向扩展。

集群运行的要求

Redis 集群至少需要3个节点,因为投票容错机制要求超过半数节点认为某个节点挂了该节点才是挂了,所以2个节点无法构成集群。

要保证集群的高可用,需要每个节点都有从节点,也就是备份节点,所以 Redis 集群至少需要6台服务器。

集群的特点
  • redis 集群内部的节点是相互通信的(PING-PONG 机制),每个节点都是一个 redis 实例;内部使用二进制协议优化传输速度和带宽。
  • 为了实现集群的高可用,即判断节点是否健康(能否正常使用),redis-cluster 有这么一个投票容错机制:如果集群中超过半数的节点投票认为某个节点挂了,那么这个节点就挂了(fail)。这是判断节点是否挂了的方法
  • Redis 集群是没有统一的入口的,客户端与 Redis 节点直连,不需要中间代理层,也不需要连接集群所有节点,即连接集群中任何一个可用节点即可。
  • 客户端(client)连接集群的时候连接集群中的任意节点(node)即可。
  • 那么如何判断集群是否挂了呢? -> 如果集群中任意一个节点挂了,而且该节点没有从节点(备份节点),那么这个集群就挂了。这是判断集群是否挂了的方法;
Cluster 相关命令

登录redis后,在里面可以进行下面命令操作

# 集群
cluster info # 打印集群的信息
cluster nodes # 列出集群当前已知的所有节点( node),以及这些节点的相关信息。
# 节点
cluster meet <ip> <port> # 将 ip 和 port 所指定的节点添加到集群当中,让它成为集群的一份子。
cluster forget <node_id> # 从集群中移除 node_id 指定的节点。
cluster replicate <master_node_id> # 将当前从节点设置为 node_id 指定的master节点的slave节点。只能针对slave节点操作。
cluster saveconfig # 将节点的配置文件保存到硬盘里面。
# 槽(slot)
cluster addslots <slot> [slot ...] # 将一个或多个槽( slot)指派( assign)给当前节点。
cluster delslots <slot> [slot ...] # 移除一个或多个槽对当前节点的指派。
cluster flushslots # 移除指派给当前节点的所有槽,让当前节点变成一个没有指派任何槽的节点。
cluster setslot <slot> node <node_id> # 将槽 slot 指派给 node_id 指定的节点,如果槽已经指派给
cluster setslot <slot> migrating <node_id> # 将本节点的槽 slot 迁移到 node_id 指定的节点中。
cluster setslot <slot> importing <node_id> # 从 node_id 指定的节点中导入槽 slot 到本节点。
cluster setslot <slot> stable # 取消对槽 slot 的导入( import)或者迁移( migrate)。
# 键
cluster keyslot <key> # 计算键 key 应该被放置在哪个槽上。
cluster countkeysinslot <slot> # 返回槽 slot 目前包含的键值对数量。
cluster getkeysinslot <slot> <count> # 返回 count 个 slot 槽中的键 。

创建集群

redis-cli --cluster create 192.168.100.100:6379 192.168.100.101:6379 192.168.100.102:6379 192.168.100.103:6379 192.168.100.104:6379 192.168.100.105:6379 --cluster-replicas 1

添加一个新主节点

# 需要添加的新节点为 "192.168.100.106:6379",先以单机版配置和启动好,然后执行命令(“192.168.100.100:6379”为集群中任一可用的节点)
redis-cli --cluster add-node 192.168.100.106:6379 192.168.100.100:6379
# 重新分配哈希槽
redis-cli --cluster reshard 192.168.100.106:6379

添加一个从节点

# "192.168.100.106:6380" 为新添加的从节点,"192.168.100.100:6379" 可为集群中已有的任意节点,这种方法随机为6380指定一个master
redis-cli --cluster add-node 192.168.100.106:6380 192.168.100.100:6379 --cluster-slave
# 如果想明确指定master,假设目标master的ID为 "23b412673af0506df6382353e3a65960d5b7e66d",则:
redis-cli --cluster add-node 192.168.100.106:6380 192.168.100.100:6379 --cluster-slave --cluster-master-id 23b412673af0506df6382353e3a65960d5b7e66d

删除节点

# 从集群中删除一个节点命令格式:`redis-cli --cluster del-node <cluster-node> <node-id>`
# `cluster-node`为集群中任意一个非待删除节点,`node-id`为待删除节点的ID。
# 如果待删除的是master节点,则在删除之前需要将该master负责的slots先全部迁到其它master。
redis-cli --cluster del-node 192.168.100.100:6379 963fb30a8ca0a424b50215f733205ff182104953

查看 key 属于哪个 slot:

redis> cluster keyslot yan

让相关的数据落到同一个节点上

# 在key里面加入 {hash tag} 即可。Redis在计算槽编号的时候只会获取 {} 之间的字符串进行槽编号计算,这样由于上面两个不同的键,{} 里面的字符串是相同的,因此他们可以被计算出相同的槽。
set mykey1:{100}
set mykey2:{100}
集群的总结

优点:

  • 无中心架构。

  • 数据按照 slot 存储分布在多个节点,节点间数据共享,可动态调整数据分布。

    • 解耦数据和节点之间的关系,简化了扩容和收缩难度;
    • 节点自身维护槽的映射关系,不需要客户端代理服务维护槽分区元数据
    • 支持节点、槽、键之间的映射查询,用于数据路由,在线伸缩等场景.
  • 可扩展性,可线性扩展到 1000个节点(官方推荐不超过 1000 个)节点可动态添加或删除。

  • 高可用性,部分节点不可用时,集群仍可用。通过增加 Slave做 standby 数据副本,能够实现故障自动 failover,节点之间通过 gossip 协议交换状态信息,用投票机制完成 Slave 到 Master 的角色提升。

  • 降低运维成本,提高系统的扩展性和可用性。

缺点:

  • Client 实现复杂,驱动要求实现 Smart Client,缓存 slots mapping 信息并及时更新,提高了开发难度,客户端的不成熟影响业务的稳定性。
  • 节点会因为某些原因发生阻塞(阻塞时间大于 clutser-node-timeout),被判断下线,这种 failover 是没有必要的。
  • 数据通过异步复制,不保证数据的强一致性
  • 多个业务使用同一套集群时,无法根据统计区分冷热数据,资源隔离性较差,容 易出现相互影响的情况。
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值