Menu

  • Home
  • Work
    • Cloud
      • Virtualization
      • IaaS
      • PaaS
    • Java
    • Go
    • C
    • C++
    • JavaScript
    • PHP
    • Python
    • Architecture
    • Others
      • Assembly
      • Ruby
      • Perl
      • Lua
      • Rust
      • XML
      • Network
      • IoT
      • GIS
      • Algorithm
      • AI
      • Math
      • RE
      • Graphic
    • OS
      • Linux
      • Windows
      • Mac OS X
    • BigData
    • Database
      • MySQL
      • Oracle
    • Mobile
      • Android
      • IOS
    • Web
      • HTML
      • CSS
  • Life
    • Cooking
    • Travel
    • Gardening
  • Gallery
  • Video
  • Music
  • Essay
  • Home
  • Work
    • Cloud
      • Virtualization
      • IaaS
      • PaaS
    • Java
    • Go
    • C
    • C++
    • JavaScript
    • PHP
    • Python
    • Architecture
    • Others
      • Assembly
      • Ruby
      • Perl
      • Lua
      • Rust
      • XML
      • Network
      • IoT
      • GIS
      • Algorithm
      • AI
      • Math
      • RE
      • Graphic
    • OS
      • Linux
      • Windows
      • Mac OS X
    • BigData
    • Database
      • MySQL
      • Oracle
    • Mobile
      • Android
      • IOS
    • Web
      • HTML
      • CSS
  • Life
    • Cooking
    • Travel
    • Gardening
  • Gallery
  • Video
  • Music
  • Essay

KeyDB学习笔记

21
Jan
2020

KeyDB学习笔记

By Alex
/ in Database
/ tags NoSQL, Redis
0 Comments
简介

KeyDB是Redis的替代品,宣称是世界上最快的NoSQL数据库,比Redis快5倍。KeyDB完全遵循Redis的协议,可以无缝的从Redis切换。

KeyDB支持多主复制、跨Region的水平扩容,支持垂直扩容。

多主复制

KeyDB支持多个保持同步的主节点,这些主节点都可以接受读、写请求。主节点可以括Region分布。不需要哨兵节点进行监控。

MVCC支持

KeyDB通过MVCC实现无锁(不需等待)的并发操作、后台保存。

ACID兼容

得益于KeyDB的MVCC支持:

  1. 事务、查询是非阻塞(不会有锁)的,同时具有原子性保证
  2. 未来将支持事务回滚
多线程

KeyDB是完全的多线程的,不像Redis那样仅仅在I/O上支持并发,这意味着它可以充分利用多核心。

这个特性让KeyDB的吞吐量比Redis 5大5倍,Redis 6大3倍。再启用TLS的情况下,吞吐量可以比Redis大7倍。

垂直扩容

多线程特性让KeyDB能最大化利用单机能力,这意味着通过配置能实现水平扩容。

单个KeyDB能够充分利用10(不使用TLS)-16(使用TLS)核心。

其它特性
更好的过期处理

支持将一个set中的所有键一起过期,过期时的删除操作是接近实时完成的,不会有延迟。

ARM友好

可以很好的在ARM体系结构下运行。

安装
Ubuntu
通过PPA
Shell
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
仅安装工具
Shell
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模式,遵循如下步骤:

  1. 两个服务器A/B都需要配置 active-replica yes
  2. 在B上执行命令 replicaof [A address] [A port],丢弃自己的数据,加载A的数据
  3. 在A上执行命令 replicaof [B address] [B port],丢弃自己的数据,加载B的数据
  4. 现在两个服务器会传播自己的写操作到对方

或者完全通过配置文件:

Conf
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配置项:

Conf
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
创建集群

创建配置文件,参考:

Conf
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

启动实例,参考:

Shell
1
keydb-server keydb.conf

所有实例启动后,使用如下命令创建集群:

Shell
1
keydb-cli --cluster create 10.0.0.1:6379   10.0.0.2:6379 ... --cluster-replicas 1
增加节点

