Redis教程

Redis(七)--分片技术详解

本文主要是介绍Redis(七)--分片技术详解,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

文章目录

  • 介绍
  • Redis集群的数据分布算法:哈希槽算法
  • 集群的请求重定向
  • Redis集群中节点的通信机制:goosip协议
    • 介绍
    • 原理
    • 通信过程
    • 优缺点
    • goosip协议的常见类型
  • 集群的扩容与收缩
    • 扩容
    • 收缩
  • 集群的故障检测与故障恢复机制
    • 故障检测
    • 故障恢复

介绍

Redis分片(Sharding)技术是指将数据分散到多个Redis实例中的方法,分片之后,每个Redis拥有一部分原数据集的子集。在数据量非常大的时候,这种技术能够将数据量分散到若干主机的Redis实例上,进而减轻单台redis实例的压力。分片技术能够以更扩展的方式使多台计算机的存储能力和计算能力:

  1. 从存储能力角度,分片技术通过使用多台计算机的内存来承担更大量的数据,如果没有分片技术,那么Redis的存储能力将受限于单台主机的内存大小。
  2. 从计算能力角度,分片技术通过将计算任务分散到多核或者多台主机中,能够充分利用多核、多台主机的计算能力。

Redis集群采用了去中心化的思想,对于客户端来说,整个集群可以看成一个整体,可以连接任意一个节点进行操作,不需要任何代理中间件,当客户端操作的key没有分配到该node上时,Redis会返回转向指令,指向正确的node。

未采用分片技术的单Redis实例:对这1000万条数据的所有增删改查操作压力都会集中在这个Redis所在的主机,此时不仅有存储和操作对该主机的压力,还有该主机失效时将导致所有操作都无法进行的问题。

在这里插入图片描述
采用分片技术:
在这里插入图片描述
采用分片技术之后,数据将被分散到4个Redis实例中,对数据的操作也被分散到每个Redis实例中,此时单台主机的压力将大大减轻。
总结
Redis单实例架构,从最开始的一主N从,到读写分离,再到哨兵机制,单实例的Redis缓存能够应对大多数的使用场景,也能实现主从故障迁移。
但是,在某些场景下,单实例Redis缓存会存在以下几个问题:

  1. 写并发:
    Redis单实例读写分离可以解决读操作的负载均衡,但对于写操作,仍然是全部落在了master节点上面,在海量数据高并发场景,一个节点写数据容易出现瓶颈,造成master节点的压力上升。
  2. 海量数据的存储压力:
    单实例Redis本质上只有一台Master作为存储,如果面对海量数据的存储,一台Redis的服务器就应付不过来了,而且数据量太大意味着持久化成本高,严重时可能会阻塞服务器,造成服务请求成功率下降,降低服务的稳定性。

针对以上的问题,Redis集群提供了较为完善的方案,解决了存储能力受到单机限制,写操作无法负载均衡的问题
在这里插入图片描述
Redis也内置了高可用机制,支持N个master节点,每个master节点都可以挂载多个slave节点,当master节点挂掉时,集群会提升它的某个slave节点作为新的master节点。
Redis集群可以看成多个主从架构组合起来的,每一个主从架构可以看成一个节点(其中,只有master节点具有处理请求的能力,slave节点主要是用于节点的高可用)

Redis集群的数据分布算法:哈希槽算法

1. 什么是哈希槽算法
对于分布式存储,需要考虑的重点是如何将数据进行拆分到不同的Redis服务器上。常见的分区算法有hash算法、一致性hash算法。

  • 普通hash算法:将key使用hash算法计算之后,按照节点数量来取余,即hash(key)%N。优点就是比较简单,但是扩容或者摘除节点时需要重新根据映射关系计算,会导致数据重新迁移。
  • 一致性hash算法:为每一个节点分配一个token,构成一个哈希环;查找时先根据key计算hash值,然后顺时针找到第一个大于等于该哈希值的token节点。优点是在加入和删除节点时只影响相邻的两个节点,缺点是加减节点会造成部分数据无法命中,所以一般用于缓存,而且用于节点量大的情况下,扩容一般增加一倍节点保障数据负载均衡。

