一、集群
1、数据分布
分布式数据库首先要解决的就是把整个数据集按照分区规则映射到多个节点的问题,即把数据集划分到多个节点上,每个节点负责整体数据的一个子集。
数据分区规则:
1)哈希分区,节点取余分区,一致性哈希分区 ----->扩容和缩容容易造成问题,重新hash数据
2)redis采用虚拟槽分区,使用分散度良好的哈希函数把所有的数据映射到一个固定范围的整数集合中,整数定义为槽(slot),redis的槽一共有16384个,槽是集群内数据管理和迁移的基本单位,采用大范围的槽的主要目的是为了方便数据拆分和集群扩展。
分区办法:对所有的键根据哈希函数映射到0-16383整数槽内,计算公式:slot=CRC16(key) &16383每一个节点负责维护一部分的槽以及槽里面所映射的数据。
虚拟槽的特点
1,解耦数据和节点的关系,方便集群扩容
2.节点自身维护槽的映射关系,不需要客户端和代理服务维护槽分区元数据
3.支持节点、槽、键之间的映射查询,用于数据路由,在线伸缩等场景
2、搭建集群
主要分为3步,准备节点,节点握手,分配槽
1.准备节点,redis集群最少需要3个主节点才能成功,而每个主节点必须逮有个子节点,所以一般创建6个节点
配置文件主要修改:
port(不同节点的端口)
dir(不同节点的rdb和aof文件)
logfile(不同节点的日志)
pid(不同节点的pid文件)
cluster-enable yes(开启redis cluster)
cluster-config-file 'xx.conf' (集群过程中生成的配置文件,包括增加节点,删除节点等)
cluster-node-timeout (节点超时时间,很重要的参数,会影响故障发现)
2.节点握手,是指一批运行在集群模式下的节点通过Gossip协议彼此通信,达到感知对方的过程,客户端发起命令:cluster meet {ip} {port} ,cluster meet 是一个异步命令,执行之后立刻返回。内部发起与目标节点进行握手通信。
1)节点6379本地创建6380节点信息对象,并发送meet消息。
2)节点6380接受到meet消息后,保存6739节点信息并回复pong消息
3)之后节点6379和6380彼此定期通过ping/pong消息进行正常节点的通信
meet,ping,pong是Gossip协议通信的载体,它的主要作用就是节点之间彼此交换状态数据信息,只需要在集群的任一节点上执行cluster meet命令加入新节点,握手状态会通过消息在集群内传播,这样其他节点会自动发现新节点并发起握手流程。节点握手之后集群还不能正常工作,这是集群处于下线状态,所有数据读写都被禁止,可以通过cluster info 查看集群状态。
3.分配槽,通过命令cluster addslots命令为节点分配槽 cluster addslots {0 ... 16384}
4.从节点复制主节点,cluster replicate 主节点nodeid
5.用redis-trib.rb搭建集群
使用redis-trib.rb create --replicas 127.0.0.1:16483 127.0.0.1:16484 127.0.0.1:16485 ... 之后会自动进行节点握手和槽分配操作
3、节点通信
1.在分布式存储中需要维护节点元数据信息的机制,所谓元数据就是,节点负责哪些数据,是否出现故障等状态信息。常见的元数据维护方式分为:集中式和P2P方式,redis集群采用的是P2P方式的Gossip协议,Gossip协议工作原理就是节点彼此不断通信交换信息,一段时间后所有的节点都会知道集群完整的信息,这种方式类似流言传播。
1)集群中的每个节点都会开辟一个TCP通道,用于节点之间彼此通信,通信端口号在基础端口上加10000
2)每个节点在固定周期内通过待定规则选择几个节点发送Ping消息
3)接收到Ping消息的节点用pong消息作为回应
Gossip协议保证的是,一个节点可能知道集群中全部节点的状态也可能知道部分节点的状态,只要这些节点都能彼此通信,最终他们会达到最终一致性,比如当节点添加,节点删除,主从变化,槽信息改变这类事件发生时,通过一段时间的集群中信息同步,最终会达到一致.
2.Gossip消息:
1) ping消息
集群中交换最频繁的消息,集群内每个节点每秒向多个其他节点发送ping消息,用于检测节点是否在线和交换节点彼此状态信息,Ping消息发送封装了自身节点和部分其他节点的状态数据
2)pong消息
当接收到Ping meet消息时,作为响应消息回复给发送方确认消息正常通信,pong消息内部封装了自身状态数据,节点也可以向集群内广播自身的pong消息来通知整个集群对自身状态进行更新
3)meet消息
用于通知新节点的加入,消息发送者通知接受者加入到集群中,meet消息通信正常后,接收节点会加入集群并进行周期性的Ping,pong消息的交换。
4)fail消息
当节点判定集群内另一个节点下线时,会向集群内广播一个fail消息,其他节点接收到fail消息之后把对应节点更新为下线状态
消息格式:
消息头(包含发送节点自身状态数据,接受者可以通过消息头就可以获取到发送节点相关数据,集群中的所有消息都采用相同的头结构clusterMsg,它包含了发送节点关键信息,如节点id,槽映射,节点标识(主从角色,是否下线))
消息体(消息体clusterMsgData定义发送消息的数据,其中ping meet pong都采用cluster MsgDataGossip数组作为消息体数据,实际消息类型使用消息头的type属性区分,每个消息体包含该节点的多个clusterMsgDataGossip结构数据,用于信息交换),当节点接收到对应的消息的时候,会解析消息内容并根据自身识别情况作出相应的处理,处理流程如下:
1)解析消息头过程,消息头包含了发送节点的信息,如果发送节点是新节点且消息是meet类型,则加入到本地节点列表,如果是已知节点,则尝试更新发送节点的状态,如槽映射,主从角色等状态
2)解析消息体过程,如果消息体的clusterMsgDataGossip数组包含的节点是新节点,则尝试发起meet握手,如果是已知节点,则根据cluster MsgDataGossip中的flags字段判断该节点是否下线,用于故障转移。
3)消息处理完之后恢复Pong消息,内容同样包含消息头和消息体,发送节点接收到回复的Pong消息,采用类似的流程解析处理消息并更新与接收节点最后通信时间,完成一次消息通信。
2.节点选择
虽然Gossip协议的信息交换机制是具有天然的分布式特性,但是它是有成本的,由于内部需要频繁的进行信息交换,而ping/pong是需要携带当前节点和其他节点的一些信息的,势必会加重带宽和计算的负担。redis集群内节点通信采用固定频率(定时任务每秒10次),所以节点每次选择需要通信的节点列表变得非常重要。如果通信节点选择过多,虽然可以做到信息及时交换,但是成本过高;如果节点选择过少,那成本降低,但是消息的实习性就难以保证,从而影响故障判定,新节点发现需求的速度。因此redis的gossip需要兼顾数据交换的实时性和成本开销。
成本开销主要在,选择发送节点,以及每个消息携带的数据量。
1)选择发送消息的节点数量,集群内每个节点维护定时任务默认每秒执行10次,每秒会随机选取5个节点找出最久没有通信的节点发送ping消息,用于保证Gossip信息交换的随机性,每100毫秒都会扫描本地消息列表,如果发现节点最近一次接收Pong消息的时间大于cluster_node_timeout/2,则立刻发送Ping消息,防止该节点信息太长时间未更新,因此cluster_node_timeout参数对消息发送影响非常大。当我们带宽紧张的时候,可以调大这个参数,可以降低消息的发送频率。但是会影响节点发现,故障转移的速度
2)消息数据量,消息头是当前节点的信息,相对固定。主要是消息体,会携带一定数量的其他节点信息用于信息交换,携带节点数量与集群节点数息息相关,更大的集群每次消息通信的成本也就越高。因此对于Redis集群来说并不是大而全的集群更好。
4、集群伸缩
扩容步骤:
1)准备新节点
2)加入集群 (cluster meet ip port) 加入集群的新节点有两种选择
1、成为主节点并分配槽和数据
2、成为从节点
可以使用redis-trib.rb 实现 redis-trib.rb add-node new_host:new_port existing_host:existing_port --slave --master-id<arg>,正式环境建议使用redis-trib.rb命令加入新节点,该命令内部会执行新节点状态检查,如果新节点已经加入了其他集群或者包含数据,则放弃集群加入操作。因为如果我们使用cluster meet命令可能导致已经加入其他集群的节点加入到我们的集群,造成数据混乱。
3)迁移槽和数据
首先需要确定槽迁移计划,打算迁移多少个槽过去,迁移哪些槽过去。
迁移数据流程说明:
1)对目标节点发送cluster setslot {slot} importing {sourceNodeId} 命令,让目标节点准备导入槽的数据
2)对源节点发送cluster setslot {slot} migrating{targetNodeId}命令,准备让源节点迁出槽的数据
3)源节点循环执行cluster getkeysinslot{slot}{count}命令,获取count个属于槽{slot}的键
4)在源节点上执行migrate {targetIp} {targetPort} “” 0 {timeout} keys {keys...},把获取的键通过流水线机制批量迁移到目标节点
5)重复步骤3)4)直到槽下的所有的键值数据都迁移到目标节点
6)向集群内所有节点发送cluster setslot {slot} node {targetNodeId}命令,通知槽分配给目标节点。为了保证槽节点映射变更及时传播,需要遍历发送给所有主节点更新被迁移的槽指向新节点
可以使用redis-trib.rb reshard host:port --from <arg> --to <arg> --slots<arg> --yes --timeout<arg> --pipeline <arg>
host:port 集群内任一节点地址
--from 指定源节点的id,如果是多个源节点,使用逗号分隔,如果是all源节点变为集群内所有主节点,在迁移过程中提示用户输入。
--to 需要迁移的目标节点的id,目标节点只能填写一个,在迁移过程中提示用户输入
--slots 需要迁移槽的总数量,在迁移过程中提示用户输入
--yes 当打印出reshard执行计划时,是否需要用户输入Yes确认后再执行reshard
--timeout 控制每次migrate操作的超时时间,默认是60000毫秒
--pipeline 控制每次批量迁移键的数量,默认是10
4)成为子节点
在需要成为子节点的节点上执行 cluster replicate 主节点nodeId,会发起全量复制,并会更新节点状态。
缩容过程:
1)首先需要判断下线的节点是否是持有槽的节点,如果是,则需要把槽给迁移到其他节点上去,保证节点下线后整个集群的槽节点映射的完整性,下线迁移槽,下线节点将本身负责的槽迁移到其他节点,迁移的过程与扩容的时候一致。
2)当下线节点不再负责槽或者本身是丛节点时,就可以通知集群内其他节点忘记下线节点,当所有的节点忘记该节点后可以正常关闭,可以使用命令cluster forget NodeId.这个命令可以使得当前节点忘记下线节点,就是将下线节点添加到拉黑列表中,60秒之内不能去给拉黑列表中的数据发送Gossip消息,所以我们有60秒进行下线处理,但是这个命令需要集群的每个节点都做,容易出问题。我们会优先选择使用 redis-trib.rb del-node {host:port} {downNodeId}命令,这个命令会帮助我们自动去轮寻每个节点,告诉它,忘记下线节点,并自动下线目标节点。
5、请求路由
在集群模式下,redis接收任何键相关命令时首先计算键对应的槽,再根据槽找出对应的节点,如果节点时本身,则处理键命令;否则回复MOVED重定向错误,通知客户端请求正确的节点,这个过程称为MOVED重定向。(MOVED重定向信息中包含对应槽所在的节点ip和port),MOVED命令只负责响应,但是并不会负责转发。
可以知道,键命令执行步骤主要是分为两步:计算槽,查找槽所对应的节点
1)计算槽,是根据对键进行CRC16函数计算出来散列值,再对16384进行取余。使得每个键都可以落在0-16384个槽。
可以使用命令cluster keyslot key 计算Key所对应的的槽
可以使用{KEY} 大括号这样的方式(hash_tag)去包装key,这样可以保证让同样的Key都在同一个槽上
2)槽节点查找
1)傀儡客户端查找 redis中的每个节点通过消息交换可以保存着每个节点负责的槽。保存在clusterState结构中。客户端可以随机连接集群内任一redis节点获取键-这种叫做傀儡客户端,实现比较简单,但是有问题是,需要两次网络开销,因为需要请求两次,第一次不对的话,需要根据MOVED命令返回的地址,再请求一遍。
typedef struct clusterState{
clusterNode *myself;//自身节点 clusterNode代表节点结构体
clusterNode *slots[CLUSTER_SLOTS]; //16384个槽和节点映射数组,数组下标代表对应的槽
}
2)智能客户端查找
也就是客户端本地自己维护slot->node的映射关系。客户端可以在本地的映射关系中就找到槽所在的节点,省去第二次因为MOVED的网络开销,但是客户端本地的映射关系可能是过期的,所以可以通过MOVED指令返回的数据来进行更新映射关系。
6、故障转移