前言
Redis 是我们目前大规模使用的缓存中间件,由于他强调高效而又便捷的功能,得到了广泛的使用。单节点的Redis已经达到了很高的性能,为了提高可用性我们可以使用Redis 集群。本文参考了Rdis的官方文档和使用Redis官方提供的Redis Cluster工具搭建Rdis集群。
Redis 集群的概念
介绍
Redis 集群是一个可以在多个 Redis 节点之间进行数据共享的设施(installation)。
Redis 集群不支持那些需要同时处理多个键的 Redis 命令, 因为执行这些命令需要在多个 Redis 节点之间移动数据, 并且在高负载的情况下, 这些命令将降低 Redis 集群的性能, 并导致不可预测的错误。
Redis 集群通过分区(partition)来提供一定程度的可用性(availability): 即使集群中有一部分节点失效或者无法进行通讯, 集群也可以继续处理命令请求。
Redis 集群提供了以下两个好处:
将数据自动切分(split)到多个节点的能力。
当集群中的一部分节点失效或者无法进行通讯时, 仍然可以继续处理命令请求的能力。
数据分片
Redis 集群使用数据分片(sharding)而非一致性哈希(consistency hashing)来实现: 一个 Redis 集群包含 16384 个哈希槽(hash slot), 数据库中的每个键都属于这 16384 个哈希槽的其中一个, 集群使用公式 CRC16(key) % 16384 来计算键 key 属于哪个槽, 其中 CRC16(key) 语句用于计算键 key 的 CRC16 校验和 。
集群中的每个节点负责处理一部分哈希槽。 举个例子, 一个集群可以有三个哈希槽, 其中:
- 节点 A 负责处理 0 号至 5500 号哈希槽。
- 节点 B 负责处理 5501 号至 11000 号哈希槽。
- 节点 C 负责处理 11001 号至 16384 号哈希槽。
这种将哈希槽分布到不同节点的做法使得用户可以很容易地向集群中添加或者删除节点。 比如说:
我现在想设置一个key,叫my_name:
set my_name zhangguoji
按照Redis Cluster的哈希槽算法,CRC16(‘my_name‘)%16384 = 2412 那么这个key就被分配到了节点A上
同样的,当我连接(A,B,C)的任意一个节点想获取my_name这个key,都会转到节点A上
再比如
如果用户将新节点 D 添加到集群中, 那么集群只需要将节点 A 、B 、 C 中的某些槽移动到节点 D 就可以了。
增加一个D节点的结果可能如下:
- 节点A覆盖1365-5460
- 节点B覆盖6827-10922
- 节点C覆盖12288-16383
- 节点D覆盖0-1364,5461-6826,10923-1228
与此类似, 如果用户要从集群中移除节点 A , 那么集群只需要将节点 A 中的所有哈希槽移动到节点 B 和节点 C , 然后再移除空白(不包含任何哈希槽)的节点 A 就可以了。
因为将一个哈希槽从一个节点移动到另一个节点不会造成节点阻塞, 所以无论是添加新节点还是移除已存在节点, 又或者改变某个节点包含的哈希槽数量, 都不会造成集群下线。
所以,Redis Cluster的模型大概是这样的形状
主从复制模型
为了使得集群在一部分节点下线或者无法与集群的大多数(majority)节点进行通讯的情况下, 仍然可以正常运作, Redis 集群对节点使用了主从复制功能: 集群中的每个节点都有 1 个至 N 个复制品(replica), 其中一个复制品为主节点(master), 而其余的 N-1 个复制品为从节点(slave)。
在之前列举的节点 A 、B 、C 的例子中, 如果节点 B 下线了, 那么集群将无法正常运行, 因为集群找不到节点来处理 5501 号至 11000号的哈希槽。
另一方面, 假如在创建集群的时候(或者至少在节点 B 下线之前), 我们为主节点 B 添加了从节点 B1 , 那么当主节点 B 下线的时候, 集群就会将 B1 设置为新的主节点, 并让它代替下线的主节点 B , 继续处理 5501 号至 11000 号的哈希槽, 这样集群就不会因为主节点 B 的下线而无法正常运作了。
不过如果节点 B 和 B1 都下线的话, Redis 集群还是会停止运作。
Redis一致性保证
Redis 并不能保证数据的强一致性. 这意味这在实际中集群在特定的条件下可能会丢失写操作:第一个原因是因为集群是用了异步复制. 写操作过程:
-
客户端向主节点B写入一条命令.
-
主节点B向客户端回复命令状态.
-
主节点将写操作复制给他得从节点 B1, B2 和 B3
主节点对命令的复制工作发生在返回命令回复之后, 因为如果每次处理命令请求都需要等待复制操作完成的话, 那么主节点处理命令请求的速度将极大地降低 —— 我们必须在性能和一致性之间做出权衡。 注意:Redis 集群可能会在将来提供同步写的方法。 Redis 集群另外一种可能会丢失命令的情况是集群出现了网络分区, 并且一个客户端与至少包括一个主节点在内的少数实例被孤立。
举个例子 假设集群包含 A 、 B 、 C 、 A1 、 B1 、 C1 六个节点, 其中 A 、B 、C 为主节点, A1 、B1 、C1 为A,B,C的从节点, 还有一个客户端 Z1 假设集群中发生网络分区,那么集群可能会分为两方,大部分的一方包含节点 A 、C 、A1 、B1 和 C1 ,小部分的一方则包含节点 B 和客户端 Z1 .
Z1仍然能够向主节点B中写入, 如果网络分区发生时间较短,那么集群将会继续正常运作,如果分区的时间足够让大部分的一方将B1选举为新的master,那么Z1写入B中得数据便丢失了.
注意, 在网络分裂出现期间, 客户端 Z1 可以向主节点 B 发送写命令的最大时间是有限制的, 这一时间限制称为节点超时时间(node timeout), 是 Redis 集群的一个重要的配置选项
Redis Cluster 部署文档
updated: 24/05/2019
1 说明
Redis Cluster 的主要特点如下:
- 无中心结构,客户端与 redis 节点直连,不需要中间代理层
- 节点冗余设计,slave->master 选举,集群容错
- 数据分片存储,且支持在线分片
- ASK / MOVED 转向机制,可通过任意节点,读写不属于本节点的数据
本文以部署一个可实现高可用的最小集群为例,集群部署在三台主机上,包含 M1、M2、M3、S1、S2、S3 六个节点。
M1、M2、M3 为主节点对应 Redis 实例:7000,7001,7002
S1、S2、S3 为从节点对应 Redis 实例:7003,7004,7005
主从节点交叉连接,对应关系为:
M1 -> S2
M2 -> S3
M3 -> S1
2 环境准备
系统环境:
主机 | IP | 节点-角色-实例(端口) |
---|---|---|
redis1 | 192.168.0.100 | M1-master-7000、S1-slave-7003 |
redis2 | 192.168.0.101 | M2-master-7001、S2-slave-7004 |
redis3 | 192.168.0.102 | M3-master-7002、S3-slave-7005 |
3 安装软件
分别在三台主机上编译安装 redis 5.0.4 版本。
安装完毕建议将 redis bin 目录加入 PATH 环境变量
4 配置集群
参考步骤 4.1 - 4.3,分别在三台主机上进行 Redis 集群配置。
4.1 创建集群目录
创建集群各节点配置和数据目录。
mkdir -p /data/redis-cluster/{7000,7001}
以 redis1 为例,目录结构如下:
/data/redis-cluster/
├── 7000
│ ├── redis.conf # redis 实例配置文件
│ └── nodes.conf # redis 集群节点的配置文件(由集群自动创建)
└── 7003
│ ├── redis.conf # redis 实例配置文件
│ └── nodes.conf # redis 集群节点的配置文件(由集群自动创建)
4.2 创建实例配置文件
分别复制 redis 源码目录下的 redis.conf 至节点配置和数据目录。
编辑 redis 实例配置文件中的各项配置。redis 默认未开启集群功能,需修改下面几个配置开启:
port 7000
bind 0.0.0.0 # 允许其他主机连接
dir /data/redis-cluster/7000 # 节点实例配置目录
cluster-enabled yes # 开启集群
cluster-config-file nodes.conf # 集群配置文件
cluster-node-timeout 5000 # 超时时间
appendonly yes # 并开启AOF模式
请根据生产环境性能需求和实际部署情况修改相关配置项,注意每个节点的实例配置文件中端口不同。
4.3 防火墙设置
根据情况修改服务器防火墙配置,允许 Redis 主机互相连接 7000-7005, 17000-17005 端口,允许所有业务服务器连接 Redis 服务器的 7000-7005 端口。
5 启动集群
5.1 启动实例
分别在三台主机上启动全部 6 个 redis 节点实例,启动时指定配置文件为各自节点配置目录中的 redis.conf。
5.2 启动集群
在任意一台 redis 节点主机上执行如下命令启动集群:
redis-cli --cluster create --cluster-replicas 1 192.168.0.100:7000 192.168.0.100:7001 192.168.0.101:7002 192.168.0.101:7003 192.168.0.102:7004 192.168.0.102:7005
注意修改命令中的 IP 为 redis 节点实例端口对应的真实主机 IP。
根据提示输入yes
,出现下列信息则表示集群创建成功。
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
6 测试
查看集群信息
在任意一个 redis 节点的主机上执行以下命令:
# redis-cli -c -h 192.168.0.100 -p 7000 cluster info
应输出类型以下信息:
cluster_state:ok
cluster_slots_assigned:16384
cluster_slots_ok:16384
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:6
cluster_size:3
cluster_current_epoch:6
cluster_my_epoch:1
cluster_stats_messages_ping_sent:101
cluster_stats_messages_pong_sent:98
cluster_stats_messages_sent:199
cluster_stats_messages_ping_received:93
cluster_stats_messages_pong_received:101
cluster_stats_messages_meet_received:5
cluster_stats_messages_received:199
列出集群节点
在任意一个 redis 节点的主机上执行以下命令,列出集群当前已知的所有节点(node),以及这些节点的相关信息。
# redis-cli -c -h 192.168.0.100 -p 7000 cluster nodes
输出类似以下信息:
50725018cd7f5f20214b0ed462975258397dfe29 192.168.0.102:7005@17005 slave 39e335386bb48f012f433287ed853174009114e5 0 1554876136000 6 connected
8874f69c2f5747cce3a02387167a00cdaba43d3c 192.168.0.100:7001@17001 master - 0 1554876137380 2 connected 5461-10922
c9cb48efbc1a83098cb730a5295402d9cdb343c2 192.168.0.101:7003@17003 slave 917301c74a9ed1ed52918b8e4c7f88de9743c361 0 1554876136375 4 connected
5633e918818033552b1adc089d99fbe9bcf36589 192.168.0.102:7004@17004 slave 8874f69c2f5747cce3a02387167a00cdaba43d3c 0 1554876136072 5 connected
917301c74a9ed1ed52918b8e4c7f88de9743c361 192.168.0.100:7000@17000 myself,master - 0 1554876136000 1 connected 0-5460
39e335386bb48f012f433287ed853174009114e5 192.168.0.101:7002@17002 master - 0 1554876136878 3 connected 10923-16383
查看集群数据槽分配
在任意一个 redis 节点的主机上执行以下命令,显示集群当前所有数据槽的分配情况。
# redis-cli -c -h 192.168.0.100 -p 7000 cluster slots
输出类似以下信息:
1) 1) (integer) 5461
2) (integer) 10922
3) 1) "127.0.0.1"
2) (integer) 7001
4) 1) "127.0.0.1"
2) (integer) 7004
2) 1) (integer) 0
2) (integer) 5460
3) 1) "127.0.0.1"
2) (integer) 7000
4) 1) "127.0.0.1"
2) (integer) 7003
3) 1) (integer) 10923
2) (integer) 16383
3) 1) "127.0.0.1"
2) (integer) 7002
4) 1) "127.0.0.1"
2) (integer) 7005