redis:持久化
redis:主从复制
redis:哨兵
redis:集群
集群,即Redis Cluster,是Redis 3.0开始引入的分布式存储方案。
集群由多个节点(Node)组成,Redis的数据分布在这些节点中。集群中的节点分为主节点和从节点:只有主节点负责读写请求和集群信息的维护;从节点只进行主节点数据和状态信息的复制。
集群的作用,可以归纳为两点:
集群将数据分散到多个节点,一方面突破了Redis单机内存大小的限制,存储容量大大增加;另一方面每个主节点都可以对外提供读服务和写服务,大大提高了集群的响应能力。
搭建一个简单的集群:所有节点在同一台服务器上,以端口号进行区分;配置从简。3个主节点端口号:7000/7001/7002
节点配置文件,以端口为7002的为例:
# 配置文件进行了精简,完整配置可自行和官方提供的完整conf文件进行对照。端口号自行对应修改
#后台启动的意思
daemonize yes
#端口号
port 7002
# 日志文件
logfile "cluster-node-7002.log"
# 开启集群
cluster-enabled yes
# 集群持久化配置文件,内容包含其它节点的状态,持久化变量等,会自动生成在上面配置的dir目录下
cluster-config-file cluster-node-7002.conf
# 集群节点不可用的最大时间(毫秒),如果主节点在指定时间内不可达,那么会进行故障转移
cluster-node-timeout 5000
# 云服务器上部署需指定公网ip
cluster-announce-ip 公网ip地址
# 允许外部连接
protected-mode no
# 定义生成rdb文件的名称
dbfilename "dump-7002.rdb"
cluster-enabled :一个节点就是一个运行在集群模式下的Redis服务器,Redis服务器在启动时会根据cluster-enabled配置选项是否为yes来决定是否开启服务器的集群模式。
cluster-config-file:该参数指定了集群配置文件的位置。每个节点在运行过程中,会维护一份集群配置文件;每当集群信息发生变化时(如增减节点),集群内所有节点会将最新信息更新到该配置文件;当节点重启后,会重新读取该配置文件,获取集群信息,可以方便的重新加入到集群中。也就是说,当Redis节点以集群模式启动时,会首先寻找是否有集群配置文件,如果有则使用文件中的配置启动,如果没有,则初始化配置并将配置保存到文件中。集群配置文件由Redis节点维护,不需要人工修改。
之后就可以启动服务:例:redis-server redis-7000.conf
然后可以通过cluster nodes命令查看节点的状态:
其中返回值第一项表示节点id,由40个16进制字符串组成,节点id与 主从复制 一文中提到的runId不同:Redis每次启动runId都会重新创建,但是节点id只在集群初始化时创建一次,然后保存到集群配置文件中,以后节点重新启动时会直接在集群配置文件中读取。
其他节点使用相同办法启动,不再赘述。需要特别注意,在启动节点阶段,节点是没有主从关系的,因此从节点不需要加slaveof配置。
节点启动之后都是一个独立的集群,并不知道其他节点的存在,需要进行节点握手,将独立的节点组成一个网络。
节点加入集群命令:
cluster meet ip port
创建集群命令:
redis-cli --cluster create 172.17.0.13:6381 172.17.0.13:6382 --cluster-replicas 1
运行上面命令结果:
查看集群的节点,以7000为例:
至此集群就创建成功了!!!
问题1:
在实际操作中发现 cluster meet ip port 中ip地址如果使用公网ip,那么加入集群的节点就会握手失败。
解决方法:
在配置文件中加入:
# 云服务器上部署需指定公网ip
cluster-announce-ip 公网ip地址
同时注意在云服务器上需要开放端口和安全组,能够外部连接。
问题2:
搭建Redis集群的过程中,执行到cluster create : … 的时候,发现程序在阻塞,显示:Waiting for the cluster to join 的字样,然后就无休无尽的等待… 如图:
解决方法:
开放Redis服务的两个TCP端口。譬如Redis客户端连接端口为6379,而Redis服务在集群中还有一个叫集群总线端口,其端口为客户端连接端口加上10000,即 6379 + 10000 = 16379。所以开放每个集群节点的客户端端口和集群总线端口才能成功创建集群!
客户端端口:客户端访问Redis服务器的端口
集群总线端口:用二进制协议(gossip协议)的点对点集群通信的端口。用于节点的失败侦测、配置更新、故障转移授权,等等。
总而言之,客户端端口提供的是外部客户端访问服务的端口;而集群总线端口是提供集群内部各个Redis服务之间的通信。
设计集群方案时,至少要考虑以下因素:
(1)高可用要求:根据故障转移的原理,至少需要3个主节点才能完成故障转移,且3个主节点不应在同一台物理机上;每个主节点至少需要1个从节点,且主从节点不应在一台物理机上;因此高可用集群至少包含6个节点。
(2)数据量和访问量:估算应用需要的数据量和总访问量(考虑业务发展,留有冗余),结合每个主节点的容量和能承受的访问量(可以通过benchmark得到较准确估计),计算需要的主节点数量。
(3)节点数量限制:Redis官方给出的节点数量限制为1000,主要是考虑节点间通信带来的消耗。在实际应用中应尽量避免大集群;如果节点数量不足以满足应用对Redis数据量和访问量的要求,可以考虑:(1)业务分割,大集群分为多个小集群;(2)减少不必要的数据;(3)调整数据过期策略等。
(4)适度冗余:Redis可以在不影响集群服务的情况下增加节点,因此节点数量适当冗余即可,不用太大。
clusterNode结构保存了一个节点的当前状态,比如节点的创建时间、节点的名字、节点当前的配置纪元、节点的IP地址和端口号等等。
每个节点都会使用-一个clusterNode结构来记录自己的状态,并为集群中的所有其他节点(包括主节点和从节点)都创建-一个相应的clusterNode结构,以此来记录其他节点的状态:
clusterNode结构的link属性是一个clusterLink结构,该结构保存了连接节点所需的有关信息:套接字描述符,输入缓冲区,输出缓存区:
最后,每个节点都保存着-一个clusterState结构,这个结构记录了在当前节点的视
角下,集群目前所处的状态,例如集群是在线还是下线,集群包含多少个节点,集群当前的
配置纪元,诸如此类: .
通过向节点A发送cluster meet ip port 命令,可以让另一个节点B加入A节点的集群中。
收到命令的节点A将于节点B进行握手(handShake)以此来确定彼此的存在,握手流程:
最后,节点A会将节点B的信息通过Gossip协议传播给集群中的其他节点,让其他节点也与节点B进行握手,最终,节点B会被集群中的所有节点认识。
数据分区有顺序分区、哈希分区等,其中哈希分区由于其天然的随机性,使用广泛;集群的分区方案便是哈希分区的一种。
哈希分区的基本思路是:对数据的特征值(如key)进行哈希,然后根据哈希值决定数据落在哪个节点。常见的哈希分区包括:哈希取余分区、一致性哈希分区、带虚拟节点的一致性哈希分区等。
衡量数据分区方法好坏的标准有很多,其中比较重要的两个因素是(1)数据分布是否均匀(2)增加或删减节点对数据分布的影响。由于哈希的随机性,哈希分区基本可以保证数据分布均匀;因此在比较哈希分区方案时,重点要看增减节点对数据分布的影响。
(1)哈希取余分区
哈希取余分区思路非常简单:计算key的hash值,然后对节点数量进行取余,从而决定数据映射到哪个节点上。该方案最大的问题是,当新增或删减节点时,节点数量发生变化,系统中所有的数据都需要重新计算映射关系,引发大规模数据迁移。
(2)一致性哈希分区
一致性哈希算法将整个哈希值空间组织成一个虚拟的圆环,如下图所示,范围为0-2^32-1;对于每个数据,根据key计算hash值,确定数据在环上的位置,然后从此位置沿环顺时针行走,找到的第一台服务器就是其应该映射到的服务器。
与哈希取余分区相比,一致性哈希分区将增减节点的影响限制在相邻节点。以上图为例,如果在node1和node2之间增加node5,则只有node2中的一部分数据会迁移到node5;如果去掉node2,则原node2中的数据只会迁移到node4中,只有node4会受影响。
一致性哈希分区的主要问题在于,当节点数量较少时,增加或删减节点,对单个节点的影响可能很大,造成数据的严重不平衡。还是以上图为例,如果去掉node2,node4中的数据由总数据的1/4左右变为1/2左右,与其他节点相比负载过高。
(3)带虚拟节点的一致性哈希分区
该方案在一致性哈希分区的基础上,引入了虚拟节点的概念。Redis集群使用的便是该方案,其中的虚拟节点称为槽(slot)。槽是介于数据和实际节点之间的虚拟概念;每个实际节点包含一定数量的槽,每个槽包含哈希值在一定范围内的数据。引入槽以后,数据的映射关系由数据hash->实际节点,变成了数据hash->槽->实际节点。
在使用了槽的一致性哈希分区中,槽是数据管理和迁移的基本单位。槽解耦了数据和实际节点之间的关系,增加或删除节点对系统的影响很小。仍以上图为例,系统中有4个实际节点,假设为其分配16个槽(0-15); 槽0-3位于node1,4-7位于node2,以此类推。如果此时删除node2,只需要将槽4-7重新分配即可,例如槽4-5分配给node1,槽6分配给node3,槽7分配给node4;可以看出删除node2后,数据在其他节点的分布仍然较为均衡。
槽的数量一般远小于2^32,远大于实际节点的数量;在Redis集群中,槽的数量为16384。
下面这张图很好的总结了Redis集群将数据映射到实际节点的过程:
Redis集群通过分片的方式来保存数据库中的键值对:集群的整个数据库被分为16384个槽( slot ),数据库中的每个键都属于这16384个槽的其中-一个,集群中的每个节点可以处理0个或最多16384 个槽。
当数据库中的16384个槽都有节点在处理时,集群处于上线状态(ok);相反地,如果数据库中有任何一个槽没有得到处理,那么集群处于下线状态( fail )。
(1)指派槽
通过向节点发送CLUSTER ADDSLOTS命令,我们可以将一个或多个槽指派(assign)给节点:
CLUSTER ADDSLOTS < slot > [slot …]
例:
127.0.0.1:8000> CLUSTER ADDSLOTS 0 1 2 … 10000
(2)保存槽信息
clusterNode结构的slots,numslots属性保存槽的相关信息。
slots属性是一个二进制位数组,这个数组的大小是16284/8=2048个字节,共包含16384个二进制位。
redis以0为起始索引,16383为终止索引,slots[i] = 1表示该节点负责槽i,=0表示不负责槽i
numslots属性则记录节点负责处理槽的数量,即slots数组中值为1的数量。
(3)传播槽信息
一个节点除了会将自己负责处理的槽记录在clusterNode结构的slots属性和numslots属性外,他还会将自己的slots数组通过消息发送给集群中的其他节点,以此来告知其它节点自己目前负责处理哪些槽。
当节点A通过消息从节点B那里接收到节点B的slots数组时,节点A会在自己的clusterState. nodes字典中查找节点B对应的clusterNode结构,并对结构中的slots数组进行保存或者更新。
因为集群中的每个节点都会将自己的slots数组通过消息发送给集群中的其他节点,并且每个接收到slots数组的节点都会将数组保存到相应节点的clusterNode结构里面。因此,集群中的每个节点都会知道数据库中的16384个槽分别被指派给了集群中的哪些节点。
(4)保存集群中所有槽信息
clusterState结构中slots数组记录集群中所有16384个槽的指派信息:
slots数组包含16384个项,每个数组项都是一个指向clusterNode结构的指针。如果为null表示这个槽没有分派给任何节点。
CLUSTER ADDSLOTS命令接受一个或多个槽作为参数,并将所有输入的槽指派给接收该命令的节点负责。
在对数据库中的16384个槽都进行了指派之后,集群就会进入上线状态,这时客户端就
可以向集群中的节点发送数据命令了。
客户端向节点发送与数据库键有关命令时,接收命令的节点会计算出命令要处理键属于哪个槽,并检查这个槽是否指派给自己:
以上代码就是计算key属于按个槽的代码,其中CRC16(key)语句计算key的CRC-16校验和。
使用cluster keyslot < key > 命令可以查看key属于哪个槽。
找到槽之后,根据clusterState中保存的slots数组判断当前节点是否是管理该槽的。如果不是则向客户端返回Moved错误,指引客户端转向管理该槽的节点,然后重新发送命令。
节点发现要处理的命令的key的槽不在自己的节点上,就会返回MOVED错误。
格式:
MOVED slot ip port
slot是处理键的槽,ip,port是这个管理这个槽的节点。
一个集群客户端通常会与集群中的多个节点创建套接字连接,而所谓的节点转向就是换一个套接字来发送命令。
如果客户端尚未与想要转向的节点创键套接字连接,那么客户端会先根据MOVED错误提供的IP地址和端口号来连接节点,然后再进行转向,之后再重新发送命令。
集群下的节点和单机服务器在存储键值对,对键值对过期时间处理方面都相同。
它们之间的一个区别就是集群下的节点只能使用一个数据库,而单机服务器没有这个限制。
节点除了将键值对保存到数据库中之外,节点还会用clusterState结构中的slots_to_keys跳跃表(有序的按分值排序,分值相同的按对象排序,对象不可以重复,分值可以重复)来保存槽和键之间的关系。
slots_to_keys 跳跃表每个节点的分值(score)都是一个槽号,而每个节点的成员对象都是一个数据库键:
Redis集群的重新分片操作可以将任意数量已经指派给某个节点(源节点)的槽改为指派给另一个节点(目标节点),并且相关槽所属的键值对也会从源节点被移动到目标节点。
重新分片操作可以在线( online)进行,在重新分片的过程中,集群不需要下线,并且源节点和目标节点都可以继续处理命令请求。
Redis集群的重新分片操作由redis的集群管理软件redis-trib负责执行的,redis提供了进行重新分片所需的所有命令,而redis-trib则通过向源节点和目标节点发送命令来进行重新分片操作。
redis-trib对集群的单个槽slot进行重新分片的步骤如下:
如果分片涉及多个槽,那么redis-trib会对每一个给的的槽执行以上步骤。
clusterState结构的importing_slots_from 数组记录了当前节点正在从其他节点导入的槽。
如果importing_solts_from[i] 的值不为null,而是指向一个clusterNode结构,那么表示当前节点正在从clusterNode所代表的节点导入槽i。
clusterState 结构的migrating_slots_to数组记录了当前节点正在迁移至其他节点的槽:
如果migrating_slots_to[i]的值不为null,而是指向一个clusterNode结构,那么表示当前节点正在将槽i迁移至clusterNode所代表的节点。
在重新分片期间,源节点目标节点迁移一个槽的过程中,可能出现一中情况:属于被迁移槽的一部分键值对保存在源节点,一部分保存在目标节点中。
当客户端向源节点发送一个与数据库键有关的命令,并且命令要处理的数据库键恰好就属于正在被迁移的槽时:
如上图所示,当出现问题时,节点向客户端返回ASK错误,接到ASK错误的客户端会根据错误提供的IP地址和端口号,转向至正在导入槽的目标节点,然后首先向目标节点发送一个ASKING命令,之后再重新发送原本想要执行的命令。
ASKING 命令唯一做的就是打开发送改命令的客户端的REDIS_ASKING标识。
为什么要首先发送一个ASKING命令,然后在发送想要执行的命令呢?
在一般情况下,如果客户端向节点发送一个关于槽i的命令,而槽i又没有指派给这个节点的话,那么节点将向客户端返回一个MOVED错误(然后转向)。但是,如果节点的clusterState.importing_slots_from[i] 显示节点正在导入槽i,并且发送命令的客户端带有REDIS_ASKING标识,那么节点将破例执行这个关于槽i的命令一次。
ASK错误和MOVED错误都会导致客户端转向,它们区别在于:
Redis集群中的节点分为主节点( master)和从节点( slave),其中主节点用于处理槽,而从节点则用于复制某个主节点,并在被复制的主节点下线时,代替下线主节点继续处理命令请求。
向一个节点发送命令:
cluster replicate < node_id>
可以让接受命令的节点成为node_id所指定节点的从节点,并开始对主节点进行复制:
一个节点成为从节点,并开始复制某个主节点这一信息会通过消息发送给集群中的其他节点,最终集群中的所有节点都会知道某个从节点正在复制某个主节点。
集群中所有节点都会在代表主节点的clusterNode结构的slaves属性和numslaves属性中记录正在复制这个主节点的从节点名单:
struct clusterNode{
//正在复制这个主节点的从节点数量
int numslaves
//一个数组,每个数组项指向一个正在复制这个主节点的从节点的clusterNode结构
struct clusterNode **slaves;
}
集群中的每个节点都会定期地向集群中的其他节点发送PING消息,以此来检测对方是否在线,如果接收PING消息的节点没有在规定的时间内,向发送PING消息的节点返回PONG消息,那么发送PING消息的节点就会将接收PING消息的节点标记为疑似下线( probable fail, PFAIL )。
集群中的各个节点会通过互相发送消息的方式来交换集群中各个节点的状态信息,节点的状态:在线,疑似下线(PFAIL),已下线(FAIL)。
当一个主节点A通过消息得知主节点B认为主节点C进入了疑似下线状态时,主节点A会在自己的clusterState.nodes字典中找到主节点C所对应的clusterNode结构,并将主节点B的下线报告添加到cluster结构的fail_reports链表里面:
struct clusterNode{
//一个链表记录了所有其他节点对该节点的下线报告
list *fail_reports;
}
下线报告由一个clusterNodeFailReport结构表示:
struct clusterNodeFailReport{
//报告目标节点以及下线的节点
struct clusterNode *node;
//最后一次从node节点收到下线报告的时间
//程序使用这个时间戳来检查下线报告是否过期,与当前时间相差太大的下线报告会被删除
mstime_t time;
}
如果在一个集群里面,半数以上负责处理槽的主节点都将某个主节点x报告为疑似下线,那么这个主节点x将被标记为已下线(FAIL),将主节点x标记为已下线的节点会向集群广播一条关于主节点x的FAIL消息,所有收到这条FAIL消息的节点都会立即将主节点x标记为已下线。
当一个从节点发现自己正在复制的主节点进人了已下线状态时,从节点将开始对下线主节点进行故障转移,以下是故障转移的执行步骤:
集群选举新的主节点的方法:
这种选举新主节点的方法和选举领头Sentinel的方法非常相似,两者都是基于Raft算法的领头选举方法实现的。
在哨兵系统中,节点分为数据节点和哨兵节点:前者存储数据,后者实现额外的控制功能。在集群中,没有数据节点与非数据节点之分:所有的节点都存储数据,也都参与集群状态的维护。为此,集群中的每个节点,都提供了两个TCP端口:
节点间通信,按照通信协议可以分为几种类型:单对单、广播、Gossip协议等。重点是广播和Gossip的对比。
广播是指向集群内所有节点发送消息;优点是集群的收敛速度快(集群收敛是指集群内所有节点获得的集群信息是一致的),缺点是每条消息都要发送给所有节点,CPU、带宽等消耗较大。
Gossip协议的特点是:在节点数量有限的网络中,每个节点都“随机”的与部分节点通信(并不是真正的随机,而是根据特定的规则选择通信的节点),经过一番杂乱无章的通信,每个节点的状态很快会达到一致。Gossip协议的优点有负载(比广播)低、去中心化、容错性高(因为通信有冗余)等;缺点主要是集群的收敛速度慢。
集群中的各个节点通过发送和接收消息来进行通信,发送消息的节点称为sender(发送者),接收消息的称为receiver(接收者)。
节点发送的消息主要有五种:
所有的消息都由:消息头(header)+ 消息正文(data) 组成。
集群
cluster info :打印集群的信息
cluster nodes :列出集群当前已知的所有节点( node),以及这些节点的相关信息。
节点
cluster meet :将 ip 和 port 所指定的节点添加到集群当中,让它成为集群的一份子。
cluster forget
cluster replicate
cluster saveconfig :将节点的配置文件保存到硬盘里面。
槽(slot)
cluster addslots [slot …] :将一个或多个槽( slot)指派( assign)给当前节点。
cluster delslots [slot …] :移除一个或多个槽对当前节点的指派。
cluster flushslots :移除指派给当前节点的所有槽,让当前节点变成一个没有指派任何槽的节点。
cluster setslot node
cluster setslot migrating
cluster setslot importing
cluster setslot stable :取消对槽 slot 的导入( import)或者迁移( migrate)。
键
cluster keyslot :计算键 key 应该被放置在哪个槽上。
cluster countkeysinslot :返回槽 slot 目前包含的键值对数量。
cluster getkeysinslot :返回 count 个 slot 槽中的键
由于集群中的数据分布在不同节点中,导致一些功能受限,包括:
(1)key批量操作受限:例如mget、mset操作,只有当操作的key都位于一个槽时,才能进行。针对该问题,一种思路是在客户端记录槽与key的信息,每次针对特定槽执行mget/mset;另外一种思路是使用Hash Tag,将在下一小节介绍。
(2)keys/flushall等操作:keys/flushall等操作可以在任一节点执行,但是结果只针对当前节点,例如keys操作只返回当前节点的所有键。针对该问题,可以在客户端使用cluster nodes获取所有节点信息,并对其中的所有主节点执行keys/flushall等操作。
(3)事务/Lua脚本:集群支持事务及Lua脚本,但前提条件是所涉及的key必须在同一个节点。Hash Tag可以解决该问题。
(4)数据库:单机Redis节点可以支持16个数据库,集群模式下只支持一个,即db0。
(5)复制结构:只支持一层复制结构,不支持嵌套。
Hash Tag原理是:当一个key包含 {} 的时候,不对整个key做hash,而仅对 {} 包括的字符串做hash。
Hash Tag可以让不同的key拥有相同的hash值,从而分配在同一个槽里;这样针对不同key的批量操作(mget/mset等),以及事务、Lua脚本等都可以支持。不过Hash Tag可能会带来数据分配不均的问题,这时需要:(1)调整不同节点中槽的数量,使数据分布尽量均匀;(2)避免对热点数据使用Hash Tag,导致请求分布不均。
下面是使用Hash Tag的一个例子;通过对product加Hash Tag,可以将所有产品信息放到同一个槽中,便于操作。
cluster_node_timeout参数在前面已经初步介绍;它的默认值是15s,影响包括:
(1)影响PING消息接收节点的选择:值越大对延迟容忍度越高,选择的接收节点越少,可以降低带宽,但会降低收敛速度;应根据带宽情况和应用要求进行调整。
(2)影响故障转移的判定和时间:值越大,越不容易误判,但完成转移消耗时间越长;应根据网络状况和应用要求进行调整。
前面提到,只有当16384个槽全部分配完毕时,集群才能上线。这样做是为了保证集群的完整性,但同时也带来了新的问题:当主节点发生故障而故障转移尚未完成,原主节点中的槽不在任何节点中,此时会集群处于下线状态,无法响应客户端的请求。
cluster-require-full-coverage参数可以改变这一设定:如果设置为no,则当槽没有完全分配时,集群仍可以上线。参数默认值为yes,如果应用对可用性要求较高,可以修改为no,但需要自己保证槽全部分配。
集群相关命令1
集群相关命令2
云服务器Redis集群部署及客户端通过公网IP连接问题
编程迷思—集群
《redis的设计与实现》