Redis 集群

Last Modified: 2022/11/25

概述

Redis 集群实现了 Redis 的水平扩展。本文将从用户视角阐述 Redis 集群的可用性和一致性特性。

注意:以下用 rc 代替 Redis 集群。另外副本和从实例会交替使用,他们表达相同的含义。

Redis Cluster 101

Redis 集群可以将数据自动分片到多个 Redis 实例中。rc 也提供了一定程度的高可用,即便一些节点挂了或者无法通信,也能正常工作。但当大规模故障,例如大部分主节点不可用时,rc 不再可用。总结下 rc 的能力:

  • 自动将数据分片到多个 Redis 实例;
  • 能够在一部分节点挂掉或者无法通信的情况下继续工作。

rc 的 TCP 端口

rc 中的每个节点都需要开放两个端口:一个是命令端口,用于服务客户端或者集群其他节点的 key 迁移;另一个是集群总线端口(以下用 bp 代替总线端口),用于节点间通信,例如,故障检测、配置更新和故障转移授权等)。bp 默认使用的端口号为命令端口号加上 10000,如果命令端口号为 6379,那么默认的 bp 为 16379。 为了减少处理时间和带宽消耗,bp 之间使用二进制通信协议。

rc 和 Docker

rc 不支持 NAT 环境和那些配置了端口映射的环境。Docker 使用了端口映射,因此为了 rc 和 Docker 能够兼容,Docker 的网络模式必须为主机网络模式(--net=host)。

rc 数据分片

rc 数据分片没有使用一致性哈希算法,而是另辟蹊径,将每个 key 归属于一个哈希槽(hash slot)。一共有 16384 个槽,计算一个 key 归属的哈希槽公式为:

CRC16(key) % 16384

集群中的每个节点都负责一部分哈希槽,假设 rc 中有三个节点:

  • 节点 A 包含的哈希槽 0 - 5500.
  • 节点 B 包含的哈希槽 5501 - 11000.
  • 节点 C 包含的哈希槽 11001 - 16383.

如果增加一个节点 D,只需要将 A、B 和 C 上的一部分槽移动到 D 上,如果删除 D,只需要将 D 上的槽移回到 A、B 和 C。这个过程是不需要任何停机时间的。多 key 命令(例如 mset)的支持是受限的,要求命令操作的多个 key 必须归属同一个哈希槽,可以借助于 rc 提供的哈希标签(hash tags)特性实现。哈希标签很容易理解,先看例子,下面两个 key 如果在不考虑哈希标签的情况下,他们大概率归属于不同的哈希槽,但是哈希标签的内容是相同的,都是 1,因此他们的哈希槽相同:

user:{1}:name
user:{1}:age

{}中的内容就是哈希标签,当 key 有含有哈希标签的时候,计算哈希槽不再使用 key,而是使用哈希标签。

为什么是 16384 个槽呢?其他人也有类似疑问,可以看这个 issue

The reason is:

Normal heartbeat packets carry the full configuration of a node, that can be replaced in an idempotent way with the old in order to update an old config. This means they contain the slots configuration for a node, in raw form, that uses 2k of space with16k slots, but would use a prohibitive 8k of space using 65k slots.
At the same time it is unlikely that Redis Cluster would scale to more than 1000 master nodes because of other design tradeoffs. So 16k was in the right range to ensure enough slots per master with a max of 1000 maters, but a small enough number to propagate the slot configuration as a raw bitmap easily. Note that in small clusters the bitmap would be hard to compress because when N is small the bitmap would have slots/N bits set that is a large percentage of bits set.

注:16384/8/1024 = 2KB

rc 主从模型

为保证 rc 在部分主实例不可用时依然正常工作,rc 使用主从模型保证每个槽都有 1 至 N个副本,N 取决于一个主实例有多少个副本。 如果我们仅有 a、b、c 三个主实例,那么当 b 不可用,那么整个集群就不可用了。但是如果 a、b、c 主实例都有一个副本 a1、b1、c1的时候,当 b 不可用,rc 会将 b1 提升为主实例,整个集群依然可用,但是如果 b 和 b1 同时不可用,那么 rc 依然不能正常工作。

rc 一致性

rc 不能保证强一致性。由于 Redis 采用异步复制,因此即便 Redis 实例已经确认了客户端的写入,仍然可能丢数据。来看下几种可能导致丢数据的情况:

  • 主实例确认了客户端的写入之后突然挂了,此时由于异步复制的原因,数据还没来得及复制到副本,副本被提升为主实例的之后,数据丢失;
  • 主实例和某个客户端遭遇了网络分区并处在了少数主实例的一侧,在一段时间内(node timeout 配置)客户端仍然可以向该主实例写入数据,之后该主实例发现自身不能和大部分其他主实例通信,停止接收写入。那么这部分写入的数据会丢失。