后续可以使用如下命令添加新的节点:

Shell
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 
再分片

通过如下命令可以进行再分片: 

Shell
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)实例一致:

  1. 当Slave和Master之间的连接良好时,Master通过发送写、键过期、键驱除等操作的数据流给Slave来保证Slave的数据和自己一致
  2. 当网络断开后,Slave会尝试重新连接到Master,并进行增量同步
  3. 如果增量同步无法实现(落后太远),Slave会请求进行全量同步。Master会创建它的数据集的快照,发送给Slave,然后继续发送增量的数据流

关于KeyDB的复制机制,你需要知道以下事实:

  1. KeyDB默认使用异步的同步方式,这种方式的优势时延迟低、性能高。Slave会在接收了一定量的复制数据之后,向Master发送Ack。类似于Redis,使用WAIT命令可以确保特定的数据被多个Slave所Ack。WAIT命令并不能保证CAP中的CP —— 已经Ack的写操作,仍然可能在故障转移时丢失,尽管几率很小
  2. 每个Master可以对应多个Slaves
  3. Slave可以有自己的Slaves,参与复制的KeyDB节点可以形成树状拓扑
  4. 在Master侧,复制是非阻塞的。不管是增量/全量同步,都不影响Master继续处理查询
  5. 在Slave侧,复制很大程度上也是非阻塞的:
    1. 通过配置,KeyDB可以在接收初始同步时使用旧数据集提供服务。你也可以配置,当复制流宕掉的情况下,让Slave返回错误给客户端
    2. 在初始同步完毕后,有个阻塞的时间窗口,在此窗口中Slave替换旧的数据集
    3. 从4.0开始,可以配置KeyDB让删除数据集的操作在其它线程执行。但是,加载新数据集的操作仍然在主线程执行,会阻塞Slave
  6. 通过复制,可以:
    1. 提升可扩容性:将缓慢的O(N)操作offload给Slave
    2. 提升数据安全性、高可用性
  7. 使用复制,可以免于Master需要将数据集写入到磁盘的开销。这需要在Slave上配置高频的存盘,或者启用AOF。需要注意,这种用法下Master重启时是空数据集,如果不进行适当处理,它会导致Slave的数据集立刻变空
主主模式

KeyDB支持Active Replicas(Active Active)模式,这大大简化了故障转移的处理 —— 不需要确定副本何时应该晋升。

默认情况下,KeyDB以类似Redis的方式运行,仅仅允许从Master到Replicas的单向数据复制,副本仅仅支持只读处理。启用Active Replica模式后,即使Replica到Master的连接中断,它也能够处理请求。

多主模式

KeyDB在复制时,支持多主模式。 使用配置: multi-master yes即可启用。

当KeyDB连接到多个Master进行复制时,其行为和传统复制不同:

  1. 多次调用replicaof命令,会为本节点添加额外的Master,而非替换掉Master
  2. 同步到Masters之前,不会丢弃自己的数据
  3. 将合并来自多个Masters的读写到自己的内部数据库中
  4. 最后一个操作生效,也就是说,有两个Masters同时修改一个键,那么后修改的那个是实际值

到目前为止,多主特性仍然是试验性的,在偶然情况下该特性可能触发流量封包。

如果仅仅有两个实例,建议使用主主模式。

脑裂处理

KeyDB能够处理脑裂(两个节点之间网络断开)的场景。脑裂发生时,每个节点各自接受写入操作,每个写操作被加上时间戳,当连接恢复后,主节点们合并数据,以后写入的数据为准。

这种last-win原则在KeyDB里很常见,其它的例子如下文会提到的configEpoch冲突处理。

只读Slave

从2.6版本开始KeyDB支持Slave的只读模式,并且默认开启。对应的选项是 slave-read-only,可以在运行时使用 CONFIG SET动态开关。

最小Slave数量

