redis分布式原理

redis分布式原理

为什么Redis需要支持分布式

性能

Redis本身的QPS已经很高了,但是如果在一些并发量非常高的情况下,性能还是会受到影响。这个时候我们希望有更多的Redis服务来分摊压力,实现负载均衡。

高可用

第二个是可用性和安全的问题,如果只有一个Redis服务,一旦服务宕机,那么所有的客户端都无法访问,会对业务造成很大的影响。另一个,如果硬件发生故障,而单机的数据无法恢复的话,带来的影响也是灾难性的。

可扩展

第三个是出于存储的考虑。因为Redis所有的数据都放在内存中,如果数据量大,很容易受到硬件的限制。升级硬件(scaleup)收效和成本比太低,所以我们需要有一种横向扩展的方法。

高性能、高可用、扩展性需要依赖两种关键的技术,一种是分片,一种是冗余。

分片的意思是把所有的数据拆分到多个节点分散存储。

冗余的意思是每个节点都有一个或者多个副本。

那么,Redis必须要提供数据分片和主从复制的功能。副本有不同的角色,如果主节点发生故障,则把某个从节点改成主节点,访问新的主节点,实现高可用。

Redis主从复制(replication)

跟Kafka、RocketMQ、MySQL、Zookeeper一样,Reids支持集群的架构,集群的节点有主节点和从节点之分。

主节点叫master,从节点叫slave。slave会通过复制的技术,自动同步master的数据。

主从复制配置

Redis的主从复制配置非常地简单,只需要在配置文件里面添加一行配置就可以了。

例如一主多从,186是主节点,在每个slave节点的redis.conf配置文件增加一行

1
replicaof 192.168.44.186 6379

从节点启动之后,就会自动连接到master节点,开始同步数据。

如果master节点变了,比如原来的master宕机,选举出了新的master,这个配置会被重写。

还有一种方式,就是在启动服务时通过参数直接指定master节点:

1
./redis-server --slaveof 192.168.44.186 6379

一个正在运行中的节点,可以变成其他节点的从节点吗?

可以,这就是第三种方式,在客户端直接执行slaveof IP port,使该Redis实例成为从节点。

1
slaveof 192.168.44.186 6379

一个从节点也可以是其他节点的主节点,形成级联复制的关系。

查看集群状态:

1
redis> info replication

从节点是只读的,不能执行写操作。执行写命令会报错:

1
(error) READONLY You can't write against a read only replica.

在主节点写入后,slave会自动从master同步数据。

如果从节点想断开怎么办?

把配置文件里面的replica of去掉重启,或者直接断开复制:

1
redis> slaveof no one

此时从节点会变成自己的主节点,不再复制数据。

主从复制原理

Redis的主从复制分为两类,一种是全量复制,就是一个节点第一次连接到maste老节点,需要全部的数据。

第二种叫做增量复制,比如之前已经连接到master节点,但是中间网络断开,或者slave节点宕机了,缺了一部分的数据。

连接阶段

1、slave节点启动时(或者执行slaveof命令时),会在自己本地保存master节点的信息,包括master node的host和ip。

2、slave节点内部有个定时任务replicationCron,每隔1秒钟检查是否有新的master node要连接和复制。

如果发现有master节点,就跟master节点建立连接。如果连接成功,从节点就会连接一个专门处理复制工作的文件事件处理器负责后续的复制工作。

为了让主节点感知到slave节点的存活,slave节点定时会给主节点发送ping请求。

建立连接以后,就可以同步数据了,这里也分成两个阶段。

数据同步阶段

如果是新加入的slave节点,那就需要全量复制。master通过bgsave命令在本地生成一份RDB快照,将RDB快照文件发给slave节点(如果超时会重连,可以调大repl-timeout的值)。

如果slave节点自己本来有数据怎么办?

slave节点首先需要清除自己的旧数据,然后用RDB文件加载数据。

master节点生成RDB期间,接收到的写命令怎么处理?

开始生成RDB文件时,master会把所有新的写命令缓存在内存中。在slave节点保存了RDB之后,再将新的写命令复制给slave节点。(跟AOF重写rewrite期间接收到的命令的处理思路是一样的)

