深入解析 Redis Cluster 架构与实现(一)
#作者:stackofumbrella
文章目录
- Redis Cluster特点
- Redis Cluster与其它集群模式的区别
- 集群目标
- 性能
- hash tags
- Mutli-key操作
- Cluster Bus
- 安全写入(write safety)
- 集群节点的属性
- 集群拓扑
- 节点间handshake
- 重定向与resharding
- MOVED重定向
- ASK重定向
- 客户端首次链接以及重定向处理
Redis Cluster特点
多主多从,去中心化:从节点作为备用,复制主节点,不做读写操作,不提供服务
不支持处理多个key:因为数据分散在多个节点,在数据量大高并发的情况下会影响性能
支持动态扩容节点:这算是Redis Cluster最大的优点之一
节点之间相互通信,相互选举,不再依赖sentinel:准确来说是主节点之间相互监督,保证及时故障转移
Redis Cluster与其它集群模式的区别
相比较sentinel模式,多个master节点保证主要业务(比如master节点主要负责写)稳定性,不需要搭建多个sentinel实例监控一个master节点
,相比较一主多从的模式,具有自我故障检测,故障转移的特点
,相比较其他两个模式而言,对数据进行分片(sharding),不同节点存储的数据是不一样的。从某种程度上来说,Sentinel模式主要针对高可用(HA),而Cluster模式是不仅针对大数据量,高并发,同时也支持HA。
Redis Cluster的实现机制和原理
集群目标
1)高性能和线性扩展,最大可以支撑到1000个节点,Cluster架构中无Proxy层,Master与slave之间使用异步replication,且不存在操作的merge(即操作不能跨多个nodes,不存在merge层)。
2)一定程度上保证writes的安全性,需要客户端容忍一定程度的数据丢失。集群将会尽可能(best-effort)保存客户端write操作的数据,通常在failover期间,会有短暂时间内的数据丢失(因为异步replication引起),当客户端与少数节点处于网络分区时(network partition),丢失数据的可能性会更高,因节点有效性检测,failover需要更长的时间。
3)只要集群中大多数master可达、且失效的master至少有一个slave可达时,集群都可以继续提供服务。同时replicas migration可以将那些拥有多个slaves的master的某个slave,迁移到没有slave的master下,即将slaves的分布在整个集群相对平衡,尽力确保每个master都有一定数量的slave备份。Redis Cluster集群由多个shard组成,每个shard可以有一个master和多个slaves构成,数据根据hash slots配额分布在多个shard节点上,节点之间建立双向TCP链接用于有效性检测、Failover等,Client直接与shard节点进行通讯,集群暂不提供动态reblance策略。
性能
Redis Cluster并没有提供Proxy层,而是告知客户端将key的请求转发给合适的node。Client保存集群中nodes与keys的映射关系(slots),并保持此数据的更新,所以通常Client总能够将请求直接发送到正确的node上。因为采用异步replication,所以master不会等待slaves保存成功后才向客户端反馈结果,除非显式的指定了WAIT指令。multi-key指令仅限于单个节点内,除了resharding操作外,节点的数据不会在节点间迁移。每个操作只会在特定的一个节点上执行,所以集群的性能为master节点的线性扩展。同时Clients与每个node保持链接,所以请求的延迟等同于单个节点,即请求的延迟并不会因为Cluster的规模增大而受到影响。高性能和扩展性,同时保持合理的数据安全性,是Redis Cluster的设计目标。
hash tags
在计算hash slots时有一个意外的情况,用于支持“hash tags”,hash tags用于确保多个keys能够被分配在同一个hash slot中,每个key都可以包含一个自定义的“tags”,那么在存储时将根据tags计算此key应该分布在哪个node上(而不是使用key计算,但是存储层面仍然是key)。此特性可以强制某些keys被保存在同一个节点上,用于支持multi-key操作。hash tags的实现比较简单,key中“{}”之间的字符串就是当前key的hash tags,如果存在多个“{}”,首个符合规则的字符串作为hash tags,如果“{}”存在多级嵌套,那最内层首个完整的字符串作为hash tags,比如“{foo}.student”,那么“foo”是hash tags。如果key中存在合法的hash tags,那么在计算hash slots时,将使用hash tags,而不再使用原始的key,即“foo”与“{foo}.student”将得到相同的slot值,不过“{foo}.student”仍作为key来保存数据,即redis中数据的key仍为“{foo}.student”。
Mutli-key操作
Redis单实例支持的命令,Cluster也都支持,但是对于“multi-key”操作(即一次RPC调用需要进行多个key的操作)比如Set类型的交集、并集等,则要求这些key必须属于同一个node。Cluster不能进行跨Node操作,也没有node提供merge层代理。在人工对slots进行resharding期间,multi-key操作可能不可用。比如这些keys不存在于同一个slot(迁移会导致keys被分离)。比如Multikeys逻辑上属于同一个slot,但是因为resharding,它们可能暂时不处于同一个node,有些可能在迁移的目标节点上(比如Multikeys包含a、b、c三个keys,逻辑上它们都属于slot 8,但是其中c在迁移期间创建,它被存储在节点B上,a、b仍然在节点A),此时将会向客户端返回“-
TRYAGAIN”错误,那么客户端此后将需要重试一次,或者直接返回错误(如果迁移操作被中断),无论如何最终Multikeys的访问逻辑是一致的,slots的状态也是最终确定的。
Cluster Bus
集群中node负责存储数据,保持集群的状态,包括keys与node的对应关系(内部其实为slots与node对应关系)。node也能够自动发现其他的node,检测失效的节点,当某个master失效时还能将合适的slave提升为master。为了达成这些行为,集群中的每个节点都通过TCP与其他所有node建立连接,它们之间的通信协议和方式称为“Redis Cluster Bus”。 每个Node都有一个特定的TCP端口,用来接收其他nodes的链接,此端口号为面向Client的端口号+10000,比如客户端端口号为6379,那么node的Bus端口号为16379,客户端端口号可以在配置文件中声明。由此可见,node之间的交互通讯是通过Bus端口进行,使用gossip协议向其他node传播集群信息,以达到自动发现的特性,通过发送ping来确认其他node工作正常,也会在合适的时机发送集群信息。当然在Failover时(包括人为failover)也会使用Bus来传播消息。
gossip最终一致性,分布式服务数据同步算法,node首先需要知道(可以读取配置)集群中至少一个seed node,此node向seed发送ping请求,此时seed节点pong返回自己已知的所有node列表,然后node解析列表并与它们都建立tcp连接,同时也会向每个node发送ping,并从它们的pong结果中merge出全局node列表,并逐步与所有的node建立连接,数据传输的方式也是类似,网络拓扑结构为full mesh。
安全写入(write safety)
在Master-slaves之间使用异步replication机制,在failover之后,新的Master将会最终替代旧的master。在出现网络分区时(network partition),总会有个窗口期(node timeout)可能会导致数据丢失。不过,Client与多数派Master、少数派Master处于一个分区(网络分区,因为网络阻断问题,导致Clients与Nodes被隔离成两部分)时,这两种情况下影响并不相同。
1)write提交到master,master执行完毕后向Client反馈“OK”,不过此时可能数据还没有传播给slaves(异步replication),如果此时master不可达的时间超过阀值(node timeout),那么将触发slave被选举为新的Master(即Failover),这意味着那些没有replication到slaves的writes将永远丢失了。
2)还有一种情况导致数据丢失:
A)因为网络分区,此时master不可达,且Master与Client处于一个分区,且是少数派分区;
B)Failover机制,将其中一个slave提升为新Master;
C)此后网络分区消除,旧的Master再次可达,此时它将被切换成slave;
D)那么在网络分区期间,处于少数派分区的Client仍然将write提交到旧的Master,因为它们觉得Master仍然有效,当旧的Master再次加入集群,切换成slave之后,这些数据将永远丢失。
在第二种情况下,如果Master无法与其他大多数Masters通讯的时间超过阀值后,此Master也将不再接收Writes,自动切换为readonly状态。当网络分区消除后,仍然会有一小段时间,客户端的write请求被拒绝,因为此时旧的Master需要更新本地的集群状态、与其他节点建立连接、角色切换为slave等等,同时Client端的路由信息也需要更新。只有当此master与大多数其他master不可达的时间达到阀值时,才会触发Failover,这个时间称为NODE_TIMEOUT,可以通过配置设定。所以当网络分区在此时间内被消除的话,writes不会有任何丢失。反之,如果网络分区持续时间超过此值,处于“小分区”(minority)端的Master将会切换为readonly状态,拒绝客户端继续提交writes请求,那么“大分区”端将会进行failover,这意味着NODE_TIMEOUT期间发生在“小分区”端的writes操作将丢失。
集群节点的属性
集群中每个节点都有唯一的名字,称之为node ID,一个160位随机数字的16进制表示,在每个节点首次启动时创建。每个节点都将各自的ID保存在实例的配置文件中,此后将一直使用此ID,或者说只要配置文件不被删除,或者没有使用“CLUSTER RESET”指令重置集群,那么此ID将永不会修改。集群通过node ID来标识节点,而不是使用IP + port,因为node可以修改它的IP和port,如果ID不变,仍然认定它是集群中合法一员。集群可以在
cluster bus中通过gossip协议来探测IP、Port的变更,并重新配置。
node ID并不是与node相关的唯一信息,不过是唯一一个全局一致的。每个node还持有如下相关的信息,有些信息是关系集群配置的,其他的信息比如最后ping时间等。每个node也保存其他节点的IP、Port、flags(比如flags表示它是master还是slave)、最近ping的时间、最近pong接收时间、当前配置的epoch、链接的状态,最重要的是还包含此node上持有的hash slots。这些信息均可通过“CLUSTER NODES”指令开查看。
集群拓扑
Redis Cluster中每个node都与其他node的Bus端口建立TCP链接(full mesh,全网)。比如在有N个节点的集群中,每个node有N-1个向外发出的TCP链接,以及N-1个其他node发过来的TCP链接。这些TCP链接总是keepalive,不是按需创建的。如果ping发出之后,node在足够长的时间内仍然没有pong响应,那么此node将会被标记为“不可达”,那么与此node的链接将会被刷新或者重建。Nodes之间通过gossip协议和配置更新的机制,来避免每次都交互大量的消息,最终确保在nodes之间的信息传送量是可控的。
节点间handshake
Nodes通过Bus端口发送ping、pong,如果一个节点不属于集群,那么它的消息将会被其他node全部丢弃。一个节点被认为是集群成员的方式有2种:
1)如果此node在“Cluster meet”指令中引入,此命令的主要意义就是将指定node加入集群。那么对于当前节点,将认为指定的node为“可信任的”(此后将会通过gossip协议传播给其他node)。
2)当其他node通过gossip引入了新的node,这些node也是被认为是“可信任的”。
只要将一个节点加入集群,最终此节点将会与其他节点建立链接,即cluster可以通过信息交换来自动发现新的节点,链接拓扑仍然是full mesh。
重定向与resharding
MOVED重定向
因为redis并不提供Proxy机制,当Client将请求发给错误的node时(此node上不存在此key所属的slot),node将会反馈“MOVED”或“ASK”错误信息,以便Client重新定向到合适的node。理论上,Client可以将请求随意发给任何一个node,包括slaves,此node解析query,如果可以执行(比如语法正确,multiple keys都应该在一个node slots上),它会查看key应该属于哪个slot、以及此slot所在的node,如果当前node持有此slot,那么query直接执行即可,否则当前node将会向Client反馈“MOVED”错误。错误信息中包括此key对应的slot(3999),以及此slot所在node的ip和port,对于Client 而言,收到MOVED信息后,它需要将请求重新发给指定的node。不过,当node向Client返回MOVED之前,集群的配置也在变更(节点调整、resharding、failover等,可能会导致slot的位置发生变更),此时Client可能需要等待更长的时间,不过最终node会反馈MOVED信息,且信息中包含指定的新的node位置。虽然Cluster使用ID标识node,但是在MOVED信息中尽可能的暴露给客户端便于使用的ip + port。
当Client遇到“MOVED”错误时,将会使用“CLUSTER NODES”或“CLUSTER SLOTS”指令获取集群的最新信息,主要是nodes与slots的映射关系。因为遇到MOVED,一般也不会仅仅一个slot发生的变更,通常是一个或者多个节点的slots发生了变化,所以进行一次全局刷新是有必要的。Client将会把集群的这些信息缓存,以便提高query的性能。还有一个错误信息“ASK”,它与“MOVED”都属于重定向错误,客户端的处理机制基本相同,只是ASK不会触发Client刷新本地的集群信息。
ASK重定向
MOVED重定向与ASK非常相似。在resharding期间,为什么不能用MOVED?MOVED意思为hash slots已经永久被另一个node接管,接下来相应的查询应该与它交互,ASK的意思是当前query暂时与指定的node交互。在迁移期间,slot 8的keys有可能仍在A上,所以Client的请求仍然需要首先经由A,对于A上不存在的,才需要到B上进行尝试。迁移期间,Redis Cluster并没有粗暴的将slot 8的请求全部阻塞直到迁移结束,这种方式尽管不再需要ASK,但是会影响集群的可用性。
1)当Client接收到ASK重定向,它仅仅将当前query重定向到指定的node,此后的请求仍然交付给旧的节点。
2)客户端并不会更新本地的slots映射,仍然保持slot 8与A的映射,直到集群迁移完毕,且遇到MOVED重定向。一旦slot 8迁移完毕之后(集群的映射信息也已更新),如果Client再次在A上访问slot 8时,将会得到MOVED重定向信息,此后客户端也更新本地的集群映射信息。
客户端首次链接以及重定向处理
可能有些Cluster客户端的实现,不会在内存中保存slots映射关系(即node与slots的关系),每次请求都从声明的、已知的node中,随机访问一个node,并根据重定向(MOVED)信息来寻找合适的node,这种访问模式,通常是非常低效的。当然,Client应该尽可能的将slots配置信息缓存在本地,不过配置信息也不需要绝对的实时更新,因为在请求时偶尔出现“重定向”,Client也能兼容此次请求的正确转发,此时再更新slots配置,所以Client通常不需要间歇性的检测Cluster中配置信息是否已经更新,客户端通常是全量更新slots配置:
1)首次链接到集群的某个节点;
2)当遇到MOVED重定向消息时。
遇到MOVED时,客户端仅仅更新特定的slot是不够的,因为集群中的reshard通常会影响到多个slots。客户端通过向任意一个node发送“CLUSTER NODES”或“CLUSTER SLOTS”指令均可以获得当前集群最新的slots映射信息,“CLUSTER SLOTS”指令返回的信息更易于Client解析。如果集群处于broken状态,即某些slots尚未被任何nodes覆盖,指令返回的结果可能是不完整的。