从2.8版本开始KeyDB支持设置:仅仅当至少有N个Slave连接到Master,此Master才允许写操作。这可以增强数据安全性,再次强调一下,任何时候都不能保证数据绝不丢失。

该特性的工作方式是:

  1. KeyDB Slaves每秒PING一次Master,Ack自己处理的复制流的量
  2. KeyDB Master记住每个Slave的最后一次PING的时间
  3. 如果PING正常的Slave数量小于配置的值,Master停止写入

相关配置项:

  1. min-slaves-to-write:正常PING的Slave的最小数量
  2. min-slaves-max-lag:如果PING延迟大于此参数指定的秒数,认为Slave不正常
集群
简介
设计目标

KeyDB集群的设计目标,依据重要性排列如下:

  1. 高性能,支持最多1000节点规模。不使用代理、异步复制,不需要对键值进行merge操作
  2. 可接受的数据安全性:集群尽可能保证连接到大部分Master节点的客户端的写操作。最小化Ack后的写操作丢失的可能性,连接到少数Master节点的客户端的写操作丢失的可能性更大
  3. 可用性:当出现网络分区后,如果大部分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数据的场景:

  1. 写操作已经到达Master,应答客户端时,异步的复制尚未传播到Slave
  2. Master宕机
  3. 过了较长时间后,Slave晋升,它的数据集成为权威

另外一个场景:

  1. Master由于网络分区不可达
  2. 发生故障转移,Slave晋升
  3. 旧Master网络恢复
  4. 某个客户端使用过期的路由表,将数据写到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请求,即使源节点不受信任。但是对于其它消息,如果源节点不是集群成员,将全部丢弃。

节点在以下情况下,接受某个源节点作为集群成员:

  1. 新节点发送 MEET消息,此消息类似于PING,但是会强迫消息接受者,接纳源节点作为集群成员。仅当系统管理员执行:
    Shell
    1
    CLUSTER MEET ip port

    节点才会发送MEET消息

  2. 通过gossip协议传递的,关于节点被加入集群的事实。如果A、B已经是集群成员,现在C新加入并且MEET了A,那么A会gossip给B,这样A、B都认可C作为成员了
不支持NAT

总线不支持在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

使用Hash tag可以确保多个键被分配到同一个Slot,以便KeyDB集群支持multi-key操作。

如果键中包含 {...}则仅仅花括号中的部分用来计算Hash Slot,这可以保证{user1000}.following、{user1000}.followers这两个键被分到同一个Slot。

节点属性
节点ID

集群中的每个节点具有唯一的、自动生成的ID,该名称是160bit的随机数。节点会将此名称存放在配置文件中,并且一直使用相同的名称,除非:

  1. 配置文件被删除
  2. 执行硬重置命令 CLUSTER RESET

节点ID用于在集群范围内唯一的标识节点,节点可以改变自己的IP地址,但是它的ID不需要改变。集群可以感知节点的IP地质变更,并使用gossip协议,通过集群总线进行重配置。

其它属性

在任何节点执行 CLUSTER NODES可以获得节点列表,并列出其关键属性。除了上面的节点ID之外,其他属性包括:

  1. 节点IP地址:端口
  2. 一系列标记位
  3. 如果节点被标记为Slave,则它的Master是谁
Shell
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有关的命令有:

Shell
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:

  1. 向B发送: CLUSTER SETSlot Slot MIGRATING A。这样,B会接受所有关于Slot 8的查询,但是如果本地没有Key,则发送 ASK重定向,让客户度询问A
  2. 向A发送: CLUSTER SETSlot Slot IMPORTING B。这样,A会接受所有关于Slot 8的查询,但是请求必须以 ASKING命令作为前导,否则MOVE重定向给Slot的负责人B
关于多键操作

重新分片牵涉到键的迁移,这个迁移是逐步进行的 —— 不是原子的移动Slot。

这意味着,在迁移过程中,多键操作的Key,可能一部分位于节点A,一部分以及迁移到节点B。由于KeyDB集群限制多键操作仅仅牵涉单个节点(所有键位于一个Hash Slot),因此这种情况下会应答客户端以 TRYAGAIN错误。