第一次全量同步完了,主从已经保持一致了,后面就是持续把接收到的命令发送给slave节点。

命令传播阶段

master node持续把写命令,异步复制给slave node。

总结起来非常地简单,前面用RDB文件,后面把命令发给slave节点,就实现了主从复制。

一般情况下我们不会用Redis做读写分离,因为Redis的吞吐量已经够高了,做集群分片之后并发的问题更少,所以不需要考虑主从延迟的问题。

跟MySQL一样,主从之间复制延迟是不可避免的,只能通过优化网络来改善。

第二种情况就是增量复制了。

如果从节点有一段时间断开了与主节点的连接,是不是要把原来的数据全部清空必重新全量复制一遍?

这样效率太低了,如果可以增量复制,怎么知道上次复制到哪里?

slave通过master_repl_offset记录的偏移量

1
repl-diskless-sync=no

为了降低主节点磁益开销Reds支持无盘复制,master生成的RDB文件不保存到磁盘而是直接通过网络发送给从节点。无盘复制适用于主节点所在机器磁盘性能较差但网络宽带较充裕的场景。

主从复制的不足

Redis主从复制解决了数据备份和一部分性能的问题,但是没有解决高可用的问题。 在一主一从或者一主多从的情况下,如果主服务器挂了,对外提供的服务就不可用了,单点问题没有得到解决。

跟MySOL一样,如果每次都是手动把之前的从服务器切换成主服务器,然后再把剩余节点设置为它的从节点,这个比较费时费力,还会造成一定时间的服务不可用。

可用性保证之Sentinel

Sentinel原理

怎么实现高可用呢?

第一个对于服务端来说,能够实现主从自动切换;

第二个,对于客户端来说,如果发生了主从切换,它需要获取最新的master节点。

Redis的高可用是通过哨兵Sentinel来保证的,它的思路就是通过运行监控服务器来保证服务的可用性。

从Redis2.8版本起,提供了一个稳定版本的Sentinel(哨兵),用来解决高可用的问题。

我们会启动奇数个的Sentinel的服务,通过src/redis-sentinel

可以用sentinel的脚本启动,也可以用redis-server的脚本加sentinel参数启动:

1
./redis-sentinel ../sentinel.conf

或者:

1
./redis-server ../sentinel.conf --sentine

它本质上只是一个运行在特殊模式之下的Redis。Sentinel通过info命令得到被监听Redis机器的master,slave等信息。

image-20210826233054797

为了保证监控服务器的可用性,会对Sentinel做集群的部署。Sentinel既监控所有的Redis服务,Sentinel之间也相互监控。

注意:Sentinel本身没有主从之分,地位是平等的,只有Redis服务节点有主从之分。

这里就有个问题了,Sentinel唯一的联系,就是他们监控相同的master,那一个Sentinel节点是怎么知道其他的Sentinle节点存在的呢?

因为Sentinel是一个特殊状态的Redis节点,它也有发布订阅的功能。

哨兵上线时,给所有的Redis节点(master/slave)的名字为_sentinel__:hello的channle发送消息。

每个哨兵都订阅了所有Redis节点名字为_sentinel_:hello的channle,所以能互相感知对方的存在,而进行监控。

Sentinel最大的作用就是管理Redis节点服务状态,还有切换主从。

服务下线

Sentinel是怎么知道master节点挂了。

Sentinel默认以每秒钟1次的频率向Redis服务节点发送PING命令,如果在指定时间内没有收到有效回复,Sentinel会将该服务器标记为下线(主观下线)。

sentinel.conf下这个参数控制:

1
sentinel down-after-milliseconds <master-name> <milliseconds>

默认是30秒。

但是,只有你发现master下线,并不代表master真的下线了。也有可能是你自己的网络出问题了。所以,这个时候第一个发现master下线的Sentinel节点会继续询问其他的Sentie节点,确这节点是下线,如果多数Sentine节点都认为mastel下线,master才真正确认被下线(客观下线),确定master下线之后,就需要重新选举master。

故障转移

Redis的选举和故障转移都是由Sentinel完成的。问题又来了,既然有这么多的Sentinel节点,由谁来做故障转移的事情呢?

