高可用(High Availability,即HA),指的是通过尽量缩短日常维护操作和突发的系统崩溃所导致的停机时间,以提高系统和应用的可用性。

高可用一般来说有两个含义:一是数据尽量不丢失,二是保证服务尽可能可用。单个节点的系统缺点明显,一旦发生故障会导致服务不可用。而且,单个节点处理所有的请求,吞吐量有限,容量也有限。

Redis实现高可用,在于提供多个节点,通常有三种部署模式:主从模式哨兵模式集群模式

主从模式

主从复制的作用主要包括:

  • 数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
  • 故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。
  • 负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。
  • 高可用基石:除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。

主从库之间采用的是读写分离的方式。

  • 读操作:主库、从库都可以接收;
  • 写操作:首先到主库执行,然后,主库将写操作同步给从库。

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

主从复制原理

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

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

增量复制:在之后的每条命令都会以增量形式同步给从库

全量复制

当我们启动多个 Redis 实例的时候,它们相互之间就可以通过 replicaof(Redis 5.0 之前使用 slaveof)命令形成主库和从库的关系,之后会按照三个阶段完成数据的第一次同步。

假设我们现在有两台redis实例 A:172.16.19.3和B:172.16.19.5,我们在B上执行命令

replicaof 172.16.19.3 6379

第一阶段是主从库间建立连接、协商同步的过程

psync {runID} {offset}

从库给主库发送 psync 命令,表示要进行数据同步,主库根据这个命令的参数来启动复制。psync 命令包含了主库的 runID 和复制进度 offset 两个参数。runID,是每个 Redis 实例启动时都会自动生成的一个随机 ID,用来唯一标记这个实例。当从库和主库第一次复制时,因为不知道主库的 runID,所以将 runID 设为“?”。offset,此时设为 -1,表示第一次复制。

FULLRESYNC {runID} {offset}

主库收到 psync 命令后,会用 FULLRESYNC 响应命令带上两个参数:主库 runID 和主库目前的复制进度 offset,返回给从库。从库收到响应后,会记录下这两个参数。这里有个地方需要注意,FULLRESYNC 响应表示第一次复制采用的全量复制,也就是说,主库会把当前所有的数据都复制给从库。

第二阶段,主库将所有数据同步给从库

从库收到数据后,在本地完成数据加载。这个过程依赖于内存快照生成的 RDB 文件。

主库返回FULLRESYNC相应后,代表要进行全量同步。主库会执行bgsave命令,生成 RDB 文件,接着将文件发给从库。为了避免之前从库的数据影响,从库会清空数据库。然后加载RDB文件。在全量同步期间,主库能够正常接收请求。这些命令会先积累到从库的输出缓冲区replication buffer中。等从库加载完RDB就会继续执行后续积压的命令。

第三个阶段,主库会把第二阶段执行过程中新收到的写命令,再发送给从库

当主库完成 RDB 文件发送后,后续所有新的操作,都会发送到从库的 replication buffer 中,从库再执行这些操作。这样一来,主从库就实现同步了。

在主从同步过程中,主从互为对方的客户端。这样主库可以接收从库的心跳和同步请求,从库能接收主库的全量和增量同步数据。

增量复制

如果主从库在命令传播时出现了网络闪断,那么,从库就会和主库重新进行一次全量复制,开销非常大。从 Redis 2.8 开始,网络断了之后,主从库会采用增量复制的方式继续同步。

先看两个概念: replication bufferrepl_backlog_buffer

repl_backlog_buffer:主库的所有修改命令,都会记录到这样一个环形缓冲区,类似redolog。如果从库中途断开,会携带最后一次复制的offset对主库请求PSYNC

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

所以repl_backlog_buffer配置尽量大一些,可以降低主从断开后全量复制的概率。而在repl_backlog_buffer中找主从差异的数据后,如何发给从库呢?这就用到了replication buffer。

replication buffer`:Redis和客户端通信也好,和从库通信也好,Redis都需要给分配一个 内存buffer进行数据交互,客户端是一个client,从库也是一个client。每个client结构上都有输入缓冲区和输出缓冲区(这个我们之前的章节有介绍过对应的数据结构)所以从库的输出换冲突,我们叫做`replication buffer

哨兵模式

当主机宕机后,需要手动把一台从(slave)服务器切换为主服务器,这就需要人工干预,费时费力,还回造成一段时间内服务不可用,所以推荐哨兵架构(Sentinel)来解决这个问题。

哨兵模式是在Redis 2.8版本开始引入的功能,一般公司采用一主-两从-三哨兵的方式搭建高可用架构