Redis集群采用的算法是哈希槽分区算法。Redis集群中有16384个哈希槽(槽的范围是 0 -16383,哈希槽),将不同的哈希槽分布在不同的Redis节点上面进行管理,也就是说每个Redis节点只负责一部分的哈希槽。在对数据进行操作的时候,集群会对使用CRC16算法对key进行计算并对16384取模(slot = CRC16(key)%16383),得到的结果就是 Key-Value 所放入的槽,通过这个值,去找到对应的槽所对应的Redis节点,然后直接到这个对应的节点上进行存取操作。
使用哈希槽的好处就在于可以方便的添加或者移除节点,并且无论是添加删除或者修改某一个节点,都不会造成集群不可用的状态。当需要增加节点时,只需要把其他节点的某些哈希槽挪到新节点就可以了;当需要移除节点时,只需要把移除节点上的哈希槽挪到其他节点就行了;

哈希槽的特点:

  • 解耦数据和节点之间的关系,简化了扩容和收缩难度;
  • 节点自身维护槽的映射关系,不需要客户端代理服务维护槽分区元数据
  • 支持节点、槽、键之间的映射查询,用于数据路由,在线伸缩等场景
    2. 哈希槽相关的数据结构
    (1)clusterNode:保存节点的当前状态,比如节点的创建时间、节点的名字、节点的当前配置纪元(用于实现故障转移)、节点的IP地址等等。
    (2)clusterState数据结构:记录当前节点所认为的集群目前所处的状态。
    (3)节点的槽指派信息:clusterNode数据结构的slots属性和numslot属性记录了节点负责处理哪些槽:
    slots属性是一个二进制位数组(bit array),这个数组的长度是16384/8=2048个字节,共包含16348个二进制位。master节点用bit来标识对于某个槽自己是否拥有,时间复杂度是O(1)。
    在这里插入图片描述
    (4)集群所有槽的指派信息:
    当收到集群中其他节点发送的信息时,通过将节点槽的指派信息保存在本地的clusterState.slots数组里面,程序要检查槽i是否已经被指派,又或者取得负责处理槽的节点,只需要访问clusterState.slots[i]的值即可,时间复杂度仅为O(1)。
    在这里插入图片描述
    如上图所示:clusterState中保存的slots数组中每个下标对应一个槽,每个槽信息中对应一个clusterNode也就是缓存的节点。这些节点会对应一个实际存在的Redis缓存服务,包括IP和Port的信息。Redis Cluster的通讯机制实际上保证了每个节点都有其他节点和槽数据的对应关系。无论Redis的客户端访问集群中的哪个节点都可以路由到对应的节点上,因为每个节点都有一份clusterState它记录了所有槽和节点的对应关系

集群的请求重定向

Redis集群在客户端层面没有采用代理,并且无论Redis的客户端访问集群中的那个节点都可以路由到对应的节点上。