故障转移流程的第一步就是在Sentinel集群选择一个Leader,由Leader完成故障转移流程。

Sentinle通过Raft算法,实现Sentinel选举。

Ratf算法

前面说过,只要有了多个副本,就必然要面对副本一致性的问题。如果要所有的节点达成一致,必然要通过复制的方式实现。但是这么多节点,以哪个节点的数据为准呢?所以必须选出一个Leader。

所以数据保持一致需要两个步骤:领导选举,数据复制。

这里关注一下选举的实现。

Raft是一个共识算法(consensus algorithm)。比如比特币之类的加密货币,就需要共识算法。Spring Cloud的注册中心解决方案Consul也用到了Raft协议。

Raft的核心思想:先到先得,少数服从多数,Sentinel的Raft实现跟原生的算法是有所区别的,但是大体思想一致。

Raft算法演示:

http://thesecretlivesofdata.com/raft/

1、分布式环境中的节点有三个状态:Follower、Candidate (虚线外框)、Lead (实线外框)。

2、一开始所有的节点都是Follower状态,如果Follower连接不到Leader (Leader挂了),它就会成为Candidate。Candidate请求其他节点的投票,其他的节点会投给它。如果它得到了大多数节点的投票,它就成为了主节点,这个过程就叫做LeaderElection。

3、现在所有的写操作需要在Leader节点上发生。Leader会记录操作日志,没有同步到其他Follower节点的日志,状态是uncommitted,等到超过半数的Follower同步了这条记录,日志状态就会变成committed。Leader会通知所有的Follower日志已经committed,这个时候所有的节点就达成了一致。这个过程叫Log Replication。

4、在Raft协议里面,选举的时候有两个超时时间,第一个叫election timeout。也就是说,为了防止同一时间大量节点参与选举,每个节点变感Candidate之前需要随机等待一段时间,时间范围是150msand 300ms之间。第一个变成Candidate的节点会先发起投票。它会先投给自己,然后请求其他节点投票(Request Vote)。

5、如果还没有收到投票结果,又到了超时时间,需要重置超时时间,只要有大部分节点投给了一个节点,它就会变成Leader。

6、成为Leader之后,它会发消息让来同步数据(Append Entries),发消息的间隔是由heartbeat timeout来控制的。Followers会回复同步数据的消息。

7、只要Followers收到了同步数据的消息,代表Leader没挂,他们就会清除heartbeat timeout的计时。

8、但是一旦Followers在heartbeat timeout时间之内没有收到Append Entries 消息,它就会认为Leader挂了,开始让其他节点投票,成为新的Leader。

9、必须超过半数以上节点投票,保证只有一个Leader被选出来。

10、如果两个Follower同时变成了Candidate,就会出现分割投票。比如有两个节点同时变成Candidate,而且各自有一个投票请求先达到了其他的节点,加上他们给自己的投票,每个Candidate手上有2票。但是,因为他们的election timeout不同,在发起新的一轮选举的时候,有一个节点收到了更多的投票,所以它变成了Leader。

总结:

Sentinle的Raft算法和Raft论文略有不同。

1、master客观下线触发选举,而不是过了election timeout时间开始选举。

2、Leader并不会把自己成为Leader的消息发给其他Sentinel,其他Sentinel等待Leader从slave选出master后,检测到新的master正常工作后,就会去掉客观下线的标识,从而不需要进入故障转移流程。

Redis的master节点的选举规则又是什么样的呢?

对于所有的slave节点,一共有四个因素影响选举的结果,分别是断开连接时长、优先级排序、复制数量、进程id。

1、如果与哨兵连接断开的比较久,超过了某个阈值,就直接失去了选举权。

2、如果拥有选举权,那就看谁的优先级高,这个在配置文件里可以设置(replica-priority 100),数值越小优先级越高。

3、如果优先级相同,就看谁从master中复制的数据最多(复制偏移量最大)选最多的那个。

4、如果复制数量也相同,就选择进程id最小的那个。

master节点确定之后,又怎么让其他的节点变成它的从节点呢?

1、选出Sentinel Leader之后,由Sentinel Leader向某个节点发送slaveof no one 命令,让它成为独立节点。

