一、数据分布
如果有一份全量的数据当单机无法满足需求的时候就需要数据分布,数据分布如下图所示:
1. 顺序分布和哈希分布
顺序分区:假如有100份数据,有三个节点顺序分区就要保证每个节点是均衡的,分布如下图所示:
哈希分区:假如有100份数据,对每个数字进行节点取余,分布如下图所示:
2. 数据分布对比
分布方式 | 特点 | 典型产品 |
哈希分布 | 数据分散度高,键值分布业务无关,无法顺序访问,支持批量操作 | 一致性哈希Memcache,Redis Cluster,其他缓存产品 |
顺序分布 | 数据分散度易倾斜,键值业务相关,可顺序访问,支持批量操作 | BigTable HBase |
3. 哈希分布
a. 节点取余分区
- 客户端分片:哈希+取余
- 节点伸缩:数据节点关系变化导致数据迁移
- 迁移数量和节点数量有关:建议翻倍扩容
b.一致性哈希
首先做一个token环(范围是0~2^32),假如在这个token环上有四个节点并为每个节点分配一个token(这个token是有范围的),假如key在node1和node3之间那个这个key就会顺时针的落入到node3节点这就是一致性哈希的原理,如下图所示
- 客户端分片:哈希+顺时针(优化取余)
- 节点伸缩:只影响临近节点但是还是有数据迁移
- 翻倍伸缩:保证最小迁移数据和负载均衡
c. 虚拟槽哈希分布
虚拟槽哈希分布式Redis Cluster使用的分区方式,首先了解下以下几个概念
- 预设虚拟槽:每个槽映射一个数据子集,一般比节点数大(Redis Cluster槽的范围是0~16383)
- 良好的哈希函数:例如CRC16
- 服务端管理节点、槽、数据:例如Redis Cluster
如下图所示,左边是个槽的范围,假设有五个节点,每个节点对应一个槽的范围,当有数据进来时会对key进行一个哈希并对16383进行取余,并发送个Redis Cluster里面的任意一个节点(每个节点中都会记录自己是否负责这个槽,假如计算出来的数据是100发送给node1后,node1发现是自己的槽范围内的就会把数据保存下来,如果node1发现不是自己负责这个数据,会返回负责这个数据的节点通知客户端去将数据发送到这个节点)这就是服务端管理节点、槽、数据的一个模式。
二、Redis Cluster
1. 基本架构
Redis Cluster 是Redis的官方多机部署方案,如下图所示,该集群时由三个Redsi节点组成的,每个节点负责整个集群的一部分数据,每个节点负责的数据多少可能不一样,这三个节点相互连接组成一个对等的集群,他们之间是通过一种特殊的二进制协议交互集群信息。
Redis Cluster将所有数据划分为16384个槽位,每个节点负责其中一部分槽位,槽位的信息存储于每个节点中,当Redis Cluster 的客户端来连接集群时也会得到一份集群槽位的配置信息,这样当客户端要查找某个key时,可以直接定位到目标节点。
客户端为了可以直接定位某个具体key所在的节点,需要缓存槽位相关信息,这样才可以快速准确的定位到相应的节点,同时因为可能会存在客户端和服务器存储槽位信息不一致的情况,还需要纠正机制来实现槽位信息的校验调整。
Redis Cluster的每个节点会将集群的配置信息持久化到配置文件中,所以必须保证配置文件是可写的 而且尽量不要依赖人工修改配置文件。
Redis Cluster基本架构主要有以下几点:
- 节点:redisCluster中有很多节点,每个节点都是负责读写的
- meet:节点之间是相互通信的,meet操作就是完成这个工作的基础
- 指派槽:一个节点只有被指派槽之后才可以进行正常的读写
- 复制:当主节点出现问题时也可以实现主备高可用(是通过节点之间的相互通信的完成的)
特性:
- 复制
- 高可用
- 分片
三、集群伸缩
1. 基本原理
如下图所示,6379~6385都是redis的节点,集群伸缩就是节点上线(如下图中的6385节点)和下线(如图中的6381和6384节点)的过程,当6385节点加入后首先要对它进行一个meet操作,此时它没有任何的操作也就是说没有solt,如果要让6385节点工作起来就需要迁移solt(如下图右侧部分)
集群伸缩=槽和数据在节点之前移动
2.扩容集群
扩容集群有以下几个步骤:
- 准备新节点
- 加入集群
- 迁移槽和数据
a. 新节点
新节点是集群模式、配置和其他节点统一、启动后是孤儿节点,启动后如下图所示,启动命令如下:
redis-server conf /redis-6385.conf
redis-server conf /redis-6386.conf
b. 加入集群
将6385,6386节点加入集群中,需要通过meet来完成,命令如下,加入后的效果如下图所示:
cluster meet 127.0.0.1 6385
cluster meet 127.0.0.1 6386
除了使用上述方法也可以使用redis-trib.rb来加入集群,命令如下,加入后的效果如下图所示:
redis-trib.rb add-node new_host:new_port existing_host:existing_port --slave--master-id<arg>
-- 示例如下
redis-trib.rb add-node 127.0.0.1:6385 127.0.0.1:6379
建议使用这种方式能够避免新节点加入其它集群造成故障
加入集群的作用:
- 为它其迁移槽和数据实现扩容
- 作为从节点负责故障转移
c.迁移槽和数据
迁移槽和数据主要有以下三步
- 槽迁移计划
- 迁移数据
- 添加从节点
槽迁移计划:要达到一个均衡的效果,如下图所示
迁移数据主要分为以下几个步骤
1) 对目标节点发送命令,命令如下,让目标节点准备导入槽的数据
cluster setslot {slot} importing {sourceNodeId}
2) 对源节点发送命令,命令如下,让源节点准备迁移出槽的数据
cluster setslot {slot} migrating {targetNodeId}
3) 对源节点执行循环命令,命令如下,每个获取count个属于槽的键
cluster getkeysinslot {slot} {count}
4) 在源节点上执行命令,命令如下,把指定key迁移
migrate {targetIp} {targetPort} key 0 {timeOut}
5) 重复执行3~4的步骤直到槽下所有数据都迁移到目标节点
6) 向集群内所有的节点发送命令,命令如下,通知槽分配给目标节点
cluster setslot {slot} node {targetNodeId}
redis迁移的单位是槽,当一个槽正在进行迁移时这个槽就处于中间过渡状态(源节点的槽状态为migrating,目标节点的状态为importing)
迁移的流程如下图所示:
key的迁移全过程:
迁移工具 redis-trib 首先会在源节点和目标节点设置好中间过渡状态,然后一次性获取源节点槽位中所有的 key 列表( keysinslot 指令可以部分获取),再挨个 key 进行迁移。每个 key 的迁移过程是以源节点作为目标节点的“客户端”,源节点对当前的 key 执行 dump 指令得到序列化的内容,然后通过“客户端”向目标节点发送 restore 指令携带序列化的内容作为参数,目标节点再进行反序列化就可以将内容恢复到目标节点的内存中,然后返回“客户端” OK,源节点的“客户端” 收到后再把当前节点的 key 删除掉就完成了 key 的迁移全过程。
注意:这里的迁移时同步的,在目标节点执行restore命令到源节点删除key之间,源节点的主线程会处于阻塞的状态,知道key被成功删除。
如果迁移过程中突然出现网络故障,整个槽位的迁移只进行了一半,这是两个节点依旧处于中间过渡状态,待下次迁移工具重新连接上时,会提示用户继续进行迁移。
在迁移过程中,如果每个 key 的内容都很小,migrate 指令会执行的很快,它不会影响客户端的正常访问。如果 key 的内容很大,因为 migrate 指令是阻塞指令会同时导致源节点和目标节点卡顿,影响集群的稳定性。
迁移过程中客户端的访问流程:
如上图所示,首先新旧两个节点对应的槽位都存在部分key数据,客户端先尝试访问旧节点,如果对应的数据还在旧节点里面,那么旧节点正常处理,如果对应的数据不再旧节点里面,那么有两种可能,要么该数据在新节点里面,要么就是不存在。旧节点不知道是那种情况,所以它会想客户端返回一个 -ASK targetNodeIdAddr 的重定向指令,客户端收到这个指令后,先去目标节点执行一个不带任何参数的 ASKING 指令,然后在目标节点载重新执行原先的操作指令。
为什么需要执行一个不带参数的 ASKING 指令呢?
因为在迁移没有完成之前,按理说这个槽位还是不归新节点管理的,如果这个时候向目标节点发送该槽位的指令,节点是不认的,它会向客户端返回一个 -MOVED 重定向指令告诉他去源节点执行。如此就会形成重定向循环。ASKING 指令的目标就是打开目标节点的选项告诉它下一条指令不能不理,而要当成自己的槽位来处理。
从以上的过程可以看出,迁移是会影响服务效率的,同样的指令在正常情况下一个 ttl 就能完成,而在迁移情况下需要3个 ttl 才能完成。
四、客户端路由
1. moved 重定向
moved 指令是用来纠正槽位的,如果我们将指令发送到了错误的节点,该节点发现对应的指令槽位不归自己管理就会将目标节点的地址随同 moved 指令回复给客户端去目标节点访问,这个时候客户端就会刷新自己的槽位关系表然后重试指令,后续所有打在该槽位的指令多会转移到目标节点 ,大致流程如下:
2. ASK 重定向
ASKING 是用来临时纠正槽位的,如果当前槽位正在处于迁移中,指令会先被发送到槽位所在的旧节点。如果旧节点粗在数据那就直接返回结果了 ,如果不存在数据,那么数据数据在新节点里面,要么就是不存在,所以旧节点会通知客户端就新节点尝试拿数据,这时就会给客户端返回一个 asking error 携带上目标节点的地址,客户端收到这个 asking error 后就会去目标节点尝试。客户端不会刷新槽位映射关系,因为它只是纠正该指令的槽位信息,不影响后续指令,大致流程如下图所示:
四、其他
1. 容错
Redis Cluster 可以为每个主节点设置若干个从节点,当主节点故障时集群会自动架构其中某个节点提升为主节点。如果某个主节点没有从节点,那么当它发生故障时集群将完全处于不可用的状态。不过redis也提供了一个参数 cluster-require-full-coverage 可以允许部分节点发生故障,其他节点还可以继续提供对外访问。
2. 网络抖动
为了解决这个问题 Redis Cluster 提供看一种选项 cluster-node-timeout 表示当某个节点持续 timeout 时间失联时,才可以认定该节点出现故障,需要进行主从切换。
cluster-slave-validity-factor 作为倍乘系数放大这个超时时间来宽容容错的紧急程度。如果这个系数大于零,那么主从切换时不会抗拒网络抖动的 。如果这个系数大于1 它就成了主从切换的松弛系数。
3. 集群变更感知
当服务器节点变更时,客户端应该立即得到通知以实时刷新自己的节点关系表,那么客户端是如何得到通知的呢?要分为以下两种情况
- 目标节点挂掉了,客户端会抛出一个 ConnectionError 紧接着会随机挑一个节点来重试,这时被重试的节点会通过 MOVED 指令告知目标槽位被分配到的新的节点地址。
- 运维手动修改了集群信息,将主节点切换到其他节点并将旧的主节点移除出集群,这时打在旧的主节点的指令会收到一个 ClusterDown 的错误告知当前节点所在集群不可用,这时客户端就会关闭所有连接,清空槽位映射关系表,然后向上抛错,待下一条指令过来时就是重新尝试初始化节点信息。
4. 数据倾斜
以下几种情况可能会造成数据倾斜:
- 节点和槽分配不均:可以通过下面的命令查看键值分布和均衡
redis-trib.rb info ip:port --查看节点、槽、键值分布
redis-trib.rb rebalance ip"port -- 进行均衡(谨慎使用)
- 不同槽对应键值数量差异较大:可以通过下面的命令获取槽的键值数量
cluster countkeysinslot {slot} --获取槽对应的键值数量
- 包含bigkey:可以在从节点中执行下面的命令获取bigKey,主要通过优化数据结构来避免这个情况
redis-cli --bigKeys
- 内存相关配置不一致:要定期检查集群的配置来避免这个情况
5. 请求倾斜
热点key(重要的key或者bigKey)会造成请求倾斜,可以从下面几个方面进行优化
- 避免bigKey
- 热键不要使用hash_tag
- 当一致性不高时可以使用本地缓存+MQ