节点对请求处理过程如下:

  1. 检查当前key是否存在当前NODE?
    (1)通过crc16(key)/16384计算出slot
    (2)查询负责该slot的节点,得到节点指针
    (3)该指针与自身节点比较
  2. 若slot不是由自身负责,则返回MOVED重定向
  3. 若slot由自身负责,且key在slot中,则返回该key的对应结果
  4. 若key不存在此slot中,检查该slot是否正在迁出(MIGRATING)?
  5. 若key正在迁出,返回ASK错误重定向客户端到迁移的目的服务器上
  6. 若slot未迁出,检查slot是否导入中?
  7. 若slot导入中且有ASKING标记,则直接操作
  8. 否则返回MOVED重定向
  • MOVED请求
    在这里插入图片描述
    Redis客户端通过CRC16(key)%16383计算出Slot的值,发现需要找“缓存节点1”进行数据操作,但是由于缓存数据迁移或者其他原因导致这个对应的slot的数据被迁移到了“缓存节点2”上面。那么这个时候Redis客户端就无法从缓存节点1中获取数据了。但是由于缓存节点1中保存了所有集群中缓存节点的信息,因此他知道这个slot的数据在缓存节点2中保存,因此向Redis客户端发送了一个MOVED的重定向请求。这个请求告诉其应该访问的缓存节点2的地址。Redis客户端拿到这个地址,继续访问缓存节点2并且拿到数据。
    在这里插入图片描述

  • ASK请求
    ASK请求发生于集群伸缩时,集群伸缩会导致槽迁移,当我们去源节点访问时,此时数据已经可能迁移到了目标节点,使用ASK重定向来解决此种情况。
    在这里插入图片描述
    Redis 客户端向“缓存节点1”发出请求,此时“缓存节点1”正向“缓存节点 2”迁移数据,如果没有命中对应的 Slot,它会返回客户端一个 ASK 重定向请求并且告诉“缓存节点2”的地址。客户端向“缓存节点2”发送 Asking 命令,询问需要的数据是否在“缓存节点2”上,“缓存节点2”接到消息以后返回数据是否存在的结果。
    在这里插入图片描述

  • 频繁重定向造成的网络开销的处理:smart客户端
    什么是smart客户端
    上述两种重定向的机制使得客户端的实现更加复杂,使用smart客户端来降低复杂性、追求更好的性能。客户端内部负责计算维护键->槽->节点映射,用于快速定位目标节点。
    JedisCluster的工作原理

  1. 从集群中随机选取一个可运行节点,使用cluster slots得到槽和节点的映射关系
  2. 将上述映射关系保存到本地。通过映射关系就可以直接对目标节点进行操作(CRC16(key) -> slot -> node),很好的避免了MOVED重定向,并为每个节点创建JedisPool
  3. 如果那个node正好还是持有那个hashslot,那么就ok;如果进行了reshard操作,可能hashslot已经不在那个node上了,就会返回moved.
  4. 如果JedisCluster API发现对应的节点返回moved,那么利用该节点返回的元数据,更新本地的hashslot->node映射表缓存
  5. 重复上面几个步骤,直到找到对应的节点,如果重试超过5次,就会报错JedisClusterMaxRedirectionException
    在这里插入图片描述
    ASK与MOVED的区别
    虽然ASK与MOVED都是对客户端的重定向控制,但是有本质区别。ASK重定向说明集群正在进行slot数据迁移,客户端无法知道迁移什么时候完成,因此只能是临时性的重定向,客户端不会更新slots缓存。但是MOVED重定向说明键对应的槽已经明确指定到新的节点,客户端需要更新slots缓存。

Redis集群中节点的通信机制:goosip协议

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

介绍

当集群的状态发生变化时,比如新节点加入、slot迁移、节点宕机、slave提升为新Master等等,我们希望这些变化尽快被其他节点发现,在Redis中,不同的节点之间采用goosip协议进行通信,节点之间通信的目的是为了维护节点之间的元数据信息,这些元数据就是每个节点包含哪些数据,是否出现故障,通过gossip协议,达到最终数据的一致性。

原理

该协议的原理是在不同的节点间不断的通信交换信息,一段时间后,所有的节点就都有了整个集群的完整信息,并且所有节点的状态都会达成一致。

通信过程

  • 集群中每个节点都会单独开一个TCP通道,用于节点间的彼此通信
  • 每个节点在固定周期内通过待定的规则选择几个节点发送ping消息
  • 接收到ping消息的节点用pong消息作为响应

优缺点

  • 优点:
    该协议的最大好处在于即使集群节点的数量增加,每个节点的负载也不会增加很多,几乎是恒定的。该协议将元数据的更新分散在不同的节点上面,降低了压力。

  • 缺点:
    (1)元数据的更新有延时,可能导致集群中的一些操作会有一些滞后。
    (2)由于 gossip 协议对服务器时间的要求较高,时间戳不准确会影响节点判断消息的有效性。
    (3)节点数量增多后的网络开销也会对服务器产生压力,同时结点数太多,意味着达到最终一致性的时间也相对变长,因此官方推荐最大节点数为1000左右。

goosip协议的常见类型

