Skip to content

Redis 高可用架构

高可用一般来说有两个含义:

  • 数据尽量不丢失
  • 服务尽可能可用

Redis 为防止数据丢失提供了两种数据持久化机制:RDB 和 AOF

Redis 有三种部署模式来提供多个节点:主从模式、哨兵模式、集群模式

主从模式

核心概念

Redis 主从复制模式是一种异步数据同步机制

  • Master 主节点:承担写操作,数据变更后异步复制给从节点
  • Replica 从节点:接收主节点数据副本,默认提供只读服务
  • 默认特性:最终一致性,非实时一致性,网络分区可能产生数据延迟

主-从复制.png

如果担心从库太多,频繁的同步占用主库的带宽,也可以选择主-从-从模式

主-从-从复制.png

主从复制原理

在 2.8 版本之前只有全量复制,而 2.8 版本后新增了增量复制

  • 全量复制:在第一次主从同步的时候,或者在从库宕机很久之后重连,会把所有数据以 RDB 形式同步给从库
  • 增量复制:在之后的每条命令以增量形式同步给从库

配置文件

主节点 redis.conf

shell
# 无需特殊配置,默认允许复制
# 也可以设置主从复制密码验证
masterauth <password> #从节点连接时使用

从节点 redis.conf

shell
# 指定主节点 ip 和 端口
replicaof <masterip> <masterport>

# 若主节点有密码
masterauth <password>

# 复制积压缓冲区大小
repl-backlog-size 64mb

# 超时时间 (主从节点双向检测)
repl-timeout 60

全量复制

  1. 从库发送 psync 命令给主库,申请同步
C
psync {runID} {offset}

从库给主库发送 psync 命令,表示要进行数据同步,主库根据这个命令来启动复制。

  • runID :每个 Redis 实例启动时会随机生成一个实例 ID ,第一次主从复制时,从库不知道主库的 ID ,会将 runID 置为 ?
  • offset:表示复制进度,第一次复制置为 -1
  1. 主库收到 psync 命令后,用 fullresync 命令响应给从库
C
FULLRESYNC {runID} {offset}

主库发送 FULLRESYNC 命令后,会执行 bgsave 命令,生成 RDB 文件发送给从库

  1. 从库收到数据后,在本地完成数据加载

这个过程依赖于主库发送的 RDB 文件,为了避免之前的从库数据影响,从库会先清空数据库再加载 RDB 文件

  1. 全量复制期间,主库能够正常接收请求

主库会把后续新接收到的请求命令不断积压到从库的输出缓冲区 replication buffer

等从库加载完 RDB 文件后,再不断地加载这部分数据,就实现主从库同步了

增量复制

如果主从库在命令传播时出现了网络闪断,那么主从库会重新进行一次全量复制,开销非常大。

于是从 Redis 2.8 开始,网络断了之后,主从库会采用增量复制的方式继续同步。

增量复制.png

主库的所有修改命令都会记录到 repl_backlog_buffer,如果从库中途断开,会携带最后一次复制的 offset 对主库请求 PSYNC

  • 如果 offset 位置没被覆盖,主库会响应 Continue ,代表可以增量复制,把 offset 之后的命令发给从库
  • 反之,主库响应 FULLRESYNC ,代表要重新进行全量复制

无磁盘化复制

全量复制主库是先在磁盘中生成 RDB 文件,再把 RDB 文件发送给从库

如果磁盘空间有限或性能较低,可以开启无盘复制,主库开启一个 socket ,在内存中生成 RDB 文件发送给 从库

shell
repl-diskless-sync yes # 无磁盘化复制
repl-diskless-sync-delay 5 # 等待 5 秒再开始复制

哨兵模式

哨兵模式.png

之前的主从复制模式,当主机宕机后,整个系统就不再可用,需要手动把一台从机切换为主机。

哨兵模式是 Redis 2.8 引入的功能,用来解决这个问题,一般公司采用 一主二从三哨兵 的方式搭建高可用架构。

3 个哨兵选择多个是为了高可用,选择奇数是为了方便选举;这 3 个哨兵只复制监控和维护集群状态不负责数据存储

sentinel.conf 配置文件

shell
# sentinel monitor <master-name> <ip> <redis-port> <quorum>
# 结尾这个 2 表示确认客观下线的最少的哨兵数量
sentinel monitor mymaster 127.0.0.1 6379 2 

# master 密码
# sentinel auth-pass <master-name> <password>

# 主节点无响应5000ms后标记为SDOWN
sentinel down-after-milliseconds mymaster 5000

