IT评测·应用市场-qidao123.com

标题: 第76篇 Redis集群详细介绍 [打印本页]

作者: 民工心事    时间: 2024-12-8 22:50
标题: 第76篇 Redis集群详细介绍
媒介

Redis单实例的架构,从最开始的一主N从,到读写分离,再到Sentinel哨兵机制,单实例的Redis缓存足以应对大多数的使用场景,也能实现主从故障迁移。
但是,在某些场景下,单实例存Redis缓存会存在的几个题目:
针对以上的题目,Redis集群提供了较为完善的方案,办理了存储本领受到单机限制,写操纵无法负载均衡的题目。
Redis提供了去中心化集群部署模式,集群内所有Redis节点之间两两连接,而许多的客户端工具会根据key将请求分发到对应的分片下的某一个节点上进行处理。Redis也内置了高可用机制,支持N个master节点,每个master节点都可以挂载多个slave节点,当master节点挂掉时,集群会提升它的某个slave节点作为新的master节点。
默认情况下,redis集群的读和写都是到master上去执行的,不支持slave节点读和写,跟Redis主从复制下读写分离不一样,因为redis集群的焦点的理念,主要是使用slave做数据的热备,以及master故障时的主备切换,实现高可用的。Redis的读写分离,是为了横向任意扩展slave节点去支撑更大的读吞吐量。而redis集群架构下,本身master就是可以任意扩展的,如果想要支撑更大的读或写的吞吐量,都可以直接对master进行横向扩展。
一个典型的Redis集群部署场景如下图所示:

在Redis集群里面,又会划分出分区的概念,一个集群中可有多个分区。分区有几个特点:
按照Cluster模式进行部署的时候,要求最少需要部署6个Redis节点(3个分片,每个分片中1主1从),其中集群中每个分片的master节点负责对外提供读写操纵,slave节点则作为故障转移使用(master出现故障的时候充当新的master)、对外提供只读请求处理。
1 集群数据分布策略

1.1 Redis Sharding(数据分片)

Redis Cluster前,为了办理数据分发到各个分区的题目,普遍采用的是Redis Sharding(数据分片)方案。所谓的Sharding,实在就是一种数据分发的策略。根据key的hash值进行取模,确定最终归属的节点。
优点就是比力简单,但是

如果需要办理这个题目,就需要对原先扩容前已经存储的数据重新进行一次hash计算和取模操纵,将全部的数据重新分发到新的正确节点上进行存储。这个操纵被称为重新Sharding,重新sharding期间服务不可用,可能会对业务造成影响。
1.2 划一性Hash

为了低沉节点的增加或者移除对于整体已有缓存数据访问的影响,最大限度的保证缓存命中率,改良后的划一性Hash算法浮出水面。
1.3 Hash槽

何为Hash槽?Hash槽的原理与HashMap有点相似,Redis集群中有16384个哈希槽(槽的范围是 0 -16383,哈希槽),将不同的哈希槽分布在不同的Redis节点上面进行管理,也就是说每个Redis节点只负责一部分的哈希槽。在对数据进行操纵的时候,集群会对使用CRC16算法对key进行计算并对16384取模(slot = CRC16(key)%16383),得到的结果就是 Key-Value 所放入的槽,通过这个值,去找到对应的槽所对应的Redis节点,然后直接到这个对应的节点上进行存取操纵。

使用哈希槽的利益就在于可以方便的添加或者移除节点,而且无论是添加删除或者修改某一个节点,都不会造成集群不可用的状态。
哈希槽数据分区算法具有以下几种特点:
为什么redis集群采用“hash槽”来办理数据分配题目,而不采用“划一性hash”算法呢?
2 Hash槽原理详解

2.1 clusterNode