注:即便使用 WAIT 命令依旧无法保证数据不丢,只是丢失的概率变小而已,因为在发生故障转移时,无法保证被提升的那个副本一定接收了客户端的写入。

rc 配置参数

  • cluster-enabled <yes/no>: 如果是 yes 则开启集群模式,否则实例为正常模式。
  • cluster-config-file <filename>: 这里指定的文件,用户是不可编辑的,当配置发生变化 rc 会自动保存变动到文件中,重启的时候 rc 会读取该文件。
  • cluster-node-timeout <milliseconds>: 集群节点不可用时间超过该配置指定的时间,节点变为不可用。对主节点而言,超过该时间不可用会发生故障转移,对于其他任意节点而言,如果超过改时间不能和大部分主节点通信,节点将不可接受查询。
  • cluster-slave-validity-factor <factor>: 控制副本能否被提升为主实例。如果为 0, 不论主从之间断连多长时间,副本总是可以被提升为主实例;大于 0 时,如果主从之间断连时间小于 factor * node-timeout 则副本被提升为主实例,否则不可以提升。需要注意的是任何大于 0 的值都有可能导致集群不可用,因为当主实例不可用的时候,可能没有任何副本满足被提升的条件,此时只能等待主实例重新加入集群。
  • cluster-migration-barrier <count>: 控制副本是否可以在主实例之间迁移。迁移的目标主实例是那种没有可用副本的孤儿主实例。副本可迁移的条件是该副本对应的主实例当前可用副本数量大于等于 count+1。
  • cluster-require-full-coverage <yes/no>: 控制当部分哈希槽不可用时,rc 是否可接受查询。默认值 yes, 表示只有全部哈希槽都可用才接受查询。如果设置为 no,rc 将会继续提供查询即便只有部分哈希槽可用。
  • cluster-allow-reads-when-down <yes/no>: 控制节点在集群被标记为 failed 或者节点不能和大部分主实例连接的情况下是否可以接受查询。默认为 no,表示不可以。

创建和使用 rc

以下将按照以下步骤创建和使用 rc:

  • 创建 rc;
  • 和 rc 交互;
  • 使用 redis-rb-cluster 和 rc 交互;
  • rc 重新分片;
  • 测试故障转移;
  • 手动故障转移;
  • 添加一个节点;
  • 删除一个节点;
  • 副本迁移;
  • 升级 rc 中节点;
  • 升级到 rc;

创建 rc

创建 rc 的第一件事是准备一些空的运行于集群模式的 redis 节点, 一个最小 rc 集群至少包含 3 个主节点。但是为了安全,建议至少给每个主节点配置一个副本,也就是 6 个节点。我们以 6 个节点为例,假设他们的端口依次为 7000 到 7005:

mkdir cluster-test
cd cluster-test
# 注意 /path/to/redis.conf 应该替换成你电脑上 redis.conf 所在的真实路径
cp /path/to/redis.conf .
# 我们就以端口为目录名称,建立 6 个以端口号命名的文件夹,然后将 redis.conf 依次复制到各个目录下
mkdir 7000 7001 7002 7003 7004 7005
cp ./redis.conf ./7000
cp ./redis.conf ./7001
cp ./redis.conf ./7002
cp ./redis.conf ./7003
cp ./redis.conf ./7004
cp ./redis.conf ./7005

依次修改每个目录下的 redis.conf 的配置:

# 配置节点端口,注意修改这里的端口号和目录名称对应
port 7000
# 开启集群模式
cluster-enabled yes
# 集群配置存储路径,该文件由 rc 管理,用户无需修改
cluster-config-file nodes.conf
# 节点超时时间
cluster-node-timeout 5000
# 持久化模式
appendonly yes

打开 6 个 终端窗口, 并依次在每个窗口中启动一个 redis 实例:

# 终端窗口1
cd cluster-test/7000 && /path/to/redis-server ./redis.conf
# 终端窗口2
cd cluster-test/7001 && /path/to/redis-server ./redis.conf
# 终端窗口3
cd cluster-test/7002 && /path/to/redis-server ./redis.conf
# 终端窗口4
cd cluster-test/7003 && /path/to/redis-server ./redis.conf
# 终端窗口5
cd cluster-test/7004 && /path/to/redis-server ./redis.conf
# 终端窗口6
cd cluster-test/7005 && /path/to/redis-server ./redis.conf