哨兵如何知道从库信息

我们只需要配置 sentinel monitor 命令,哨兵就能对整个集群进行监控了,那么哨兵是如何直到从库地址呢?

主库有个 info 命令,里面有很多信息,包括从库列表

哨兵之间如何通信呢

哨兵通信.png

主从模式下,主库上有一个名为 _sentinel_:hello 的频道

哨兵1 把自己的 IP 和 端口发布到这个频道,哨兵 2 和 哨兵 3 订阅了该频道,那么哨兵 2 和 哨兵 3 就可以通过该信息和哨兵 1 建立网络连接

哨兵运行流程

  1. 3 个哨兵监控一主二从,正常运行中
  2. SDOWN (主观下线):指单个 Sentinel 实例认为某个服务下线,即 Redis 实例在 sentinel down-after-milliseconds 时间都没有响应 Ping 命令
  3. ODOWN (客观下线):至少有 quorum 个哨兵认为某个服务下线,认为该服务客观下线
  4. 哨兵选举:3 个哨兵通过 Raft 算法选出一个 leader
  5. 哨兵 leader 负责故障转移,原主节点变为从节点,原从节点中的一个变为主节点

Raft 选举算法

哨兵模式中的 Raft 算法是精简版的,任何一个想成为 Leader 的哨兵需要满足两个条件

  1. 拿到半数以上的赞成票
  2. 拿到的票数同时还需要大于等于 quorum

如果不满足上面两个条件,即使判断出了客观下线,但无法选择出 Leader 进行故障转移

Redis 脑裂问题

主节点由于负载太大(执行大 key),或者因为网络问题。导致哨兵没及时收到主节点的心跳,超过 quorum 数量的哨兵判断主节点客观下线,选举出了新的主节点。

但是原主节点和客户端网络分区在同一区,客户端还在正常和原主节点进行通信,导致短时间有两个 master 节点的情况,像大脑分裂了。

脑裂的影响

脑裂最大的问题是数据丢失或者不一致

在新主节点选举过程中,原主节点和客户端执行的命令都没有同步给新主节点;新主发出 slave of命令后,原主变从节点会清空自己的数据,导致数据丢失

脑裂的避免

应对脑裂的解决办法是去限制原主库接收请求,Redis 提供了两个配置项

shell
# 主节点必须有至少 N 个从节点在线才能写入
min-replicas-to-write 2
min-replicas-max-lag 10  # 从节点延迟 ≤10秒才算有效

不过,以上配置并无法彻底解决哨兵模式脑裂问题,因为 Redis 没有符合 Raft 强一致性协议; ETCD 通过 Raft 强一致性协议彻底解决了脑裂问题,不过牺牲了部分写入性能

集群模式

集群模式.png

哨兵模式基本实现了高可用,但还有两个痛点:

  • 每个节点上都存储相同的内容,很浪费内存
  • 没有解决 Master 节点写数据的压力

为了解决这些问题,Redis 集群模式应运而生,有多个 master ,每个 master 上存储一部分数据,而且内置高可用服务,某个 master 宕机,服务还可以正常运行。

哈希槽

如何分配数据存储到哪个 master 节点呢 ?

通过哈希槽的方式分配存储节点,保证数据均匀地分配到不同节点,防止数据倾斜

Redis 中有 16384 (2 的 14 次方) 个哈希槽,每个 key 通过 CRC16 校验后对 16383 取模来决定放置哪个槽, 每个 master 节点负责一部分 hash 槽

比如集群中有 3 个 master 节点:

  • 节点 A 包含 0 ~ 5500 号哈希槽
  • 节点 B 包含 5501 ~ 11000 号哈希槽
  • 节点 C 包含 11001 ~ 16384 号哈希槽

为什么 Redis 集群 Hash 槽数为 16384

即为什么为 2 的 14 次方,不选择 CRC16 支持的最大值 2 的 16 次方 65536

  • Redis 每秒会发送心跳包,心跳包会带有节点的完整配置,如果为 65536 ,会加大心跳包大小,浪费带宽
  • Redis 集群主节点个数不可能超过 1000 个, 16384 足够了
  • 槽位越小,压缩比越高,容易传输

心跳包

心跳包包含的数据:

  • Header:发送者自己的信息
    • 负责 slots 的信息
    • 主从信息
    • ip port 信息
    • 状态信息
  • Gossip:发送者所了解的部分其他节点的信息
    • ping_sent,pong_received (上一次 ping 发送的时间 和上一次 pong 接收的时间)
    • ip , port 信息
    • 状态信息