保存节点的当前状态,比如节点的创建时间,节点的名字,节点当前的设置纪元,节点的IP和地址,等等。
  1. typedef struct clusterNode {
  2.         //创建节点的时间
  3.         mstime_t ctime;
  4.         //节点的名字,由40个16进制字符组成
  5.         char name[CLUSTER_NAMELEN];
  6.         //节点标识 使用各种不同的标识值记录节点的角色(比如主节点或者从节点)
  7.         char shard_id[CLUSTER_NAMELEN];
  8.         //以及节点目前所处的状态(比如在线或者下线)
  9.         int flags;     
  10.         //节点当前的配置纪元,用于实现故障转移
  11.         uint64_t configEpoch;
  12.         //由这个节点负责处理的槽
  13.         //一共有REDISCLUSTER SLOTS/8个字节长 每个字节的每个位记录了一个槽的保存状态
  14.         // 位的值为 1 表示槽正由本节点处理,值为0则表示槽并非本节点处理
  15.         unsigned char slots[CLUSTER_SLOTS/8];
  16.         uint16_t *slot_info_pairs; /* Slots info represented as (start/end) pair (consecutive index). */
  17.         int slot_info_pairs_count; /* Used number of slots in slot_info_pairs */
  18.         //该节点负责处理的槽数里
  19.         int numslots;
  20.         //如果本节点是主节点,那么用这个属性记录从节点的数里
  21.         int numslaves;  
  22.         //指针数组,指向各个从节点
  23.         struct clusterNode **slaves;
  24.         //如果这是一个从节点,那么指向主节点
  25.         struct clusterNode *slaveof;
  26.         unsigned long long last_in_ping_gossip; /* The number of the last carried in the ping gossip section */
  27.         //最后一次发送 PING 命令的时间
  28.         mstime_t ping_sent;   
  29.         //最后一次接收 PONG 回复的时间戳
  30.         mstime_t pong_received;  
  31.         mstime_t data_received;  
  32.         //最后一次被设置为 FAIL 状态的时间
  33.         mstime_t fail_time;     
  34.         // 最后一次给某个从节点投票的时间
  35.         mstime_t voted_time;     
  36.         //最后一次从这个节点接收到复制偏移里的时间
  37.         mstime_t repl_offset_time;
  38.         mstime_t orphaned_time;     
  39.         //这个节点的复制偏移里
  40.         long long repl_offset;     
  41.         //节点的 IP 地址
  42.         char ip[NET_IP_STR_LEN];   
  43.         sds hostname;               /* The known hostname for this node */
  44.         //节点的端口号
  45.         int port;                  
  46.         int pport;                  /* Latest known clients plaintext port. Only used
  47.                                                                    if the main clients port is for TLS. */
  48.         int cport;                  /* Latest known cluster port of this node. */
  49.         //保存连接节点所需的有关信息
  50.         clusterLink *link;         
  51.         clusterLink *inbound_link;  /* TCP/IP link accepted from this node */
  52.         //链表,记录了所有其他节点对该节点的下线报告
  53.         list *fail_reports;         
  54. } clusterNode;
复制代码
2.2 clusterState

记录当前节点以是为的集群目前所处的状态。
  1. typedef struct clusterState {
  2.         //指向当前节点的指针
  3.         clusterNode *myself;  
  4.         //集群当前的配置纪元,用于实现故障转移
  5.         uint64_t currentEpoch;
  6.         // 集群当前的状态:是在线还是下线
  7.         int state;            
  8.         //集群中至少处理着一个槽的节点的数量。
  9.         int size;      
  10.         //集群节点名单(包括 myself 节点)
  11.         //字典的键为节点的名字,字典的值为 clusterWode 结构
  12.         dict *nodes;         
  13.         dict *shards;         /* Hash table of shard_id -> list (of nodes) structures */
  14.         //节点黑名单,用于CLUSTER FORGET 命令
  15.         //防止被 FORGET 的命令重新被添加到集群里面
  16.         dict *nodes_black_list;
  17.         //记录要从当前节点迁移到目标节点的槽,以及迁移的目标节点
  18.         //migrating_slots_to[i]= NULL 表示槽 i 未被迁移
  19.         //migrating_slots_to[i]= clusterNode_A 表示槽i要从本节点迁移至节点 A
  20.         clusterNode *migrating_slots_to[CLUSTER_SLOTS];
  21.         //记录要从源节点迁移到本节点的槽,以及进行迁移的源节点
  22.         //importing_slots_from[i]= NULL 表示槽 i 未进行导入
  23.         //importing_slots from[i]=clusterNode A 表示正从节点A中导入槽 i
  24.         clusterNode *importing_slots_from[CLUSTER_SLOTS];
  25.         //负责处理各个槽的节点
  26.         //例如 slots[i]=clusterNode_A  表示槽i由节点A处理
  27.         clusterNode *slots[CLUSTER_SLOTS];
  28.         rax *slots_to_channels;
  29.    //以下这些值被用于进行故障转移选举
  30.         //上次执行选举或者下次执行选举的时间
  31.         mstime_t failover_auth_time;
  32.         //节点获得的投票数量
  33.         int failover_auth_count;   
  34.         //如果值为 1,表示本节点已经向其他节点发送了投票请求
  35.         int failover_auth_sent;     
  36.         int failover_auth_rank;     /* This slave rank for current auth request. */
  37.         uint64_t failover_auth_epoch; /* Epoch of the current election. */
  38.         int cant_failover_reason;   /* Why a slave is currently not able to
  39.                                                                    failover. See the CANT_FAILOVER_* macros. */
  40.         /*共用的手动故障转移状态*/
  41.         //手动故障转移执行的时间限制
  42.         mstime_t mf_end;            
  43.         /*主服务器的手动故障转移状态 */
  44.         clusterNode *mf_slave;      
  45.         /*丛服务器的手动故障转移状态 */
  46.         long long mf_master_offset;
  47.         // 指示手动故障转移是否可以开始的标志值 值为非 0 时表示各个主服务器可以开始投票
  48.         int mf_can_start;   
  49.         /*以下这些值由主服务器使用,用于记录选举时的状态*/
  50.         //集群最后一次进行投票的纪元
  51.         uint64_t lastVoteEpoch;  
  52.         //在进入下个事件循环之前要做的事情,以各个 flag 来记录
  53.         int todo_before_sleep;
  54.         /* Stats */
  55.         //通过 cluster 连接发送的消息数量
  56.         long long stats_bus_messages_sent[CLUSTERMSG_TYPE_COUNT];
  57.         //通过cluster 接收到的消息数量
  58.         long long stats_bus_messages_received[CLUSTERMSG_TYPE_COUNT];
  59.         long long stats_pfail_nodes;    /* Number of nodes in PFAIL status,
  60.                                                                            excluding nodes without address. */
  61.         unsigned long long stat_cluster_links_buffer_limit_exceeded;  /* Total number of cluster links freed due to exceeding buffer limit */
  62. } clusterState;