哨兵实现了什么功能呢?下面是Redis官方文档的描述:

  • 监控(Monitoring):哨兵会不断地检查主节点和从节点是否运作正常。
  • 自动故障转移(Automatic failover):当主节点不能正常工作时,哨兵会开始自动故障转移操作,它会将失效主节点的其中一个从节点升级为新的主节点,并让其他从节点改为复制新的主节点。
  • 配置提供者(Configuration provider):客户端在初始化时,通过连接哨兵来获得当前Redis服务的主节点地址。
  • 通知(Notification):哨兵可以将故障转移的结果发送给客户端。

运行原理

#哨兵sentine1监控的redis主节点的ip port# master-name可以自己命名的主节点名字 只能由字母A-z、 数字0-9、这三个字符".-_"组成。# quorum 配置多少个sentine3哨兵统一认为master主节点失联 那么这时客观上认为主节点失联了# sentine1 monitor <master-name> <ip> <redis-port> <quorum>sentine1 monitor mymaster 127.0.0.1 6379 2

我们只需要这样简单的配置了主库,哨兵就能对整个集群进行监控了。那么哨兵是如何知道从库地址的呢?

主库有个info命令,可以看见非常多的信息,包括所有从库列表。

哨兵以每10秒一次的频率向主库发送INFO命令,就能获取所有从库列表,刷新从库状态了。

那么哨兵间又是如何知道对方的呢?

在主从集群中,主库上有一个名为__sentinel__:hello的频道

哨兵 1 把自己的 IP(172.16.19.3)和端口(26579)发布到__sentinel__:hello频道上,哨兵 2 和 3 订阅了该频道。那么此时,哨兵 2 和 3 就可以从这个频道直接获取哨兵 1 的 IP 地址和端口号。然后,哨兵 2、3 可以和哨兵 1 建立网络连接。

故障判定

首先要理解两个概念:主观下线客观下线

  • 主观下线:任何一个哨兵都是可以监控探测,并作出Redis节点下线的判断;
  • 客观下线:有哨兵集群共同决定Redis节点是否下线;

哨兵会以每秒一次的频率对所有它创建连接的实例(哨兵,主服务,从服务)发送PING命令,如果在一定的时间内,都收不到有效回复PONG,哨兵认为节点主观下线。这个时间由对应的配置决定

当其中一个哨兵认为节点主观下线后,它会询问其他的哨兵,看其他节点是否也认为节点下线。如果接收到足够多的的数量证明节点下线,那么就会认为节点客观下线。具体的数量由下面的2来决定。

sentine1 monitor mymaster 127.0.0.1 6379 2

哨兵选举

当有一个哨兵认为主节点客观下线,就会开始哨兵领头节点的选举。

为什么必然会出现选举/共识机制

为了避免哨兵的单点情况发生,所以需要一个哨兵的分布式集群。作为分布式集群,必然涉及共识问题(即选举问题);同时故障的转移和通知都只需要一个主的哨兵节点就可以了。

哨兵的选举机制是什么样的

哨兵的选举机制其实很简单,就是一个Raft选举算法: 选举的票数大于等于num(sentinels)/2+1时,将成为领导者,如果没有超过,继续选举

Raft算法你可以参看这篇文章分布式算法 – Raft算法

任何一个想成为 Leader 的哨兵,要满足两个条件

  • 第一,拿到半数以上的赞成票;
  • 第二,拿到的票数同时还需要大于等于哨兵配置文件中的 quorum 值。

以 3 个哨兵为例,假设此时的 quorum 设置为 2,那么,任何一个想成为 Leader 的哨兵只要拿到 2 张赞成票,就可以了。

更进一步理解

这里很多人会搞混 判定客观下线是否能够主从切换(用到选举机制) 两个概念,我们再看一个例子。

Redis 1主4从,5个哨兵,哨兵配置quorum为2,如果3个哨兵故障,当主库宕机时,哨兵能否判断主库“客观下线”?能否自动切换?

经过实际测试:

1、哨兵集群可以判定主库“主观下线”。由于quorum=2,所以当一个哨兵判断主库“主观下线”后,询问另外一个哨兵后也会得到同样的结果,2个哨兵都判定“主观下线”,达到了quorum的值,因此,哨兵集群可以判定主库为“客观下线”

2、但哨兵不能完成主从切换。哨兵标记主库“客观下线后”,在选举“哨兵领导者”时,一个哨兵必须拿到超过多数的选票(5/2+1=3票)。但目前只有2个哨兵活着,无论怎么投票,一个哨兵最多只能拿到2票,永远无法达到N/2+1选票的结果。

小细节:如果哨兵在选举的时候投票很分散,没有哨兵达到票数大于一半。那么所有哨兵会休眠一断随机时间。再下一次选举纪元+1,然后继续开始投票。

故障转移

  1. 将slave-1脱离原从节点(PS: 5.0 中应该是replicaof no one),升级主节点,
  2. 将从节点slave-2指向新的主节点replicaof new master
  3. 通知客户端主节点已更换
  4. 将原主节点(oldMaster)变成从节点,指向新的主节点