注: 需要将 /path/to/redis-server 改成你电脑上 redis-server 所在的实际路径。启动每个实例,你可以从终端窗口中看到一下输出:

[82462] 26 Nov 11:56:55.329 * No cluster configuration found, I'm 97a3a64667477371c4479320d683e4c8db5858b1

其中 97a3a64667477371c4479320d683e4c8db5858b1 称之为节点 id,每个运行于集群模式的节点都会生成一个唯一的节点 id 且保持不变,节点的 ip 和端口可能会变,但是节点的 id 不会变,集群中的每个节点都会记住集群中其他节点的节点 id。

以上仅仅是启动了每个节点,下面开始创建集群:

# 注意将 /path/to/redis-cli 给成你电脑上真实的 redis-cli 路径
/path/to/redis-cli --cluster create 127.0.0.1:7000 127.0.0.1:7001 \
127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 \
--cluster-replicas 1

运行以上命令之后,redis-cli 会给一个推荐的集群配置,需要做出选择是否接受该配置。从以下输出可以看出,对于 6 个节点,redis-cli 推荐的配置是 3 主 3 从。

>>> Performing hash slots allocation on 6 nodes...
Master[0] -> Slots 0 - 5460
Master[1] -> Slots 5461 - 10922
Master[2] -> Slots 10923 - 16383
Adding replica 127.0.0.1:7004 to 127.0.0.1:7000
Adding replica 127.0.0.1:7005 to 127.0.0.1:7001
Adding replica 127.0.0.1:7003 to 127.0.0.1:7002
>>> Trying to optimize slaves allocation for anti-affinity
[WARNING] Some slaves are in the same host as their master
M: a85dfdb9c351526396500ee8c3bafd3d7c0a084c 127.0.0.1:7000
   slots:[0-5460] (5461 slots) master
M: 3d14f453887ffac57e3f4dbdf95650f0d8b0a923 127.0.0.1:7001
   slots:[5461-10922] (5462 slots) master
M: bc1bc1e30df89c7b17f01dbe304c3bd76252ad0b 127.0.0.1:7002
   slots:[10923-16383] (5461 slots) master
S: abe6e9b1ae8e70c00ce86120d5e8142b59346861 127.0.0.1:7003
   replicates 3d14f453887ffac57e3f4dbdf95650f0d8b0a923
S: f81b41a77af7f7a76da736b6d0fb4fcb32be04db 127.0.0.1:7004
   replicates bc1bc1e30df89c7b17f01dbe304c3bd76252ad0b
S: fc249624b2abead71229c14a166395ff12648a1e 127.0.0.1:7005
   replicates a85dfdb9c351526396500ee8c3bafd3d7c0a084c
Can I set the above configuration? (type 'yes' to accept): 

如果我们选择 yes,接受以上自动配置,即可完成 rc 的创建,从以下输出可以看出,正好创建了 3 主 3 从,且所有的 16384 个哈希槽都被覆盖。

Can I set the above configuration? (type 'yes' to accept): yes
>>> Nodes configuration updated
>>> Assign a different config epoch to each node
>>> Sending CLUSTER MEET messages to join the cluster
Waiting for the cluster to join
.
>>> Performing Cluster Check (using node 127.0.0.1:7000)
M: a85dfdb9c351526396500ee8c3bafd3d7c0a084c 127.0.0.1:7000
   slots:[0-5460] (5461 slots) master
   1 additional replica(s)
S: f81b41a77af7f7a76da736b6d0fb4fcb32be04db 127.0.0.1:7004
   slots: (0 slots) slave
   replicates bc1bc1e30df89c7b17f01dbe304c3bd76252ad0b
S: abe6e9b1ae8e70c00ce86120d5e8142b59346861 127.0.0.1:7003
   slots: (0 slots) slave
   replicates 3d14f453887ffac57e3f4dbdf95650f0d8b0a923
M: 3d14f453887ffac57e3f4dbdf95650f0d8b0a923 127.0.0.1:7001
   slots:[5461-10922] (5462 slots) master
   1 additional replica(s)
S: fc249624b2abead71229c14a166395ff12648a1e 127.0.0.1:7005
   slots: (0 slots) slave
   replicates a85dfdb9c351526396500ee8c3bafd3d7c0a084c
M: bc1bc1e30df89c7b17f01dbe304c3bd76252ad0b 127.0.0.1:7002
   slots:[10923-16383] (5461 slots) master
   1 additional replica(s)
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.

以上我们手动创建的集群,还有更简便的方式,我们可以利用 Redis 发布包中的脚本创建集群,在 utils/create-cluster 目录下有个 create-cluster 脚本。