复制代码
2.3 节点的槽指派信息

clusterNode数据结构的slots属性和numslot属性记录了节点负责处理那些槽:
slots属性是一个二进制位数组(bit array),这个数组的长度为16384/8=2048个字节,共包罗16384个二进制位。Master节点用bit来标识对于某个槽本身是否拥有,时间复杂度为O(1)
2.4 集群所有槽的指派信息

当收到集群中其他节点发送的信息时,通过将节点槽的指派信息保存在当地的clusterState.slots数组里面,程序要检查槽i是否已经被指派,又或者取得负责处理槽i的节点,只需要访问clusterState.slots的值即可,时间复杂度仅为O(1)

如上图所示,ClusterState 中保存的 Slots 数组中每个下标对应一个槽,每个槽信息中对应一个 clusterNode 也就是缓存的节点。这些节点会对应一个实际存在的 Redis 缓存服务,包罗 IP 和 Port 的信息。Redis Cluster 的通讯机制实际上保证了每个节点都有其他节点和槽数据的对应关系。无论Redis 的客户端访问集群中的哪个节点都可以路由到对应的节点上,因为每个节点都有一份 ClusterState,它记录了所有槽和节点的对应关系。
3 集群的请求重定向

Redis集群在客户端层面是没有采用署理的,而且无论Redis 的客户端访问集群中的哪个节点都可以路由到对应的节点上,下面来看看 Redis 客户端是如何通过路由来调用缓存节点的:
3.1 MOVED请求


如上图所示,Redis 客户端通过 CRC16(key)%16383 计算出 Slot 的值,发现需要找“缓存节点1”进行数据操纵,但是由于缓存数据迁移或者其他缘故原由导致这个对应的 Slot 的数据被迁移到了“缓存节点2”上面。那么这个时候 Redis 客户端就无法从“缓存节点1”中获取数据了。但是由于“缓存节点1”中保存了所有集群中缓存节点的信息,因此它知道这个 Slot 的数据在“缓存节点2”中保存,因此向 Redis 客户端发送了一个 MOVED 的重定向请求。这个请求告诉其应该访问的“缓存节点2”的地址。Redis 客户端拿到这个地址,继续访问“缓存节点2”而且拿到数据。
3.2 ASK请求

上面的例子说明确,数据 Slot 从“缓存节点1”已经迁移到“缓存节点2”了,那么客户端可以直接找“缓存节点2”要数据。那么如果两个缓存节点正在做节点的数据迁移,此时客户端请求会如何处理呢?