哈希取模算法

这是最简单地数据分布算法,如果有 A 、 B 、C 三台机器,有 3 万 个 key,希望将这些数据均匀地分配到这 3 台机器上,那么可以采用取模算法

key 进行 hash 后的结果对 3 取模,得到的结果一定是 0 、 1 或 2 ,正好对应 A 、 B 、C 节点

但当 A 节点挂了,计算公式 hash (key) % 3 变成了 hash (key) % 2

这样之不但之前在 A 节点存储的位置变了,节点 B 和 C 的缓冲位置也有极大可能发生变化

大量缓存在同一时间失效,造成缓存雪崩,从而导致整个缓存系统的不可用,这是不可接收的,因此 Redis 没有采用哈希取模算法

一致性 hash 算法

一致性 hash 算法把 hash 空间 (0 ~ 2^32)抽象为了一个环,每个数据计算 hash 后决定落在环上的哪个位置

哈希环.png

然后把节点映射到环上,数据落在两个节点之间时,数据存储到后一个节点上即可

而且这种方式当某个一个节点失效或新增节点时,并不会影响原来其他节点上的数据分布

一致性 hash.png

那么 Redis 为什么不选择一致性 hash,而是选择 hash 槽

  • 一致性 hash 数据分布不均
  • 为了解决数据倾斜,节点增减需要计算虚拟节点映射并迁移数据,自动化运维不友好
  • 无法直接指定某节点的负载范围,难以定制数据分布 (如冷热数据分离)

Redis 持久化

1. RDB (Redis Database)

RDB 是 Redis 以指定时间间隔产生的数据快照

  • redis.conf 配置自动触发策略
shell
# 持久化数据文件保存目录 
dir /data
# rdb dump 文件名 
dbfilename dump.rdb 

save 900 1 # 900秒内至少1个key变化
save 300 10
save 60 10000
  • 也可以通过命令进行手动触发
shell
SAVE # 阻塞式 RDB 持久化 
BGSAVE # 非阻塞式 RDB 持久化
  • 修复命令

redis 提供了 RDB 修复命令 redis-check-rdb

rdb 修复命令.png

2. AOF (Append Only File)

以日志的形式来记录每个写操作(存储于 appendonly.aof,只许追加文件但不可用改写文件,Redis 启动之初会读取该文件重新构建数据

默认情况下,Redis 没有开启 AOF 持久化,需要在 redis.conf 配置文件中开启

如果 AOF 和 RDB 同时开启,重启时只会加载 AOF 文件 (AOF优先级更高)

shell
appendonly yes # 开启 AOF 持久化

三种写回策略

  • always:每次写操作都会写回 AOF 文件
  • everysec:每秒写回一次
  • no:由操作系统控制

3.混合持久化

可以通过配置 aof-use-rdb-preamble 来开启混合持久化(默认为 true)

开启混合模式后,AOF文件的前半段为 RDB 格式快照,后半段为增量 AOF 命令

shell
aof-use-rdb-preamble yes

4. RDB 与 AOF 的优劣

  • RDB:适合冷备,恢复快,但可能会丢失部分数据
  • AOF:保证了数据一致性,但文件体积大、恢复慢

过期策略

1. 被动删除(惰性删除)

当客户端访问某个键时, Redis 会检测该键是否过期,如果过期则删除该键、

这种方式省 CPU ,但内存不友好,可能发生内存泄露(过期键一直没访问就一直没法删除)

2. 主动删除(定期删除)

Redis 每隔一段时间(默认 100 ms)随机扫描一定数量的设置了 TTL 的键,如果过期则删除该键;如果一轮扫描中过期键比例超过 25%,则继续扫描

这种方式会更消耗 CPU ,但对内存更友好

内存淘汰策略

当 Redis 内存达到 maxmemory 配置上限时(默认无限制),触发内存淘汰策略,Redis 提供8种策略

默认策略

  • noeviction:不淘汰,当内存达到上限时,拒绝所有写操作,读请求正常响应

淘汰设置了 TTL 的键

  • volatile-ttl:优先删除剩余存活时间最短的键
  • volatile-random:随机淘汰设置了过期时间的键
  • volatile-lru:基于 LRU 算法,优先删除最久未被访问的键
  • volatile-lfu:基于 LFU 算法,优先删除访问频率最低的键

无幸免范围

  • allkeys-random:随机删除任意键
  • allkeys-lru:基于 LRU 算法,优先删除最久未被访问的键
  • allkeys-lfu:基于 LFU 算法,优先删除访问频率最低的键