2、然后向其他节点发送slaveof x.x.x.xxxxx(本机IP端口),让它们成为这个节点的从节点,故障转移完成。

Sentinel的功能总结

监控:Sentinel会不断检查主服务器和从服务器是否正常运行。

通知:如果某一个被监控的实例出现问题,Sentinel可以通过API发出通知。

自动故障转移(failover):如果主服务器发生故障,Sentinel可以启动故障转移过名程。把某台服务器升级为主服务器,并发出通知。

配置管理:客户端连接到Sentinel,获取当前的Redis主服务器的地址。

Sentinel机制的不足

主从切换的过程中会丢失数据,因为只有一个master。

只能单点写,没有解决水平扩容的问题。

如果数据量非常大,这个时候就要对Redis的数据进行分片了。

这个时候我们需要多个master-slave的group,把数据分布到不同的group中。

Redis分布式方案

如果要实现Redis数据的分片,有三种方案。

第一种是在客户端实现相关的逻辑,例如用取模或者一致性哈希对key进行分片,查询和修改都先判断key的路由。

第二种是把做分片处理的逻辑抽取出来,运行一个独立的代理服务,客户端连接到这个代理服务,代理服务做请求的转发。

第三种就是基于服务端实现。

客户端Sharding

image-20210827000153997

在我们用得非常多的Jedis客户端中,支持分片功能。它是Spring Boot2.x版本之前默认的Redis客户端,RedisTemplate就是对Jedis的封装。

ShardedJedis

Jedis有几种连接池,其中有一种支持分片。

ShardedJedis是怎么做到的呢?

如果是希望数据分布相对均匀的话,首先可以考虑哈希后取模(因为key不一定是整数,所以先计算哈希)。

哈希后取模
例如,hash(key)%N,根据余数,决定映射到哪一个节点。这种方式比较简单,属于静态的分片规则。

但是一旦节点数量变化(新增或者减少),由于取模的N发生变化,数据需要重新分布。

为了解决这个问题,又有了一致性哈希算法。ShardedJedis实际上用的就是一致性哈希算法。

一致性哈希

原理:

把所有的哈希值空间组织成一个虚拟的圆环(哈希环),整个空间按顺时针方向组因为是环形空间,0和2^32-1是重叠的。

假设我们有四台机器要哈希环来实现映射(分布数据),我们先根据机器的名称或者IP计算哈希值,然后分布到哈希环中(红色圆圈)。

image-20210827000527746

现在有4条数据或者4个访问请求,对key计算后,得到哈希环中的位置(绿色圆圈)。沿哈希环顺时针找到的第一个Node,就是数据存储的节点。

image-20210827000611449

在这种情况下,新增了一个Node5节点,只影响一部分数据的分布。

image-20210827000647419

删除了一个节点Node4,只影响相邻的一个节点。

image-20210827000711216

一致性哈希解决了动态增减节点时,所有数据都需要重新分布的问题,它只会影响入到下一个相邻的节点,对其他节点没有影响。

但是这样的一致性哈希算法有一个缺点,因为节点不一定是均匀地分布的,特别是在节点数比较少的情况下,所以数据不能得到均匀分布。

解决这个问题的办法是引入虚拟节点(Virtual Node)。

比如:2个节点,5条数据,只有1条分布到Node2,4条分布到Node1,不均匀。

image-20210827000813642

Node1设置了两个虚拟节点,Node2也设置了两个虚拟节点(虚线圆圈)。 这时候有3条数据分布到Node1,1条数据分布到Node2。

image-20210827000921042

致性哈希在分布式系统中,负载均衡、分库分表等场景中都有应用,跟LRU一样,是一个很基础的算法。

使用ShardedJedis之类的客户端分片代码的优势是配置简单,不依赖于其他中间件,分区的逻辑可以自定义,比较灵活。

但是基于客户端的方案,不能实现动态的服务增减,每个客户端需要自行维护分片策略,存在重复代码。

第二种思路就是把分片的代码抽取出来,做成一个公共服务,所有的客户端都连接学到这个代理层。由代理层来实现请求和转发。

代理Proxy

image-20210827001149875

典型的代理分区方案有Twitter开源的Twemproxy和国内的豌豆荚开源的Codis。

Redis Cluster

