概述
Redis作为常用的缓存中间件,因其高性能,丰富的数据结构,使用简单等,常被用在需要一定高性能的To C业务场景中,如「秒杀场景」「用户信息中心」「帖子」「群聊」等等大家常见的业务场景中,以提高服务的响应速度,降低接口RT,同时也可以降低对DB层的压力,以提高整个服务集群的可靠性与稳定性等,提升用户体验。当然,Redis还有很多的应用场景,如分布式锁,限流等等的应用场景,后续会逐一进行讨论分析。
To C的高流量场景往往反映着该服务所对应的业务域具有较高的重要性,对服务集群的高性能与高可靠性都具有较高的要求,Redis作为常用的缓存中间件嵌入在服务中来抗大流量,其自生就应该具备一定的高可用性。
从Redis诞生至今,其高可用架构也在不断的演进中,以满足不同业务体量下服务的性能与可用性需求。从最初的主从架构,到基于Sentinel(哨兵模式)的高可用架构,再到目前被广泛使用的集群架构,Redis都在不断发展演进。每一种结构都存在其对应的优缺点,也对应着应用场景,本文将着重介绍Redis不同的高可用架构,对应的优缺点,以及每种架构的实现原理,应用场景等,其高性能的实现原理不在本文的讨论范围内(想必大家也都了解Redis为何性能好的原因)。
希望本文对想了解Redis高可用架构的同学有一定的帮助,最后也欢迎大家指出其中的问题,一起讨论。
内容说明:
1. 本文将按照Redis演进的时间线,逐一的介绍不同的架构。分别为:「基本的主从架构」「改进的主从架构」「基于Sentinel(哨兵模式)的高可用架构」「集群模式」。
2. 因整体内容较多,本文将分阶段进行完善。
阅读说明:中间涉及到一些分布式相关知识,有一定分布式基础的可更好理解相关内容。
主从架构
基本的主从架构
主从架构模式:部署多台redis节点,其中只有一台节点是主节点(master),其他的节点都是从节点(slave),也叫备份节点(replica)。只有master节点提供数据的事务性操作(增删改),slave节点只提供读操作。所有slave节点的数据都是从master节点同步过来的。
问题与缺点
上图只是最简单的一种主从结构方式,所有的slave节点都挂在master节点上,这样做的好处是slave节点与master节点的数据延迟较小;缺点是如果slave节点数量很多,master同步一次数据的耗时就很长,影响master节点的性能。
改进的主从架构
主从从架构
当slave过多时,可以将主从架构调整为主从从的拓扑结构,以减少过多的slave节点数据同步(全量同步/增加同步)带来的cpu/网络等的开销,降低master节点的压力;
问题与缺点
一主多从变为树桩结构,由于层级深度,导致深度越高的slave与最顶层master间数据同步延迟较大,数据一致性变差,所以最多不要超过三层。
主要使用场景
- 读多写少 - 读写分离 - 有一定的并发量
主从同步机制
全量同步(快照同步)
触发场景
- 集群中加入新的节点
- 主从节点偏移量差距超过一定的阈值
- 潜在的原因:
- 网络问题
- 从节点离线较长后再次连上集群,导致主从数据偏移量相差过大
- 主节点写入数据的速度大于主从同步的数据等场景
- 阈值:由redis buffer 缓冲区的大小决定
- 潜在的原因:
同步原理
- 从节点启动之后会与主节点建立socket长连接,然后向主节点发送psync指令
- 主节点收到从节点的psync指令之后,会执行BGsave命令生成新的rbd持久化文件,在生成过程中处理的客户端写操作命令会放在repl缓冲区。
- 主节点将生成的rbd文件发送给从节点,从节点清空老数据,加载新的rbd文件
- 主节点将生成的repl缓冲区的指令发送给从节点,从节点将执行该命令,将数据写入到内存
- 主节点通过socket长连接把修改命令发送给从节点,保证主从一致
全量同步开销
主节点:生成RDB文件会占用内存、硬盘资源,网络传输RDB的时候会占用一定的网络带宽资源
从节点:清空数据,若数据量大,需要消耗一定的时间,加载RDB也需要一定的时间
命令传播(增量同步)
在master节点和slave节点同步完成后,主服务器的每次更新都会发送相应的命令到从服务器。从服务器执行对应操作,将数据库状态更新到主服务器一致的状态。
1.支持断点续传 - 通过 master的redis buffer缓冲区 / master节点offset / slave节点的offset实现
无盘复制
通常,全量复制需要在磁盘上创建RDB文件,然后加载到内存中,Redis支持无盘复制,生成的RDB文件不保存到磁盘而是直接通过网络发送给从节点。无盘复制适用于主节点所在机器磁盘性能较差但网络宽带较充裕的场景。需要注意的是,无盘复制目前依然处于实验阶段。(类似零拷贝)
问题与缺点
- 主从架构的问题在于所有的写入操作都在master节点上,实际上是一个具有单点问题的架构;若master节点出现异常(节点下线等导致的不可用等),则整个集群将不可用
- 主从切换需要人工干预(无选举机制),同时需要变更客户端连接地址
基于Sentinel(哨兵模式)的高可用架构
出现背景
Sentinel架构解决redis主从架构人工干预的问题。
Redis官方文档
https://redis.io/docs/latest/operate/oss_and_stack/management/sentinel/
简介
Redis Sentinel 是 Redis 的高可用实现方案。Sentinel不是一个单独的进程,而是有多个哨兵服务组成的分布式系统。Sentinel集群独立于 Redis 集群,哨兵之间彼此建立连接,共同监控、管理所有的 Redis 节点。哨兵间使用流言协议(gossip protocols)进行消息传播,使用投票协议(agreement protocols)决定是否执行自动故障迁移和选择新的主节点。
作用
- 监 控:监控所有 Redis 节点的状态。
- 故障转移:当哨兵发现主节点下线时,会在所有从节点中选择一个作为新的主节点,并将所有其他节点的 Master 指向新的主节点。同时已下线的原主节点也会被降级为从节点,并修改配置将 原Master 指向新的主节点,等到它重新上线时就会自动以从节点进行工作。
- 通 知:当哨兵选举了新的主节点之后,可以通过 API 向客户端进行通知。
基本原理
Sentinel服务负责持续监控主从节点的健康状况,当主节点挂掉时,自动选择一个最优的从节点切换为主节点。客户端来连接集群时,会首先连接 sentinel,通过 sentinel 来查询主节点的地址,然后再去连接主节点进行数据交互。当主节点发生故障时,客户端会重新向 sentinel 要地址,sentinel 会将最新的主节点地址告诉客户端。如此应用程序将无需重启即可自动完成节点切换。
从库发现
对于哨兵的配置,我们只需要配置主库的信息,哨兵在连接主库之后,会调用 INFO
命令获取主库的信息,再从中解析出连接主库的从库信息,再以此和其他从库建立连接进行监控。
哨兵对所有节点都会每隔 10s 发送一次 INFO
命令,从各节点获取 Redis 集群实时的拓扑图信息。如果新节点加入,哨兵就会去监控新的节点。
**INFO
**命令信息
# Replication
role:master
connected_slaves:2
slave0:ip=172.25.0.102,port=6379,state=online,offset=258369,lag=1
slave1:ip=172.25.0.103,port=6379,state=online,offset=258508,lag=0
master_failover_state:no-failover
master_replid:a4a6a7f3b2e15d9a43c01d4ba6c842539e582d6a
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:258508
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:258508
发布/订阅机制
哨兵们在连接同一个主库之后,是通过发布/订阅(pub/sub)模式来发现彼此的存在的。
发布/订阅(pub/sub)是一种消息通信模式,主要的目的是解耦消息发布者和消息订阅者之间的耦合。Redis 作为一个 pub/sub server,在订阅者和发布者之间起到了消息路由的功能。订阅者可以通过 subscribe 和 psubscribe 命令从 Redis 订阅自己感兴趣的消息类型,Redis 将消息类型称为频道(channel)。当发布者通过 publish 命令向 Redis 发送特定类型的消息时,该频道的全部订阅者都会收到此消息。这里消息的传递是多对多的。一个 client 可以订阅多个 channel,也可以向多个 channel 发送消息。
在哨兵模式下,哨兵们会在每个 Redis 服务上创建并订阅一个名为 sentinel:hello
的频道,哨兵们就是通过它来相互发现,实现相互通信的。
订阅后,每个哨兵每隔 2 秒都会向 hello
频道发布一条携带自身信息的 hello 信息,这样哨兵就能知道其他哨兵的状态、监控的主节点和是否有新的哨兵加入:
//Hello频道消息
127.0.0.1:6371> subscribe __sentinel__:hello
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "__sentinel__:hello"
3) (integer) 1
1) "message"
2) "__sentinel__:hello"
3) "172.25.0.202,26379,5134e342cc62ac76494c140b66b7fda80340e3a8,0,mymaster,172.25.0.101,6379,0"
1) "message"
2) "__sentinel__:hello"
3) "172.25.0.203,26379,5f5ce54a6f22f71c7d273cfb9eb14377b103d4ad,0,mymaster,172.25.0.101,6379,0"
1) "message"
2) "__sentinel__:hello"
3) "172.25.0.201,26379,4fa3486dfbaca9abc62b2976e821d18e697ab2db,0,mymaster,172.25.0.101,6379,0"
监控
哨兵在对 Redis 节点建立 TCP 连接之后,会周期性地发送 PING
命令给节点(默认是 1s),以此判断节点是否正常。如果在 down-after-millisenconds
时间内没有收到节点的响应,它就认为这个节点掉线了。
主观下线
当哨兵发现与自己连接的其他节点断开连接,它就会将该节点标记为主观下线(+sdown
),包括主节点、从节点或者其他哨兵都可以标记为 sdown
状态。
//从节点主观下线
1:X 19 Mar 2024 13:26:29.837 # +sdown slave 172.25.0.103:6379 172.25.0.103 6379 @ mymaster 172.25.0.101 6379#
//哨兵主观下线
1:X 19 Mar 2024 13:19:19.799 # +sdown sentinel 5134e342cc62ac76494c140b66b7fda80340e3a8 172.25.0.202 26379 @ mymaster 172.25.0.101 6379#
//主节点主观下线
1:X 19 Mar 2024 13:24:06.612 # +sdown master mymaster 172.25.0.101 6379
当该节点重新连接之后,哨兵会取消对它的主观下线标记,操作是 -sdown
。
1:X 19 Mar 2024 13:20:04.811 # -sdown sentinel 5134e342cc62ac76494c140b66b7fda80340e3a8 172.25.0.202 26379 @ mymaster 172.25.0.101 6379
如果哨兵判断从节点或者其他哨兵节点主观下线,哨兵并不会执行其他操作。如果是主节点主观下线,哨兵就要采取措施,确定主节点是否真的宕机,并执行故障转移。
客观下线
哨兵确认主节点是否真的宕机这一步成为客观下线确认,如果主节点真的宕机了,哨兵就会将主节点标记为客观下线(+odown
)状态。
1:X 19 Mar 2024 13:24:06.612 # +sdown master mymaster 172.25.0.101 6379
1:X 19 Mar 2024 13:24:06.685 # +odown master mymaster 172.25.0.101 6379 #quorum 2/2
要判断主节点是否客观下线,需要与其他哨兵达成共识,如果大多数哨兵认为主节点主观下线了,哨兵才能确认主节点客观下线。达成共识的方式就是发起一轮投票,如果票数超过哨兵节点数的一半,并且大于等于 quorum
设置的数量,就是投票成功。否则哨兵就不能说主节点客观下线了。
quorum
是法定人数的意思,该信息在哨兵配置信息中进行配置:
# sentinel.conf
sentinel monitor <master-name> <ip> <redis-port> <quorum>
客观下线投票过程
- 当哨兵发现主节点下线,标记主节点为
sdown
状态。 - 哨兵向其他哨兵发送
SENTINEL is-master-down-by-addr
命令,询问其他哨兵该主节点是否已下线。 - 其他哨兵在收到投票请求之后,会检查本地主缓存中主节点的状态并进行回复(
1
表示下线,0
表示正常)。 - 发起的询问的哨兵在接收到回复之后,会累加“下线”的得票数。
- 当下线的票数大于一半哨兵数量并且不小于
quorum
时,就会将主节点标记为odown
状态。并开始准备故障转移。 - 发起投票的哨兵有一个投票倒计时,倒计时结束如果票数仍然不够的话,则放弃本次客观线下投票。并尝试继续与主节点建立连接。
【注意】
1.当哨兵把主节点标记为 odown 时,并不会通知其他哨兵,因为这样自己才更有机会进行故障转移。
2.如果多个哨兵在同一段时间内发现主节点下线,那么每个发现的哨兵都会发起投票,投票的结果只是让发起投票的哨兵能够确认主节点是否下线,并不会与其他哨兵共享。因此这个下线确认的动作是多个节点同时发起进行的。
故障转移
哨兵在将主节点标记为 odown
状态之后,就会马上开始尝试故障转移了。
故障转移主要由 sentinelFailoverStateMachineZ(sentinelRedisInstance)
函数负责。该函数由一个状态机组成,共有五个状态,标志着故障转移共分为五个大步骤:
| SENTINEL_FAILOVER_STATE | desc | invoke
|:------------------------|:---------------|:-------------------------------------------------------|
| `WAIT_START` | Leader 选举 | sentinelFailoverWaitStart(sentinelRedisInstance) |
| `SELECT_SLAVE` | Master 选取 | sentinelFailoverSelectSlave(sentinelRedisInstance) |
| `SEND_SLAVEOF_NOONE` | Slave 身份去除 | sentinelFailoverSendSlaveOfNoOne(sentinelRedisInstance) |
| `WAIT_PROMOTION` | 提升 Master | sentinelFailoverWaitPromotion(sentinelRedisInstance) |
| `RECONF_SLAVES` | 配置从节点 | sentinelFailoverReconfNextSlave(sentinelRedisInstance) |
详见:https://www.jianshu.com/p/178d0b10809c
整体架构
集群模式(Redis Cluster)
节点负载均衡
一致性哈希
数据分布算法
虚拟节点机制
集群请求重定向
MOVED 错误
ASK 错误
Goosip协议
说明:集群节点通信机制