集群模式

哨兵模式基本已经实现了高可用,但是每个节点都存储相同的内容,很浪费内存。而且,哨兵模式没有解决master写数据的压力。并且如果单台master的内存过大,会造成rdb或者主从重新复制时很大的压力。为了解决这些问题,就有了集群模式,实现分布式存储,每个节点存储不同的内容。集群部署的方式能自动将数据进行分片,每个master上放一部分数据,提供了内置的高可用服务,即使某个master宕机了,服务还可以正常地提供,架构如下图所示:

hash槽

Redis-cluster没有使用一致性hash,而是引入了哈希槽的概念。Redis-cluster中有16384(即2的14次方)个哈希槽,每个key通过CRC16校验后对16383取模来决定放置哪个槽。Cluster中的每个节点负责一部分hash槽(hash slot)。

比如集群中存在三个节点,则可能存在的一种分配如下:

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

当然如果我们想让一部分key在同一个节点下,redis也提供了打标的方法Hash tags我们可以让key带上一个{tag}的方式,这样hash计算的时候只会取{}里面的值

比如 set {activity}user:10086 "小明"

请求重定向

由于集群节点会出现伸缩,有可能我们记录的槽和集群的映射关系已经过时了,这时候我们如何访问到准确的集群节点呢?这就依赖了moved重定向ask重定向

MOVED 重定向

前键命令所请求的键不在当前请求的节点中,则当前节点会向客户端发送一个Moved 重定向,客户端根据Moved 重定向所包含的内容找到目标节点,再一次发送命令。

ASK 重定向

Ask重定向发生于集群伸缩时,集群伸缩会导致槽迁移,当我们去源节点访问时,此时数据已经可能已经迁移到了目标节点,使用Ask重定向来解决此种情况。

在伸缩期间,如果重定向不带Asking的话,请求会直接被拒绝的

客户端工具

对于这些重定向的操作,客户端是可以无感知的。我们只需要接入jedis,jedis发现被重定向后,自然会重新维护好集群和slot的映射关系。

Gossip协议

Redis Cluster 通讯底层是Gossip协议。

gossip 协议(gossip protocol)是基于流行病传播方式的节点或者进程之间信息交换的协议。 在分布式系统中被广泛使用,比如我们可以使用 gossip 协议来确保网络中所有节点的数据一样。

Gossip协议已经是P2P网络中比较成熟的协议了。Gossip协议的最大的好处是,即使集群节点的数量增加,每个节点的负载也不会增加很多,几乎是恒定的。这就允许Consul管理的集群规模能横向扩展到数千个节点

连接原理

Redis 集群是去中心化的,彼此之间状态同步靠 gossip 协议通信,集群的消息有以下几种类型:

  • Meet 通过「cluster meet ip port」命令,已有集群的节点会向新的节点发送邀请,加入现有集群。
  • Ping 节点每秒会向集群中其他节点发送 ping 消息,消息中带有自己已知的两个节点的地址、槽、状态信息、最后一次通信时间等。
  • Pong 节点收到 ping 消息后会回复 pong 消息,消息中同样带有自己已知的两个节点信息。
  • Fail 节点 ping 不通某节点后,会向集群所有节点广播该节点挂掉的消息。其他节点收到消息后标记已下线。

我们只需要根据cluster meet 构建集群,再根据cluster addslots 命令委派好所有的槽,就可以正常启动集群了。

故障检测

集群中的每个节点都会定期地向集群中的其他节点发送PING消息,以此交换各个节点状态信息,检测各个节点状态:在线状态、主观下线PFAIL、客观下线FAIL。

集群里的每个节点默认每隔一秒钟就会从已知节点列表中随机选出五个古董节点,然后对这五个节点中最长时间没有发送过PING消息的节点发送PING消息,以此来检测被选中的节点是否在线。如果发现有下线的节点,那么会在clusterNode结构的fail_reports链表中保存主观下线的状态PFAIL

相应的节点状态会跟随消息在集群内传播。 ping/pong消息的消息体会携带集群1/10的其他节点状态数据,当接受节点发现消息体中含有主观下线的节点状态时, 也会将主观下线PFAIL状态保存到本地中。

当集群中超过半数认为主观下线后,会进入客观下线的动作。

  • 通知集群内所有的节点标记故障节点为客观下线状态并立刻生效
  • 通知故障节点的从节点触发故障转移流程。
故障转移

