你了解Redis集群中的秘密吗?
来源:左耳君(ID:qaqzuoer)
作者: 湿兄
前言
在之前的Redis系列文章中,介绍了Redis的持久化、主从复制以及哨兵机制,主从复制+哨兵机制,既可以解决主master和从slave的解耦合实现多服务器备份数据,又可以通过哨兵机制来解决主宕机之后的主从切换,来实现高可用,但是,上面这种方式仍然有不足,最主要的问题就是存储能力受到单机限制,以及无法实现写操作的负载均衡(写操作均由主来完成)
在Redis3.0在开始使用引入的分布式存储方案,即Redis集群cluster,集群是由多个节点组成的,Redis集群的数据分区在这些节点中,节点分为主节点和从节点,一个集群有多个主节点和从节点,只有主节点负责读写请求和集群的维护,而从节点只负责主节点数据和状态信息的复制
集群的搭建
这一部分我们来一起搭建一个含有6个节点的简单的集群,三主三从,可以选择所有节点在同一台服务器上,也可以选择不在同一服务器上,这里演示以同一服务器为准,以端口号不同来区分主从,配置从简,三个主节点端口号为:6001、6002、6003,三个对应从节点端口号为7001、7002、7003,搭建集群大致需要四个步骤:
(1)启动节点:以集群的模式启动相应的Redis节点,节点独立
(2)节点握手:节点之间握手,形成一个网络
(3)分配槽位:将16384个槽位分配给多个主节点
(4)指定从节点:为每一个主节点指定相应的从节点
1、启动节点
其实启动集群的节点也是和正常节点一样的,也是通过redis-server来启动,但是配置文件需要修改,以集群的方式来启动,看下配置文件案例
#redis-6001.conf
port 6001
cluster-enabled yes
cluster-config-file "node-6001.conf"
logfile "log-6001.log"
dbfilename "dump-6001.rdb"
daemonize yes
其中的cluster-enabled和cluster-config-file都是和集群相关的配置;
cluster-enabled配置设置相应的启动模式是单机模式或者集群模式,如果是yes则为按照集群模式启动,no则为单机模式,可以通过info server命令来查看服务器信息,然后会看到redis_mode这一项对应的值是cluster,代表集群模式,如果是standalone,则为单机模式;
cluster-config-file则指定了集群配置文件的位置,每个节点在运行的时候维护一个集群配置文件,这和刚刚上面配置文件是不同的,属于集群特有,每当集群信息发生变化时,集群中的所有节点会将信息更新到集群配置文件中,节点重启的时候也可以重新读取该配置文件,获取到集群的信息
Redis节点启动的时候,首先检查是否含有集群配置文件,如果有则使用集群配置文件进行配置启动,如果没有则初始化配置并且保存到集群配置文件中,集群配置文件是由节点自动维护更新的,不需要人工维护
我们按照上述代码块编辑好配置文件之后,使用正常的 redis-server redis-6001.conf 来启动redis节点,节点成功启动之后,返回值的第一项就是节点id,由40个16进制字符组成,节点id只在集群初始化的时候创建一次,然后保存到配置文件中,下一次启动的时候自动读取使用该id;和主从复制篇中runid不同的是,runid是每次重启都会重新创建,但是节点id只在集群初始化的时候创建一次
2、节点握手
节点启动之后,是互相独立的,并不知道其它节点的存在,需要进行节点的握手,才可以把这些节点组成一个网络,互相知道对方的存在,节点握手使用cluster mett ip port 命令来实现
比如我们现在在端口号为6001上的客户端上执行cluster meet 127.0.0.1 6002,可以完成6001服务器和6002服务器的握手,同理,我们把其余的6003、7001、7002、7003的节点都握手,这样6000节点便可以感受到其余五个服务器了,通过节点之间的通信,每个节点也可以感受到其它的节点的存在
3、分配槽位
集群中有16384个槽位,槽是数据管理和迁移的基本单位,当数据库中的16384个槽位都进行了分配之后,集群处于线程状态,如果有任意一个槽未进行分配,则集群处于下线状态,我们可以通过cluster info命令来查看集群的状态,看到cluster_state的状态是fail,分配槽使用cluster addslots命令,分配槽给主节点
redis-cli -p 6001cluster addslots {0..5461}
redis-cli -p 6002cluster addslots {5462..10922}
redis-cli -p 6003cluster addslots {10923..16383}
此时再用cluster info查看集群状态,处于OK状态
4、指定主从关系
集群中指定主从关系不再像主从复制那样使用slaveof命令,而是使用cluster replicate命令,参数是节点的id,就是我们上面提到的节点id,可以通过cluster node来查看节点id
redis-cli -p 8000 cluster replicate 2d87d7708efa6df6feb3b1e0cce081dd8984420d
redis-cli -p 8001 cluster replicate f1e370caddac65e23477e098863177ba4387a506
redis-cli -p 8002 cluster replicate 171d843413b3d3d0b63f644b6cf07a467108c214
此时再用cluster node查看节点状态,可以看到节点的主从关系已经建立,实际上,前三步完成后便可以对外提供服务,但是需要指定相应的从节点之后,集群才可以提供高可用的服务
集群搭建完毕,开始原理部分
集群内部
我们虽然上面搞会了搭建集群,但是我们还不知道关于集群的内部原理部分,内部原理这块我大体分为数据分区、节点通信、集群状态三部分来说明
1、数据分区:
数据分区有顺序分区,哈希分区等,而哈希分区具有天然的随机性,集群使用的分区方案便是哈希分区的一种。衡量数据分区方法好坏的标准有很多,其中比较重要的两个因素是
- 数据分布是否均匀
- 增加或删减节点对数据分布的影响。
由于哈希的随机性,哈希分区基本可以保证数据分布均匀,因此在比较哈希分区方案时,重点要看增减节点对数据分布的影响。哈希分区又可分为哈希取余分区、一致性哈希分区、带虚拟节点的一致性哈希分区,接下来简单介绍下
哈希取余分区
哈希取余分区思路非常简单:计算key的hash值,然后对节点数量进行取余,从而决定数据映射到哪个节点上。该方案最大的问题是,当新增或删减节点时,节点数量发生变化,系统中所有的数据都需要重新计算映射关系,引发大规模数据迁移
一致性哈希
将整个哈希值空间组织成一个虚拟的圆环,范围为0-2^32-1;对于每个数据,根据key计算hash值,确定数据在环上的位置,然后从此位置沿环顺时针行走,找到的第一台服务器就是其应该映射到的服务器
与哈希取余分区相比,一致性哈希分区将增减节点的影响限制在相邻节点。以上图为例,如果在node1和node2之间增加node5,则只有node2中的一部分数据会迁移到node5;如果去掉node2,则原node2中的数据只会迁移到node4中,只有node4会受影响
一致性哈希分区的主要问题在于,当节点数量较少时,增加或删减节点,对单个节点的影响可能很大,造成数据的严重不平衡。还是以上图为例,如果去掉node2,node4中的数据由总数据的1/4左右变为1/2左右,与其他节点相比负载过高
带虚拟节点的一致性哈希
集群采用的是带虚拟节点的一致性哈希分区,在redis中这里的虚拟节点被称为槽(slot),redis被设计为16384个槽。
槽是介于数据和实际节点之间的虚拟概念;每个实际节点包含一定数量的槽,每个槽包含哈希值在一定范围内的数据。引入槽以后,数据的映射关系由数据hash->实际节点,变成了数据hash->槽->实际节点,在使用了槽的一致性哈希分区中,槽是数据管理和迁移的基本单位。槽解耦了数据和实际节点之间的关系,增加或删除节点对系统的影响很小
举个简单例子说明:我们存取的key会根据crc16的算法得出一个结果,然后把结果对 16384 求余数,这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,通过这个值,去找到对应的插槽所对应的节点,然后直接自动跳转到这个对应的节点上进行存取操作
2、节点通信:
节点通信主要是依靠端口号、Gossip协议来完成的
端口号
我们通过哨兵那一篇知道了哨兵系统中,哨兵节点是不存储数据的,主要是负责控制故障转移;而在集群系统中,所有的节点都是存储数据的,也都参与集群的维护,更像是一个大家庭,大家一起来维护,集群中的每个节点都提供了两个端口号:
- 普通端口:即我们在前面指定的端口(6001这些)。普通端口主要用于为客户端提供服务,在节点间数据迁移时也会使用
- 集群端口:集群端口是普通端口+10000(10000是固定值,无法改变),如6001节点的集群端口为16001。集群端口只用于节点之间的通信,如搭建集群、增减节点等操作时节点间的通信,为了保证集群可以正常工作,可以关闭本地防火墙,不要使用客户端连接集群接口Gossip协议
节点之间通信,主要分为几种类型:单对单、广播、Gossip协议几种类型
单对单,这个应该不用我多哔哔;广播的话,指的是向集群内所有节点发送消息,优点是集群内所有节点获得的集群信息是一致的,速度快,缺点是每条消息都要发送给所有节点,消耗带宽;Gossip协议,在有限的节点网络中,每个节点随机的和部分节点通信(其实并不真随机,而是根据特定的规则选择相应的节点),经过看似无序的通信之后每个节点的状态很快会达到一致,Gossip协议的优点是负载低、去中心化、容错性高;缺点主要是集群的收敛速度慢。
3、集群状态:
集群的状态这个概念是比较广的,包括集群是否处于上线状态、集群中有哪些节点、节点是否可达、槽的状态、节点的主从状态等,那么,集群状态的信息是由谁来维护的呢?又是怎样维护的呢?
集群的状态是由集群中所有节点共同维护的,向上面说的,每个节点对应着有个集群中的端口号,也会生成相应的集群配置文件;节点为了存储集群状态,最主要的是通过clusterNode和clusterState这两种结构维护集群,前者是记录了一个节点的状态,后者是记录了集群整体的状态
clusterNode
clusterNode结构保存了一个节点的当前状态,包括创建时间、节点id、ip和端口号等。每个节点都会用一个clusterNode结构记录自己的状态,并为集群内所有其他节点都创建一个clusterNode结构来记录节点状态
typedef struct clusterNode {
//节点创建时间
mstime_t ctime;
//节点id
char name[REDIS_CLUSTER_NAMELEN];
//节点的ip和端口号
char ip[REDIS_IP_STR_LEN];
int port;
//节点标识:整型,每个bit都代表了不同状态,如节点的主从状态、是否在线、是否在握手等
int flags;
//配置纪元:故障转移时起作用,类似于哨兵的配置纪元
uint64_t configEpoch;
//槽在该节点中的分布:占用16384/8个字节,16384个比特;每个比特对应一个槽:比特值为1,则该比特对应的槽在节点中;比特值为0,则该比特对应的槽不在节点中
unsigned char slots[16384/8];
//节点中槽的数量
int numslots;
…………
} clusterNode;
clusterState
clusterState结构保存了在当前节点视角下,集群所处的状态。主要字段包括:
typedef struct clusterState {
//自身节点
clusterNode *myself;
//配置纪元
uint64_t currentEpoch;
//集群状态:在线还是下线
int state;
//集群中至少包含一个槽的节点数量
int size;
//哈希表,节点名称->clusterNode节点指针
dict *nodes;
//槽分布信息:数组的每个元素都是一个指向clusterNode结构的指针;如果槽还没有分配给任何节点,则为NULL
clusterNode *slots[16384];
…………
} clusterState;
集群方案的设计
设计集群方案时,至少要考虑以下因素:
- 数据量和访问量:估算应用需要的数据量和总访问量(考虑业务发展,留有冗余),结合每个主节点的容量和能承受的访问量(可以通过benchmark得到较准确估计),计算需要的主节点数量
- 高可用要求:根据故障转移的原理,至少需要3个主节点才能完成故障转移,且3个主节点不应在同一台物理机上;每个主节点至少需要1个从节点,且主从节点不应在一台物理机上;因此高可用集群至少包含6个节点
- 节点数量限制:Redis官方给出的节点数量限制为1000,主要是考虑节点间通信带来的消耗。在实际应用中应尽量避免大集群;如果节点数量不足以满足应用对Redis数据量和访问量的要求,可以考虑:(1)业务分割,大集群分为多个小集群;(2)减少不必要的数据;(3)调整数据过期策略等
- 适度冗余:Redis可以在不影响集群服务的情况下增加节点,因此节点数量适当冗余即可,不用太大从节点服务器内部维护了masterhost、masterport两个字段,用于保存主节点服务器的IP和端口信息
关于Redis集群的问题还有很多,下面我会单独肝一篇来分析集群中存在的问题以及解决办法。