RedisCluster集群架构原理与通信原理
生产级Redis 高并发分布式锁实战1:高并发分布式锁如何实现 https://www.cnblogs.com/yizhiamumu/p/16556153.html
生产级Redis 高并发分布式锁实战2:缓存架构设计问题优化 https://www.cnblogs.com/yizhiamumu/p/16556667.html
总结篇3:redis 典型缓存架构设计问题及性能优化 https://www.cnblogs.com/yizhiamumu/p/16557996.html
总结篇4:redis 核心数据存储结构及核心业务模型实现应用场景 https://www.cnblogs.com/yizhiamumu/p/16566540.html
DB\redis\zookeeper分布式锁设计 https://www.cnblogs.com/yizhiamumu/p/16663243.html
在缓存和数据库双写场景下,一致性是如何保证的 https://www.cnblogs.com/yizhiamumu/p/16686751.html
如何保证 Redis 的高并发和高可用?讨论redis的单点,高可用,集群 https://www.cnblogs.com/yizhiamumu/p/16586968.html
分布式缓存应用场景与redis持久化机制 https://www.cnblogs.com/yizhiamumu/p/16702154.html
redis zset 使用场景 https://www.cnblogs.com/yizhiamumu/p/16736456.html
Redisson 源码分析及实际应用场景介绍 https://www.cnblogs.com/yizhiamumu/p/16706048.html
Redis 高可用方案原理初探 https://www.cnblogs.com/yizhiamumu/p/16709290.html
RedisCluster集群架构原理与通信原理 https://www.cnblogs.com/yizhiamumu/p/16704556.html
redis 基准性能测试与变慢优化 https://www.cnblogs.com/yizhiamumu/p/16712463.html
Redis过期策略以及Redis的内存淘汰机制 https://www.cnblogs.com/yizhiamumu/p/16725009.html
Raft协议 https://www.cnblogs.com/yizhiamumu/p/16737578.html
接上文我们了解到,redis 持久化仍存在的问题:
-
一旦主节点宕机,从节点晋升成主节点,同时需要修改应用方的主节点地址,还需要命令所有从节点去复制新的主节点,整个过程需要人工干预。[哨兵已解决]
-
节点的写能力受到单机的限制。[集群已解决]
-
节点的存储能力受到单机的限制。[集群已解决]
redis主从,解决了redis单点问题,但是没有实现redis状态监控及故障自动切换。
后来引入了sentinel(哨兵)解决此问题,但是依然没能解决数据的一个并发读写的问题。
那么Redis 集群就是来解决此问题的,它是一个提供在多个Redis节点间共享数据的程序集。Redis 集群通过分区来提供一定程度的可用性,在实际环境中当某个节点宕机或者不可达的情况下可以继续处理命令。
为方便查阅,我们做些记录,对redis 4.0 和redis 5.0 的相关特性做下说明。
Redis 4.0 新功能说明
Redis4.0版本增加了很多新的特性,如:
1 Redis Memeory Command:详细分析内存使用情况,内存使用诊断,内存碎片回收; 2 PSYNC2:解决failover和从实例重启不能部分同步; 3 LazyFree: 再也不用怕big key的删除引起集群故障切换; 4 LFU: 支持近似的LFU内存淘汰算法; 5 Active Memory Defragmentation:内存碎片回收效果很好(实验阶段); 6 Modules: Redis成为更多的可能(觉得像mongo/mysql引入engine的阶段);
Redis 5.0 新功能说明
Redis5.0版是Redis产品的重大版本发布,它的最新特点:
1 新的流数据类型(Stream data type) https://redis.io/topics/streams-intro 2 新的 Redis 模块 API:定时器、集群和字典 API(Timers, Cluster and Dictionary APIs) 3 RDB 增加 LFU 和 LRU 信息 4 集群管理器从 Ruby (redis-trib.rb) 移植到了redis-cli 中的 C 语言代码 5 新的有序集合(sorted set)命令:ZPOPMIN/MAX 和阻塞变体(blocking variants) 6 升级 Active defragmentation 至 v2 版本 7 增强 HyperLogLog 的实现 8 更好的内存统计报告 9 许多包含子命令的命令现在都有一个 HELP 子命令 10 客户端频繁连接和断开连接时,性能表现更好 11 许多错误修复和其他方面的改进 12 升级 Jemalloc 至 5.1 版本 13 引入 CLIENT UNBLOCK 和 CLIENT ID 14 新增 LOLWUT 命令 http://antirez.com/news/123 15 在不存在需要保持向后兼容性的地方,弃用 "slave" 术语 16 网络层中的差异优化 17 Lua 相关的改进 18 引入动态的 HZ(Dynamic HZ) 以平衡空闲 CPU 使用率和响应性 19 对 Redis 核心代码进行了重构并在许多方面进行了改进
前言:Redis集群产生的背景?
-
Redis单实例的架构,从最开始的一主N从,到读写分离,再到Sentinel哨兵机制,单实例的Redis缓存足以应对大多数的使用场景,也能实现主从故障迁移。
但是,在某些场景下,单实例存Redis缓存会存在的几个问题,也就是Redis主从架构+Sentinel仍存在问题:
写并发的压力
- Redis单实例读写分离可以解决读操作的负载均衡,但对于写操作,仍然是全部落在了master节点上面,在海量数据高并发场景,一个节点写数据容易出现瓶颈,造成master节点的压力上升。
海量数据的存储压力
-
(内存容量的限制)Redis的最大缺点和局限性就在于内存存储数据,这样子对容量而言会有相当大的限制。
-
(持久化和硬盘的限制)Redis单实例本质上只有一台Master作为存储,如果面对海量数据的存储,一台Redis的服务器就应付不过来了,而且数据量太大意味着持久化成本高,严重时可能会阻塞服务器,造成服务请求成功率下降,降低服务的稳定性。
Redis集群提供了较为完善的方案,解决了存储能力受到单机限制,写并发操作无法负载均衡的问题。
一:什么是Redis集群?
Redis Cluster是一个高性能高可用的分布式系统。
由多个Redis实例组成的整体,数据按照Slot存储分布在多个Redis实例上,通过Gossip协议来进行节点之间通信。功能特点如下:
1 所有的节点相互连接 2 集群消息通信通过集群总线通信,集群总线端口大小为客户端服务端口+10000(固定值) 3 节点与节点之间通过二进制协议进行通信 4 客户端和集群节点之间通信和通常一样,通过文本协议进行 5 集群节点不会代理查询 6 数据按照Slot存储分布在多个Redis实例上 7 集群节点挂掉会自动故障转移 8 可以相对平滑扩/缩容节点
1.1【Redis集群】是一种服务器Sharding(分片)技术,3.0版本开始正式提供。
Redis的哨兵模式基本已经可以实现高可用,读写分离 ,但是在这种模式下每台Redis服务器都存储相同的数据,很浪费内存。
所以在redis3.0上加入了Cluster集群模式,实现了Redis的分布式存储,也就是说每台 Redis 节点上存储不同的内容。
-
(分片存储)Redis3.0加入了 Redis 的集群模式,实现了数据的分布式存储,对数据进行分片,将不同的数据存储在不同的master节点上面,从而解决了海量数据的存储问题。
-
(指令转换)Redis集群采用去中心化的思想,没有中心节点的说法,对于客户端来说,整个集群可以看成一个整体,可以连接任意一个节点进行操作,就像操作单一Redis实例一样,不需要任何代理中间件,当客户端操作的key没有分配到该node上时,Redis会返回转向指令,指向正确的Redis节点。
-
(主从和哨兵)Redis也内置了高可用机制,支持N个master节点,每个master节点都可以挂载多个slave节点,当master节点挂掉时,集群会提升它的某个slave节点作为新的master节点。
如上图所示,Redis集群可以看成多个主从架构组合起来的,每一个主从架构可以看成一个节点(只有master节点具有处理请求的能力,slave节点主要是用于节点的高可用)
1.2 集群的设计目标
Redis Cluster 集群模式通常具有 高可用、可扩展性、分布式、容错 等特性。
在官方文档Cluster Spec中,作者详细介绍了Redis集群为什么要设计成现在的样子。
其中最核心的目标有三个:
1 性能:增加集群功能后不能对性能产生太大影响,所以Redis采取了P2P而非Proxy方式、异步复制、客户端重定向等设计。 2 水平扩展:文档中称可以线性扩展到1000结点。 3 可用性:在Cluster推出之前,可用性要靠Sentinel保证。有了集群之后也自动具有了Sentinel的监控和自动Failover能力。
如果需要全面的了解,那一定要看官方文档Cluster Tutorial。
二、通信
2.1 CLUSTER MEET
需要组建一个真正的可工作的集群,我们必须将各个独立的节点连接起来,构成一个包含多个节点的集群。连接各个节点的工作使用CLUSTER MEET命令来完成。
CLUSTER MEET <ip> <port>
CLUSTER MEET命令实现:
1 节点A会为节点B创建一个 clusterNode 结构,并将该结构添加到自己的 clusterState.nodes 字典里面。 2 节点A根据CLUSTER MEET命令给定的IP地址和端口号,向节点B发送一条MEET消息。 3 节点B接收到节点A发送的MEET消息,节点B会为节点A创建一个clusterNode结构,并将该结构添加到自己的clusterState.nodes字典里面。 4 节点B向节点A返回一条PONG消息。 5 节点A将受到节点B返回的PONG消息,通过这条PONG消息节点A可以知道节点B已经成功的接收了自己发送的MEET消息。 6 节点A将向节点B返回一条PING消息。 7 节点B将接收到的节点A返回的PING消息,通过这条PING消息节点B可以知道节点A已经成功的接收到了自己返回的PONG消息,握手完成。 8 节点A会将节点B的信息通过Gossip协议传播给集群中的其他节点,让其他节点也与节点B进行握手,最终,经过一段时间后,节点B会被集群中的所有节点认识。
2.2 消息处理 clusterProcessPacket
1 更新接收消息计数器 2 查找发送者节点并且不是handshake节点 3 更新自己的epoch和slave的offset信息 4 处理MEET消息,使加入集群 5 从goosip中发现未知节点,发起handshake 6 对PING,MEET回复PONG 7 根据收到的心跳信息更新自己clusterState中的master-slave,slots信息 8 对FAILOVER_AUTH_REQUEST消息,检查并投票 9 处理FAIL,FAILOVER_AUTH_ACK,UPDATE信息
2.3 定时任务clusterCron
1 对handshake节点建立Link,发送Ping或Meet 2 向随机节点发送Ping 3 如果是从查看是否需要做Failover 4 统计并决定是否进行slave的迁移,来平衡不同master的slave数 5 判断所有pfail报告数是否过半数
2.4 心跳数据
集群中的节点会不停(每几秒)的互相交换ping、pong包,ping和pong包具有相同的结构,只是类型不同,ping、pong包合在一起叫做心跳包。通常节点会发送ping包并接收接收者返回的pong包,不过这也不是绝对,节点也有可能只发送pong包,而不需要让接收者发送返回包。
节点间通过ping保持心跳以及进行gossip集群状态同步,每次心跳时,节点会带上多个clusterMsgDataGossip消息体,经过多次心跳,该节点包含的其他节点信息将同步到其他节点。
ping和pong包的内容可以分为header和gossip消息两部分:
- 发送消息头信息Header
- 所负责slots的信息
- 主从信息
- ip, port信息
- 状态信息
包含的信息:
1 NODE ID是一个160bit的伪随机字符串,它是节点在集群中的唯一标识 2 currentEpoch和configEpoch字段 3 node flag,标识节点是master还是slave,另外还有一些其他的标识位,如PFAIL和FAIL。 4 节点提供服务的hash slot的bitmap 5 发送者的TCP端口 6 发送者认为的集群状态(down or ok) 7 如果是slave,则包含master的NODE ID
- 发送其他节点Gossip信息。包含了该节点认为的其他节点的状态,不过不是集群的全部节点(随机)
- ping_sent, pong_received
- ip, port信息
- 状态信息,比如发送者认为该节点已经不可达,会在状态信息中标记其为PFAIL或FAIL
包含的信息:
1 NODE ID 2 节点的IP和端口 3 NODE flags
clusterMsg结构的currentEpoch、sender、myslots等属性记录了发送者自身的节点信息,接收者会根据这些信息,在自己的clusterState.nodes字典里找到发送者对应的结构,并对结构进行更新。
Redis集群中的各个节点通过ping来心跳,通过Gossip协议来交换各自关于不同节点的状态信息,其中Gossip协议由MEET、PING、PONG三种消息实现,这三种消息的正文都由两个clusterMsgDataGossip结构组成。
每次发送MEET、PING、PONG消息时,发送者都从自己的已知节点列表中随机选出两个节点(可以是主节点或者从节点),并将这两个被选中节点的信息分别保存到两个结构中。当接收者收到消息时,接收者会访问消息正文中的两个结构,并根据自己是否认识clusterMsgDataGossip结构中记录的被选中节点进行操作:
1 如果被选中节点不存在于接收者的已知节点列表,那么说明接收者是第一次接触到被选中节点,接收者将根据结构中记录的IP地址和端口号等信息,与被选择节点进行握手。 2 如果被选中节点已经存在于接收者的已知节点列表,那么说明接收者之前已经与被选中节点进行过接触,接收者将根据clusterMsgDataGossip结构记录的信息,对被选中节点对应的clusterNode结构进行更新。
2.5 数据结构
clusterNode 结构保存了一个节点的当前信息, 如记录了节点负责处理那些槽、创建时间、节点的名字、节点当前的配置纪元、节点的 IP 和端口等:
1 slots:位图,由当前clusterNode负责的slot为1 2 salve, slaveof:主从关系信息 3 ping_sent, pong_received:心跳包收发时间 4 clusterLink *link:节点间的连接 5 list *fail_reports:收到的节点不可达投票
clusterState 结构记录了在当前节点的集群目前所处的状态还有所有槽的指派信息:
1 myself:指针指向自己的clusterNode 2 currentEpoch:当前节点的最大epoch,可能在心跳包的处理中更新 3 nodes:当前节点记录的所有节点的字典,为clusterNode指针数组 4 slots:slot与clusterNode指针映射关系 5 migrating_slots_to,importing_slots_from:记录slots的迁移信息 6 failover_auth_time,failover_auth_count,failover_auth_sent,failover_auth_rank,failover_auth_epoch:Failover相关信息
clusterLink 结构保存了连接节点的有关信息, 比如套接字描述符, 输入缓冲区和输出缓冲区。
三、数据分布及槽信
3.1 槽(slot)概念
Redis Cluster中有一个16384长度的槽的概念,他们的编号为0、1、2、3……16382、16383。这个槽是一个虚拟的槽,并不是真正存在的。正常工作的时候,Redis Cluster中的每个Master节点都会负责一部分的槽,当有某个key被映射到某个Master负责的槽,那么这个Master负责为这个key提供服务,至于哪个Master节点负责哪个槽,这是可以由用户指定的,也可以在初始化的时候自动生成。在Redis Cluster中,只有Master才拥有槽的所有权,如果是某个Master的slave,这个slave只负责槽的使用,但是没有所有权。
3.2 数据分片
在Redis Cluster中,拥有16384个slot,这个数是固定的,存储在Redis Cluster中的所有的键都会被映射到这些slot中。
数据库中的每个键都属于这 16384 个哈希槽的其中一个,集群使用公式 CRC16(key) % 16384 来计算键 key 属于哪个槽,其中 CRC16(key) 用于计算键 key 的 CRC16 校验和,集群中的每个节点负责处理一部分哈希槽。
3.3 节点的槽指派信息
clusterNode结构的slots属性和numslot属性记录了节点负责处理那些槽:
struct clusterNode { //… unsignedchar slots[16384/8]; };
Slots属性是一个二进制位数组(bitarray),这个数组的长度为16384/8=2048个字节,共包含16384个二进制位。Master节点用bit来标识对于某个槽自己是否拥有。比如对于编号为1的槽,Master只要判断序列的第二位(索引从0开始)是不是为1即可。时间复杂度为O(1)。
3.4 集群所有槽的指派信息
通过将所有槽的指派信息保存在clusterState.slots数组里面,程序要检查槽i是否已经被指派,又或者取得负责处理槽i的节点,只需要访问clusterState.slots[i]的值即可,复杂度仅为O(1)。
3.5 请求重定向
由于每个节点只负责部分slot,以及slot可能从一个节点迁移到另一节点,造成客户端有可能会向错误的节点发起请求。因此需要有一种机制来对其进行发现和修正,这就是请求重定向。有两种不同的重定向场景:
a) MOVED错误
-
请求的key对应的槽不在该节点上,节点将查看自身内部所保存的哈希槽到节点 ID 的映射记录,节点回复一个 MOVED 错误。
-
需要客户端进行再次重试。
b) ASK错误
-
请求的key对应的槽目前的状态属于MIGRATING状态,并且当前节点找不到这个key了,节点回复ASK错误。ASK会把对应槽的IMPORTING节点返回给你,告诉你去IMPORTING的节点查找。
-
客户端进行重试 首先发送ASKING命令,节点将为客户端设置一个一次性的标志(flag),使得客户端可以执行一次针对 IMPORTING 状态的槽的命令请求,然后再发送真正的命令请求。
-
不必更新客户端所记录的槽至节点的映射。
四、数据迁移
当槽x从Node A向Node B迁移时,Node A和Node B都会有这个槽x,Node A上槽x的状态设置为MIGRATING,Node B上槽x的状态被设置为IMPORTING。
MIGRATING状态
-
如果key存在则成功处理
-
如果key不存在,则返回客户端ASK,客户端根据ASK首先发送ASKING命令到目标节点,然后发送请求的命令到目标节点
-
当key包含多个:
-
如果都存在则成功处理
-
如果都不存在,则返回客户端ASK
-
如果一部分存在,则返回客户端TRYAGAIN,通知客户端稍后重试,这样当所有的key都迁移完毕的时候客户端重试请求的时候回得到ASK,然后经过一次重定向就可以获取这批键
-
此时不刷新客户端中node的映射关系
IMPORTING状态
-
如果key不在该节点上,会被MOVED重定向,刷新客户端中node的映射关系
-
如果是ASKING则命令会被执行,key不在迁移的节点已经被迁移到目标的节点
-
Key不存在则新建
Key迁移的命令:
1 DUMP:在源(migrate)上执行 2 RESTORE:在目标(importing)上执行 3 DEL:在源(migrate)上执行
经过上面三步可以将键迁移,然后再将处于MIGRATING和IMPORTING状态的槽变为常态,完成整个重新分片的过程 。
4.1 读写请求
槽里面的key还未迁移,并且槽属于迁移中。
假如槽x在Node A,需要迁移到Node B上,槽x的状态为migrating,其中的key1还没轮到迁移。此时访问key1则先计算key1所在的Slot,存在key1则直接返回。
4.2 MOVED请求
槽里面的key已经迁移过去,并且槽属于迁移完。
假如槽x在Node A,需要迁移到Node B上,迁移完成。此时访问key1则先计算key1所在的Slot,因为已经迁移至Node B上,Node A上不存在,则返回 moved slotid IP:PORT,再根据返回的信息去Node B访问key1,此时更新slot和node的映射。
4.3 ASK请求
槽里面的key已经迁移完,并且槽属于迁移中的状态。
假如槽x在Node A,需要迁移到Node B上,迁移完成,但槽x的状态为migrating。此时访问key1则先计算key1所在的Slot,不存在key1则返回ask slotid IP:PORT,再根据ask返回的信息发送asking请求到Node B,没问题后则最后再去Node B上访问key1,此时不更新slot和node的映射。
五、通信故障
5.1 故障检测
集群中的每个节点都会定期地向集群中的其他节点发送PING消息,以此交换各个节点状态信息,检测各个节点状态:在线状态、疑似下线状态PFAIL、已下线状态FAIL。
当主节点A通过消息得知主节点B认为主节点D进入了疑似下线(PFAIL)状态时,主节点A会在自己的clusterState.nodes字典中找到主节点D所对应的clusterNode结构,并将主节点B的下线报告(failure report)添加到clusterNode结构的fail_reports链表中。
struct clusterNode { //... //记录所有其他节点对该节点的下线报告 list*fail_reports; //... };
如果集群里面,半数以上的主节点都将主节点D报告为疑似下线,那么主节点D将被标记为已下线(FAIL)状态,将主节点D标记为已下线的节点会向集群广播主节点D的FAIL消息,所有收到FAIL消息的节点都会立即更新nodes里面主节点D状态标记为已下线。
将 node 标记为 FAIL 需要满足以下两个条件:
1 有半数以上的主节点将 node 标记为 PFAIL 状态。 2 当前节点也将 node 标记为 PFAIL 状态。
5.2 多个从节点选主
选新主的过程基于Raft协议选举方式来实现的:
1 当从节点发现自己的主节点进行已下线状态时,从节点会广播一条CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST消息,要求所有收到这条消息,并且具有投票权的主节点向这个从节点投票 2 如果一个主节点具有投票权,并且这个主节点尚未投票给其他从节点,那么主节点将向要求投票的从节点返回一条,CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,表示这个主节点支持从节点成为新的主节点 3 每个参与选举的从节点都会接收CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,并根据自己收到了多少条这种消息来统计自己获得了多少主节点的支持 4 如果集群里有N个具有投票权的主节点,那么当一个从节点收集到大于等于集群N/2+1张支持票时,这个从节点就成为新的主节点 5 如果在一个配置纪元没有从能够收集到足够的支持票数,那么集群进入一个新的配置纪元,并再次进行选主,直到选出新的主节点为止
5.3 故障转移
错误检测用于识别集群中的不可达节点是否已下线,如果一个master下线,会将它的slave提升为master,在gossip消息中,NODE flags的值包括两种PFAIL和FAIL。
PFAIL flag:
如果一个节点发现另外一个节点不可达的时间超过NODE_TIMEOUT ,则会将这个节点标记为PFAIL,即Possible failure(可能下线)。节点不可达是说一个节点发送了ping包,但是等待了超过NODE_TIMEOUT时间仍然没有收到回应(NODE_TIMEOUT必须大于一个网络包来回的时间)。
FAIL flag:
PFAIL标志只是一个节点本地的信息,为了使slave提升为master,需要将PFAIL升级为FAIL。PFAIL升级为FAIL需要满足一些条件:
1 A节点将B节点标记为PFAIL 2 A节点通过gossip消息收集其他大部分master节点标识的B节点的状态 3 大部分master节点在NODE_TIMEOUT * FAIL_REPORT_VALIDITY_MULT(2s)时间段内,标识B节点为PFAIL或FAIL
如果满足以上条件,A节点会将B节点标识为FAIL并且向所有节点发送B节点FAIL的消息。收到消息的节点也都会将B标为FAIL。
注意:FAIL状态是单向的,只能从PFAIL升级为FAIL,而不能从FAIL降为PFAIL。
清除FAIL状态:
- 节点重新可达,并且是slave节点
- 节点重新可达,并且是master节点,但是不提供任何slot服务
- 节点重新可达,并且是master节点,但是长时间没有slave被提升为master来顶替它
PFAIL提升到FAIL使用的是一种弱协议:
- 节点收集的状态不在同一时间点,会丢弃时间较早的报告信息,但是也只能保证节点的状态在一段时间内大部分master达成了一致
- 检测到一个FAIL后,需要通知所有节点,但是没有办法保证每个节点都能成功收到消息
当从节点发现自己的主节点变为已下线(FAIL)状态时,便尝试进Failover,成为新的主。
以下是故障转移的执行步骤:
1 在下线主节点的所有从节点中选中一个从节点 2 被选中的从节点执行SLAVEOF NO NOE命令,成为新的主节点 3 新的主节点会撤销所有对已下线主节点的槽指派,并将这些槽全部指派给自己 4 新的主节点对集群进行广播PONG消息,告知其他节点已经成为新的主节点 5 新的主节点开始接收和处理槽相关的请求
六:集群的方案:
如果要实现 Redis 数据的分片,我们有三种方案。
-
(客户端负载)第一种是在客户端实现相关的逻辑,例如用取模或者一致性哈希对key进行分片,查询和修改都先判断key的路由。
- Jedis客户端提供了Redis Sharding的方案,并且支持连接池。
- Sharded 分片的原理?怎么连接到某一个Redis服务:提供了一致性hash和md5散列两种hash算法,默认使用一致性hash算法。
- 并且为了使得请求能均匀的落在不同的节点上,Sharded Jedis会使用节点的名称(如果节点没有名称使用默认名称)虚拟化出160个虚拟节点。也可以根据不同节点的weight,虚拟化出160,weight个节点。
- 当客户端访问redis时,首先根据key计算出其落在哪个节点上,然后找到节点的ip和端口进行连接访问。
- (中间代理负载)第二种是把做分片处理的逻辑抽取出来,运行一个独立的代理服务,客户端连接到这个代理服务,代理服务做请求的转发。
典型的代理分区方案有 Twitter 开源的 Twemproxy 和国内的豌豆荚开源的 Codis
- (服务端负载)第三种就是基于服务端实现。
1 客户端分区方案
客户端 就已经决定数据会被 存储 到哪个 redis 节点或者从哪个 redis 节点 读取数据。其主要思想是采用 哈希算法 将 Redis 数据的 key 进行散列,通过 hash 函数,特定的 key会 映射 到特定的 Redis 节点上。
客户端分区方案 的代表为 Redis Sharding,Redis Sharding 是 Redis Cluster 出来之前,业界普遍使用的 Redis 多实例集群 方法。Java 的 Redis 客户端驱动库 Jedis,支持 Redis Sharding 功能,即 ShardedJedis 以及 结合缓存池 的 ShardedJedisPool。
优点
不使用 第三方中间件,分区逻辑 可控,配置 简单,节点之间无关联,容易 线性扩展,灵活性强。
缺点
客户端 无法 动态增删 服务节点,客户端需要自行维护 分发逻辑,客户端之间 无连接共享,会造成 连接浪费。
2 代理分区方案
客户端 发送请求到一个 代理组件,代理 解析 客户端 的数据,并将请求转发至正确的节点,最后将结果回复给客户端。
优点:简化 客户端 的分布式逻辑,客户端 透明接入,切换成本低,代理的 转发 和 存储 分离。
缺点:多了一层 代理层,加重了 架构部署复杂度 和 性能损耗。
代理分区 主流实现的有方案有 Twemproxy 和 Codis。
2.1. Twemproxy
Twemproxy 也叫 nutcraker,是 twitter 开源的一个 redis 和 memcache 的 中间代理服务器 程序。Twemproxy 作为 代理,可接受来自多个程序的访问,按照 路由规则,转发给后台的各个 Redis 服务器,再原路返回。Twemproxy 存在 单点故障 问题,需要结合 Lvs 和 Keepalived 做 高可用方案。
优点:应用范围广,稳定性较高,中间代理层 高可用。
缺点:无法平滑地 水平扩容/缩容,无 可视化管理界面,运维不友好,出现故障,不能 自动转移。
2.2. Codis
Codis 是一个 分布式 Redis 解决方案,对于上层应用来说,连接 Codis-Proxy 和直接连接 原生的 Redis-Server 没有的区别。Codis 底层会 处理请求的转发,不停机的进行 数据迁移 等工作。Codis 采用了无状态的 代理层,对于 客户端 来说,一切都是透明的。
优点
实现了上层 Proxy 和底层 Redis 的 高可用,数据分片 和 自动平衡,提供 命令行接口 和 RESTful API,提供 监控 和 管理 界面,可以动态 添加 和 删除 Redis 节点。
缺点
部署架构 和 配置 复杂,不支持 跨机房 和 多租户,不支持 鉴权管理。
3 查询路由方案
客户端随机地 请求任意一个 Redis 实例,然后由 Redis 将请求 转发 给 正确 的 Redis 节点。Redis Cluster 实现了一种 混合形式 的 查询路由,但并不是 直接 将请求从一个 Redis 节点 转发 到另一个 Redis 节点,而是在 客户端 的帮助下直接 重定向( redirected)到正确的 Redis 节点。
优点
无中心节点,数据按照 槽 存储分布在多个 Redis 实例上,可以平滑的进行节点 扩容/缩容,支持 高可用 和 自动故障转移,运维成本低。
缺点
严重依赖 Redis-trib 工具,缺乏 监控管理,需要依赖 Smart Client (维护连接,缓存路由表,MultiOp 和 Pipeline 支持)。Failover 节点的 检测过慢,不如 中心节点 ZooKeeper 及时。Gossip 消息具有一定开销。无法根据统计区分 冷热数据。
七:集群的数据分片
前面讲到,Redis集群通过分布式存储的方式解决了单节点的海量数据存储的问题,对于分布式存储,需要考虑的重点就是如何将数据进行拆分到不同的Redis服务器上。常见的分区算法有hash算法、一致性hash算法
普通hash算法
-
如果是希望数据分布相对均匀的话,可以考虑哈希后取模。
-
将key使用hash算法计算之后:hash(key)%N,根据余数,决定映射到那一个节点。
-
优点就是比较简单,属于静态的分片规则。但是一旦节点数量变化,新增或者减少,由于取模的 N 发生变化, 数据需要重新分布和迁移。
一致性hash算法
-
把所有的哈希值空间组织成一个虚拟的圆环(哈希环),整个空间按顺时针方向组织。因为是环形空间,0 和 2^32-1 是重叠的(总共2^32个)。
-
先根据机器的名称或者 IP计算哈希值。
-
然后分布到哈希环中。查找时先根据key计算哈希值,得到哈希环中的位置。
-
最后顺时针找到第一个大于等于该哈希值的第一个Node,就是数据存储的节点。
- 优点是在加入和删除节点时只影响相邻的两个节点。
- 缺点是加减节点会造成部分数据无法命中
- 此外,针对于hash节点分散不均匀或者倾倒状态,采用一个节点分为多个虚拟节点做优化
-
-
所以一般用于缓存,而且用于节点量大的情况下,扩容一般增加一倍节点保障数据负载均衡。
哈希槽Slot算法
Redis 集群既没有用哈希取模,也没有用一致性哈希,而是用Hash槽来实现的。Redis集群创建了 16384 个槽(slot),每个节点负责一定区间的slot。
Redis集群的哈希槽的分区
-
Redis集群中有16384(2^14)个哈希槽(槽的范围是 0 -16383,哈希槽),将不同的哈希槽分布在不同的Redis节点上面进行管理,也就是说每个Redis节点只负责一部分区间的哈希槽。
-
对数据进行操作的时候:
- 集群会对使用CRC16算法对key进行计算并对16384取模 (slot = CRC16(key)%16384) 。】
- 得到的结果就是Key-Value所放入的槽,通过这个槽值,去找对应的槽所对应的Redis节点。
- 然后直接到这个对应的节点上进行存取操作。
- 好处:(自动更新hash槽的映射数据关系)使用哈希槽的好处就在于可以方便的添加或者移除节点,并且无论是添加删除或者修改某一个节点,都不会造成集群不可用的状态。
- 当需要增加节点时,只需要把其他节点的某些哈希槽挪到新节点就可以了;
- 当需要移除节点时,只需要把移除节点上的哈希槽挪到其他节点就行了;
哈希槽分区的分析
集群的每个节点负责一部分hash槽,举个例子,比如当前集群有3个节点,那么:
-
节点 A 包含 0 到 5460 号哈希槽
-
节点 B 包含 5461 到 10922 号哈希槽
-
节点 C 包含 10923 到 16383 号哈希槽
这种结构很容易添加或者删除节点。
-
(1)如果我想新添加个节点 D , 我需要从节点 A, B, C 中得部分槽到 D 上。
-
(2)如果我想移除节点 A ,需要将 A 中的槽移到 B 和 C 节点上,然后将没有任何槽的 A 节点从集群中移除即可。
由于从一个节点将哈希槽移动到另一个节点并不会停止服务,所以无论添加删除或者改变某个节点的哈希槽的数量都不会造成集群不可用的状态。
查看 key 属于哪个 slot:
redis> cluster keyslot yan
注意:key 与 slot 的关系是永远不会变的,会变的只有 slot 和 Redis 节点的关系
(tips) 怎么让相关的数据落到同一个节点上
在key里面加入{hash tag}即可。Redis在计算槽编号的时候只会获取{}之间的字符串进行槽编号计算,这样由于上面两个不同的键,{}里面的字符串是相同的,因此他们可以被计算出相同的槽
客户端重定向
客户端连接到哪一台服务器?访问的数据不在当前节点上,怎么办?
比如在 7291 端口的 Redis 的 redis-cli 客户端操作:
127.0.0.1:7291> set qs 1
(error) MOVED 13724 127.0.0.1:7293
-
服务端返回 MOVED,也就是根据 key 计算出来的 slot 不归 7191 端口管理,而是 归 7293 端口管理,服务端返回 MOVED 告诉客户端去 7293 端口操作。
-
这个时候更换端口,用 redis-cli –p 7293 操作,才会返回 OK。这样客户端需要连接两次。或者用./redis-cli -c -p port 的命令(c 代表 cluster)自动重定向。
新增或下线了 Master 节点,数据怎么迁移(重新分配)?
-
因为 key 和 slot 的关系是永远不会变的,当新增了节点的时候,需要把原有的 slot 分配给新的节点负责,并且把相关的数据迁移过来。
-
新增的节点没有哈希槽,不能分布数据,在原来的任意一个节点上执行。
redis-cli --cluster reshard 127.0.0.1:7291
- 槽的迁移与指派命令:
CLUSTER ADDSLOTS 0 1 2 3 4 ... 5000
输入需要分配的哈希槽的数量(比如 500),和哈希槽的来源节点(可以输入 all 或 者 id)
集群的主从复制模型
-
为了保证高可用,redis-cluster集群引入了主从复制模型,一个主节点对应一个或者多个从节点,当主节点宕机的时候,就会启用从节点 。当其它主节点 ping 一个主节点 A 时,如果半数以上的主节点与 A 通信超时,那么认为主节点 A 宕机了。如果主节点 A 和它的从节点 A1 都宕机了,那么该集群就无法再提供服务了。
-
默认情况下,redis集群的读和写都是到master上去执行的,不支持slave节点读和写,跟Redis主从复制下读写分离不一样,因为redis集群的核心的理念,主要是使用slave做数据的热备,以及master故障时的主备切换,实现高可用的。
-
Redis的读写分离,是为了横向任意扩展slave节点去支撑更大的读吞吐量。而redis集群架构下,本身master就是可以任意扩展的,如果想要支撑更大的读或写的吞吐量,都可以直接对master进行横向扩展。
集群的特点
-
redis集群内部的节点是相互通信的(PING-PONG机制),每个节点都是一个redis实例;内部使用二进制协议优化传输速度和带宽。
-
为了实现集群的高可用,即判断节点是否健康(能否正常使用),redis-cluster有这么一个投票容错机制:如果集群中超过半数的节点投票认为某个节点挂了,那么这个节点就挂了(fail)。这是判断节点是否挂了的方法
-
Redis集群是没有统一的入口的,客户端与 Redis 节点直连,不需要中间代理层,也不需要连接集群所有节点,即连接集群中任何一个可用节点即可。
-
客户端(client)连接集群的时候连接集群中的任意节点(node)即可。
-
那么如何判断集群是否挂了呢? -> 如果集群中任意一个节点挂了,而且该节点没有从节点(备份节点),那么这个集群就挂了。这是判断集群是否挂了的方法;
集群运行的要求
-
Redis集群至少需要3个节点,因为投票容错机制要求超过半数节点认为某个节点挂了该节点才是挂了,所以2个节点无法构成集群。
-
要保证集群的高可用,需要每个节点都有从节点,也就是备份节点,所以Redis集群至少需要6台服务器。
集群的总结
优势
-
无中心架构。
-
数据按照slot存储分布在多个节点,节点间数据共享,可动态调整数据分布。
- 解耦数据和节点之间的关系,简化了扩容和收缩难度;
- 节点自身维护槽的映射关系,不需要客户端代理服务维护槽分区元数据
- 支持节点、槽、键之间的映射查询,用于数据路由,在线伸缩等场景.
-
可扩展性,可线性扩展到1000个节点(官方推荐不超过 1000 个)节点可动态添加或删除。
-
高可用性,部分节点不可用时,集群仍可用。通过增加Slave做 standby 数据副本,能够实现故障自动failover,节点之间通过gossip协议交换状态信息,用投票机制完成 Slave到Master的角色提升。
-
降低运维成本,提高系统的扩展性和可用性。
不足
-
Client 实现复杂,驱动要求实现 Smart Client,缓存 slots mapping 信息并及时更新,提高了开发难度,客户端的不成熟影响业务的稳定性。
-
节点会因为某些原因发生阻塞(阻塞时间大于 clutser-node-timeout),被判断下线,这种 failover是没有必要的。
-
数据通过异步复制,不保证数据的强一致性
-
多个业务使用同一套集群时,无法根据统计区分冷热数据,资源隔离性较差,容 易出现相互影响的情况。