Redis 系列--06. Redis 架构

Redis 在实际使用的过程中,针对不同的场景需要对应的架构,这篇博客主要是总结 Redis 在实际在生产中遇到的架构以及各自的有点和缺点。

1. 单机架构

Redis 架构

优点

  • 部署简单,成本低,无备用节点;
  • 高性能,单机不需要同步数据,数据天然一致性。

缺点

  • 可靠性保证不是很好,单节点有宕机的风险。

  • 单机高性能受限于 CPU 的处理能力,Redis 是单线程的。

2. 主从架构

2.1 架构的描述

随着数据访问量的增加, 单机节点无法满足性能的要求,这时就需要对读写的场景进行分离。所以产生了 master-slave 集群架构:master 负责写数据,slave 负责数据的读取,数据由 master 同步到 slave 节点。

数据的复制是单向的,只能由主节点到从节点,简单理解就是从节点只支持读操作,不允许写操作。 具体架构图如下:

Redis 架构 

优点:

  • 可以水平扩展
  • 降低 Master 读压力,转交给 Slave 节点;
  • master节点宕机,可以选用其他的节点继续服务,提高了服务的可用性。

缺点  

  • 没有解决主节点写的压力;
  • 由于存在同步数据的问题,所以就会带来数据不一致和延迟的问题;

2.2 节点的同步原理


♦ 旧版同步


在 redis 早期版本中,复制的实现主要是分为两个操作同步(sync)和命令传播(command propagate)

  1. 同步:将从服务的数据库状态更新至当时主服务的数据库状态。
  2. 命令传播:主服务的数据库状态被修改后,通过命令传播使从服务数据库状态保证一致。

1. 同步

  1. slave 向 master 发送 sync 命令;
  2. master 收到 sync 命令后,执行 bgsave 命令生成基于当时 master 数据库状态的 RDB 文件,并且在 bgsave 命令执行期间,master 将该期间的写入命令,写入到一个缓存区;
  3. 将 RDB 文件发送给 slave,slave 同步数据;
  4. RDB 同步完成,将缓存区命令发送给 slave,同步数据;
  5. master 和 slave 数据一致。

流程如下:

Redis 架构

2. 命令传播

在同步操作完成之后,基于当时的状态,主从服务数据达成一致状态。但是当后续 master 继续接受写入命令后,保证主从一致,通过命令传播操作完成。

Redis 架构

3. 缺陷

目前的复制功能可以完成主从服务的数据一致性。但是当 slave 掉线后,再次重新连接 master 服务后,为了保证数据一致性,就需要再次完成同步操作。同步操作是一个很消耗性能的操作,有可能导致服务的同步等待,并且可能 slave 已存在一大半的数据,并不需要全量的 RDB 文件。

在这种情况下,为了让从服务器补足一小部分缺失的数据,却要让主从服务器重新执行一次同步操作,这种做法无疑是非常低效的。

♦ 新版复制


1. 增量同步

增量同步很好的解决了旧版复制的出现问题。它的实现主要依赖:

  1. 主服务器的偏移量和从服务器的偏移量
  2. 主服务的复制积压缓存区
  3. 服务的运行ID(RUNID)

2. 偏移量

执行复制之后,主服务和从服务都会去维护一个各自的复制偏移量,主服务命令传播 N 个字节,偏移量增加N;从服务接受 N 个字节的命令,偏移量增加 N。当主从服务的偏移量相等,主从数据即保持一致。

3. 复制积压缓存区(backlog)

在主服务给从服务命令传播时,主服务会维护一个固定大小(默认1M)先进先出的队列作为复制积压缓存区。

  1. 当每次发生命令传播时,该命令既会被发送给从服务,也会被写入到积压缓存区。

  2. 进入队列后,队列会对每一个字节设置当前的所对应偏移量。

  3. 当队列已满时,会弹出最先进入的命令。

    Redis 架构

当 slave 服务断线重连后,会优先用 slave 的偏移量去队列查找,队列存在该偏移量,将该偏移量后面的命令发送给 slave 服务,部分重同步数据;负责执行完整同步操作。缓冲区大小的配置

1
2
3
// 默认1M
# repl-backlog-size 1mb

4. 运行ID

运行ID在服务器启动时自动生成,由 40 个随机的十六进制字符组成。当从服务器对主服务器进行初次复制时,主服务器会将自己的运行 ID 传送给从服务器,而从服务器则会将这个运行 ID 保存起来。

当从服务断线重连之后,从服务将之前保存的主服务的运行 ID 发送给主服务,主服务和自身比较运行ID。

运行 ID 相同,主服务为断线之前的服务,尝试执行部分重同步操作。
运行 ID 不相同,主服务重启或者不是断线之前的主服务,执行完整重同步操作。

