前言:redis cluster是redis分布式解决方案,集群通过分片来进行数据共享,并提供复制和故障转移功能;redisCluster 也是学习分布式存储的绝佳案例
目录
一.数据分布
分布式数据库首先要解决的问题是把整个数据集按照分区规则映射到多个节点的问题,即把数据划分到多个节点,每个节点负责一个子集。这里简单介绍下分区规则
1.常见的分区规则有:
- 哈希分区:离散度好,数据分布和业务无关,无法顺序访问
- 顺序分区:离散度易倾斜,数据分布和业务相关,可顺序访问
2.哈希分区的规则有:
- 节点取余:
- 缺点:当节点数量变化时,如扩容或收缩节点,数据节点的映射关系需要重新计算,会导致数据重新迁移
- 扩容时通常采用翻倍扩容,这样只需迁移50%数据
- 一致性hash分区
- 缺点:增加或减少节点时影响相邻节点(这也是相对节点取余的优点)
3.如何解决普通一致性hash算法的问题?
-
虚拟槽分区:采用大范围槽的主要目的是为了方便数据拆分和集群扩展,当增加或者删除节点时,把这个节点负责的槽转移就好,这样节点怎么变换,key映射到 槽的 规则不会变
4.redis数据分区
-
Redis cluser采用虚拟槽,槽的范围是0~16383,每个槽负责一部分槽以及槽所映射的键值数据
5.槽特点:
-
解耦数据和节点之间的关系,简化节点扩容和收缩难度
-
节点自身维护槽的映射关系,
-
支持节点、槽、键之间的映射查询,用于数据路由,在线伸缩等场景
注:数据分区是分布式存储的核心
二. redis集群功能的限制
- key批量操作只支持具有相同solt值的key执行(可以使用pipeline 模拟mget等操作)
- 原因:key可能分布在不同的槽节点
- key正常分布在多个节点
- 由于数据迁移正进行到一半造成的key分布在多个节点(ACK错误)
- 原因:key可能分布在不同的槽节点
- 同理只支持多key在同一个节点上的事务操作
- 不支持多数据库空间,支持数据库0
- 复制只支持一层,即从节点只能复制主节点,不支持嵌套树状复制
三.搭建集群
节点数量至少为6个才能保证组成高可用集群(三主三从)
流程:准备节点,节点握手,分配槽,流程图如下:
- 准备节点
- 每个节点需要开启配置 cluster-enabled yes,依次启动(生成一个节点ID,用于标识集群内唯一节点。节点ID在集群初始化的时候只创建一次,节点重启时会加载集群配置文件进行重用,注,区别于运行ID,运行ID每次重启都会变化)
- 可以通过 cluster nodes 命令查看当前集群节点信息
- 节点握手
定义:指一批运行在集群模式下的节点通过Gossip协议彼此通信
- 由客户端发起命令 :cluster meet {ip} {port},如图:
- 节点6379本地创建6380节点信息对象,并发送meet消息
- 节点6380接受到meet消息后,保存6379节点信息并回复pong消息
- 之后节点6379和6380彼此定期通过ping/pong消息进行正常的节点通信
- 之后分别让其余的节点加入到该集群就好(执行上边命令)
- 注:可以通过cluster info 查看当前集群状态(分配槽的状态,集群是否上线等)
- 分配槽
集群会把所有数据映射到16384个槽。只有当节点分配了槽,才能响应和这些槽相关联的键命令。
- 通过 cluster addslots 为节点分配槽
- 只有把所有的槽分配给节点后,集群才会进入在线状态3
-
从节点加入集群
使用 cluster replicate {nodeId} 让一个从节点复制主
注:必须由从节点发起
-
使用 redis-trib.rb工具搭建集群
。。。
四. 节点通信
在分布式存储中需要提供维护节点元数据信息的机制,如节点负责哪些数据,故障检测机制等;
各个节点通过彼此交互信息来知道当前集群的状态,如各个节点负责哪些槽,节点健康状态
- 通信流程
- 元数据维护方式分为
- 集中式
- P2P方式(redis使用Gossip协议,最终一致性)
- 通信过程
- 每个节点单独开辟一个TCP通道,通信端口号在基础端口号上+10000
- 每个节点在固定周期内通过特定规则选择几个节点发送ping消息
- 接收到ping消息的节点用pong消息回复
- 注:在某一时间每个节点可能知道全部节点,也可能仅知道部分节点
- 元数据维护方式分为
- Gossip 消息
- 类型有:
- meet消息:用于通知新节点加入
- ping消息:集群内每个节点每秒向多个其他节点发送ping消息
- 作用:检查节点是否在线和交换自身状态(封装了自身节点和部分其他节点信息)
- pong消息:
- 接收到meet,ping消息时的应答(封装了自身状态信息)
- 节点也可向集群内广播自身节点状态改变信息
- fail消息:当一个节点判断某节点下线时,像集群中广播一个fail消息,收到fail节点后把对应节点标记为下线
- 类型有:
- 通信节点选择:
redis集群节点通信采用固定频率(定时任务每秒执行10次),主要是为了兼顾信息实时性和交换信息的成本选择:
- 每个节点维护定时任务默认每秒执行10次,每100ms 会扫描本地节点列表,如果找到最近一次接受ping消息的时间大于cluster_node_timeout/2,则立刻发送ping消息
- 每秒会随机选取5个节点找出最久没有通信的节点发送ping消息
五. 集群伸缩
集群伸缩的本质为 :槽和数据在节点之间的移动
注:集群在伸缩或者扩容的时候会维护哪些槽在做迁移(迁移出去或迁移数据到本节点)
- 扩容节点
- 准备新节点
- 和现有集群配置保持一致
- 加入集群
- 通过 cluster meet 命令加入集群
- 迁移槽和数据到新节点
- 槽迁移计划
- 保证各个节点间槽数量平衡
- 迁移数据:数据迁移过程是逐个槽进行的
- 让目标节点准备导入槽的数据
- 对目标节点发送cluster setslot {slot} importing {sourceNodeId} 命令
- 让源节点准备迁出槽的数据
- 对源节点发送cluster setslot {slot} migrating {targerNodeId} 命令
- 从源节点取出要迁移的数据
- 循环执行命令cluster getketsinslot {slot} {count}
- 让目标节点迁移C步骤中导出的数据
- 在源节点上执行命令:migrate {targetIp} {targetPort} "" 0 {timeout} keys {kets…},通过流水线机制批量迁移键到目标节点,原子的
- 重复步骤 c,d 直到槽下所有key迁移完
- 通知集群所有节点槽的所有权变更
- 让目标节点准备导入槽的数据
- redis-trib.rb 提供了槽分片功能,reshard 命令
- 用法。。。。。。。。。
- 槽迁移计划
- 给新节点添加从节点
- 让从节点加入集群(cluster meet)
- 从节点上执行cluster replicate {masterNodeId}
- 复制流程 和普通复制流程一样一样的~~~
- 准备新节点
- 收缩节点
- 流程说明:
- 确定下线节点是否复制槽,如果是迁移到其他节点(过程同扩容节点的迁移数据)
- 当下线节点不在负责槽的时候,通知集群内其他几点忘记下线节点
- 可以是用redis-trib.rb del-node {host-port} {downNodeId},实现代码如下
- 流程说明:
槽计算
使用hash_tag功能,可以让不通的key映射到通一个槽中
如:set key:{hash_tag}:111 value
set key:{hash_tag}:222 value
....
在进行槽映射计算的时候只计算大括号内的key(不建议这样,数据分布不均匀)
六. 客户端请求路由
redis支持在线迁移槽和数据来完成水平伸缩,当slot对应的数据从节点a迁移到节点b过程中,期间可能出现一部分数据在a节点,一部分数据出现在b节点,或者数据刚迁移到b节点,客户端还没来的及更新槽和节点的映射关系,这时候就需要ack 和moven 登场了:
- moven重定向:
- redis首先会计算key所在的槽的位置,在根据槽找出所对应的节点,如果节点不是自身,则回复MOVEN重定向错误,通知客户端请求正确的节点
- 注:redis集群是客户端负载,所以槽所有权变更可能没有及时的通知到客户端(客户端遇到moven错误,会把请求重定向,并更新本地槽映射的关系)
- ASK重定向:
- redis支持在线迁移槽来完成水平伸缩(问题:会出现部分数据在源节点,部分数据在目标节点)。客户端对该情况的执行流程为:
- 客户端根据本地slots缓存发送命令到源节点,如果该命令存在则直接返回
- 如果该命令对象不存在,该命令可能已经被迁移到目标节点,源节点返回一个ack重定向异常给客户端
- 客户端从ACK重定向异常中提取出目标节点的信息,发送asking命令道目标节点打开客户端连接标识,在执行键命令。如果存在返回结果,不存在返回不存在
- redis支持在线迁移槽来完成水平伸缩(问题:会出现部分数据在源节点,部分数据在目标节点)。客户端对该情况的执行流程为:
- ASK 与 moven 的区别
- 都是对客户端重定向控制
- ASK重定向说明集群正在进行slot数据迁移,客户端不知道什么时候迁移完,只是临时的重定向,并不会更新客户端的slots缓存
- moven重定向说明键对应的槽已经不属于当前节点且迁移完数据了,因此需要更新客户端本地缓存
七.clusterNode介绍
![clusterNode](https://img-blog.csdnimg.cn/2020043015103244.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMxMzg3MzE3,size_16,color_FFFFFF,t_70)
集群中每个节点都会使用一个ClusterNode结构保存自己的状态,使用一个clusterState结构保存当前集群状态;需要着重介绍下以下结构:ClusterNode.clusterState.fail_reports
- clusterNode.slots:是一个长度为16384的二进制数组,用来保存当前节点负责哪些槽,如果第 i 位是1说明当前节点负责该槽,反之就是不负责;
- ClusterNode.clusterState.nodes: 是一个字典结构,用来存储当前集群节点名单,key为节点名称,value为ClusterNode指针
- ClusterNode.clusterState.slots[16384] : 记录了所有槽的指派信息,每个数据项都是一个指向clusterNode的指针,在数据查找的时候知道了 一个key属于哪个槽 就可以根据ClusterNode.clusterState.slots[x] 知道该槽属于哪个节点
- ClusterNode.clusterState.migrating_slots_to[16384] :该数组记录了正在迁移出的数据,第x 位代表了 x槽位迁移的节点指针,所以该数组存的是一个ClusterNode指针,指针为槽正在迁移的数组
问题1:clusterNode.clusterState.slots 数组中保存了所有槽的指派信息,clusterNode.slots保存单个节点的槽分派信息还是有必要吗?
- 节点间交互信息时候,只需要把clusterNode.slots发送给其他节点就可以了
- 如果不使用clusterNode.slots,每次将x节点信息发送给其他节点的时候需要遍历整个clusterNode.clusterState.slots 数组,太低效了
问题2:在clusterNode中已经保存了槽节点的信息为什么还要在clusterState中保存全量的槽分配信息?
- 如果我们想知道槽i被指派给了哪个槽节点或者槽节点i是否被指派了。我们需要遍历clusterState.nodes数组下所有clusterNode节点下的 slots信息,直到找到槽i的信息,该过程为O(N);
- 而在clusterState.slots结构查询,只需要O(1)
上文中我们知道集群会把所有数据映射到16384个槽,每个主节点都负责一部分槽。那么一个节点是如何知道key属于哪个槽,以及这个槽属于哪个节点的呢?如果发现ack 或者 moven错误当前节点是如何计算出这个key属于哪个节点呢?
1.计算key属于哪个槽大概伪代码如下:
def slot_number(key):
return CRC16(key) & 16383
redis提供了命令 cluster keyslot "key",来计算一个key到底属于哪个槽
2.获取这个key的过程
当前节点判断出key属于哪个槽后,节点会检测当前节点的ClusterNode.clusterState.slots数组中的第 i 项,并和ClusterNode.clusterState.myself 做比较:
- 如果不相等:说明该槽为别的节点负责,然后返回给客户端一个moven错误并把真正负责该槽的节点ip,port,客户端会更新本地槽和节点的映射关系并重定向到新节点
- 如果相等:上文中我们提到redisCluster 支持水平扩展,即在槽迁移的时候redis也对外提供服务。所以槽由当前节点负责可能出现槽数据迁移一部分的情况。这个时候如果键在当前节点没有查到并不能说明这个键在整个集群是不存在的所以需要在查的时候判断ClusterNode.clusterState.migrating_slots_to [i] :
- 如果该数组第 i 位为 null,那说明这key是真的不存在
- 如果不为null, ClusterNode.clusterState.migrating_slots_to [i] 指向的节点就是正在迁移的节点,如果ClusterNode.clusterState.migrating_slots_to [i] 代表的节点也不存在该key数据 那说明这个key是真的不存在
- 我们知道redisCluster是客户端负载均衡,所以当前节点得返回给客户端一个ACK错误用来标识下这种情况
八.故障转移
假设现在有集群 主节点7000,8000,9000, 从节点7001,8001,9001, 7001 为7000的从~
一.故障发现
当集群内某个节点出现问题时,需要通过某种方式识别出节点是否发生了故障,上文中在节点通信的时候我们知道集群中是用过ping、pong消息实现节点通信的。消息不仅可以传播各自负责的槽节点,还能够知道传播其他状态:主从状态,节点故障等。这里我们主要介绍的是 :主观下线 ,客观下线
1.主观下线
集群中每个节点都会定期向其他节点发送ping消息,接收节点回复ping消息作为响应,如果在cluster-node-timeout时间内没有收到节点回复的pong 。当前节点就把该节点标记为 主观下线(pfail),并在当前的ClusterNode.fail_reports结构记录下记录一个下线报告,记录内容主要有当前被标记的下线记录节点,以及最后上报下线报告的时间
只有一个节点认为主观下线并不能准确判断是否故障,如7000节点判断9000节点为下线,8000判断9000位正常~
2.客观下线
当某个节点判断一个节点主观下线后,相应的节点状态会随着ping、pong在集群内传播,ping、pong每次都会携带集群内1/10的其他节点信息数据,当接受节点发现消息体中含有主观下线的节点状态是,会找到故障节点的ClusterNode.fail_reports结构,保存到下线报告链表中,来刷新当前节点对故障节点的认知
当半数以后持有槽的主节点都标记某个节点是主观下线时,就触发客观下线流程
- 为什么是半数以上持有槽的主节点?为了防止网络分区操作的集群分裂
3.客观下线流程
每个下线报告都存在了有效期,每次在尝试触发客观下线时,都会检查下线报告是否过期,对应过期的下线报告进行删除
- 有效期为 cluster-node-timeout *2,主要是为了防止误报,或者过了一小段时间后被标记下线的节点又好了~
集群中的每个节点在收到其他节点的下线报告时,都会尝试触发客观下线
- 计算有效下线报告数量
- 如果大于槽节点总数的一般(不大于直接退出)
- 更新为客观下线
- 向集群广播下线节点的fail消息
- 收到fail消息的节点,标记故障几点为客观下线
二.故障恢复
故障节点如果为主节点,则需要在他的从节点中选出一个替换它
1.资质检查
检查超时时间是否超过 cluster-node-timeout * cluster-slave-validity-factor,cluster-slave-validity-factor用于从节点的有效因子。默认为10
2.准备选举时间
故障选举为延迟处理,这里主要是从节点之间计算优先级,总的来说就是 延迟低的先触发故障选举,向持有槽的主节点发现选举消息
3选举投票
只有持有槽的主节点才有投票权限,收到票最多的从节点当选为主节点
问题:为什么只有主节点有投票权限?
- 从节点有投票权限的话 会造成,只有一个从节点也会成功选举