# 启动 redis 实例
create-cluster start
# 创建集群,集群创建成功后,下次只需要启动运行上面的启动命令即可,无需再次创建。
create-cluster create
# 停止集群,利用 redis-cli 连上每个实例,并运行 shutdown
create-cluster stop

和 rc 交互

利用 redis-cli 随便连接其中一个节点,便可和 rc 交互。例如我们这里连接 7000 端口,并执行 set foo bar,由于 foo 归属的哈希槽在 7002,rc 节点自动将客户端重定向到了 7002。但是一个严肃完备的客户端能够保存哈希槽和节点间的映射关系,并直接连接到正确的节点。并在集群节点映射关系发生(例如增加或删除了节点)变化时更新映射关系。

> /path/to/redis-cli -c -p 7000
127.0.0.1:7000> set foo bar
-> Redirected to slot [12182] located at 127.0.0.1:7002
OK
127.0.0.1:7002> get foo
"bar"
127.0.0.1:7002>

使用 redis-rb-cluster 和 rc 交互

require './cluster'

startup_nodes = [
      {:host => "127.0.0.1", :port => 7000},
      {:host => "127.0.0.1", :port => 7001}
  ]

rc = RedisCluster.new(startup_nodes,32,:timeout => 0.1)

last = false

while not last
  begin
      last = rc.get("__last__")
      last = 0 if !last
  rescue => e
      puts "error #{e.to_s}"
      sleep 1
  end
end

((last.to_i+1)..1000000000).each{|x|
  begin
      rc.set("foo#{x}",x)
      puts rc.get("foo#{x}")
      rc.set("__last__",x)
  rescue => e
      puts "error #{e.to_s}"
  end
  sleep 0.1
}

这个程序运行就相当于往集群中写入 0 到 1000000000 的数值,key 值分别为 foo0 到 fo1000000000。startup_nodes 指定连接到集群的节点,一般来说指定集群中的任意节点均可以,只要这个节点本身是可达的,其他节点能够自动发现。

SET foo0 0
SET foo1 1
SET foo2 2
...

运行程序将会得到如下输出:

ruby ./example.rb
1
2
3
4
5
6
7
8
9
^C (由于输出内容多到爆炸,这里提前中断程序)

rc 重新分片(Reshard)

保持之前的程序一直运行,与此同时我们对集群重新分片,即将一部分哈希槽从一个节点迁移到其他节点。先利用 redis-cli 执行 reshard 命令:

# 127.0.0.1:7000 是集群中的任一节点均可,因为其他节点可以自动发现
redis-cli --cluster reshard 127.0.0.1:7000

执行分片命令后,会询问想要移动多少哈希槽。

How many slots do you want to move (from 1 to 16384)?
# 假如输入1000,则进一步询问这 1000 个哈希槽移动到的目标节点 id
What is the receiving node ID? 

假如我们想将 1000 个哈希槽移动到使用 7000 端口的节点上,可以运行以下命令获得该节点的 id。

$ redis-cli -p 7000 cluster nodes | grep myself
97a3a64667477371c4479320d683e4c8db5858b1 :0 myself,master - 0 0 0 connected 0-5460

以上过程是交互式的,我们也可以用一条命令完成迁移:

redis-cli --cluster reshard <host>:<port> --cluster-from <node-id> --cluster-to <node-id> --cluster-slots <number of slots> --cluster-yes

完成哈希槽迁移之后,我们使用下面的命令检查集群状态,我们会发现 7000 端口的实例中的哈希槽数量增加了 1000。

redis-cli --cluster check 127.0.0.1:7000

我们是在程序持续写入的情况下执行分片操作的,发现程序能够正常运行,集群也保持正常。

测试故障转移

保持 ruby 程序运行的同时,将其中一个主实例干掉,观察程序是否正常运行。可以 redis-cli 执行下面的命令将运行于 7002 端口的程序干掉。

$ redis-cli -p 7002 debug segfault
Error: Server closed the connection

由于主实例崩溃,rc 会自动执行故障转移,在故障转移期间,发现程序输出了很多写入错误信息。当我们再次将 7002 重启的之后,它不在是主实例,它变成了它之前副本(现在已经是主实例)的副本。

手动故障转移