重定向
MOVED

KeyDB客户端可以自由的向任何节点发送查询请求,节点会进行以下处理:

  1. 分析查询请求是否是acceptable的 —— 要么查询仅仅牵涉一个键,要么牵涉到在同一Hash Slot中的multikey
  2. 查找其内部的Slot-node映射,找到负责目标Hash Slot的节点:
    1. 如果是当前节点负责,直接处理请求
    2. 如果是其它节点负责,应答客户端以MOVED错误:
      Shell
      1
      2
      3
      GET x
      #      键所属Slot   负责处理此Slot的节点
      -MOVED 3999        127.0.0.1:6381

重定向中已经给出正确节点的信息,客户端应该(但不必须)缓存并且向正确节点发请求。

一旦重定向发生,可能潜在的Slot-node映射发生很大变化,客户端可以通过 CLUSTER NODES或 CLUSTER SlotS命令获取最新映射关系,并缓存。

ASK

MOVED用于提示客户端,很明确Hash Slot永久的被另外一个服务器所处理了。

ASK重定向则是提示客户端,Hash Slot可能处于迁移过程中,我虽然负责此Slot,但是没有找到你需要的Key,你去问问原来的所有者吧。

在Slave上读

默认情况下,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包和其他消息具有相同的头,该头的内容:

  1. Node ID,发送节点的标识符
  2. 发送节点的currentEpoch、configEpoch,这两个字段和KeyDB集群使用的分布式算法有关。Slave的configEpoch是它的Master的最后一个已知的configEpoch
  3. 节点标记,提示节点是Slave还是Master,以及其他的单比特节点属性
  4. 发送节点所负责的Hash Slot的位图信息,如果发送节点是Slave则发送它的Master的信息
  5. 发送节点的客户端TCP端口
  6. 从发送节点的角度来看,集群的状态(down/ok)
  7. 对于Slave,其Master的Node ID

PING/PONG包还包括一个gossip段,其中包含发送者所认为的,集群(一部分随机的,否则消息太大,具体数量取决于集群规模)其它节点的状态。每个节点的信息包括:Node ID、IP:PORT、节点标记。这个gossip段可以用来进行故障发现、发现新节点。

故障发现

故障发现(Failure Detection)用于发现这样的异常:Master/Slave不再被集群中的大部分节点可见,进而引起的Slave晋升为Master的过程。

如果无法通过晋升来解决故障,则集群进入错误状态,不再接受客户端查询请求。

通过前面的心跳机制我们了解到,每个节点都维护自己所认为的,其它节点的状态。状态表现为一系列标记,其中和故障发现有关的是:

  1. PFAIL,P表示Possible,即某个节点可能出故障了,但是尚未确认。如果目标节点在NODE_TIMEOUT时间范围内,从当前节点不可达,则标注为PFAIL。Master/Slave都有权标记某个节点为PFAIL
  2. FAIL,节点故障在固定的时间范围内被大多数Master所认可,该状态从PFAIL升级而来

前面我们了解到心跳的gossip段包含当前节点所认为的,随机的其它几个节点的状态。经过若干次心跳后,每个节点的的认知会传播到所有其它节点。当满足以下条件后,PFAIL变为FAIL:

  1. 某个节点A,将节点B标记为PFAIL
  2. 节点A通过心跳接收到的gossip分析集群中大部分节点关于B状态的声明
  3. 如果大部分节点在 NODE_TIMEOUT * FAIL_REPORT_VALIDITY_MULT的时间区间内,标记节点B为PFAIL或FAIL。那么A标记B为FAIL,并发送 FAIL消息给所有可达节点
  4. FAIL消息会导致所有接收节点都将节点B标记为FAIL,不管它是不是认为B处于PFAIL状态

当前实现将FAIL_REPORT_VALIDITY_MULT设置为2。这意味着,在两倍NODE_TIMEOUT时间内,大多数Master将某个节点标记为故障,则集群认为该节点宕掉了。

