集群: Redis Cluster是Redis分布式解决方案
主要解决了单机内存、并发(OPS)、流量等瓶颈,采用Cluster架构方案达到负载均衡的目的,比如虽然redis官网说10万/s条命令,但是有些业务需要100万/每秒,机器内存16-256G但是业务需要500G,
数据分布:
分布式数据首先要解决把整个数据集按照分区规则映射到多个节点的问题
常见的分区规则有哈希分区和顺序分区两种
键值分布业务无关 像用户id;数据分散易倾斜:主要是用户可能存在之前注册的访问多
Redis 集群采用是哈希分区规则,常见的哈希分区有这几种:
1>节点取余分区 客户端分片:哈希+取余
使用特定的数据,如Redis的键或用户ID,再根据节点数量N使用公式:hash(key)%N计算出哈希值,用来决定数据映射在哪一个节点,其有点就是简单
缺点:添加或者移除服务器时,几乎所有缓存都要重建,还要考虑雪崩式崩溃问题
如果使用多倍扩容,可以使得迁移降到50%,这个迁移会存在一个问题,数据进行迁移后第一次是无法从缓存中获取到数据的,要在数据库中去取数据,然后进行回写,写到新的节点,大量的回写也会对系统性能带来问题
2> 一致性哈希分区(在memcache使用比较广泛) 客户端分片:哈希+顺时针(优化取余)
为系统中每一个节点分配一个token,范围一般在0-2的32次方,这些token构成一个哈希环。数据读写执行节点查找操作时,先根据key计算hash值,然后顺时针找到第一个大于等于该哈希值的token
使得服务器的变动只能影响一个很小的范围,但是也没有解决雪崩问题,但是雪崩问题不只是算法能解决的问题了
好处:在于加入和删除节点只影响哈希中相邻节点,对其他节点无影响,对于节点比较多的情况下,影响范围特别小。
缺点:
a>加减节点会造成哈希环中部分数据无法命中,比如添加了n5,将会在n5节点查找数据,但是数据不存在,就会去数据库获取数据,无法实现将n2数据迁移到n5,需要手动处理或者过略这部分数据,因此一致性哈希常用与缓存场景
b>当使用少量节点,节点变化将大范围影响哈希环中数据映射,因此不适合数据节点的分布式方案
c>普通的一致性哈希分区在增减节点时需要增加一倍或减去一半节点才能保证数据和负载的均衡,比如添加n5,但是没有改变n3和n4的流量
3>虚拟槽哈希分区: 服务端管理节点、槽、数据
虚拟槽分区巧妙使用了哈希空间,使用分散度良好的哈希函数把所有数据映射到一个固定范围的整数集合中,整数定义为槽(slot)。这个范围一般远远大于节点数,槽是集群内数据管理和迁移的基本单位。目的就是为了方便数据拆分和集群扩展。每个节点会负责一定数量的槽
Redis Cluster将所有数据划分为16384的slots,每个节点负责其中一部分槽位。槽位的信息存储于每个节点,当Redis cluster的客户端来连接集群时,它会得到一份集群的槽位配置信息,这样当客户端要查找某个key时,可以直接定位到目标节点
键根据哈希函数映射到0-16383整数槽内,计算公式:slot=CRC16(key)&16383.每个节点负责维护一部分槽以及槽所映射的键值数据
比如当前集群有5个节点,每个节点平均大约负责3276个槽。由于采用高质量的哈希算法,所以每个槽所映射的数据通常比较平均,将数据平均划分到5个节点进行数据分区。(比如redis 集群槽范围0-16383)
如果算出这个值是100,发送给n1,n1返回数据,如果不在自己槽范围内,redis Cluster之间是一个共享消息模式,所以返回一个结果,让其在其他节点去取数据,不像之前一致性哈希在增加节点存在丢数据的问题,因为每个节点的范围时固定的,在添加节点时,需要节点把槽的范围和数据分配给新的节点
集群功能限制:
1)key批量操作和实务操作,只支持具有相同slot值的key执行批量操作。对于映射为不同slot值的key由于执行这些操作可能存在于多个节点因此不被支持
2)key作为数据节点分区最小粒度,因此不能将一个大的键值对象如hash、list等映射到不同节点,不支持多数据库空间。(单机下redis支持16个),复制结构只能一层,从节点只能复制主节点,不支持嵌套树状复制结构
三步:准备节点、节点握手、分配槽
一般为所有节点统一目录,一般划分三个目录:conf、data、log分别存放配置、数据和日志相关文件
如果启动时存在集群配置文件,节点会使用配置文件初始化集群信息
集群模式的redis除了原有的配置文件外又添加了一份集群配置文件。当集群内节点信息发生变化,如添加节点、节点下线、故障转移等。节点会自动保存集群状态到配置文件中(注意:reids自动维护集群配置文件,不要手动修改,防止节点重启尝试集群信息错乱),文件内容记录了节点ID,是一个40位16进制字符串,用于唯一表示集群内一个节点,之后很多集群操作都要借助于节点ID来完成(不同于运行ID,节点ID集群初始化只创建一次,节点重启后只创建一次,节点重启时会加载集群配置文件进行重用,而Redis的运行ID每次重启都会变化)
节点握手:
节点通过Gossip协议笔试通信,达到感知对方过程:cluster meet{ip}{port} 这里的ping、pong、meet消息都是Gossip协议通信的载体,作用是节点彼此交换数据信息
节点6379创建6378节点信息对象,并发送meet信息,节点6380接受后,保存6379节点信息并回复pong消息,之后两节点定期通过ping/pong消息进行正常节点通信
cluster meet命令进行节点握手的过程
执行命令:redis-cli -h 127.0.0.1 -p 7000 cluster meet 127.0.0.1 7005 节点建立握手后集群还不能正常工作,这时处于下线状态,所以数据读写被禁止
通过cluster info 查看集群信息,被分配的槽(cluster_slots_assigned:0),由于目前所有的槽没有分配到槽,因此集群无法完成槽到及诶单的映射,只有当16384个槽全部分配节点,集群才进入在线状态
分配槽:
redis集群把所有的数据映射到16384个槽中。每个key映射为一个固定的槽,只有当节点分配了槽,才能相应和这些槽关联的键命令 : redis-cli -h 127.0.0.1 -p 7000 cluster addslots {0..5461} 目前作为一个完整的集群,每个负责处理槽的节点应该具有从节点,首次启动的节点和被分配的槽的节点都是主节点,从节点负责复制主节点槽信息和相关数据。使用cluster replicate {nodeld}命令让一个节点变成从节点。命令执行必须要对应从节点执行。集群搭建完成
1.通信流程:分布式部署需要提供维护节点元数据信息的机制(元数据是指:节点负责哪些数据,是否出现故障等状态信息。)
常见的元数据维护方式:集中式和p2p方式。Redis集群采用P2P的Gossip(流言)协议
Gossip协议工作原理就是节点彼此不断通信交换信息,一般时间后所有节点都会知道集群完整的信息
通信过程:a>集群中的每个节点会单独开启一个TCP通道,用于节点之间彼此通信,通信端口都在基础端口加1万
b>每个节点在周期内选择几个节点发送ping消息,收到ping消息的节点通过pong消息作为响应
有些节点可能知道全部也有可能知道部分节点,只要这些节点彼此可以正常通信,最终他们状态会达到一致,当节点出故障、新节点加入、主从角色变化、槽信息变更等事件发送通过不断ping/pong消息通信,所有节点就知道集群全部节点的最新状态,达到集群状态同步
2.GOSSIP消息
Gossip协议主要职责就是信息交换。常用的消息可分为ping、pong、meet消息、fail消息等
meet:通知新节点加入,之后在集群中进行周期性的ping、pong消息交换
fail消息:当节点预判集群内另一个节点下线时,会向集群内广播一个fail消息
当接收到ping、meet消息时,接受节点会解析消息内容并根据自身的识别情况作出相应处理,流程如下:
消息解析流程
如果发送节点是新节点消息时meet类型,则加入到本地节点列表;如果是已知节点,则尝试更新发送节点的状态,如槽映射关系、主从角色状态,消息处理完恢复pong消息,内容同样包含消息头和消息体,发送节点接收到回复的pong消息后,采用类似流程处理消息并更新,完成一次消息通信
节点选择:虽然Gossip协议的信息交互机制具有天然的分布式特性,但是内部需要频繁的进行节点信息交换,因为ping/pong消息会携带当前节点和部分其他节点的状态信息,势必加重带宽和计算负担。redis集群节点通信采用固定频率(定时任务每秒执行10次)。通信节点过多可以消息即使但是成本过高,如果过少降低交换频率影响故障判断,redis集群的Gossip协议兼顾了这两者的优缺点
集群内每个节点维护定时默认每秒执行10次,每秒随机选取5个节点找出最久没有通信的节点发送ping消息,保证Gossip信息交换的随机性
伸缩原理:可以为集群添加节点或者扩容也可以下线部分节点进行缩容
其中原理可抽象为槽和对应数据在不同节点之间灵活移动
扩容集群:三步骤: 1>准备新节点 2>加入集群 3>迁移槽和数据
2>cluster meet 127.0.0.01 port 在经过一段时间的ping/pong消息通信后,所有节点会发现新节点并将它们状态保存在本地 可以执行 cluster nodes 查看新节点信息,但是因为没有分配槽,所以不能任何读写操作
redis-trib.rb add-node 127.0.0.1:7000 127.0.0.1:7001正常情况见识使用这个命令加入新节点,该命令内部会执行新节点状态检查,如果新节点已经加入其它集群或者包含信息,则放弃集群加入操作,如果手动执行cluster meet命令加入已经存在于其它集群的节点或造成被加入节点的集群合并到现在集群情况,从而造成数据丢失和错乱,后果非常严重
3>槽的迁移过程中集群可以正常提供读写服务,迁移过程是集群扩容最核心的环节
(1)槽迁移计划
槽是Redis集群管理数据的基本单位,迁移计划需要确保每个节点㺓相似数量的槽,从而保证节点的数据均匀,计划确定后开始逐个把槽内数据从源节点迁移到目标节点
(2)迁移数据
数据迁移过程是逐个槽进行的,每个槽数据迁移流程如下
流程说明:
a)对目标节点发送 cluster setslot {slot} importing {source NodeId}命令,让目标节点准备导入槽的数据
b)对源节点发送 cluster setlot{slot} migrating {targetNodeId}命令,让源节点准备迁出槽数据
c)源节点循环执行cluster getkeysinslot{slot}{count}命令,获取count个属于槽{slot}的键
d)在源节点执行migrate{targeIp}{targePort}""0{timeout}keys{keys...}命令,把获取的键通过流水线(pipeline)机制批量迁移到目标节点(批量迁移键将极大降低节点之间网络IO次数)
e)重复c)d)步骤,直到槽下所有键值数据迁移到目标节点
f)向集群内所有主节点发送cluster setslot {slot} node {targetNodeId}命令,通知槽分配给目标节点。为了保证槽节点映射变更及时传播,需要遍历发送给所有主节点更新被迁移的槽指向新节点。
忘记节点:
集群内的节点不停地通过Gossip消息彼此交互节点状态,因此通过cluster forget {down Nodeld}实现实现忘记下线的节点。
当节点收到cluster forget{down Nodeld}命令后,会把nodeld指定的节点加入禁用列表,禁用列表内的节点不再发送Gossip消息。禁用列表有效期是60s,超过60s节点再次参与消息交换。也就说当第一次forget命令发出后,有60s的时候让集群内所有节点忘记下线节点
注意:1、不建议线上直接使用cluster forget,需要跟大量节点命令交互,实际操作过于频繁并且容易遗漏forget节点
2、当下线主节点具有从节点时,需要把该丛节点指向其他主节点,因此对于主从节点都下线的时候,建议先下线从节点再下线主节点,避免不必要的全量复制
1、请求重定向
在集群模式下,Redis接受任何键相关命令时首先计算键对应的槽(根据键有效部分使用CRC16计算出散列值,再取16383语句,使每个键都可以映射到0~16383槽范围内),再根据槽找出所对应的节点,如果节点是自身,则处理键命令,负责恢复MOVED重定向错误,通知客户端请求正确的节点。MOVED重定向。重定向信息包含了键所对应的槽以及负责该槽的节点地址,根据这些信息客户端就可以向正确的节点发起请求。(phpredis客户端可以根据重定向信息直接再次向键所在节点发起请求, 从而获取数据)
如果键对应槽,不属于6379节点
重定向信息包含了键所对应的槽以及负责该槽的节点地址,根据这些信息,客户端就可以向正确的节点发送请求。使用redis-cli命令,可以介入 -c参数支持自动重定向
1、集群完整性:默认情况下当集群16384个槽任何一个没有指派到节点时整个集群不可用。但是当持有槽的主节点下线时,从故障发现到自动完成转移期间整个集群是不可用状态。建议修改 cluster-require-full-coverage配置为no,当主节点故障时只影响它负责槽的相关命令执行。
2、带宽消耗:集群内Gossip消息通信本身会消耗带宽,官方建议集群最大规模在1000内。集群内所有节点通过ping/pong消息彼此交换信息, 集群带宽消耗主要分为:读写命令消耗+Gossip消息消耗。
3、Pub/Sub(发布/订阅)广播问题:用于针对频道实现消息的发布和订阅
在集群模式下内部实现对所有的publish命令都会向所有节点进行广播一次。加重带宽负担
当频繁应用pub/sub功能应该避免在大量节点的集群内使用,负责严重消耗集群内网络带宽。建议使用sentinel结构专门用于Pub/Sub功能,从而规避这个问题
4、集群倾斜
a>数据倾斜:节点和槽分配严重不均 、不同槽对应键数量差异过大、集合对象包含大量元素、内存相关配置不一致
b>请求倾斜:(常出现在热点键场景)避免方式:1)合理设计键,热点集合对象做拆分或使用hmget代替hgetall避免整体读取 2)不要使用热键作为hash_tag,避免映射到同一槽 3)对于一致性要求不高的场景,可使用本地缓存减少热点调用
5、集群读写分离:
1)集群模式下的读写分离会遇到:复制延迟、读取过期数据,从节点故障等问题
2)集群模式下读写分离成本比较高,可以直接扩展主节点数量提高集群性能,一般不建议集群模式下做读写分离
集群读写分离有时用于特殊业务场景:
利用复制的最终一致性使用多个从节点做跨机房部署降低读命令网络延迟。主节点故障转移时间过长,业务端可以把读请求路由给从节点保证读操作可用。
7、数据迁移
当把单机Redis数据迁移到集群环境,redis-trib.rb提供导入功能
1)迁移只能从单机节点向集群环境导入数据
2)不支持再现迁移数据,迁移数据时应用方必须停写,无法平滑迁移数据,如果过程中途出现超时等错误,不支持断点续传只能重新全量导入,使用单线程进行数据迁移,大数据量迁移速度过慢。
总结:
Redis集群搭建的方式有多种,例如使用zookeeper等,但从redis 3.0之后版本支持redis-cluster集群,Redis-Cluster采用无中心结构,每个节点保存数据和整个集群状态,每个节点都和其他所有节点连接。其redis-cluster架构图如下:
其结构特点:
1、所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽。
2、节点的fail是通过集群中超过半数的节点检测失效时才生效。
3、客户端与redis节点直连,不需要中间proxy层.客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可。
4、redis-cluster把所有的物理节点映射到[0-16383]slot上(不一定是平均分配),cluster 负责维护node<->slot<->value。
5、Redis集群预分好16384个桶,当需要在 Redis 集群中放置一个 key-value 时,根据 CRC16(key) mod 16384的值,决定将一个key放到哪个桶中。
分片的缺点
Redis 的一些特性与分片在一起时玩转的不是很好:
涉及多个键的操作通常不支持。例如,你不能对映射在两个不同 Redis 实例上的键执行交集(事实上有办法做到,但不是直接这么干)。
涉及多个键的事务不能使用。
分片的粒度(granularity)是键,所以不能使用一个很大的键来分片数据集,例如一个很大的有序集合。
当使用了分片,数据处理变得更复杂,例如,你需要处理多个 RDB/AOF 文件,备份数据时你需要聚合多个实例和主机的持久化文件。
添加和删除容量也很复杂。例如,Redis 集群具有运行时动态添加和删除节点的能力来支持透明地再均衡数据,但是其他方式,像客户端分片和代理都不支持这个特性。但是,有一种称为预分片(Presharding)的技术在这一点上能帮上忙。
1、故障发现
Redis集群内节点通过ping/pong消息实现节点通信(消息不仅传播节点槽信息还可以传播主从状态、节点故障等)
2、主观下线
指某个节点认为另一个节点不可用,即下线状态,当然这个状态不是最终的故障判定,只能代表这个节点自身的意见,也有可能存在误判,如果节点与节点通信出现问题则断开连接,下次会进行重连,如果一直通信失败,则它们的最后通信时间将无法更新,如果超过cluster-node-timeout时,更新本地对节点状态为主管下线
3、客观下线:指真正的下线,集群内多个节点都认为该节点不可用,达成共识,将它下线,如果下线的节点为主节点,还要对它进行故障转移
假如节点a标记节点b为主观下线,一段时间后节点a通过消息把节点b的状态发到其它节点,当节点c接受到消息并解析出消息体时,会发现节点b的pfail状态时,会触发客观下线流程;
通过Gossip消息传播,此时redis集群为统计持有槽的主节点投票数是否达到一半,当下线报告统计数大于一半时,被标记为客观下线状态。
问题:为什么必须是持有槽的主节点参与故障发现决策?
因为集群模式下只有处理槽的主节点才负责读写请求和集群槽等关键信息维护,而从节点只进行主节点数据和状态信息的复制
4、故障恢复:
故障主节点下线后,如果下线节点的是主节点,则需要在它的从节点中选一个替换它,保证集群的高可用;转移过程如下:
1,资格检查:检查该从节点是否有资格替换故障主节点,如果此从节点与主节点断开过通信,那么当前从节点不具体故障转移;
2,准备选举时间:当从节点符合故障转移资格后,更新触发故障选举时间,只有到达该时间后才能执行后续流程;(这里采用延迟触发机制因为复制偏移量越大说明从节点延迟越低,具有高的优先级)
3,发起选举:当到达故障选举时间时,进行选举;
4,选举投票:只有持有槽的主节点才有票,会处理故障选举消息,投票过程其实是一个领导者选举(选举从节点为领导者)的过程,每个主节点只能投一张票给从节点,当从节点收集到足够的选票(大于N/2+1)后,触发替换主节点操作,撤销原故障主节点的槽,委派给自己,并广播自己的委派消息,通知集群内所有节点
问题:Redis集群没有直接使用从节点进行leader选举
因为从节点必须大于等于3个才能保证N/2+1节点,将导致从节点资源浪费,使用集群内所有持有槽主节点进行领导选举,即使只有一个从节点也可以完成选举过程