gossip协议常见的消息类型包含: ping、pong、meet、fail等等。

  1. meet:主要用于通知新节点加入到集群中,通过cluster meet ip port命令,已有集群的节点会向新的节点发送邀请,加入现有集群。

  2. ping:用于交换节点的元数据。每个节点每秒会向集群中其他节点发送 ping 消息,消息中封装了自身节点状态还有其他部分节点的状态数据,也包括自身所管理的槽信息等等。

  1. 因为发送ping命令时要携带一些元数据,如果很频繁,可能会加重网络负担。因此,一般每个节点每秒会执行 10 次 ping,每次会选择 5个最久没有通信的其它节点。
  2. 如果发现某个节点通信延时达到了 cluster_node_timeout / 2,那么立即发送ping,避免数据交换延时过长导致信息严重滞后。比如说,两个节点之间都 10分钟没有交换数据了,那么整个集群处于严重的元数据不一致的情况,就会有问题。所以 cluster_node_timeout可以调节,如果调得比较大,那么会降低 ping 的频率。
  3. 每次 ping,会带上自己节点的信息,还有就是带上 1/10其它节点的信息,发送出去,进行交换。至少包含 3 个其它节点的信息,最多包含 (总节点数 - 2)个其它节点的信息。
  1. pong:ping和meet消息的响应,同样包含了自身节点的状态和集群元数据信息。
  2. fail:某个节点判断另一个节点 fail 之后,向集群所有节点广播该节点挂掉的消息,其他节点收到消息后标记已下线。

由于Redis集群的去中心化以及gossip通信机制,Redis集群中的节点只能保证最终一致性。例如当加入新节点时(meet),只有邀请节点和被邀请节点知道这件事,其余节点要等待 ping 消息一层一层扩散。除了 Fail 是立即全网通知的,其他诸如新节点、节点重上线、从节点选举成为主节点、槽变化等,都需要等待被通知到,也就是Gossip协议是最终一致性的协议。
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会被集群中的所有节点认识。

集群的扩容与收缩

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

扩容

  1. 启动新节点
  2. 使用cluster meet命令将新节点加入到集群
  3. 迁移槽和数据:添加新节点后,需要将一些槽和数据从旧节点迁移到新节点

新节点加入到集群的时候,作为孤儿节点是没有和其他节点进行通讯的。因此需要在集群中任意节点执行 cluster meet 命令让新节点加入进来。这个是由老节点发起的,有点老成员欢迎新成员加入的意思。新节点刚刚建立没有建立槽对应的数据,也就是说没有缓存任何数据。如果这个节点是主节点,需要对其进行槽数据的扩容;如果这个节点是从节点,就需要同步主节点上的数据。总之就是要同步数据。
在这里插入图片描述
(1)客户端对目标节点发起准备导入槽数据的命令,让目标节点准备好导入槽数据。这里使用 cluster setslot {slot} importing {sourceNodeId}命令。
(2)之后对源节点发起送命令,让源节点准备迁出对应的槽数据。使用命令cluster setslot {slot} importing {sourceNodeId}
(3)此时源节点准备迁移数据了,在迁移之前把要迁移的数据获取出来。通过命令 cluster getkeysinslot {slot} {count}。Count 表示迁移的 Slot 的个数。
(4)然后在源节点上执行,migrate {targetIP} {targetPort} “” 0 {timeout} keys {keys}命令,把获取的键通过流水线批量迁移到目标节点。
(5)重复 3 和 4 两步不断将数据迁移到目标节点。
(6)完成数据迁移到目标节点以后,通过 cluster setslot {slot} node {targetNodeId} 命令通知对应的槽被分配到目标节点,并且广播这个信息给全网的其他主节点,更新自身的槽节点对应表。

收缩

迁移槽
忘记节点。通过命令cluster forget {downNodeId}通知其他节点

在这里插入图片描述
为了安全删除节点,Redis集群只能下线没有负责槽的节点。因此如果要下线有负责槽的master节点,则需要先将它负责的槽迁移到其他节点。迁移的过程也与上线操作类似,不同的是下线的时候需要通知全网的其他节点忘记自己,此时通过命令 cluster forget {downNodeId}通知其他的节点。