需要了解PFAIL到FAIL的转换,依赖于一种弱(一致)的协议:

  1. 节点在一段时间范围内,收集其它节点的(关于节点状态)视图。由于时间窗口的存在,我们无法确认(也不需要)在某个时间点是否大多数Master达成一致
  2. FAIL消息会被传播,但是无法保证所有节点都能收到并修改故障节点状态,这是因为故障常常伴随网络分区
故障恢复

节点状态可以从PFAIL变成FAIL,但是FAIL状态仅仅在以下情况下才能清除掉:

  1. 节点变得可达,并且成为Slave。由于Slave不能参与故障转移,因此FAIL状态清除
  2. 节点变得可达,成为Master,但是不负责任何Slot。这种情况下FAIL状态可以被清除,因为该节点并没有正常参与到集群中
  3. 节点变得可达,成为Master,过了很长时间(NODE_TIMEOUT的N倍)仍然没有可觉察的Slave晋升。这种情况下,最好是让该节点重新加入集群
配置传播 

KeyDB使用类似于Raft算法中的Term的概念,叫Epoch。当多个节点提供了冲突的信息时,Epoch可用于确定谁的状态是up-to-date的:

  1. currentEpoch,可以认为是集群状态的版本号,最终所有节点应该具有相同的currentEpoch
  2. configEpoch,每个Master节点具有独特的configEpoch,主要包含它负责的Hash Slots列表

每个新创建的节点,它的 currentEpoch(64bit无符号整数)都是0。每当从其它节点接收到一个消息,如果消息中的epoch大于本地的currentEpoch,则更新currentEpoch为接收到的epoch。

过了一段时间后,集群中所有节点都会使用最大的configEpoch作为自己的currentEpoch。

currentEpoch在集群节点需要协商,以决定执行某操作的时候用到,currentEpoch大的节点具有话语权。目前只支持Slave晋升这一种操作。

configEpoch

所有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的变化由于故障转移、再分片导致。

configEpoch冲突

如果由于Slave晋升导致configEpoch增加,集群会确保它是唯一的。

以下两种操作,仅仅简单的更新本地的configEpoch,可能导致configEpoch冲突:

  1. 使用带 TAKEOVER选项的 CLUSTER FAILOVER命令,可以强制晋升一个Slave节点,不需要大多数Master可用
  2. 手工迁移Slot来进行再分片,也仅仅在本地节点生成新的epoch

手工再分片时,如果Slot从A迁移到B,分片程序会强制B增加自己的epoch为可发现集群范围内的最大值 + 1,除非节点的epoch已经是最大。由于再分片常常牵涉到大量Slot的迁移,为了每个Slot,进行协商并获得更高的configEpoch是很低效的,因此增加epoch仅在本地进行,并且仅仅在迁移第一个Slot时进行。

尽管可能性较低,还是会出现多个节点声明相同configEpoch的情况 —— 如果手工再分片和自动故障转移发生外加运气差的话。KeyDB使用下面的冲突解决算法:

  1. 如果Master A发现Master B正在通告和自己相同的configEpoch
  2. 并且根据字典序比较,A的Node ID比B大
  3. 那么A将currentEpoch+1,并且将其作为自己的configEpoch

也就是说,如果集群中有若干节点具有相同configEpoch,那么其中ID最大的取胜,出现Hash Slot冲突时以它为准。

Slots配置传播

Hash Slots - 节点映射关系的传播,不管对于新集群,还是由于故障转移/手工重分片导致的Slot负责节点变更,都是关键的。

Hash Slots配置信息可以通过两种方式传播:

  1. 心跳消息:节点发送PING/PONG时总是携带它(或者它的Master)所负责的Slot的信息
  2. UPDATE消息:由于心跳中还携带了自己的epoch,如果接受者发现此epoch过期,则会发送UPDATE消息,强制过期节点更新配置