5. psync

  1. 从服务器首次复制,发送 psync ?-1 命令,表示首次连接,执行完整重同步。
  2. 从服务断线重连,发送 psync 命令
  3. 主服务收到的 runid 与自身相同,在积压缓存区查找 offset 存在,执行部分重同步。
  4. runid 不同或者 offset 积压缓存区不存在,执行完整重同步。

6. 复制实现

  1. 从服务发送 saveof host:port命令,并保存主服务的 ip 和端口号;
  2. 主从服务器建立 socket 连接;
  3. 从服务发送 ping, 主服务返回 pong 响应;
  4. 主从服务身份验证成功;
  5. 从服务发送端口号,主服务保存该属性;
  6. 同步操作保证当前数据一致;
  7. 命令传播保证后续数据一致。

3. 哨兵机制

主从复制模型解决了 master 节点写压力过大的问题,却没有解决 master 节点宕机高可用的问题。如果 master 宕机,程序员则需要手动把 slave 节点转换为 master 节点,所以我们需要一个程序来帮我们实现这个功能,类似一个哨兵(Sentinel),哨兵机制的模型图如下:

Redis 架构

3.1 哨兵机制的原理

redis的哨兵系统可以监测一个或多个主服务以及改主服务对应的从服务。当检测到主服务进程挂掉,哨兵系统会从该主服务对应的从服务中选举出来一个接替主服务的位置。旧的主服务重新连接,被修改为新主服务的从服务。

3.2 哨兵的监控过程

主服务

默认状态下Sentinel会以每十秒一次的频率,通过命令连接向被监视的主服务器发送 INFO 命令,并通过分析 INFO 命令的回复来获取主服务器的当前信息。

信息包含俩部分:

  1. 主服务的信息:关于主服务器本身的信息,包括 run_id 域记录的服务器运行 ID,以及 role 域记录的服务器角色;
  2. 主服务下的从服务信息:关于从服务的ip地址、端口号,offset 等相关信息。
  3. 根据返回的信息,更新主服务的 sentinelRedisInstance 结构相关信息,例如 runId;
  4. 更新主服务结构中 slaves 字段对应字典的从服务 sentinelRedisInstance 结构,存在即更新,不存在新建。

从服务

从服务和主服务一样,也会定时发送 INFO 命令,通过 INFO 的返回值来获从服务的信息。
信息主要包含:

  1. 从服务器的运行 ID run_id。
  2. 从服务器的角色 role。
  3. 主服务器的IP地址 master_host,以及主服务器的端口号 master_port。
  4. 主从服务器的连接状态 master_link_status。
  5. 从服务器的优先级 slave_priority。
  6. 从服务器的复制偏移量 slave_repl_offset。

3.3 主观宕机(sdown)

在 Sentinel.conf 配置文件,指定了 Sentinel 判断实例进入主观宕机所需的时间长度,时间单位为 ms,默认值为30000。

1
2
# sentinel down-after-milliseconds <master-name> <milliseconds>
sentinel down-after-milliseconds mymaster 30000

以上配置,当该配置下的一个 Sentinel 服务在 3000 毫秒内,没有收到 master 服务 A 的有效回复,就会认定 master 服务A宕机。Sentinel 会修改 master 服务A所对应的 sentinelRedisInstance 实例结构,把结构的 flags 属性修改为 SRI_S_DOWN 标识,以此来表示这个实例已经进入宕机状态。

down-after-milliseconds 的值不仅适用于对 master 宕机状态,也是适用于该 master 服务下的所有 slave,和监视该 master 的其他 Sentinel。

每一个 Sentinel 服务在配置 sentinel down-after-milliseconds 可以不同,所以就会出现 SentinelA 配置 3000ms,SentinelB 配置 5000ms,在 3000ms 没有收到有效回复,SentinelA 会认为 master 宕机,而 SentinelB 不认为 master 宕机。该情况就被认定为主观宕机。

3.4 客观宕机

SentinelA 发现 master 主观宕机后,但自己不是很确定 master 是不是真的宕机。所以就需要主动去询问也在监视这个 master 的其他 Sentinel 兄弟,当一定数量的 Sentinel 服务都认定 master 宕机,那认定master 客观宕机,开始故障转移。

在 Sentinel.conf 配置文件

1
2
3
4
5
# 哨兵 sentinel 监控的 redis 主节点的 ip port 
# master-name 可以自己命名的主节点名字 只能由字母A-z、数字0-9 、这三个字符".-_"组成。
# quorum 当这些 quorum 个数 sentinel 哨兵认为 master 主节点失联 那么这时 客观上认为主节点失联了
# sentinel monitor <master-name> <ip> <redis-port> <quorum>
sentinel monitor mymaster 192.168.1.1 6379 2

