Redis 高可用架构
高可用一般来说有两个含义:
- 数据尽量不丢失
- 服务尽可能可用
Redis 为防止数据丢失提供了两种数据持久化机制:RDB 和 AOF
Redis 有三种部署模式来提供多个节点:主从模式、哨兵模式、集群模式
主从模式
核心概念
Redis 主从复制模式是一种异步数据同步机制:
- Master 主节点:承担写操作,数据变更后异步复制给从节点
- Replica 从节点:接收主节点数据副本,默认提供只读服务
- 默认特性:最终一致性,非实时一致性,网络分区可能产生数据延迟
如果担心从库太多,频繁的同步占用主库的带宽,也可以选择主-从-从模式
主从复制原理
在 2.8 版本之前只有全量复制,而 2.8 版本后新增了增量复制
- 全量复制:在第一次主从同步的时候,或者在从库宕机很久之后重连,会把所有数据以 RDB 形式同步给从库
- 增量复制:在之后的每条命令以增量形式同步给从库
配置文件
主节点 redis.conf
# 无需特殊配置,默认允许复制
# 也可以设置主从复制密码验证
masterauth <password> #从节点连接时使用
从节点 redis.conf
# 指定主节点 ip 和 端口
replicaof <masterip> <masterport>
# 若主节点有密码
masterauth <password>
# 复制积压缓冲区大小
repl-backlog-size 64mb
# 超时时间 (主从节点双向检测)
repl-timeout 60
全量复制
- 从库发送
psync
命令给主库,申请同步
psync {runID} {offset}
从库给主库发送 psync 命令,表示要进行数据同步,主库根据这个命令来启动复制。
runID
:每个 Redis 实例启动时会随机生成一个实例 ID ,第一次主从复制时,从库不知道主库的 ID ,会将runID
置为?
offset
:表示复制进度,第一次复制置为-1
- 主库收到
psync
命令后,用fullresync
命令响应给从库
FULLRESYNC {runID} {offset}
主库发送 FULLRESYNC
命令后,会执行 bgsave
命令,生成 RDB 文件发送给从库
- 从库收到数据后,在本地完成数据加载
这个过程依赖于主库发送的 RDB 文件,为了避免之前的从库数据影响,从库会先清空数据库再加载 RDB 文件
- 全量复制期间,主库能够正常接收请求
主库会把后续新接收到的请求命令不断积压到从库的输出缓冲区 replication buffer
中
等从库加载完 RDB 文件后,再不断地加载这部分数据,就实现主从库同步了
增量复制
如果主从库在命令传播时出现了网络闪断,那么主从库会重新进行一次全量复制,开销非常大。
于是从 Redis 2.8 开始,网络断了之后,主从库会采用增量复制的方式继续同步。
主库的所有修改命令都会记录到 repl_backlog_buffer
,如果从库中途断开,会携带最后一次复制的 offset
对主库请求 PSYNC
- 如果
offset
位置没被覆盖,主库会响应Continue
,代表可以增量复制,把offset
之后的命令发给从库 - 反之,主库响应
FULLRESYNC
,代表要重新进行全量复制
无磁盘化复制
全量复制主库是先在磁盘中生成 RDB 文件,再把 RDB 文件发送给从库
如果磁盘空间有限或性能较低,可以开启无盘复制,主库开启一个 socket
,在内存中生成 RDB 文件发送给 从库
repl-diskless-sync yes # 无磁盘化复制
repl-diskless-sync-delay 5 # 等待 5 秒再开始复制
哨兵模式
之前的主从复制模式,当主机宕机后,整个系统就不再可用,需要手动把一台从机切换为主机。
哨兵模式是 Redis 2.8 引入的功能,用来解决这个问题,一般公司采用 一主二从三哨兵 的方式搭建高可用架构。
3 个哨兵选择多个是为了高可用,选择奇数是为了方便选举;这 3 个哨兵只复制监控和维护集群状态不负责数据存储
sentinel.conf
配置文件
# 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
命令,里面有很多信息,包括从库列表
哨兵之间如何通信呢?
主从模式下,主库上有一个名为 _sentinel_:hello
的频道
哨兵1 把自己的 IP 和 端口发布到这个频道,哨兵 2 和 哨兵 3 订阅了该频道,那么哨兵 2 和 哨兵 3 就可以通过该信息和哨兵 1 建立网络连接
哨兵运行流程
- 3 个哨兵监控一主二从,正常运行中
SDOWN
(主观下线):指单个 Sentinel 实例认为某个服务下线,即 Redis 实例在sentinel down-after-milliseconds
时间都没有响应 Ping 命令ODOWN
(客观下线):至少有quorum
个哨兵认为某个服务下线,认为该服务客观下线- 哨兵选举:3 个哨兵通过
Raft
算法选出一个leader
- 哨兵
leader
负责故障转移,原主节点变为从节点,原从节点中的一个变为主节点
Raft 选举算法
哨兵模式中的 Raft 算法是精简版的,任何一个想成为 Leader 的哨兵需要满足两个条件
- 拿到半数以上的赞成票
- 拿到的票数同时还需要大于等于
quorum
值
如果不满足上面两个条件,即使判断出了客观下线,但无法选择出 Leader
进行故障转移
Redis 脑裂问题
主节点由于负载太大(执行大 key),或者因为网络问题。导致哨兵没及时收到主节点的心跳,超过 quorum
数量的哨兵判断主节点客观下线,选举出了新的主节点。
但是原主节点和客户端网络分区在同一区,客户端还在正常和原主节点进行通信,导致短时间有两个 master 节点的情况,像大脑分裂了。
脑裂的影响
脑裂最大的问题是数据丢失或者不一致。
在新主节点选举过程中,原主节点和客户端执行的命令都没有同步给新主节点;新主发出 slave of
命令后,原主变从节点会清空自己的数据,导致数据丢失
脑裂的避免
应对脑裂的解决办法是去限制原主库接收请求,Redis 提供了两个配置项
# 主节点必须有至少 N 个从节点在线才能写入
min-replicas-to-write 2
min-replicas-max-lag 10 # 从节点延迟 ≤10秒才算有效
不过,以上配置并无法彻底解决哨兵模式脑裂问题,因为 Redis 没有符合 Raft 强一致性协议; ETCD 通过 Raft 强一致性协议彻底解决了脑裂问题,不过牺牲了部分写入性能
集群模式
哨兵模式基本实现了高可用,但还有两个痛点:
- 每个节点上都存储相同的内容,很浪费内存
- 没有解决 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 后决定落在环上的哪个位置
然后把节点映射到环上,数据落在两个节点之间时,数据存储到后一个节点上即可
而且这种方式当某个一个节点失效或新增节点时,并不会影响原来其他节点上的数据分布
那么 Redis 为什么不选择一致性 hash,而是选择 hash 槽
- 一致性 hash 数据分布不均
- 为了解决数据倾斜,节点增减需要计算虚拟节点映射并迁移数据,自动化运维不友好
- 无法直接指定某节点的负载范围,难以定制数据分布 (如冷热数据分离)
Redis 持久化
1. RDB (Redis Database)
RDB 是 Redis 以指定时间间隔产生的数据快照
- 在
redis.conf
配置自动触发策略
# 持久化数据文件保存目录
dir /data
# rdb dump 文件名
dbfilename dump.rdb
save 900 1 # 900秒内至少1个key变化
save 300 10
save 60 10000
- 也可以通过命令进行手动触发
SAVE # 阻塞式 RDB 持久化
BGSAVE # 非阻塞式 RDB 持久化
- 修复命令
redis 提供了 RDB 修复命令 redis-check-rdb
2. AOF (Append Only File)
以日志的形式来记录每个写操作(存储于 appendonly.aof
),只许追加文件但不可用改写文件,Redis 启动之初会读取该文件重新构建数据
默认情况下,Redis 没有开启 AOF 持久化,需要在 redis.conf
配置文件中开启
如果 AOF 和 RDB 同时开启,重启时只会加载 AOF 文件 (AOF优先级更高)
appendonly yes # 开启 AOF 持久化
三种写回策略
always
:每次写操作都会写回 AOF 文件everysec
:每秒写回一次no
:由操作系统控制
3.混合持久化
可以通过配置 aof-use-rdb-preamble
来开启混合持久化(默认为 true
)
开启混合模式后,AOF文件的前半段为 RDB 格式快照,后半段为增量 AOF 命令
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 算法,优先删除访问频率最低的键