KeyDB学习笔记
KeyDB是Redis的替代品,宣称是世界上最快的NoSQL数据库,比Redis快5倍。KeyDB完全遵循Redis的协议,可以无缝的从Redis切换。
KeyDB支持多主复制、跨Region的水平扩容,支持垂直扩容。
KeyDB支持多个保持同步的主节点,这些主节点都可以接受读、写请求。主节点可以括Region分布。不需要哨兵节点进行监控。
KeyDB通过MVCC实现无锁(不需等待)的并发操作、后台保存。
得益于KeyDB的MVCC支持:
- 事务、查询是非阻塞(不会有锁)的,同时具有原子性保证
- 未来将支持事务回滚
KeyDB是完全的多线程的,不像Redis那样仅仅在I/O上支持并发,这意味着它可以充分利用多核心。
这个特性让KeyDB的吞吐量比Redis 5大5倍,Redis 6大3倍。再启用TLS的情况下,吞吐量可以比Redis大7倍。
多线程特性让KeyDB能最大化利用单机能力,这意味着通过配置能实现水平扩容。
单个KeyDB能够充分利用10(不使用TLS)-16(使用TLS)核心。
支持将一个set中的所有键一起过期,过期时的删除操作是接近实时完成的,不会有延迟。
可以很好的在ARM体系结构下运行。
1 2 3 4 |
sudo curl -s --compressed -o /etc/apt/trusted.gpg.d/keydb.gpg https://download.keydb.dev/keydb-ppa/keydb.gpg sudo curl -s --compressed -o /etc/apt/sources.list.d/keydb.list https://download.keydb.dev/keydb-ppa/keydb.list sudo apt update sudo apt install keydb |
1 2 |
wget https://download.keydb.dev/packages/deb/ubuntu16.04_xenial/amd64/keydb-latest/keydb-tools_6.0.16-1~xenial1_amd64.deb sudo dpkg -i keydb-tools_6.0.16-1~xenial1_amd64.deb |
要启用Active Replica模式,遵循如下步骤:
- 两个服务器A/B都需要配置 active-replica yes
- 在B上执行命令 replicaof [A address] [A port],丢弃自己的数据,加载A的数据
- 在A上执行命令 replicaof [B address] [B port],丢弃自己的数据,加载B的数据
- 现在两个服务器会传播自己的写操作到对方
或者完全通过配置文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
# 节点A的配置 port 6379 # 当前节点的密码 requirepass mypassword123 # 连接到Master的密码 masterauth mypassword123 # 增加下面的 active-replica yes # 从Redis 5.0开始,slaveof改成replicaof replicaof 10.0.0.3 6379 # 节点B的配置 port 6379 requirepass mypassword123 masterauth mypassword123 # 增加下面的 active-replica yes replicaof 10.0.0.2 6379 |
在每个节点上增加多个replicaof配置项:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
# 一共有3个节点 A/B/C 10.0.0.2/3/4 # 节点A multi-master yes active-replica yes replicaof 10.0.0.3 6379 replicaof 10.0.0.4 6379 # 节点B multi-master yes active-replica yes replicaof 10.0.0.2 6379 replicaof 10.0.0.4 6379 # 节点C multi-master yes active-replica yes replicaof 10.0.0.2 6379 replicaof 10.0.0.3 6379 |
创建配置文件,参考:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
port 6379 cluster-enabled yes cluster-config-file nodes.conf cluster-node-timeout 5000 appendonly yes dir ./ loglevel notice logfile keydb.log save 900 1 save 300 10 save 60 10000 |
启动实例,参考:
1 |
keydb-server keydb.conf |
所有实例启动后,使用如下命令创建集群:
1 |
keydb-cli --cluster create 10.0.0.1:6379 10.0.0.2:6379 ... --cluster-replicas 1 |
后续可以使用如下命令添加新的节点:
1 2 3 4 5 6 7 8 9 |
# 作为新的Master加入 # 新节点 通过谁连接到既有集群 keydb-cli --cluster add-node 10.0.0.11:6379 10.0.0.1:6379 # 自动作为副本少的Master的Slave keydb-cli --cluster add-node 10.0.0.11:6379 10.0.0.1:6379 --cluster-slave # 强制指定Master keydb-cli --cluster add-node 10.0.0.11:6379 10.0.0.1:6379 --cluster-slave --cluster-master-id 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e |
通过如下命令可以进行再分片:
1 2 |
keydb-cli reshard 10.0.0.11:6379 --cluster-from <node-id> --cluster-to <node-id> \ --cluster-slots <number of slots> --cluster-yes |
大部分配置项和Redis是相通的,参考Redis学习笔记。这里仅仅列出Redis新版本增加 / KeyDB特有的配置项。
配置项 | 说明 |
supervised no |
如果KeyDB由Systemd/Upstart管理,此配置项用于和supervision tree交互 |
always-show-logo yes | 总是为交互式会话显示KeyDB的Logo |
server-threads |
建议4,默认1 处理请求的线程数量,应该取决于你的网络接口的队列数量(而不是CPU核心数量) 由于KeyDB使用自旋锁来减少延迟,将此值设置的过大会降低性能 |
server-thread-affinity | true/false,是否启用亲和性 |
db-s3-object |
添加S3的桶路径,需要在本地具有S3客户端,并且已经配置好 如果配置该项,则保存RDB时,首先保存在本地,然后上传到S3。尝试加载时,首先尝试从本地加载,如果失败从S3加载 |
active-replica yes | 启用Active Active复制。主从都可以接受读写请求 |
通过复制(Replication),一个KeyDB节点可以保证自己的数据集和和Master(s)实例一致:
- 当Slave和Master之间的连接良好时,Master通过发送写、键过期、键驱除等操作的数据流给Slave来保证Slave的数据和自己一致
- 当网络断开后,Slave会尝试重新连接到Master,并进行增量同步
- 如果增量同步无法实现(落后太远),Slave会请求进行全量同步。Master会创建它的数据集的快照,发送给Slave,然后继续发送增量的数据流
关于KeyDB的复制机制,你需要知道以下事实:
- KeyDB默认使用异步的同步方式,这种方式的优势时延迟低、性能高。Slave会在接收了一定量的复制数据之后,向Master发送Ack。类似于Redis,使用WAIT命令可以确保特定的数据被多个Slave所Ack。WAIT命令并不能保证CAP中的CP —— 已经Ack的写操作,仍然可能在故障转移时丢失,尽管几率很小
- 每个Master可以对应多个Slaves
- Slave可以有自己的Slaves,参与复制的KeyDB节点可以形成树状拓扑
- 在Master侧,复制是非阻塞的。不管是增量/全量同步,都不影响Master继续处理查询
- 在Slave侧,复制很大程度上也是非阻塞的:
- 通过配置,KeyDB可以在接收初始同步时使用旧数据集提供服务。你也可以配置,当复制流宕掉的情况下,让Slave返回错误给客户端
- 在初始同步完毕后,有个阻塞的时间窗口,在此窗口中Slave替换旧的数据集
- 从4.0开始,可以配置KeyDB让删除数据集的操作在其它线程执行。但是,加载新数据集的操作仍然在主线程执行,会阻塞Slave
- 通过复制,可以:
- 提升可扩容性:将缓慢的O(N)操作offload给Slave
- 提升数据安全性、高可用性
- 使用复制,可以免于Master需要将数据集写入到磁盘的开销。这需要在Slave上配置高频的存盘,或者启用AOF。需要注意,这种用法下Master重启时是空数据集,如果不进行适当处理,它会导致Slave的数据集立刻变空
KeyDB支持Active Replicas(Active Active)模式,这大大简化了故障转移的处理 —— 不需要确定副本何时应该晋升。
默认情况下,KeyDB以类似Redis的方式运行,仅仅允许从Master到Replicas的单向数据复制,副本仅仅支持只读处理。启用Active Replica模式后,即使Replica到Master的连接中断,它也能够处理请求。
KeyDB在复制时,支持多主模式。 使用配置: multi-master yes即可启用。
当KeyDB连接到多个Master进行复制时,其行为和传统复制不同:
- 多次调用replicaof命令,会为本节点添加额外的Master,而非替换掉Master
- 同步到Masters之前,不会丢弃自己的数据
- 将合并来自多个Masters的读写到自己的内部数据库中
- 最后一个操作生效,也就是说,有两个Masters同时修改一个键,那么后修改的那个是实际值
到目前为止,多主特性仍然是试验性的,在偶然情况下该特性可能触发流量封包。
如果仅仅有两个实例,建议使用主主模式。
KeyDB能够处理脑裂(两个节点之间网络断开)的场景。脑裂发生时,每个节点各自接受写入操作,每个写操作被加上时间戳,当连接恢复后,主节点们合并数据,以后写入的数据为准。
这种last-win原则在KeyDB里很常见,其它的例子如下文会提到的configEpoch冲突处理。
从2.6版本开始KeyDB支持Slave的只读模式,并且默认开启。对应的选项是 slave-read-only,可以在运行时使用 CONFIG SET动态开关。
从2.8版本开始KeyDB支持设置:仅仅当至少有N个Slave连接到Master,此Master才允许写操作。这可以增强数据安全性,再次强调一下,任何时候都不能保证数据绝不丢失。
该特性的工作方式是:
- KeyDB Slaves每秒PING一次Master,Ack自己处理的复制流的量
- KeyDB Master记住每个Slave的最后一次PING的时间
- 如果PING正常的Slave数量小于配置的值,Master停止写入
相关配置项:
- min-slaves-to-write:正常PING的Slave的最小数量
- min-slaves-max-lag:如果PING延迟大于此参数指定的秒数,认为Slave不正常
KeyDB集群的设计目标,依据重要性排列如下:
- 高性能,支持最多1000节点规模。不使用代理、异步复制,不需要对键值进行merge操作
- 可接受的数据安全性:集群尽可能保证连接到大部分Master节点的客户端的写操作。最小化Ack后的写操作丢失的可能性,连接到少数Master节点的客户端的写操作丢失的可能性更大
- 可用性:当出现网络分区后,如果大部分Master节点可以连接,并且对于无法连接的Master节点们至少各有一个Slave节点可以连接,则可用性得到保证。此外,如果使用副本迁移(replicas migration),没有Slave的节点可以从Slave过多的节点夺取Slave
KeyDB集群使用异步复制,Last Failover Wins原则意味着最后晋升的主的数据集会覆盖其它副本的数据集。由于异步复制的天然特征,这意味着总是存在丢失数据的窗口。
如果出现网络分区,连接到大部分Master所在分区的客户端,丢失数据的时间窗口比连接到少数Master所在分区的客户端要小。发生故障转移的必要条件是集群中大多数Master到某个Master的通信不可达时间超过NODE_TIMEOUT,如果网络分区在超时前恢复,不会丢失数据(被新的Master覆盖)。当连接断开超过NODE_TIMEOUT之后,如果网络分区中Master数量少于一半,则所有Master停止接受写入。
下面是连接到大多数Master的客户端丢失已经Ack数据的场景:
- 写操作已经到达Master,应答客户端时,异步的复制尚未传播到Slave
- Master宕机
- 过了较长时间后,Slave晋升,它的数据集成为权威
另外一个场景:
- Master由于网络分区不可达
- 发生故障转移,Slave晋升
- 旧Master网络恢复
- 某个客户端使用过期的路由表,将数据写到Master
第二种场景的发生概率很小,需要多种巧合同时发生。
对于连接到少数Master的客户端,数据丢失的时间窗口可能较大,直到NODE_TIMEOUT之前的写入都可能丢失。
出现网络分区后,少数Master所在的分区失去可用性。
在多数Master所在的分区,如果对于不可达的Master,至少有一个Slave也在该分区,则可用性在NODE_TIMEOUT+若干秒(Slave晋升为Master,通常需要1-2秒)后恢复。
副本迁移(replicas migration)可以提升可用性,在大多数Master分区中,如果某个Master有多余的Replica,可以迁移给(在当前分区)没有Slave的(不在当前分区的)Master。
KeyDB集群不会代理请求,只会向客户端发起重定向,让它访问匹配的键空间分片的节点。
最终客户端会得到up-to-date的集群、键空间分片-节点映射关系的信息,并且直接访问正确的节点,避免重定向。
由于通常的操作仅仅发生在单个节点上,因此可以认为N主节点的KeyDB集群的性能和单个KeyDB实例是相似的。
KeyDB集群节点提供总线进行通信,总线使用客户端端口+10000(默认16379),协议是KeyDB Cluster protocol。
在KeyDB Cluster protocol中,节点负责存储数据、集群状态(包括键到节点的映射)、发现其它节点、检测无效节点、晋升Slave节点为Master。
节点之间使用一种gossip协议来传播关于集群的信息,从而发现新节点、发送PING包来检测节点是否正常工作、同步关于集群特定状态的信号。
KeyDB Cluster Bus还用于在集群范围内传播Pub/Sub消息,协调用户发起的手工故障转移。
为了实现总线的功能,所有节点通过二进制协议KeyDB Cluster Bus连接在一起,支持1000节点需要维持百万级的TCP连接。
为了避免在节点之间交换过多的信息,KeyDB使用gossip协议 + 一种配置更新机制。这保证了消息量不会随着集群规模而指数级增长。
KeyDB集群不会代理(转发)客户端请求,它可能给出响应并触发重定向。理论上客户端可以发送请求给任何节点,并在必要时获得重定向,因此它不需要持有集群的状态信息。但是,客户端可以缓存键-节点映射,来改善性能。
节点总是接受总线端口上的连接,并且应答PONG给PING请求,即使源节点不受信任。但是对于其它消息,如果源节点不是集群成员,将全部丢弃。
节点在以下情况下,接受某个源节点作为集群成员:
- 新节点发送
MEET消息,此消息类似于PING,但是会强迫消息接受者,接纳源节点作为集群成员。仅当系统管理员执行:
1CLUSTER MEET ip port节点才会发送MEET消息
- 通过gossip协议传递的,关于节点被加入集群的事实。如果A、B已经是集群成员,现在C新加入并且MEET了A,那么A会gossip给B,这样A、B都认可C作为成员了
总线不支持在NAT环境下,或者任何TCP被映射的环境下工作。对于Docker来说,你可能需要设置 --net=host来避免NAT。
整个键空间被划分为16384个Hash Slots,Slot是分片的最小单位,意味着理论上集群最多有16384个主节点(实际上建议的上限是1000左右)。
集群中每个Master处理这些Slots的子集。如果没有进行中的集群再配置(reconfiguration)—— 没有正在移动的Hash Slots ——则我们称集群为stable的。稳定集群中每个Slot被单个Master节点所处理
键到Slot的映射算法是: HASH_Slot = CRC16(key) mod 16384
使用Hash tag可以确保多个键被分配到同一个Slot,以便KeyDB集群支持multi-key操作。
如果键中包含 {...}则仅仅花括号中的部分用来计算Hash Slot,这可以保证{user1000}.following、{user1000}.followers这两个键被分到同一个Slot。
集群中的每个节点具有唯一的、自动生成的ID,该名称是160bit的随机数。节点会将此名称存放在配置文件中,并且一直使用相同的名称,除非:
- 配置文件被删除
- 执行硬重置命令 CLUSTER RESET
节点ID用于在集群范围内唯一的标识节点,节点可以改变自己的IP地址,但是它的ID不需要改变。集群可以感知节点的IP地质变更,并使用gossip协议,通过集群总线进行重配置。
在任何节点执行 CLUSTER NODES可以获得节点列表,并列出其关键属性。除了上面的节点ID之外,其他属性包括:
- 节点IP地址:端口
- 一系列标记位
- 如果节点被标记为Slave,则它的Master是谁
1 2 3 4 5 6 |
keydb-cli cluster nodes d1861060fe 127.0.0.1:6379 myself - 0 1318428930 1 connected 0-1364 # 节点ID 地质:端口 标记 最后PING/PONG时间 配置epoch 连接状态 Slots 3886e65cc9 127.0.0.1:6380 master - 1318428930 1318428931 2 connected 1365-2729 d289c575dc 127.0.0.1:6381 master - 1318428931 1318428931 3 connected 2730-4095 |
KeyDB集群允许在运行时添加、删除节点。这都潜在的意味着Hash Slot的重新平衡。和调整Hash Slot有关的命令有:
1 2 3 4 5 6 7 |
# 添加Slot,通常加入新节点时使用 CLUSTER ADDSlotS Slot1 [Slot2] ... [SlotN] # 很少使用 CLUSTER DELSlotS Slot1 [Slot2] ... [SlotN] # 将Slot分配给特定节点 CLUSTER SETSlot Slot NODE node |
Hash Slot重新分配之后,将通过gossip协议进行配置传播( configuration propagation )。
SETSlot子命令还可以拆分为两条。假设我们希望将Slot 8从A迁移到B:
- 向B发送: CLUSTER SETSlot Slot MIGRATING A。这样,B会接受所有关于Slot 8的查询,但是如果本地没有Key,则发送 ASK重定向,让客户度询问A
- 向A发送: CLUSTER SETSlot Slot IMPORTING B。这样,A会接受所有关于Slot 8的查询,但是请求必须以 ASKING命令作为前导,否则MOVE重定向给Slot的负责人B
重新分片牵涉到键的迁移,这个迁移是逐步进行的 —— 不是原子的移动Slot。
这意味着,在迁移过程中,多键操作的Key,可能一部分位于节点A,一部分以及迁移到节点B。由于KeyDB集群限制多键操作仅仅牵涉单个节点(所有键位于一个Hash Slot),因此这种情况下会应答客户端以 TRYAGAIN错误。
KeyDB客户端可以自由的向任何节点发送查询请求,节点会进行以下处理:
- 分析查询请求是否是acceptable的 —— 要么查询仅仅牵涉一个键,要么牵涉到在同一Hash Slot中的multikey
- 查找其内部的Slot-node映射,找到负责目标Hash Slot的节点:
- 如果是当前节点负责,直接处理请求
- 如果是其它节点负责,应答客户端以MOVED错误:
123GET x# 键所属Slot 负责处理此Slot的节点-MOVED 3999 127.0.0.1:6381
重定向中已经给出正确节点的信息,客户端应该(但不必须)缓存并且向正确节点发请求。
一旦重定向发生,可能潜在的Slot-node映射发生很大变化,客户端可以通过 CLUSTER NODES或 CLUSTER SlotS命令获取最新映射关系,并缓存。
MOVED用于提示客户端,很明确Hash Slot永久的被另外一个服务器所处理了。
ASK重定向则是提示客户端,Hash Slot可能处于迁移过程中,我虽然负责此Slot,但是没有找到你需要的Key,你去问问原来的所有者吧。
默认情况下,Slave节点仅仅会重定向请求给处理Hash Slot的Master节点。
客户端可以使用 READONLY命令,提示Slave节点,我可以读取(可能是陈旧的)你的数据,我不进行写查询。
当一个连接处于只读默认下时,仅当查询牵涉不被Slave的Master负责的Key(可以由于重新分片导致)时才会发送重定向。
使用命令 READWRITE可以清除连接的只读状态。
KeyDB节点持续的交换PING/PONG封包,这两种消息的差别仅仅是消息类型。PING/PONG消息也叫心跳。
通常情况下,PING会触发接受者回复PONG。但是PONG也可以用来携带重要的配置信息,并且不需要应答。这种用法可以尽快广播新的配置。
通常情况下,发送心跳时,节点会随机选择几个节点作为目标,而不是发给所有节点。这避免了随着集群规模的增大而出现消息风暴。不过,作为一个前提,节点确保在NODE_TIMEOUT / 2的时间内,至少发送一个心跳给从未沟通过(不管谁主动发送PING都可以)的节点。
即使NODE_TIMEOUT到达了,节点也会重新发起一次TCP连接,防止因为连接的问题导致误判。注意NODE_TIMEOUT必须要大于RTT。
需要注意根据集群规模来配置NODE_TIMEOUT,如果集群规模很大,而NODE_TIMEOUT又偏小,会导致大量的心跳包。
PING/PONG包和其他消息具有相同的头,该头的内容:
- Node ID,发送节点的标识符
- 发送节点的currentEpoch、configEpoch,这两个字段和KeyDB集群使用的分布式算法有关。Slave的configEpoch是它的Master的最后一个已知的configEpoch
- 节点标记,提示节点是Slave还是Master,以及其他的单比特节点属性
- 发送节点所负责的Hash Slot的位图信息,如果发送节点是Slave则发送它的Master的信息
- 发送节点的客户端TCP端口
- 从发送节点的角度来看,集群的状态(down/ok)
- 对于Slave,其Master的Node ID
PING/PONG包还包括一个gossip段,其中包含发送者所认为的,集群(一部分随机的,否则消息太大,具体数量取决于集群规模)其它节点的状态。每个节点的信息包括:Node ID、IP:PORT、节点标记。这个gossip段可以用来进行故障发现、发现新节点。
故障发现(Failure Detection)用于发现这样的异常:Master/Slave不再被集群中的大部分节点可见,进而引起的Slave晋升为Master的过程。
如果无法通过晋升来解决故障,则集群进入错误状态,不再接受客户端查询请求。
通过前面的心跳机制我们了解到,每个节点都维护自己所认为的,其它节点的状态。状态表现为一系列标记,其中和故障发现有关的是:
- PFAIL,P表示Possible,即某个节点可能出故障了,但是尚未确认。如果目标节点在NODE_TIMEOUT时间范围内,从当前节点不可达,则标注为PFAIL。Master/Slave都有权标记某个节点为PFAIL
- FAIL,节点故障在固定的时间范围内被大多数Master所认可,该状态从PFAIL升级而来
前面我们了解到心跳的gossip段包含当前节点所认为的,随机的其它几个节点的状态。经过若干次心跳后,每个节点的的认知会传播到所有其它节点。当满足以下条件后,PFAIL变为FAIL:
- 某个节点A,将节点B标记为PFAIL
- 节点A通过心跳接收到的gossip分析集群中大部分节点关于B状态的声明
- 如果大部分节点在 NODE_TIMEOUT * FAIL_REPORT_VALIDITY_MULT的时间区间内,标记节点B为PFAIL或FAIL。那么A标记B为FAIL,并发送 FAIL消息给所有可达节点
- FAIL消息会导致所有接收节点都将节点B标记为FAIL,不管它是不是认为B处于PFAIL状态
当前实现将FAIL_REPORT_VALIDITY_MULT设置为2。这意味着,在两倍NODE_TIMEOUT时间内,大多数Master将某个节点标记为故障,则集群认为该节点宕掉了。
需要了解PFAIL到FAIL的转换,依赖于一种弱(一致)的协议:
- 节点在一段时间范围内,收集其它节点的(关于节点状态)视图。由于时间窗口的存在,我们无法确认(也不需要)在某个时间点是否大多数Master达成一致
- FAIL消息会被传播,但是无法保证所有节点都能收到并修改故障节点状态,这是因为故障常常伴随网络分区
节点状态可以从PFAIL变成FAIL,但是FAIL状态仅仅在以下情况下才能清除掉:
- 节点变得可达,并且成为Slave。由于Slave不能参与故障转移,因此FAIL状态清除
- 节点变得可达,成为Master,但是不负责任何Slot。这种情况下FAIL状态可以被清除,因为该节点并没有正常参与到集群中
- 节点变得可达,成为Master,过了很长时间(NODE_TIMEOUT的N倍)仍然没有可觉察的Slave晋升。这种情况下,最好是让该节点重新加入集群
KeyDB使用类似于Raft算法中的Term的概念,叫Epoch。当多个节点提供了冲突的信息时,Epoch可用于确定谁的状态是up-to-date的:
- currentEpoch,可以认为是集群状态的版本号,最终所有节点应该具有相同的currentEpoch
- configEpoch,每个Master节点具有独特的configEpoch,主要包含它负责的Hash Slots列表
每个新创建的节点,它的 currentEpoch(64bit无符号整数)都是0。每当从其它节点接收到一个消息,如果消息中的epoch大于本地的currentEpoch,则更新currentEpoch为接收到的epoch。
过了一段时间后,集群中所有节点都会使用最大的configEpoch作为自己的currentEpoch。
currentEpoch在集群节点需要协商,以决定执行某操作的时候用到,currentEpoch大的节点具有话语权。目前只支持Slave晋升这一种操作。
所有Master在PING/PONG的时候,都会通告自己的configEpoch(连同自己负责的Hash Slots)。
当一个新节点加入集群后,Master将configEpoch设置为0。Slave尝试晋升自己的时候,会增加configEpoch的值,并尝试获得大多数Master的授权。一旦Slave获得授权,则它成为使用此新configEpoch的Master。
Slave在PING/PONG中也会通告configEpoch,通告的是它(通过最后一次消息得到)的Master的configEpoch。如果它通告的configEpoch小于Master的真实configEpoch,这意味着它需要更新,选举时Master不会投票给持有过期configEpoch的Slave。
节点的configEpoch变更,会被所有接收到相关心跳的节点fsync到nodes.config中。如果引起currentEpoch的变更,也会fsync到nodes.config。
configEpoch值的递增有一个简单算法负责,此算法确保它是新的、递增的、唯一的。configEpoch的变化由于故障转移、再分片导致。
如果由于Slave晋升导致configEpoch增加,集群会确保它是唯一的。
以下两种操作,仅仅简单的更新本地的configEpoch,可能导致configEpoch冲突:
- 使用带 TAKEOVER选项的 CLUSTER FAILOVER命令,可以强制晋升一个Slave节点,不需要大多数Master可用
- 手工迁移Slot来进行再分片,也仅仅在本地节点生成新的epoch
手工再分片时,如果Slot从A迁移到B,分片程序会强制B增加自己的epoch为可发现集群范围内的最大值 + 1,除非节点的epoch已经是最大。由于再分片常常牵涉到大量Slot的迁移,为了每个Slot,进行协商并获得更高的configEpoch是很低效的,因此增加epoch仅在本地进行,并且仅仅在迁移第一个Slot时进行。
尽管可能性较低,还是会出现多个节点声明相同configEpoch的情况 —— 如果手工再分片和自动故障转移发生外加运气差的话。KeyDB使用下面的冲突解决算法:
- 如果Master A发现Master B正在通告和自己相同的configEpoch
- 并且根据字典序比较,A的Node ID比B大
- 那么A将currentEpoch+1,并且将其作为自己的configEpoch
也就是说,如果集群中有若干节点具有相同configEpoch,那么其中ID最大的取胜,出现Hash Slot冲突时以它为准。
Hash Slots - 节点映射关系的传播,不管对于新集群,还是由于故障转移/手工重分片导致的Slot负责节点变更,都是关键的。
Hash Slots配置信息可以通过两种方式传播:
- 心跳消息:节点发送PING/PONG时总是携带它(或者它的Master)所负责的Slot的信息
- UPDATE消息:由于心跳中还携带了自己的epoch,如果接受者发现此epoch过期,则会发送UPDATE消息,强制过期节点更新配置
当一个新的节点加入到集群后,它的Hash Slot映射(16383 个key)简单的置空(表示未分配)。当接收到心跳/UPDATE消息后,根据以下规则来更新映射:
- 如果Slot为空,并且某个已知的节点claim该Slot,分配Slot给节点
- 如果Slot不为空,并且它映射给Master A,如果现在接收到Master B的消息,声明Slot归它管。这种情况下,如果B的configEpoch大于A,则重新绑定Slot给B
由于规则2的存在,最终集群中所有的节点都认可configEpoch最大的节点通告的映射关系,这就是所谓last failover wins机制。
故障转移:
- 依赖于上面两节提到的故障发现、配置传播机制
- 由出问题的Master的Slave主导,其它Master节点参与投票完成
- 触发时机为:
- Master进入FAIL状态,并且经过了选举时延
- Master至少负责1个Slot
- Slave和Master的复制连接断开时间小于,这是为了保证Slave的数据不过于陈旧
故障转移过程如下:
- Slave增加自己的configEpoch,并同步currentEpoch
- 请求Masters来投票,具体做法是广播 FAILOVER_AUTH_REQUEST消息给所有Master节点
- 等待Master的应答,最长 NODE_TIMEOUT * 2,最短2秒
- 如果应答的Master的epoch小于发起晋升时Slave的currentEpoch,则应答被丢弃。这个可以防止接收到关于上一次选举的投票
- 如果Slave在限定时间内获得大多数Master的投票,则晋升成功。否则,等待 NODE_TIMEOUT * 4再次进行选举
Master参与投票时遵循以下规则:
- 对于一个给定的epoch,每个Master仅会投票一次:如果Master投票给Slave,它应答FAILOVER_AUTH_ACK,同时将lastVoteEpoch字段持久化到配置文件。Master在NODE_TIMEOUT * 2的时间内,不再能投票给故障Master的其它Slave,这可以防止同时选出多个新Master。
- 只有Master任何被故障转移的Master处于FAIL状态,才会进行投票
- 如果FAILOVER_AUTH_REQUEST中的currentEpoch小于Master的currentEpoch,请求被忽略 —— 这意味着投票应答的epoch总是和请求的相同
Master进入FAIL状态后,Slave还需要等待一个选举延迟:
DELAY = 500 毫秒 + 0 -500 毫秒随机延迟 + SLAVE_RANK * 1000 毫秒
才会发起选举投票。固定延迟让FAIL消息有机会传播到整个集群,随机延迟可以防止两个SLAVE_RANK相同的Slave同时尝试晋升。
SLAVE_RANK的大小取决于复制的进度。当Master故障时,Slave会相互通信,交换自己的复制进度,并进行排名。进度最快的Slave的SLAVE_RANK为0,依次递增。
一旦Slave赢得选举,它将获得大于所有其它Master的configEpoch,并且通过心跳通告自己的configEpoch、负责的Slots。
为了让集群尽快完成重新配置,Slave将发送一个PONG包给整个集群。当前不可达的节点可能通过下面两种方式之一获得更新:
- 接收到其它节点的PING/PONG包
- 其自己发送的心跳,由于信息过期,被其它节点应答以UPDATE
重新配置的内容包括:
- 对于故障的Master的原有其它Slave,需要更新复制源
- 对于所有节点,需要更新Hash Slot - 节点映射关系
如果一个Master节点因为网络分区,脱离集群,并且时间足够长,发生了故障转移。那么网络恢复后,旧的Master如何处理?
在KeyDB中,旧Master将配置为偷取了它的Slot的那个节点的Slave。
重置节点,可以让它以另外一个角色加入集群,或者加入其它集群。
执行 CLUSTER RESET可以软重置一个节点,如果不指定选项,默认为SOFT。制定HARD则进行硬重置。在重置时会进行如下处理:
- 如果节点是Slave,则角色切换为Master,丢弃数据集;如果节点是Master并且持有Key,则中止重置
- 释放所有Hash Slot,重置手工故障转移状态
- 移除节点表中所有其它节点,这样节点对原先集群一无所知
- 对于硬重置:currentEpoch、configEpoch、lastVoteEpoch均置零
- 对于硬重置:Node ID修改为新的随机值
通过重新分片,将Master的Slot全部迁移走,就可以关闭它了。
由于其它节点仍然记着移除节点的Node ID和地址,还会尝试连接它,因此,你应当调用 CLUSTER FORGET <node-id>,该命令的作用:
- 从所有节点的nodes table中移除指定节点
- 60s内禁止被移除节点(根据ID)再次加入集群。防止因为gossip导致重新加入
集群的客户端可以在任何节点上订阅,在任何节点上发布。KeyDB会保证消息被正确的转发 —— 目前的实现仅仅是简单的广播。
Leave a Reply