以上配置 quorum 为 2,在 5 个 Sentinel 的集群中,有 2 个 Sentinel 认定 master 宕机,即为客观宕机。

宕机认定流程:

  1. Sentinel 没有收到有效回复,判定为主观宕机。
  2. 询问其他 Sentinel 情况。其他 Sentinel 去判定宕机状态后,返回判定消息。
  3. 收到其他 Sentinel 的回复,统计认定主观宕机的 Sentinel 数量,如果超过 quorum 配置数,判定为客观宕机。
  4. Sentinel 修改 master 服务 A 所对应的 sentinelRedisInstance 实例结构,把结构的 flags 属性修改为 SRI_O_DOWN 标识。
  5. 开始故障转移。

3.5 领头 Sentinel

故障转移开始时,只需要一个 Sentinel 来执行这个工作。所以对于 Sentinel 集群,就需要选举一个领头 Sentinel 来做这个事。

选举流程:

  1. 每一个 Sentinel 都有选举权和被选举权,并且在一轮选举中只有一次机会。
  2. 每个发现 master 宕机的 Sentinel 会发送命令给其他 Sentinel,希望把自己设置为局部领头 Sentinel。
  3. 设置局部领头 Sentinel 规则为先导先得,Sentinel 会把最先收到命令中的 runId 设为自己的局部领头 Sentinel;并返回消息。
  4. 收到返回消息并解析,判断自己是否已被设置为局部领头 Sentinel。
  5. 当一个 Sentinel 被超过一半 Sentinel 服务设局部领头 Sentinel,则认定该 Sentinel 为领头 Sentinel 服务。
  6. 如果没有选举出来,则开始重新一轮选举。

3.6 故障转移

Sentinel 集群选出领头 Sentinel 之后,开始进行故障转移,主要包含三部分:

  1. 选举新的 master。
  2. 修改 slave 的复制目标。
  3. 旧的 master 重新上线修改为 slave。

选举新的 master

Sentinel 会从剩下的从服务中选举出一个从服务作为新的 master,但是不能随便来,要符合以下选举规则:

  1. 排除部分从服务:下线/断线、与主服务断开连接超过 10 * down - after - milliseconds 和五秒内没有回复领头 Sentinel 的 info 命令的服务。
  2. 选出优先级最高的从服务。
  3. 优先级一样,选择复制偏移量大的的服务。
  4. 偏移量一样,选择 runId 小的从服务。

转移

选举出来新的主服务后,领头 Sentinel 会给其他从服务发送 saveOf 命令,让它们去复制新的 master。当旧的 master 重新上线之后,被修改从服务去并去复制新的 master。

3.3 哨兵投票机制

为了高可用的要求,服务一般都是会从单机版转换成集群模式,哨兵也是相同。

哨兵集群中 , 通过 redis 的pub/sub 系统实现哨兵互相之间的发现。每隔两秒钟,每个哨兵都会往 sentinel:hello 这个 channel 里发送一个消息,这时候监视同一个 redis 服务的 Sentinel 都可以消费到这个消息,并感知到其他哨兵的存在。消息内容是自身 host、ip 和 runid 还有对这个 master 的监控配置。

Sentinel收到消息判断 runId 与自身:

  • 相同,忽略该条消息;
  • 不相同,去解析消息,并对相应主服务器的 sentinelRedisInstance 结构进行更新。

在哨兵集群中,对于一个服务的宕机可能存在差异,例如:

如果一个 redisA 实例访问压力很大,哨兵A 可能得到了该实例宕机了,哨兵B 可能认为该实例没有宕机,这样就导致哨兵集群内部的"意见不统一",有可能导致出现脑裂(多个 master )现象

脑裂方案解决

1
2
min-slaves-to-write 1
min-slaves-max-lag 10

上面两个配置可以减少异步复制和脑裂导致的数据丢失

要求至少有 1 个 slave,数据复制和同步的延迟不能超过 10 秒;如果说一旦所有的 slave,数据复制和同步的延迟都超过了 10 秒钟,那么 master 就不会再接收任何请求了

  1. 有了 min-slaves-max-lag 这个配置,一旦 slave 复制数据和 ack 延时太长,那么就拒绝写请求,这样可以把 master 宕机时由于部分数据未同步到 slave 导致的数据丢失降低到可控范围内。

  2. 如果一个 master 出现了脑裂,跟其他 slave 丢了连接,那么上面两个配置可以确保说,如果不能继续给指定数量的 slave 发送数据,而且 slave 超过 10 秒没有给自己 ack 消息,那么就直接拒绝客户端的写请求。这样脑裂后的旧 master 就不会接受 client 的新数据,也就避免了数据丢失。

  3. 上面的配置就确保了,如果跟任何一个 slave 丢了连接,在 10 秒后发现没有 slave 给自己 ack,那么就拒绝新的写请求,因此在脑裂场景下,最多就丢失 10 秒的数据。

