redis学习(八)集群
Redis Cluster是redis的分布式解决方案,采用cluster架构能打倒负载均衡的目的。
数据分布
数据分布理论
分布式数据库首先要解决把整个数据集按照分区规则映射到多个节点的问题,即把数据集划分到多个节点上,每个节点负责整体数据的一个子集。
重点是数据分区规则
一致性哈希分区:
一致性哈希分区(Distributed Hash Table)实现思路是为系统中每个节点分配一个token,范围一般在0~232,这些token构成一个哈希环。数据读写执行节点查找操作时,先根据key计算hash值,然后顺时针找到第一个大于等于该哈希值的token节点。
避免数据映射全部被打乱导致全量迁移。
但是一致性哈希还是有缺点的:
- 加减节点会造成哈希环中部分数据无法命中,需要手动处理或者忽略这部分数据,因此一致性哈希常用于缓存场景。
- 当使用少量节点时,节点变化将大范围影响哈希环中数据映射,因此这种方式不适合少量数据节点的分布式方案。
- 普通的一致性哈希分区在增减节点时需要增加一倍或减去一半节点才能保证数据和负载的均衡。
虚拟槽分区
分布式系统采用虚拟槽对一致性哈希进行改进。
虚拟槽分区巧妙地使用了哈希空间,使用分散度良好的哈希函数把所有数据映射到一个固定范围的整数集合中,整数定义为槽(slot)。
这个范围一般远远大于节点数,比如Redis Cluster槽范围是0~16383。
槽是集群内数据管理和迁移的基本单位。
采用大范围槽的主要目的是为了方便数据拆分和集群扩展。每个节点会负责一定数量的槽
在一个分布式系统中,如果节点数量少,那么每个节点负责的槽数就多,比如100个槽,5个节点,每个节点按照顺序负责处理20个槽,如果10个节点,节点负责的槽数就是10。
集群功能限制,这个非常重要
- key批量操作支持有限。比如mset,mget,目前支持具有相同slot槽值的key执行批量操作。
- key事务操作有限,同理只支持多key在同一节点上的事务操作,当多个key分布在不同的节点上时无法使用事务功能。
- key作为数据分区的最小粒度,因此不能将一个大的键值对象如hash、list等映射到不同的节点。
- 不支持多数据库空间。单机下的Redis可以支持16个数据库,集群模式下只能使用一个数据库空间,即db0。
- 复制结构只支持一层,从节点只能复制主节点,不支持嵌套树状复制结构。
搭建集群
准备节点
Redis集群一般由多个节点组成,节点数量至少为6个才能保证组成完整高可用的集群。每个节点需要开启配置cluster-enabled yes
,让Redis运行在集群模式下。
建议为集群内所有节点统一目录,一般划分三个目录:conf、data、log,分别存放配置、数据和日志相关文件。
原有的配置文件一个属性cluster-config-file
是指明集群配置文件的。
集群模式的Redis除了原有的配置文件之外又加了一份集群配置文件。当集群内节点信息发生变化,如添加节点、节点下线、故障转移等。
节点会自动保存集群状态到配置文件中。需要注意的是,Redis自动维护集群配置文件,不要手动修改,防止节点重启时产生集群信息错乱。
集群配置文件
文件内容记录了集群初始状态,这里最重要的是节点ID,它是一个40位16进制字符串,用于唯一标识集群内一个节点,之后很多集群操作都要借助于节点ID来完成。需要注意是,节点ID不同于运行ID。节点ID在集群初始化时只创建一次,节点重启时会加载集群配置文件进行重用,而Redis的运行ID每次重启都会变化。
cluster nodes命令获取集群节点状态。
节点握手
节点握手是指一批运行在集群模式下的节点通过Gossip协议彼此通信,达到感知对方的过程。节点握手是集群彼此通信的第一步,由客户端发起命令:cluster meet{ip}{port}
分配槽
节点建立握手之后集群还不能正常工作,这时集群处于下线状态,所有的数据读写都被禁止。
可以通过cluster info
命令获取集群当前状态
分配的槽(cluster_slots_assigned)是0,由于目前所有的槽没有分配到节点,因此集群无法完成槽到节点的映射。
只有当16384个槽全部分配给节点后,集群才进入在线状态。
通过cluster addslots命令为节点分配槽。这里利用bash特性批量设置槽。
如redis-cli -h -p cluster addslots {0...5490}
所有的槽都已经分配给节点,执行cluster nodes
命令可以看到节点和槽的分配关系
作为一个完整的集群,每个负责处理槽的节点应该具有从节点,保证当它出现故障时可以自动进行故障转移。
集群模式下,Reids节点角色分为主节点和从节点。
首次启动的节点和被分配槽的节点都是主节点,从节点负责复制主节点槽信息和相关的数据。
使用cluster replicate{nodeId}
命令让一个节点成为从节点。其中命令执行必须在对应的从节点上执行,nodeId是要复制主节点的节点ID
用redis-trib.rb搭建集群
redis-trib.rb是采用Ruby实现的Redis集群管理工具。内部通过Cluster相关命令帮我们简化集群创建、检查、槽迁移和均衡等常见运维操作。
节点通讯
在分布式存储中需要提供维护节点元数据信息的机制,所谓元数据是指:节点负责哪些数据,是否出现故障等状态信息。常见的元数据维护方式分为:集中式和P2P方式。
Redis集群采用P2P的Gossip(流言)协议,Gossip协议工作原理就是节点彼此不断通信交换信息,一段时间后所有的节点都会知道集群完整的信息,这种方式类似流言传播。
通过不断的ping/pong消息通信,经过一段时间后所有的节点都会知道整个集群全部节点的最新状态,从而达到集群状态同步的目的。
集群伸缩
扩容集群,新增节点,迁移槽和数据
使用cluster meet命令新增,
- 对目标节点发送cluster setslot{slot}importing{sourceNodeId}命令,让目标节点准备导入槽的数据。
- 对源节点发送cluster setslot{slot}migrating{targetNodeId}命令,让源节点准备迁出槽的数据。
- 源节点循环执行cluster getkeysinslot{slot}{count}命令,获取count个属于槽{slot}的键。
- 在源节点上执行migrate{targetIp}{targetPort}""0{timeout}keys{keys...}命令,把获取的键通过流水线(pipeline)机制批量迁移到目标节点,批量迁移版本的migrate命令在Redis3.0.6以上版本提供,之前的migrate命令只能单个键迁移。对于大量key的场景,批量键迁移将极大降低节点之间网络IO次数。
- 重复执行步骤3)和步骤4)直到槽下所有的键值数据迁移到目标节
- 向集群内所有主节点发送cluster setslot{slot}node{targetNodeId}命令,通知槽分配给目标节点。为了保证槽节点映射变更及时传播,需要遍历发送给所有主节点更新被迁移的槽指向新节点。
收缩集群
下线节点需要把自己负责的槽迁移到其他节点,原理与之前节点扩容的迁移槽过程一致。
由于集群内的节点不停地通过Gossip消息彼此交换节点状态,因此需要通过一种健壮的机制让集群内所有节点忘记下线的节点。也就是说让其他节点不再与要下线节点进行Gossip消息交换。
Redis提供了cluster forget{downNodeId}
命令实现该功能
请求路由
Redis集群对客户端通信协议做了比较大的修改,为了追求性能最大化,并没有采用代理的方式而是采用客户端直连节点的方式。
请求重定向
在集群模式下,Redis接收任何键相关命令时首先计算键对应的槽,再根据槽找出所对应的节点,如果节点是自身,则处理键命令;否则回复MOVED重定向错误,通知客户端请求正确的节点。这个过程称为MOVED重定向
重定向信息包含了键所对应的槽以及负责该槽的节点地址,根据这些信息客户端就可以向正确的节点发起请求。
使用redis-cli命令时,可以加入-c参数支持自动重定向,简化手动发起重定向操作。
键命令执行步骤主要分两步:计算槽,查找槽所对应的节点。
- Redis首先需要计算键所对应的槽。根据键的有效部分使用CRC16函数计算出散列值,再取对16383的余数,使每个键都可以映射到0~16383槽范围内。
- Redis计算得到键对应的槽后,需要查找槽所对应的节点。集群内通过消息交换每个节点都会知道所有节点的槽信息,内部保存在clusterState结构。
cluster keyslot命令就是采用key_hash_slot函数实现的,
127.0.0.1:6379> cluster keyslot key:test:111
(integer) 10050
127.0.0.1:6379> cluster keyslot key:{hash_tag}:111
(integer) 2515
127.0.0.1:6379> cluster keyslot key:{hash_tag}:222
(integer) 2515
其中键内部使用大括号包含的内容又叫做hash_tag,它提供不同的键可以具备相同slot的功能,常用于Redis IO优化。
例如在集群模式下使用mget等命令优化批量调用时,键列表必须具有相同的slot,否则会报错。这时可以利用hash_tag让不同的键具有相同的slot达到优化的目的。
Pipeline同样可以受益于hash_tag,由于Pipeline只能向一个节点批量发送执行命令,而相同slot必然会对应到唯一的节点,降低了集群使用Pipeline的门槛。
Smart客户端
Smart客户端通过在内部维护slot→node的映射关系,本地就可实现键到节点的查找,从而保证IO效率的最大化,而MOVED重定向负责协助Smart客户端更新slot→node映射。
如JedisCluster
键命令执行流程:
- 计算slot并根据slots缓存获取目标节点连接,发送命令。
- 如果出现连接错误,使用随机连接重新执行键命令,每次命令重试对redi-rections参数减1。
- 捕获到MOVED重定向错误,使用cluster slots命令更新slots缓存(renewSlotCache方法)。
- 重复执行1)~3)步,直到命令执行成功,或者当redirections<=0时抛出Jedis ClusterMaxRedirectionsException异常。
ASK重定向
客户端ASK重定向流程
Redis集群支持在线迁移槽(slot)和数据来完成水平伸缩,当slot对应的数据从源节点到目标节点迁移过程中,客户端需要做到智能识别,保证键命令可正常执行。
例如当一个slot数据从源节点迁移到目标节点时,期间可能出现一部分数据在源节点,而另一部分在目标节点。
ASK重定向流程
1)客户端根据本地slots缓存发送命令到源节点,如果存在键对象则直接执行并返回结果给客户端。
2)如果键对象不存在,则可能存在于目标节点,这时源节点会回复ASK重定向异常。格式如下:(error)ASK{slot}{targetIP}:{targetPort}。
3)客户端从ASK重定向异常提取出目标节点信息,发送asking命令到目标节点打开客户端连接标识,再执行键命令。如果存在则执行,不存在则返回不存在信息。
ASK与MOVED虽然都是对客户端的重定向控制,但是有着本质区别。
ASK重定向说明集群正在进行slot数据迁移,客户端无法知道什么时候迁移完成,因此只能是临时性的重定向,客户端不会更新slots缓存。但是MOVED重定向说明键对应的槽已经明确指定到新的节点,因此需要更新slots缓存。
ASK重定向对单键命令支持得很完善,但是,在开发中我们经常使用批量操作,如mget或pipeline。当槽处于迁移状态时,批量操作会受到影响。
集群环境下对于使用批量操作的场景,建议优先使用Pipeline方式,在客户端实现对ASK重定向的正确处理,这样既可以受益于批量操作的IO优化,又可以兼容slot迁移场景。
故障转移
主观下线:
集群中每个节点都会定期向其他节点发送ping消息,接收节点回复pong消息作为响应。如果在cluster-node-timeout
时间内通信一直失败,则发送节点会认为接收节点存在故障,把接收节点标记为主观下线(pfail)状态。
客观下线:
半数以上的主节点认为故障节点主观下线,触发客观下线流程。
为什么必须是负责槽的主节点参与故障发现决策?
因为集群模式下只有处理槽的主节点才负责读写请求和集群槽等关键信息维护,而从节点只进行主节点数据和状态信息的复制。
故障恢复
故障节点变为客观下线后,如果下线节点是持有槽的主节点则需要在它的从节点中选出一个替换它,从而保证集群的高可用。下线主节点的所有从节点承担故障恢复的义务,当从节点通过内部定时任务发现自身复制的主节点进入客观下线时,将会触发故障恢复流程。
从节点触发流程
Redis集群提供了手动故障转移功能:指定从节点发起转移流程,主从节点角色进行切换,从节点变为新的主节点对外提供服务,旧的主节点变为它的从节点。
在从节点上执行cluster failover
命令发起转移流程,默认情况下转移期间客户端请求会有短暂的阻塞,但不会丢失数据