手动故障转移一般是出于运维需要而非真正的故障,比如我们要升级一个主节点(假设是 M1),此时我们可以连到该主实例的其中一个副本上(假设是 C1),并在 C1 上执行 CLUSTER FAILOVER。这就完成了故障转移,相对于真实的故障转移,手动故障转移更安全,不会导致数据的丢失。过程大概是这样的:

  • 连接到 M1 的客户端被暂停,与此同时主实例将复制偏移(replication offset)发送给 C1 并等待 C1 同步到该偏移;
  • 当 C1 同步到主实例的偏移后,故障转移才真正开始,M1 被通知配置切换,此时连到 M1 的客户端被解除阻塞并被重定向到新的主实例 C1;

注:将 C1 升级提升为主实例,需要得到多数主实例的授权。

添加一个节点

添加一个节点,首先需要考虑的是该节点作为主节点还是副本节点。不论那种情况,应该保证新添加的节点是空的。添加节点的方式和前面创建集群时添加节点的方式完全相同,由于前面端口号已经使用到 7005,新节点我们使用端口号 7006。待节点启动完毕,我们可以使用下面的命令将该节点加入集群:

redis-cli --cluster add-node 127.0.0.1:7006 127.0.0.1:7000

add-node 命令后面有两个 ip 和端口,第一个是新节点的 ip 和端口,另外一个是集群中任一节点的 ip 和端口。通过以上命令加入集群的节点类型是主节点,但是这个主节点目前没有分配任何的哈希槽,由于没有哈希槽,该主节点也不参与故障转移的选举。我们可以分配一些哈希槽给该节点,分配方式在前面的“rc 数据分片”中已经做过说明,这里不再赘述。

如果要使新节点作为副本节点,我们需要对以上命令做一点变化:

# 该命令没有指定副本的主节点,因此该副本的主实例是随机选择的
redis-cli --cluster add-node 127.0.0.1:7006 127.0.0.1:7000 --cluster-slave
# 我们也可以通过 cluster-master-id 明确指定副本的主节点
redis-cli --cluster add-node 127.0.0.1:7006 127.0.0.1:7000 --cluster-slave --cluster-master-id 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912

除了上述方式之外,我们还可以将添加的新节点作为主节点添加,之后再使用 cluster replicate 命令将其类型转化为副本。

redis 127.0.0.1:7006> cluster replicate 3c3a0c74aae0b56170ccb03a76b60cfe7dc1912e

删除节点

删除副本节点很简单,我们只需要执行以下命令:

# 127.0.0.1:7000 是集群中的任一节点,用于连接集群。 node-id 才是要删除的节点。
redis-cli --cluster del-node 127.0.0.1:7000 `<node-id>`

但是删除主节点,我们需要确保主节点是空节点,可以使用 reshard 命令将哈希槽移动到其他主实例,之后可以用以上命令删除主节点。

副本迁移

副本迁移的主要目的是增强集群的可用性。迁移指的是副本从当前主实例迁移到另一个当前没有副本的主实例下(该主实例可能原先是副本经过故障转移被提升为主实例)。副本迁移特性能够在降低成本的同时,提升集群的可用性。为什么说降低成本?假如我们给每个主节点都配置两个或两个以上的副本,显然成本很高,有了副本迁移特性后,我们可以给每个主实例配置一个副本,同时给其中一部分主节点再增配几个副本,当某个主节点发生故障转移时,可以将那些有多个副本的主实例下的一个副本迁移到当前没有副本的主实例下。

升级 rc 中节点

之前有提到手动故障转移,可以利用该特性升级 rc 中的节点,不再赘述。

迁移到 rc

我们可以使用以下步骤将已有的实例迁移到集群中。需要注意的是如果应用使用了多 key 操作的命令,我们需要改造应用。注意迁移过程中,我们需要停止所有的客户端。 升级步骤如下:

  • 停止所有的客户端;
  • 在所有待迁移实例上执行 BGREWRITEAOF 命令生成 aof 文件;
  • Save your AOF files from aof-1 to aof-N somewhere. At this point you can stop your old instances if you wish (this is useful since in non-virtualized deployments you often need to reuse the same computers).
  • Create a Redis Cluster composed of N masters and zero replicas. You'll add replicas later. Make sure all your nodes are using the append only file for persistence.
  • Stop all the cluster nodes, substitute their append only file with your pre-existing append only files, aof-1 for the first node, aof-2 for the second node, up to aof-N.
  • Restart your Redis Cluster nodes with the new AOF files. They'll complain that there are keys that should not be there according to their configuration.
  • Use redis-cli --cluster fix command in order to fix the cluster so that keys will be migrated according to the hash slots each node is authoritative or not.
  • Use redis-cli --cluster check at the end to make sure your cluster is ok.
  • Restart your clients modified to use a Redis Cluster aware client library.
有问题吗?点此反馈!

温馨提示:反馈需要登录