设计的主要和特性
redis集群目标
- 高性能,线性scalable到1000节点级别。无proxy。
- 对于写的保证:尽最大努力交付。意味着即使通过客户端写成功了,但实际上写操作可能丢失。
- 可用性:"大多数master节点可达,并且每个不可达的master节点至少有一个slave可达"
实现了的子集
所有的单key命令都是可用的。复杂的多key操作如果属于同一个节点,也是实现了的。
实现了hash tags的概念,可以将特定的key强制存储在相同结点上面。但是手动reshard期间,多key操作可能不可用,而单key操作一直是可用的。
redis集群不支持多数据库。也不允许select命令。
集群协议中客户端和服务器的角色
集群节点负责存储数据以及交换集群状态,包括将key映射到正确的结点。自动发现其它节点。检测不工作的节点。必要是会将slave提升为master。
集群节点使用TCP,二进制协议,Redis Cluster Bus。每个节点会连到其它所有节点。gossip协议。处理结点发现,ping,通知一些特殊条件,pubsub传递,手动failover。
不代理,所以会返回MOVED和-ASK消息。客户端不知道集群状态也能工作。
写安全性
由于是异步写副本,master挂掉后,最终状态为顶上来的那个slave中的数据。意味着,一段时间窗口内的写可能丢失。
举两个写不一致的例子。 1.master收到client写请求,master回复client写ok了。但是master还没把这个写同步到slave中,master挂掉了。slave提升为master。该写请求丢失。 2.master暂时不可达了。slave提升为master。原来的master又可达了(但身份还没切换到slave),client的路由还没更新过来,继续往它里面写数据。写请求会丢失。
要检测到master挂掉,需要超过半数的master都发现它不可达,并且持续至少NODE_TIMEOUT。
可用性
假设集群分区割裂了。那么minority那部分的分区将不可用。而majority端那边,"大多数master节点可达,并且每个不可达的master节点至少有一个slave可达",那么在NODE_TIMEOUT时间加上选举新的master这段时间之内都是available的。
性能
嗯,单机N倍!
为什么不做merge操作
redis集群设计不记录数据版本,主要考虑是value通常很大,比如说list或者set,还有就是数据类型很复杂。说白了就是版本不好做,数据不好合并。
其实也不算技术limit啦,CRDT是可以做到的。不过那样就不符合redis集群的设计了。
redis集群组件综述
key的分布模型
key被hash到16384个slot。正常情况下每个slot只由一个节点负责(不算slave)。
HASH_SLOT = CRC16(key) mod 16384
key hash tags
对hash slot的计算有一个例外就是hash tags。hash tags是一种确保多个key分配到同一个hash slot的方式,用来实现集群的多key操作。
如果key中包含一个"{...}"模式的子串,只有{}之间的子串会被hash于计算slot。使用第一个出现的{}
- 对{user1000},user1000将被用于计算hash slot
- 对foo{}{bar},使用整个串,因为第一个{}里面是空的
- foo{{bar}}zap,使用{bar,因为它是在第一个{}内面
- foo{bar}{zap},使用bar
集群节点属性
集群内每个节点都有一个唯一的名字。第一次从/dev/urandom读,后面都不会变。除非改配置文件或者使用CLUSTER RESET命令。
节点ID是用于识别集群内的节点的。可能IP会变,但是节点ID不会,集群会识别出IP/端口的变化并通过gossip协议重新配置。
cluster nodes命令可以打印当前集群的节点信息。
集群总线
集群中每个节点都监听一个额外的TCP端口用于接受来自其它redis节点的连接。这个端口是正常服务端口加10000,如果redis端口是6379,那么用于集群总线的端口就是16379。
集群拓扑
redis集群是一个每个节点都和其它所有节点连接的网格。如果有N个节点,那么每个节点都有N-1条往外的TCP连接,以及N-1条输入的连接。并且是keepalive而不是按需创建的。
尽管集群节点形成了一个完整的网格,节点之间是使用gossip协议和配置更新技术来避免节点间过多的消息通信,这样消息数不会成指数增长。
节点握手
节点总是在cluster bus端口接受新连接。如果收到ping就会回复,但是如果收到(除了ping以外)不属于集群节点的包,会将它丢弃。
有两种方式节点会接受其它节点成为cluster一部分:
- 如果节点发一个MEET消息。MEET消息跟PING消息很类似,但是会把节点当作属于集群。只有系统管理员使用下面命令时,节点会发送MEET消息:
CLUSTER MEET ip port
- 如果节点已经通过gossip协议成为一个受信任的节点。即,如果A知道B,B知道C,最终B会给A发送关于C的gossip消息。这里,A会注册C成为网络的一部分,并尝试连接C。
这个机制保证了最终节点可以知道其它节点,并且阻止了在改IP或者一些其它事件时redis集群可能弄混。
重定向和resharding
MOVED重定向
client可以随意给集群任意节点发请求,包括slave。节点会分析请求,如果hash slot是这个节点负责的,那就简单的处理这个查询,否则节点会检查映射表,并返回MOVED错误,如下所示:
GET X -MOVED 3999 127.0.0.1:6381
client需要将请求重新发到指定节点。如果在client等了很久才重发消息,期间集群配置变动了,目标节点会回复MOVED。client联系到过时的节点都是这种情况。
尽管集群中是用ID来作为节点标识的,这里跟client通过都是简单的使用IP:port。
虽然不强制要求,但是client要记下slot 3999是由127.0.0.1:6381提供的。这样新的命令就会挑选正确的节点发送了。
还有一种做法是client在收到MOVED的时候,使用CLUSTER NODES和CLUSTER SLOTS命令,因为遇到重定向时,很可能许多个slot都变化了,因此client尽快更新配置可能是最后的策略。
client还必须能够处理-ASK重定向,不然不能算完整的client。
集群在线改配置
redis集群支持在运行时添加和删除节点。实际上添加和删除节点都是抽象成同一种操作,将hash slot从一个节点迁移到其它。
核心部分就是移动hash slot的能力。实际上hash slot就是一系列的key,因此resharding就是将一些key从一个实例移到其它实例。
手动迁移命令:
- CLUSTER ADDSLOTS slot1 [slot2] ... [slotN]
- CLUSTER DELSLOTS slot1 [slot2] ... [slotN]
- CLUSTER SETSLOT slot NODE node
- CLUSTER SETSLOT slot MIGRATING node
- CLUSTER SETSLOT slot IMPORTING node
前两个是简单的将slot赋到redis节点。ADDSLOTS通常是创建新集群的时候用。DELSLOTS主要用于手动修改集群配置或者调试,实际上很少使用。
SETSLOT <slot> NODE形式的子命令用于将一个slot赋给特定的节点ID。MIGRATING和IMPORTING用于迁移hash slot从一个节点到其它节点。
- 如果一个slot状态是MIGRATING,如果请求的key存在,节点会接受查询请求,否则会返回一个-ASK重定向。
- 如果一个slot的状态是IMPORTING,节点会接受所有的查询请求,但是只处理ASKING命令,如果不是ASKING命令,会返回-MOVED重定向。
ASK重定向
为什么是ASK而不是MOVED? ASK意味着,只是下一条命令要查询特定的节点,而MOVED是后面的查询都是到了其它节点。
我们要限定client的行为。因此,IMPORTING的节点只接受ASKING命令。
从client的角度,ASK重定向的完整语义:
- 如果收到ASK重定向,只是下一次把请求发到特定节点,之后仍然是查询老的节点。
- 使用ASKING命令启动重定向查询。
- 暂时不要更新本地的hash slot
客户端首次连接和重定向处理
client要尽量聪明一些,记下slot的配置。不过不需要是最新的,因为连到错的节点后会收到重定向。
通常在以下两种情况,client要拿一份完整的slot到节点映射表。
- 启动的时候
- 收到MOVED重定向时
CLUSTER SLOTS命令可以拿到相关信息。
多key操作
使用hash tags可以很容易做多key操作。
但是reshard的时候,多key操作将不可用。因为迁移的时候数据分散在两个节点之间了。这个时候会返回一个-TRYAGAIN错误。
利用slave节点提升读性能
正常情况下,slave节点会将client重定向到master。但是可以用READONLY命令,从slave读。
在只读模式下,只有slave的master不拥有对应的slot的时候才会发重定向。
容错
节点心跳和gossip消息
集群节点会持续交换ping和pong。这两类消息结构相同,都带了配置信息。实际上只有type字段有区别。统一都叫心跳包。
通常是发ping然后返回pong。不过也可以直接发pong,这样就不需要返回,可以尽快把配置信息广播出去。
通常ping会随机挑选出一些节点,这样每个节点的发送消息是常量,跟集群规模无关。但是要确保,每个节点在NODE_TIMEOUT/2时间之内,会ping到所有的其它节点。
心跳包内容
异常检测
如果多数节点都不可访问某个master或者slave节点,slave会被提升为master,如果不行,集群将进入error状态并停止接受client请求。
每个节点都维护了一个它知道的节点的列表,有两个flag用于异常检测。PFAIL和FAIL。PFAIL意思是Possible failure,并不确认异常类型。FAIL意思是节点挂了并且大部分的master都确认过。
PFAIL标记
如果一个节点超过了NODE_TIMEOUT无法连到另一个节点,它会将这个节点加上PFAIL标记。master和slave都可以标记其它节点为PFAIL。
不可达的概念是发了一个ping,超过NODETIMEOUT还没收到响应。为了增加正常情况下的可靠性,在超过NODETIMNEOUT一半时间后,会尝试重连节点。这样子可以确保连接是keep alive的并且连接坏了不会导致错误的报告节点挂掉。
FAIL标记
PFLAG只是本地信息,并且不足以触发slave提升。PFAIL变为FAIL后才能确认节点挂了。
A节点中标记了B为PFAIL。A通过gossip收集到其它节点关于B的信息。多数master标记了PFAIL。那么A会将B标记为FAIL,并给所有可达的节点发FAIL消息。
变成FAIL需要经历PFAIL,清除FAIL有两种情况:
- 节点可达了,并且它是slave。直接清除FAIL标记,因为slave不需要容错。
- 节点可达了,它是master但是不对应任何slot。也可以直接清除FAIL标记
- 节点可达了,它是master,并且过了N倍的NODE_TIMEOUT时间没有检测到slave提升。这种情况有利于节点重新加入到集群。
配置处理,传递和容错
集群当前epoch
redis集群用了一个类似raft算法中"term"的概念,在redis集群中叫做epoch。使用它是为了给事件一个递增的版本,这样,当多个节点提供的信息冲突了,其它节点可以知道哪个状态是最新的。
集群创建的时候,所有节点,包含master和slave,都将currentEpoch设置为0。每次收到其它节点发来的包,如果发送者的epoch大于本地,则将currentEpoch更新到发送者的epoch。
slave选举和提升
slave选举和提升是由slave节点处理的,master节点会投票。当master处理FAIL状态,并且至少有一个满足条件的slave时,会执行slave选举。
Read full article from redis集群规范笔记
No comments:
Post a Comment