Redis Cluster是在Redis3.0的版本正式推出的,用来解决分布式的需求,同时也可以实现高可用。跟Codis不一样,它是去中心化的,客户端可以连接到任意一个可用节点。

数据分片有几个关键的问题需要解决:

1、数据怎么相对均匀地分片

2、客户端怎么访问到相应的节点和数据

3、重新分片的过程,怎么保证正常服务

架构

Redis Cluster可以看成是由多个Redis实例组成的数据集合。客户端不需要关注数据的子集到底存储在哪个节点,只需要关注这个集合整体。

以3主3从为例,节点之间两两交互,共享数据分片、节点状态等信息。

image-20210827001454944.

数据分布

Redis既没有用哈希取模,也没有用一致性哈希,而是用虚拟槽来实现的。

Redis创建了16384个槽(slot),每个节点负责一定区间的slot。比如Node1负责0-5460,Node2负责5461-10922,Node3负责10923-16383。

image-20210827001633046

对象分布到Redis节点上时,对key用CRC16算法计算再%16384,得到一个slot的值,数据落到负责这个slot的Redis节点上。

Redis的每个master节点都会维护自己负责的slot。

用一个bit序列实现,比如: 序列的第0位是1,就代表第一个slot是它负责;序列的第1位是0,代表第二个slot不归它负责。

注意:key与slot的关系是永远不会变的,会变的只有slot和 Redis节点的关系。

怎么让相关的数据落到同一个节点上?

比如有些multi key操作是不能跨节点的,例如用户2673的基本信息和金融信息?在key里面加入{hash tag}即可。

Redis在计算槽编号的时候只会获取{}之间的字符串进行槽编号计算,这样由于上面两个不同的键,{}里面的字符串是相同的,因此他们可以被计算出相同的槽。

客户端重定向

问题:客户端连接到哪一台服务器?访问的数据不在当前节点上,怎么办?

比如在7291端口的Redis的redis-cli客户端操作:

1
2
127.0.0.1:7291>set qs 1
(error) MOVED 13724 127.0.0.1:7293

服务端返回MOVED,也就是根据key计算出来的slot不归7291端口管理,而是归7293端口管理,服务端返回MOVED告诉客户端去7293端口操作。

这个时候更换端口,用redis-cli -p 7293操作,才会返回OK。或者用./redis-cli -c -p port的命令。

这样客户端需要连接两次,Jedis等客户端会在本地维护一份slot一node的映射关系,大部分时候不需要重定向,所以叫做smartjedis(需要客户端支持)。

数据迁移

因为key和slot的关系是永远不会变的,当新增了节点的时候,需要把原有的slot分配给新的节点负责,并且把相关的数据迁移过来。

高可用和主从切换原理

只有主节点可以写,一个主节点挂了,从节点怎么变成主节点?

当slave发现自己的master变为FAIL状态时,便尝试进行Failover,以期成为新的master。由于挂掉的master可能多个slave,存在多个slave竞争成为mastel的过程,其过程如下:

1、slave发现自己的master变为FAIL

2、将自己记录的集群currentEpoch 加1,并广播FAILOVER_AUTH_REQUEST信息

3、其他节点收到该信息,只有master响应,判断请求者的合法性,并发送FAILOVER_AUTH_ACK,对每一个epoch只发送一次ack

4、尝试failover的slave收集FAILOVER_AUTH_ACK

5、超过半数后变成新Master

6、广播Pong通知其他集群节点。

总结:Redis Cluster既能够实现主从的角色分配,又能够实现主从切换,相当于集成了Replication和Sentinel的功能。

总结

Redis Cluster特点:

1、无中心架构。

2、数据按照slot存储分布在多个节点,节点间数据共享,可动态调整数据分布。

3、可扩展性,可线性扩展到1000个节点(官方推荐不超过1000个),节点可动态添加或删除。

4、高可用性,部分节点不可用时,集群仍可用。通过增加Slave做standby数据副本,能够实现故障自动failover,节点之间通过gossip协议交换状态信息,用投票机制完成Slave到Master的角色提升。

5、降低运维成本,提高系统的扩展性和可用性。

打赏

请我喝杯咖啡吧~

支付宝
微信