当一个新的节点加入到集群后,它的Hash Slot映射(16383 个key)简单的置空(表示未分配)。当接收到心跳/UPDATE消息后,根据以下规则来更新映射:

  1. 如果Slot为空,并且某个已知的节点claim该Slot,分配Slot给节点
  2. 如果Slot不为空,并且它映射给Master A,如果现在接收到Master B的消息,声明Slot归它管。这种情况下,如果B的configEpoch大于A,则重新绑定Slot给B

由于规则2的存在,最终集群中所有的节点都认可configEpoch最大的节点通告的映射关系,这就是所谓last failover wins机制。

故障转移

故障转移:

  1. 依赖于上面两节提到的故障发现、配置传播机制
  2. 由出问题的Master的Slave主导,其它Master节点参与投票完成
  3. 触发时机为:
    1. Master进入FAIL状态,并且经过了选举时延
    2. Master至少负责1个Slot
    3. Slave和Master的复制连接断开时间小于,这是为了保证Slave的数据不过于陈旧

故障转移过程如下:

  1. Slave增加自己的configEpoch,并同步currentEpoch
  2. 请求Masters来投票,具体做法是广播 FAILOVER_AUTH_REQUEST消息给所有Master节点
  3. 等待Master的应答,最长 NODE_TIMEOUT * 2,最短2秒
  4. 如果应答的Master的epoch小于发起晋升时Slave的currentEpoch,则应答被丢弃。这个可以防止接收到关于上一次选举的投票
  5. 如果Slave在限定时间内获得大多数Master的投票,则晋升成功。否则,等待 NODE_TIMEOUT * 4再次进行选举

Master参与投票时遵循以下规则:

  1. 对于一个给定的epoch,每个Master仅会投票一次:如果Master投票给Slave,它应答FAILOVER_AUTH_ACK,同时将lastVoteEpoch字段持久化到配置文件。Master在NODE_TIMEOUT * 2的时间内,不再能投票给故障Master的其它Slave,这可以防止同时选出多个新Master。
  2. 只有Master任何被故障转移的Master处于FAIL状态,才会进行投票
  3. 如果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包给整个集群。当前不可达的节点可能通过下面两种方式之一获得更新:

  1. 接收到其它节点的PING/PONG包
  2. 其自己发送的心跳,由于信息过期,被其它节点应答以UPDATE

重新配置的内容包括:

  1. 对于故障的Master的原有其它Slave,需要更新复制源
  2. 对于所有节点,需要更新Hash Slot - 节点映射关系 
旧Master重新加入

如果一个Master节点因为网络分区,脱离集群,并且时间足够长,发生了故障转移。那么网络恢复后,旧的Master如何处理?

在KeyDB中,旧Master将配置为偷取了它的Slot的那个节点的Slave。

节点重置

重置节点,可以让它以另外一个角色加入集群,或者加入其它集群。

执行 CLUSTER RESET可以软重置一个节点,如果不指定选项,默认为SOFT。制定HARD则进行硬重置。在重置时会进行如下处理:

  1. 如果节点是Slave,则角色切换为Master,丢弃数据集;如果节点是Master并且持有Key,则中止重置
  2. 释放所有Hash Slot,重置手工故障转移状态
  3. 移除节点表中所有其它节点,这样节点对原先集群一无所知
  4. 对于硬重置:currentEpoch、configEpoch、lastVoteEpoch均置零
  5. 对于硬重置:Node ID修改为新的随机值
移除节点

通过重新分片,将Master的Slot全部迁移走,就可以关闭它了。

由于其它节点仍然记着移除节点的Node ID和地址,还会尝试连接它,因此,你应当调用 CLUSTER FORGET <node-id>,该命令的作用:

  1. 从所有节点的nodes table中移除指定节点
  2. 60s内禁止被移除节点(根据ID)再次加入集群。防止因为gossip导致重新加入
订阅/发布