Redis 客户端向“缓存节点1”发出请求,此时“缓存节点1”正向“缓存节点 2”迁移数据,如果没有命中对应的 Slot,它会返回客户端一个 ASK 重定向请求而且告诉“缓存节点2”的地址。客户端向“缓存节点2”发送 Asking 命令,询问需要的数据是否在“缓存节点2”上,“缓存节点2”接到消息以后返回数据是否存在的结果。
3.3 频繁重定向造成的网络开销的处理:smart客户端

什么是 smart客户端:
在大部分情况下,可能都会出现一次请求重定向才气找到正确的节点,这个重定向过程显然会增加集群的网络负担和单次请求耗时。以是大部分的客户端都是smart的。所谓 smart客户端,就是指客户端当地维护一份hashslot => node的映射表缓存,大部分情况下,直接走当地缓存就可以找到hashslot => node,不需要通过节点进行moved重定向,
JedisCluster的工作原理:
hashslot迁移和ask重定向:
如果hashslot正在迁移,那么会返回ask重定向给客户端。客户端接收到ask重定向之后,会重新定位到目标节点去执行,但是因为ask发生在hashslot迁移过程中,以是JedisCluster API收到ask是不会更新hashslot当地缓存。
虽然ASK与MOVED都是对客户端的重定向控制,但是有本质区别。ASK重定向说明集群正在进行slot数据迁移,客户端无法知道迁移什么时候完成,因此只能是临时性的重定向,客户端不会更新slots缓存。但是MOVED重定向说明键对应的槽已经明确指定到新的节点,客户端需要更新slots缓存。
4 Redis集群中节点的通讯机制:goosip协议

redis集群的哈希槽算法办理的是数据的存取题目,不同的哈希槽位于不同的节点上,而不同的节点维护着一份它以是为的当前集群的状态,同时,Redis集群是去中心化的架构。那么,当集群的状态发生变革时,比如新节点参加、slot迁移、节点宕机、slave提升为新Master等等,我们盼望这些变革尽快被其他节点发现,Redis是如何进行处理的呢?也就是说,Redis不同节点之间是如何进行通讯进行维护集群的同步状态呢?
在Redis集群中,不同的节点之间采用gossip协议进行通讯,节点之间通讯的目的是为了维护节点之间的元数据信息。这些元数据就是每个节点包罗哪些数据,是否出现故障,通过gossip协议,达到最终数据的划一性。
  1. gossip协议,是基于流行病传播方式的节点或者进程之间信息交换的协议。原理就是在不同的节点间不断地通信交换信息,一段时间后,所有的节点就都有了整个集群的完整信息,并且所有节点的状态都会达成一致。每个节点可能知道所有其他节点,也可能仅知道几个邻居节点,但只要这些节可以通过网络连通,最终他们的状态就会是一致的。Gossip协议最大的好处在于,即使集群节点的数量增加,每个节点的负载也不会增加很多,几乎是恒定的。
复制代码
Redis集群中节点的通讯过程如下:
使用gossip协议的优点在于将元数据的更新分散在不同的节点上面,低沉了压力;但是缺点就是元数据的更新有延时,可能导致集群中的一些操纵会有一些滞后。别的,由于 gossip 协议对服务器时间的要求较高,时间戳不正确会影响节点判断消息的有用性。而且节点数量增多后的网络开销也会对服务器产生压力,同时结点数太多,意味着达到最终划一性的时间也相对变长,因此官方推荐最大节点数为1000左右。
redis cluster架构下的每个redis都要开放两个端口号,比如一个是6379,另一个就是加1w的端口号16379。
5 集群的扩容与收缩

作为分布式部署的缓存节点总会遇到缓存扩容和缓存故障的题目。这就会导致缓存节点的上线和下线的题目。由于每个节点中保存着槽数据,因此当缓存节点数出现变动时,这些槽数据会根据对应的虚拟槽算法被迁移到其他的缓存节点上。以是对于redis集群,集群伸缩主要在于槽和数据在节点之间移动。
5.1 扩容


如上图所示,集群中本来存在“缓存节点1”和“缓存节点2”,此时“缓存节点3”上线了而且参加到集群中。此时根据虚拟槽的算法,“缓存节点1”和“缓存节点2”中对应槽的数据会应该新节点的参加被迁移到“缓存节点3”上面。
新节点参加到集群的时候,作为孤儿节点是没有和其他节点进行通讯的。因此需要在集群中任意节点执行 cluster meet 命令让新节点参加进来。假设新节点是 192.168.1.1 5002,老节点是 192.168.1.1 5003,那么运行以下命令将新节点参加到集群中。
  1. 192.168.1.1 5003> cluster meet 192.168.1.1 5002
