本文将对集群的节点、槽指派、命令执行、重新分片、转向、故障转移、消息等各个方面进行深入拆解。
Redis集群原理总览目的在于掌握什么是Cluster?Cluster分片原理,客户端定位数据原理、故障切换,选主,什么场景使用Cluster,如何部署集群…...
将数据分成多份存在不同实例上
哈希槽与Redis实例映射
复制与故障转移
故障检测
故障转移
选主流程
用表保存键值对和实例的关联关系可行么
重新分配哈希槽
MOVED错误
ASK错误
Gossip消息
实例的通信频率
降低实例间的通信开销
为什么需要Cluster“最近遇到一个糟心的问题,Redis需要保存万个键值对,占用20GB的内存。
我就使用了一台32G的内存主机部署,但是Redis响应有时候非常慢,使用INFO命令查看latest_fork_usec指标(最近一次fork耗时),发现特别高。
”主要是RedisRDB持久化机制导致的,Redis会Fork子进程完成RDB持久化操作,fork执行的耗时与Redis数据量成正相关。
而Fork执行的时候会阻塞主线程,由于数据量过大导致阻塞主线程过长,所以出现了Redis响应慢的表象。
“65哥:随着业务规模的拓展,数据量越来越大。主从架构升级单个实例硬件难以拓展,且保存大数据量会导致响应慢问题,有什么办法可以解决么?
”保存大量数据,除了使用大内存主机的方式,我们还可以使用切片集群。俗话说「众人拾材火焰高」,一台机器无法保存所有数据,那就多台分担。
使用RedisCluster集群,主要解决了大数据量存储导致的各种慢问题,同时也便于横向拓展。
两种方案对应着Redis数据增多的两种拓展方案:垂直扩展(scaleup)、水平扩展(scaleout)。
垂直拓展:升级单个Redis的硬件配置,比如增加内存容量、磁盘容量、使用更强大的CPU。水平拓展:横向增加Redis实例个数,每个节点负责一部分数据。比如需要一个内存24GB磁盘GB的服务器资源,有以下两种方案:
水平拓展与垂直拓展在面向百万、千万级别的用户规模时,横向扩展的Redis切片集群会是一个非常好的选择。
“65哥:那这两种方案都有什么优缺点呢?
”垂直拓展部署简单,但是当数据量大并且使用RDB实现持久化,会造成阻塞导致响应慢。另外受限于硬件和成本,拓展内存的成本太大,比如拓展到1T内存。水平拓展便于拓展,同时不需要担心单个实例的硬件和成本的限制。但是,切片集群会涉及多个实例的分布式管理问题,需要解决如何将数据合理分布到不同实例,同时还要让客户端能正确访问到实例上的数据。什么是Cluster集群Redis集群是一种分布式数据库方案,集群通过分片(sharding)来进行数据管理(「分治思想」的一种实践),并提供复制和故障转移功能。
将数据划分为的slots,每个节点负责一部分槽位。槽位的信息存储于每个节点中。
它是去中心化的,如图所示,该集群有三个Redis节点组成,每个节点负责整个集群的一部分数据,每个节点负责的数据多少可能不一样。
Redis集群架构三个节点相互连接组成一个对等的集群,它们之间通过Gossip协议相互交互集群信息,最后每个节点都保存着其他节点的slots分配情况。
开篇寄语“技术不是万能的,程序员也不是最厉害的,一定要搞清楚,不要觉得「老子天下第一」。一旦有了这个意识,可能会耽误我们的成长。
技术是为了解决问题的,如果说一个技术不能解决问题,那这个技术就一文不值。
不要去炫技,没有意义。”
一个Redis集群通常由多个节点(node)组成,在刚开始的时候,每个节点都是相互独立的,它们都处于一个只包含自己的集群当中,要组建一个真正可工作的集群,我们必须将各个独立的节点连接起来,构成一个包含多个节点的集群。
连接各个节点的工作可以通过CLUSTERMEET命令完成:CLUSTERMEETipport。
向一个节点node发送CLUSTERMEET命令,可以让node节点与ip和port所指定的节点进行握手(handshake),当握手成功时,node节点就会将ip和port所指定的节点添加到node节点当前所在的集群中。
CLUSTERMEETCluster实现原理“65哥:数据切片后,需要将数据分布在不同实例上,数据和实例之间如何对应上呢?
”Redis3.0开始,官方提供了RedisCluster方案实现了切片集群,该方案就实现了数据和实例的规则。RedisCluster方案采用哈希槽(HashSlot,接下来我会直接称之为Slot),来处理数据和实例之间的映射关系。
跟着「码哥字节」一起进入Cluster实现原理探索之旅…...
将数据分成多份存在不同实例上集群的整个数据库被分为个槽(slot),数据库中的每个键都属于这个槽的其中一个,集群中的每个节点可以处理0个或最多个槽。
Key与哈希槽映射过程可以分为两大步骤:
根据键值对的key,使用CRC16算法,计算出一个16bit的值;将16bit的值对执行取模,得到0~的数表示key对应的哈希槽。Cluster还允许用户强制某个key挂在特定槽位上,通过在key字符串里面嵌入tag标记,这就可以强制key所挂在的槽位等于tag所在的槽位。
哈希槽与Redis实例映射“65哥:哈希槽又是如何映射到Redis实例上呢?
”在部署集群的样例中通过clustercreate创建,Redis会自动将个哈希槽平均分布在集群实例上,比如N个节点,每个节点上的哈希槽数=/N个。
除此之外,可以通过CLUSTERMEET命令将、、三个节点连在一个集群,但是集群目前依然处于下线状态,因为三个实例都没有处理任何哈希槽。
可以使用clusteraddslots命令,指定每个实例上的哈希槽个数。
“65哥:为啥要手动制定呢?
”能者多劳嘛,加入集群中的Redis实例配置不一样,如果承担一样的压力,对于垃圾机器来说就太难了,让牛逼的机器多支持一点。
三个实例的集群,通过下面的指令为每个实例分配哈希槽:实例1负责0~哈希槽,实例2负责~哈希槽,实例3负责~哈希槽。
redis-cli-h.16.19.1–pclusteraddslots0,redis-cli-h.16.19.2–pclusteraddslots,redis-cli-h.16.19.3–pclusteraddslots,
键值对数据、哈希槽、Redis实例之间的映射关系如下:
数据、Slot与实例的映射Redis键值对的key「码哥字节」「牛逼」经过CRC16计算后再对哈希槽总个数取模,模数结果分别映射到实例1与实例2上。
切记,当个槽都分配完全,Redis集群才能正常工作。
复制与故障转移“65哥:Redis集群如何实现高可用呢?Master与Slave还是读写分离么?
”Master用于处理槽,Slave节点则通过《Redis主从架构数据同步》方式同步主节点数据。
当Master下线,Slave代替主节点继续处理请求。主从节点之间并没有读写分离,Slave只用作Master宕机的高可用备份。
RedisCluster可以为每个主节点设置若干个从节点,单主节点故障时,集群会自动将其中某个从节点提升为主节点。
如果某个主节点没有从节点,那么当它发生故障时,集群将完全处于不可用状态。
不过Redis也提供了一个参数cluster-require-full-coverage可以允许部分节点故障,其它节点还可以继续提供对外访问。
比如主节点宕机,作为slave的成为Master节点继续提供服务。当下线的节点重新上线,它将成为当前3的从节点。
故障检测”一个节点认为某个节点失联了并不代表所有的节点都认为它失联了。只有当大多数负责处理slot节点都认定了某个节点下线了,集群才认为该节点需要进行主从切换。
Redis集群节点采用Gossip协议来广播自己的状态以及自己对整个集群认知的改变。比如一个节点发现某个节点失联了(PFail),它会将这条信息向整个集群广播,其它节点也就可以收到这点失联信息。
关于Gossip协议可阅读悟空哥的一篇文章:《病*入侵,全靠分布式》
如果一个节点收到了某个节点失联的数量(PFailCount)已经达到了集群的大多数,就可以标记该节点为确定下线状态(Fail),然后向整个集群广播,强迫其它节点也接收该节点已经下线的事实,并立即对该失联节点进行主从切换。
故障转移当一个Slave发现自己的主节点进入已下线状态后,从节点将开始对下线的主节点进行故障转移。
从下线的Master及节点的Slave节点列表选择一个节点成为新主节点。新主节点会撤销所有对已下线主节点的slot指派,并将这些slots指派给自己。新的主节点向集群广播一条PONG消息,这条PONG消息可以让集群中的其他节点立即知道这个节点已经由从节点变成了主节点,并且这个主节点已经接管了原本由已下线节点负责处理的槽。新的主节点开始接收处理槽有关的命令请求,故障转移完成。选主流程“65哥:新的主节点如何选举产生的?
”集群的配置纪元+1,是一个自曾计数器,初始值0,每次执行故障转移都会+1。检测到主节点下线的从节点向集群广播一条CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST消息,要求所有收到这条消息、并且具有投票权的主节点向这个从节点投票。这个主节点尚未投票给其他从节点,那么主节点将向要求投票的从节点返回一条CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,表示这个主节点支持从节点成为新的主节点。参与选举的从节点都会接收CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,如果收集到的票=(N/2)+1支持,那么这个从节点就被选举为新主节点。如果在一个配置纪元里面没有从节点能收集到足够多的支持票,那么集群进入一个新的配置纪元,并再次进行选举,直到选出新的主节点为止。跟哨兵类似,两者都是基于Raft算法来实现的,流程如图所示:
集群Leader选举用表保存键值对和实例的关联关系可行么“65哥,我来考考你:“RedisCluster方案通过哈希槽的方式把键值对分配到不同的实例上,这个过程需要对键值对的key做CRC计算并对哈希槽总数取模映射到实例上。如果用一个表直接把键值对和实例的对应关系记录下来(例如键值对1在实例2上,键值对2在实例1上),这样就不用计算key和哈希槽的对应关系了,只用查表就行了,Redis为什么不这么做呢?”
”使用一个全局表记录的话,假如键值对和实例之间的关系改变(重新分片、实例增减),需要修改表。如果是单线程操作,所有操作都要串行,性能太慢。
多线程的话,就涉及到加锁,另外,如果键值对数据量非常大,保存键值对与实例关系的表数据所需要的存储空间也会很大。
而哈希槽计算,虽然也要记录哈希槽与实例时间的关系,但是哈希槽的数量少得多,只有个,开销很小。
客户端如何定位数据所在实例“65哥:客户端又怎么确定访问的数据到底分布在哪个实例上呢?
”Redis实例会将自己的哈希槽信息通过Gossip协议发送给集群中其他的实例,实现了哈希槽分配信息的扩散。
这样,集群中的每个实例都有所有哈希槽与实例之间的映射关系信息。
在切片数据的时候是将key通过CRC16计算出一个值再对取模得到对应的Slot,这个计算任务可以在客户端上执行发送请求的时候执行。
但是,定位到槽以后还需要进一步定位到该Slot所在Redis实例。
当客户端连接任何一个实例,实例就将哈希槽与实例的映射关系响应给客户端,客户端就会将哈希槽与实例映射信息缓存在本地。
当客户端请求时,会计算出键所对应的哈希槽,在通过本地缓存的哈希槽实例映射信息定位到数据所在实例上,再将请求发送给对应的实例。
Redis客户端定位数据所在节点重新分配哈希槽“65哥:哈希槽与实例之间的映射关系由于新增实例或者负载均衡重新分配导致改变了咋办?
”集群中的实例通过Gossip协议互相传递消息获取最新的哈希槽分配信息,但是,客户端无法感知。
RedisCluster提供了重定向机制:客户端将请求发送到实例上,这个实例没有相应的数据,该Redis实例会告诉客户端将请求发送到其他的实例上。
“65哥:Redis如何告知客户端重定向访问新实例呢?
”分为两种情况:MOVED错误、ASK错误。
MOVED错误MOVED错误(负载均衡,数据已经迁移到其他实例上):当客户端将一个键值对操作请求发送给某个实例,而这个键所在的槽并非由自己负责的时候,该实例会返回一个MOVED错误指引转向正在负责该槽的节点。
GET