集群的客户端可以在任何节点上订阅,在任何节点上发布。KeyDB会保证消息被正确的转发 —— 目前的实现仅仅是简单的广播。

← CRIU和Pod在线迁移
2020年春节快乐 →

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code class="" title="" data-url=""> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre class="" title="" data-url=""> <span class="" title="" data-url="">

Related Posts

  • 记录一次KeyDB缓慢的定位过程
  • 使用Grafana展示时间序列数据
  • Prometheus学习笔记
  • OpenTSDB学习笔记
  • InfluxDB学习笔记

Recent Posts

  • Investigating and Solving the Issue of Failed Certificate Request with ZeroSSL and Cert-Manager
  • A Comprehensive Study of Kotlin for Java Developers
  • 背诵营笔记
  • 利用LangChain和语言模型交互
  • 享学营笔记
ABOUT ME

汪震 | Alex Wong

江苏淮安人,现居北京。目前供职于腾讯云,专注容器方向。

GitHub:gmemcc

Git:git.gmem.cc

Email:gmemjunk@gmem.cc@me.com

ABOUT GMEM

绿色记忆是我的个人网站,域名gmem.cc中G是Green的简写,MEM是Memory的简写,CC则是我的小天使彩彩名字的简写。

我在这里记录自己的工作与生活,同时和大家分享一些编程方面的知识。

GMEM HISTORY
v2.00:微风
v1.03:单车旅行
v1.02:夏日版
v1.01:未完成
v0.10:彩虹天堂
v0.01:阳光海岸
MIRROR INFO
Meta
  • Log in
  • Entries RSS
  • Comments RSS
  • WordPress.org