复制代码
这个是由老节点发起的,有点老成员欢迎新成员参加的意思。新节点刚刚创建没有创建槽对应的数据,也就是说没有缓存任何数据。如果这个节点是主节点,需要对其进行槽数据的扩容;如果这个节点是从节点,就需要同步主节点上的数据。总之就是要同步数据。

如上图所示,由客户端发起节点之间的槽数据迁移,数据从源节点往目标节点迁移。
5.2 收缩


为了安全删除节点,Redis集群只能下线没有负责槽的节点。因此如果要下线有负责槽的master节点,则需要先将它负责的槽迁移到其他节点。迁移的过程也与上线操纵类似,不同的是下线的时候需要关照全网的其他节点忘记本身,此时通过命令 **cluster forget {downNodeId} **关照其他的节点。
6 集群的故障检测与故障转恢复机制:

6.1 集群的故障检测

Redis集群的故障检测是基于gossip协议的,集群中的每个节点都会定期地向集群中的其他节点发送PING消息,以此互换各个节点状态信息,检测各个节点状态:在线状态、疑似下线状态PFAIL、已下线状态FAIL。
6.2 主观下线(pfail)

当节点A检测到与节点B的通讯时间凌驾了cluster-node-timeout 的时候,就会更新当地节点状态,把节点B更新为主观下线。
主观下线并不能代表某个节点真的下线了,有可能是节点A与节点B之间的网络断开了,但是其他的节点依旧可以和节点B进行通讯。
6.3 客观下线

由于集群内的节点会不断地与其他节点进行通讯,下线信息也会通过 Gossip 消息传遍所有节点,因此集群内的节点会不断收到下线报告。
当半数以上的主节点标记了节点B是主观下线时,便会触发客观下线的流程(该流程只针对主节点,如果是从节点就会忽略)。将主观下线的报告保存到当地的 ClusterNode 的结构fail_reports链表中,而且对主观下线报告的时效性进行检查,如果凌驾 cluster-node-timeout*2 的时间,就忽略这个报告,否则就记录报告内容,将其标记为客观下线。
接着向集群广播一条主节点B的Fail 消息,所有收到消息的节点都会标记节点B为客观下线。
6.4 集群地故障恢复

当故障节点下线后,如果是持有槽的主节点则需要在其从节点中找出一个替换它,从而保证高可用。此时下线主节点的所有从节点都担负着恢复义务,这些从节点会定时监测主节点是否进入客观下线状态,如果是,则触发故障恢复流程。故障恢复也就是选举一个节点充当新的master,选举的过程是基于Raft协议选举方式来实现的。

7 Redis集群的运维

7.1 数据迁移题目

Redis集群可以进行节点的动态扩容缩容,这一过程目前还处于半自动状态,需要人工介入。在扩缩容的时候,需要进行数据迁移。而 Redis为了保证迁移的划一性,迁移所有操纵都是同步操纵,执行迁移时,两端的 Redis均会进入时长不等的阻塞状态,对于小Key,该时间可以忽略不计,但如果一旦Key的内存使用过大,严峻的时候会打仗发集群内的故障转移,造成不须要的切换。
7.2 带宽消耗题目

Redis集群是无中心节点的集群架构,依赖Gossip协议协同自动化修复集群的状态,但goosip有消息延时和消息冗余的题目,在集群节点数量过多的时候,goosip协议通讯会消耗大量的带宽,主要体现在以下几个方面:
也就是说,每个节点的slot不能有太多,否则集群节点之间互相通讯时,redis会有大量的时间和带宽在完成通讯
集群带宽消耗主要分为:读写命令消耗+Gossip消息消耗,因此搭建Redis集群需要根据业务数据规模和消息通讯成本做出合理规划:
7.3 Pub/Sub广播题目

集群模式下内部对所有publish命令都会向所有节点进行广播,加重带宽负担,以是集群应该避免频繁使用Pub/sub功能
7.4 集群倾斜

集群倾斜是指不同节点之间数据量和请求量出现显着差别,这种情况将加大负载均衡和开发运维的难度。因此需要理解集群倾斜的缘故原由

7.5 集群读写分离

集群模式下读写分离成本比力高,直接扩展主节点数量来提高集群性能是更好的选择。
出自:https://www.cnblogs.com/seven97-top/p/18587487

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 IT评测·应用市场-qidao123.com (https://dis.qidao123.com/) Powered by Discuz! X3.4