当slave发现自己的master变为FAIL状态时,便尝试进行Failover,以期成为新的master。由于挂掉的master可能会有多个slave。Failover的过程需要经过类Raft协议的过程在整个集群内达到一致, 其过程如下:

  1. slave发现自己的master变为FAIL
  2. 将自己记录的集群currentEpoch加1,并广播Failover Request信息
  3. 其他节点收到该信息,只有master响应,判断请求者的合法性,并发送FAILOVER_AUTH_ACK,对每一个epoch只发送一次ack
  4. 尝试failover的slave收集FAILOVER_AUTH_ACK
  5. 超过半数后变成新Master
  6. 广播Pong通知其他集群节点

哨兵模式是哨兵请求选举,得到超过半数哨兵的支持,然后领头哨兵选举从节点晋升。

集群模式是从节点请求超过半数的集群节点支持,自己主动晋升

维护心跳

Redis节点会记录其向每一个节点上一次发出ping和收到pong的时间,心跳发送时机与这两个值有关。通过下面的方式既能保证及时更新集群状态,又不至于使心跳数过多:

  • 每次Cron向所有未建立链接的节点发送ping或meet
  • 每1秒从所有已知节点中随机选取5个,向其中上次收到pong最久远的一个发送ping
  • 每次Cron向收到pong超过timeout/2的节点发送ping
  • 收到ping或meet,立即回复pong
心跳包包含的数据

Header,发送者自己的信息

  • 所负责slots的信息
  • 主从信息
  • ip port信息
  • 状态信息

Gossip,发送者所了解的部分其他节点的信息

  • ping_sent, pong_received
  • ip, port信息
  • 状态信息,比如发送者认为该节点已经不可达,会在状态信息中标记其为PFAIL或FAIL
新节点加入
  • 发送meet包加入集群
  • 从pong包中的gossip得到未知的其他节点
  • 循环上述过程,直到最终加入集群

Slots信息
  • 判断发送者声明的slots信息,跟本地记录的是否有不同
  • 如果不同,且发送者epoch较大,更新本地记录
  • 如果不同,且发送者epoch小,发送Update信息通知发送者
Master slave信息

发现发送者的master、slave信息变化,更新本地状态

节点Fail探测(故障发现)
  • 超过超时时间仍然没有收到pong包的节点会被当前节点标记为PFAIL
  • PFAIL标记会随着gossip传播
  • 每次收到心跳包会检测其中对其他节点的PFAIL标记,当做对该节点FAIL的投票维护在本机
  • 对某个节点的PFAIL标记达到大多数时,将其变为FAIL标记并广播FAIL消息

注:Gossip的存在使得集群状态的改变可以更快的达到整个集群。每个心跳包中会包含多个Gossip包,那么多少个才是合适的呢,redis的选择是N/10,其中N是节点数,这样可以保证在PFAIL投票的过期时间内,节点可以收到80%机器关于失败节点的gossip,从而使其顺利进入FAIL状态。

http://t.zoukankan.com/gqtcgq-p-7247044.html

广播信息

当需要发布一些非常重要需要立即送达的信息时,上述心跳加Gossip的方式就显得捉襟见肘了,这时就需要向所有集群内机器的广播信息,使用广播发的场景:

  • 节点的Fail信息:当发现某一节点不可达时,探测节点会将其标记为PFAIL状态,并通过心跳传播出去。当某一节点发现这个节点的PFAIL超过半数时修改其为FAIL并发起广播。
  • Failover Request信息:slave尝试发起FailOver时广播其要求投票的信息
  • 新Master信息:Failover成功的节点向整个集群广播自己的信息

扩缩容

当集群出现容量限制或者其他一些原因需要扩容时,redis cluster提供了比较优雅的集群扩容方案。

  1. 首先将新节点加入到集群中,可以通过在集群中任何一个客户端执行cluster meet 新节点ip:端口,或者通过redis-trib add node添加,新添加的节点默认在集群中都是主节点。
  2. 迁移数据 迁移数据的大致流程是,首先需要确定哪些槽需要被迁移到目标节点,然后获取槽中key,将槽中的key全部迁移到目标节点,然后向集群所有主节点广播槽(数据)全部迁移到了目标节点。直接通过redis-trib工具做数据迁移很方便。 现在假设将节点A的槽10迁移到B节点,过程如下:
B:cluster setslot 10 importing A.nodeIdA:cluster setslot 10 migrating B.nodeId

redis-trib客户端会不断的从A获取槽里的节点,然后对A执行migrate命令,让A将每个键导入给B

A:cluster getkeysinslot 10 100A:migrate B.ip B.port "" 0 5000 keys key1[ key2....]

参考资料

https://time.geekbang.org/column/article/275337

https://time.geekbang.org/column/article/274483

https://www.cnblogs.com/andy6/p/10829929.html

哨兵

https://blog.csdn.net/weixin_44183721/article/details/126195582

https://pdai.tech/md/db/nosql-redis/db-redis-x-cluster.html#gossip%E5%8D%8F%E8%AE%AE