4. Redis集群架构

前面说了哨兵的模式,解决了主从模式下自动主从切换的问题。但是在极限场景下哨兵模式下 master 服务器还是单台机器,整体的性能还是受限于单台机器的性能。为了解决这个问题,需要对主从模式的架构模型进行水平的扩展,即 Redis 集群(cluster)
Redis集群架构

4.1 分片算法探究

既然水平扩展就存在一个数据怎么分配的问题(即数据分片),简而言之,当一个数据需要写入 Redis ,如何决定写入那个节点?

对于简单的分片,我们可以很简单的相当用 hash(key) % count (count 代表机器的个数),根据求余数的结果,来决定最终入那一个节点。这样看似问题是很好的解决的了,但是也存在很大的问题,因为如果有机器数的变动,存在着数据的迁移。

例如:
假设有 3 台机器 1、2、3,如果有 9 个 key,其中 1、4、7->1,2、5、8->2 ,3、6、9->3 。如果此时增加一台服务器,则会变成 1、5、9 ->1,2、6 ->2,3、7->3 ,4、8 ->4 。

从上面的例子中可以看到,每增加一个节点都会带来数据的大量的迁移。同理,如果减少一个节点,也会带来同样的问题。

怎么样才能让数据分片不受节点影响呢?其实这个问题也很简单,就是加大数据分片的数量。同样上面的例子,如果把三台服务器虚拟成 30 个节点,当其中有数量发生变动的时候,会产生两种情况:

  1. 小于分片数量的分片不受影响
  2. 大于或者在分片内的数据,移动到下一个虚拟分片段。

4.2 哈希槽

redis集群将整个数据库分为16384个槽(slot)。每个redis节点负责其中一部分的槽,当16384个槽都有redis节点负责时,整个redis集群才能正常工作。为啥是16384个槽(slot)

所以往redis集群写入一个命令后,当前节点通过CRC16(key)和&12384计算出一个位于1~16384之间的值,该值决定这对键值对属于哪个槽。

4.3 命令写入

在 redis 每一个节点中都保存在这个一个关于 redis 集群的 clusterState 结构,这个结构包含这个当前这个集群的相关信息,其中也包含着所有槽的指派信息。

Redis 架构

命令写入的流程:

  1. 首先写入其中一个节点,该节点开始通过槽分配算法计算槽值 M。
  2. 在 slots 属性中,redisNode[m] 快速定位到该槽负责的节点。
  3. 判断负责的节点是否为自己,是执行命令,否返回 moved 命令(moved :)。该命令包含正确的节点的ip和端口号。
  4. 客户端接收到 moved 的错位信息,将命令发送到正确的节点。

4.4 重新分配

在三个节点的 redis 集群,当要给该集群在增加一个节点 node4 时,槽在已被全部分配,但是 node4 需要被分配一部分槽给他,不然node4 相当于没有工作,所以需要重新分片。以 node3 需要把槽 15000~16384 重新分配给 node4 为例:

  1. 开始对 15000 槽重新分配。
  2. node4 准备导入属于 15000 槽的键值对。
  3. node3 准备迁移属于 15000 槽的键值对。
  4. 如果 node3 存在 15000 槽的键值对,将这些键值对导入 node4。
  5. 将槽指派给 node4,完成对 15000 槽的重新指派。
  6. 其他槽的重新指派重复以上步骤。

当 15555 槽正在从 node3 转移到 node4 时,会出现一种情况就是 15555 槽对应的键值对有一部分在node3,另一部分在 node4。当客户端需要对 15555 槽下的一个键值对的更新时,node3 会首先检查该键值对的key是否在当前节点,在就更新,不在返回 ask 错信信息,并指引客户端将该键值对写入 node4。

4.5 故障转转移

集群中的每个节点互相 ping 其他节点,当没有收到有效回复,就会认定其节点下线。当集群中有一半的节点认定该节点下线,就会广播一条消息告知全部节点,将该节点认定为下线状态,并开始故障转移。

故障转移步骤:

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

新的主节点选举基本和在 redis 的 Sentinel 模式中选举领头 Sentinel 基本相似。


参考博客:
【redis cluster模式】
【Redis哨兵模式】
【redis主从架构】
【最通俗易懂的 Redis 架构模式详解】