Recent Posts
  • Investigating and Solving the Issue of Failed Certificate Request with ZeroSSL and Cert-Manager
    In this blog post, I will walk ...
  • A Comprehensive Study of Kotlin for Java Developers
    Introduction Purpose of the Study Understanding the Mo ...
  • 背诵营笔记
    Day 1 Find Your Greatness 原文 Greatness. It’s just ...
  • 利用LangChain和语言模型交互
    LangChain是什么 从名字上可以看出来,LangChain可以用来构建自然语言处理能力的链条。它是一个库 ...
  • 享学营笔记
    Unit 1 At home Lesson 1 In the ...
  • K8S集群跨云迁移
    要将K8S集群从一个云服务商迁移到另外一个,需要解决以下问题: 各种K8S资源的迁移 工作负载所挂载的数 ...
  • Terraform快速参考
    简介 Terraform用于实现基础设施即代码(infrastructure as code)—— 通过代码( ...
  • 草缸2021
    经过四个多月的努力,我的小小荷兰景到达极致了状态。

  • 编写Kubernetes风格的APIServer
    背景 前段时间接到一个需求做一个工具,工具将在K8S中运行。需求很适合用控制器模式实现,很自然的就基于kube ...
  • 记录一次KeyDB缓慢的定位过程
    环境说明 运行环境 这个问题出现在一套搭建在虚拟机上的Kubernetes 1.18集群上。集群有三个节点: ...
  • eBPF学习笔记
    简介 BPF,即Berkeley Packet Filter,是一个古老的网络封包过滤机制。它允许从用户空间注 ...
  • IPVS模式下ClusterIP泄露宿主机端口的问题
    问题 在一个启用了IPVS模式kube-proxy的K8S集群中,运行着一个Docker Registry服务 ...
  • 念爷爷
      今天是爷爷的头七,十二月七日、阴历十月廿三中午,老人家与世长辞。   九月初,回家看望刚动完手术的爸爸,发

  • 6 杨梅坑

  • liuhuashan
    深圳人才公园的网红景点 —— 流花山

  • 1 2020年10月拈花湾

  • 内核缺陷触发的NodePort服务63秒延迟问题
    现象 我们有一个新创建的TKE 1.3.0集群,使用基于Galaxy + Flannel(VXLAN模式)的容 ...
  • Galaxy学习笔记
    简介 Galaxy是TKEStack的一个网络组件,支持为TKE集群提供Overlay/Underlay容器网 ...
TOPLINKS
  • Zitahli's blue 91 people like this
  • 梦中的婚礼 64 people like this
  • 汪静好 61 people like this
  • 那年我一岁 36 people like this
  • 为了爱 28 people like this
  • 小绿彩 26 people like this
  • 彩虹姐姐的笑脸 24 people like this
  • 杨梅坑 6 people like this
  • 亚龙湾之旅 1 people like this
  • 汪昌博 people like this
  • 2013年11月香山 10 people like this
  • 2013年7月秦皇岛 6 people like this
  • 2013年6月蓟县盘山 5 people like this
  • 2013年2月梅花山 2 people like this
  • 2013年淮阴自贡迎春灯会 3 people like this
  • 2012年镇江金山游 1 people like this
  • 2012年徽杭古道 9 people like this
  • 2011年清明节后扬州行 1 people like this
  • 2008年十一云龙公园 5 people like this
  • 2008年之秋忆 7 people like this
  • 老照片 13 people like this
  • 火一样的六月 16 people like this
  • 发黄的相片 3 people like this
  • Cesium学习笔记 90 people like this
  • IntelliJ IDEA知识集锦 59 people like this
  • 基于Kurento搭建WebRTC服务器 38 people like this
  • Bazel学习笔记 37 people like this
  • PhoneGap学习笔记 32 people like this
  • NaCl学习笔记 32 people like this
  • 使用Oracle Java Mission Control监控JVM运行状态 29 people like this
  • Ceph学习笔记 27 people like this
  • 基于Calico的CNI 27 people like this
Tag Cloud
ActiveMQ AspectJ CDT Ceph Chrome CNI Command Cordova Coroutine CXF Cygwin DNS Docker eBPF Eclipse ExtJS F7 FAQ Groovy Hibernate HTTP IntelliJ IO编程 IPVS JacksonJSON JMS JSON JVM K8S kernel LB libvirt Linux知识 Linux编程 LOG Maven MinGW Mock Monitoring Multimedia MVC MySQL netfs Netty Nginx NIO Node.js NoSQL Oracle PDT PHP Redis RPC Scheduler ServiceMesh SNMP Spring SSL svn Tomcat TSDB Ubuntu WebGL WebRTC WebService WebSocket wxWidgets XDebug XML XPath XRM ZooKeeper 亚龙湾 单元测试 学习笔记 实时处理 并发编程 彩姐 性能剖析 性能调优 文本处理 新特性 架构模式 系统编程 网络编程 视频监控 设计模式 远程调试 配置文件 齐塔莉
Recent Comments
  • qg on Istio中的透明代理问题
  • heao on 基于本地gRPC的Go插件系统
  • 黄豆豆 on Ginkgo学习笔记
  • cloud on OpenStack学习笔记
  • 5dragoncon on Cilium学习笔记
  • Archeb on 重温iptables
  • C/C++编程:WebSocketpp(Linux + Clion + boostAsio) – 源码巴士 on 基于C/C++的WebSocket库
  • jerbin on eBPF学习笔记
  • point on Istio中的透明代理问题
  • G on Istio中的透明代理问题
  • 绿色记忆:Go语言单元测试和仿冒 on Ginkgo学习笔记
  • point on Istio中的透明代理问题
  • 【Maven】maven插件开发实战 – IT汇 on Maven插件开发
  • chenlx on eBPF学习笔记
  • Alex on eBPF学习笔记
  • CFC4N on eBPF学习笔记
  • 李运田 on 念爷爷
  • yongman on 记录一次KeyDB缓慢的定位过程
  • Alex on Istio中的透明代理问题
  • will on Istio中的透明代理问题
  • will on Istio中的透明代理问题
  • haolipeng on 基于本地gRPC的Go插件系统
  • 吴杰 on 基于C/C++的WebSocket库
©2005-2025 Gmem.cc | Powered by WordPress | 京ICP备18007345号-2