集群的故障检测与故障恢复机制

故障检测

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

  • 主观下线(pfail):当节点A检测到与节点B的通讯时间超过了cluster-node-timeout的时候,就会更新本地节点状态,把节点B更新为主观下线。主观下线并不能代表某个节点真的下线了,有可能是节点A与节点B之间的网络断开了,但是其他的节点依旧可以和节点B进行通讯。
  • 客观下线:
    由于集群内的节点会不断地与其他节点进行通讯,下线信息也会通过 gossip 消息传遍所有节点,因此集群内的节点会不断收到下线报告。
    当半数以上的主节点标记了节点B是主观下线时,便会触发客观下线的流程(该流程只针对主节点,如果是从节点就会忽略)。将主观下线的报告保存到本地的 ClusterNode 的结构fail_reports链表中,并且对主观下线报告的时效性进行检查,如果超过cluster-node-timeout*2 的时间,就忽略这个报告,否则就记录报告内容,将其标记为客观下线。
    接着向集群广播一条主节点B的Fail 消息,所有收到消息的节点都会标记节点B为客观下线。

故障恢复

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

  • 从节点过滤
    检查每个slave节点与master节点断开连接的时间,如果超过了cluster-node-timeout * cluster-slave-validity-factor,那么就没有资格切换成master。

  • 投票选举
    (1)节点排序
    对通过过滤条件的所有从节点进行排序,按照priority、offset、run id排序,排序越靠前的节点,越优先进行选举。

  • priority的值越低,优先级越高
  • offset越大,表示从master节点复制的数据越多,选举时间越靠前,优先进行选举
  • 如果offset相同,run id越小,优先级越高

(2)更新配置纪元
每个主节点会去更新配置纪元(clusterNode.configEpoch),这个值是不断增加的整数。这个值记录了每个节点的版本和整个集群的版本。每当发生重要事情的时候(例如:出现新节点,从节点精选)都会增加全局的配置纪元并且赋给相关的主节点,用来记录这个事件。更新这个值目的是,保证所有主节点对这件“大事”保持一致,大家都统一成一个配置纪元,表示大家都知道这个“大事”了。
(3)发起选举
更新完配置纪元以后,从节点会向集群发起广播选举的消息(CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST),要求所有收到这条消息,并且具有投票权的主节点进行投票。每个从节点在一个纪元中只能发起一次选举
(4)选举投票
如果一个主节点具有投票权,并且这个主节点尚未投票给其他从节点,那么主节点将向要求投票的从节点返回一条CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,表示这个主节点支持从节点成为新的主节点。每个参与选举的从节点都会接收CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,并根据自己收到了多少条这种消息来统计自己获得了多少主节点的支持。
如果超过(N/2 + 1)数量的master节点都投票给了某个从节点,那么选举通过,这个从节点可以切换成master,如果在 cluster-node-timeout*2的时间内从节点没有获得足够数量的票数,本次选举作废,更新配置纪元,并进行第二轮选举,直到选出新的主节点为止。
在第(1)步排序领先的从节点通常会获得更多的票,因为它触发选举的时间更早一些,获得票的机会更大

  • 替换主节点
    当满足投票条件的从节点被选出来以后,会触发替换主节点的操作。删除原主节点负责的槽数据,把这些槽数据添加到自己节点上,并且广播让其他的节点都知道这件事情,新的主节点诞生了。
    (1)被选中的从节点执行SLAVEOF NO ONE命令,使其成为新的主节点
    (2)新的主节点会撤销所有对已下线主节点的槽指派,并将这些槽全部指派给自己
    (3)新的主节点对集群进行广播PONG消息,告知其他节点已经成为新的主节点
    (4)新的主节点开始接收和处理槽相关的请求
    如果集群中某个节点的master和slave节点都宕机了,那么集群就会进入fail状态,因为集群的slot映射不完整。如果集群超过半数以上的master挂掉,无论是否有slave,集群都会进入fail状态。
这篇关于Redis(七)--分片技术详解的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!