课程目标
1、 掌握 Redis 主从复制的配置和原理
2、 掌握 Redis 哨兵机制(Sentinel)原理和实战
3、 掌握Redis分布式的各种方案对比,包括客户端Sharding、代理Proxy和Redis Cluster
1 为什么需要Redis集群
- 性能
Redis 本身的 QPS 已经很高了,但是如果在一些并发量非常高的情况下,性能还是会受到影响。这个时候我们希望有更多的 Redis 服务来完成工作。 - 扩展
第二个是出于存储的考虑。因为 Redis 所有的数据都放在内存中,如果数据量大,很容易受到硬件的限制。升级硬件收效和成本比太低,所以我们需要有一种横向扩展的方法。 - 可用性
第三个是可用性和安全的问题。如果只有一个 Redis 服务,一旦服务宕机,那么所有的客户端都无法访问,会对业务造成很大的影响。另一个,如果硬件发生故障,而单机的数据无法恢复的话,带来的影响也是灾难性的。
可用性、数据安全、性能都可以通过搭建多个 Reids 服务实现。其中有一个是主节点(master),可以有多个从节点(slave)。主从之间通过数据同步,存储完全相同的数据。如果主节点发生故障,则把某个从节点改成主节点,访问新的主节点。
2 Redis 主从复制
2.1 主从复制配置
例如一主多从,203 是主节点,在每个slave节点的redis.conf配置文件增加一行
slaveof 192.168.8.203 6379
//在主从切换的时候,这个配置会被重写成:
# Generated by CONFIG REWRITE
replicaof 192.168.8.203 6379
或者在启动服务时通过参数指定 master 节点:
/redis-server --slaveof 192.168.8.203 6379
或在客户端直接执行 slaveof xx xx,使该 Redis 实例成为从节点。
启动后,查看集群状态:
redis> info replication
从节点不能写入数据(只读),只能从 master 节点同步数据。get 成功,set 失败。
127.0.0.1:6379> set gupao 666
(error) READONLY You can't write against a read only replica.
主节点写入后,slave 会自动从 master 同步数据。
断开复制:
redis> slaveof no one
此时从节点会变成自己的主节点,不再复制数据。
2.2 主从复制原理
- 连接阶段
- slave node 启动时(执行 slaveof 命令),会在自己本地保存 master node 的信息,包括 master node 的 host 和 ip。
- slave node 内部有个定时任务 replicationCron(源码 replication.c),每隔 1秒钟检查是否有新的 master node 要连接和复制,如果发现,就跟 master node 建立socket 网络连接,如果连接成功,从节点为该 socket 建立一个专门处理复制工作的文件事件处理器,负责后续的复制工作,如接收 RDB 文件、接收命令传播等。当从节点变成了主节点的一个客户端之后,会给主节点发送 ping 请求。
- 数据同步阶段
master node 第一次执行全量复制,通过 bgsave 命令在本地生成一份 RDB 快照,将 RDB 快照文件发给 slave node(如果超时会重连,可以调大 repl-timeout 的值)。slave node 首先清除自己的旧数据,然后用 RDB 文件加载数据。
问题:生成 RDB 期间,master 接收到的命令怎么处理?
开始生成 RDB 文件时,master 会把所有新的写命令缓存在内存中。在 slave node保存了 RDB 之后,再将新的写命令复制给 slave node。
- 命令传播阶段
master node 持续将写命令,异步复制给 slave node。
延迟是不可避免的,只能通过优化网络
repl-disable-tcp-nodelay no
当设置为 yes 时,TCP 会对包进行合并从而减少带宽,但是发送的频率会降低,从节点数据延迟增加,一致性变差;具体发送频率与 Linux 内核的配置有关,默认配置为40ms。当设置为 no 时,TCP 会立马将主节点的数据发送给从节点,带宽增加但延迟变小。
一般来说,只有当应用对 Redis 数据不一致的容忍度较高,且主从节点之间网络状况不好时,才会设置为 yes;多数情况使用默认值 no。
问题:如果从节点有一段时间断开了与主节点的连接是不是要重新全量复制一遍?如果可以增量复制,怎么知道上次复制到哪里?
通过 master_repl_offset 记录的偏移量
redis> info replication
2.3 主从复制的不足
主从模式解决了数据备份和性能(通过读写分离)的问题,但是还是存在一些不足:
- RDB 文件过大的情况下,同步非常耗时。
- 在一主一从或者一主多从的情况下,如果主服务器挂了,对外提供的服务就不可用了,单点问题没有得到解决。如果每次都是手动把之前的从服务器切换成主服务器,这个比较费时费力,还会造成一定时间的服务不可用。
3 可用性保证之 Sentinel
3.1 Sentinel 原理
如何实现主从的自动切换?我们的思路:
创建一台监控服务器来监控所有 Redis 服务节点的状态,比如,master 节点超过一定时间没有给监控服务器发送心跳报文,就把 master 标记为下线,然后把某一个 slave变成 master。应用每一次都是从这个监控服务器拿到 master 的地址。
问题是:如果监控服务器本身出问题了怎么办?那我们就拿不到 master 的地址了,应用也没有办法访问。
那我们再创建一个监控服务器,来监控监控服务器……似乎陷入死循环了,这个问题怎么解决?这个问题先放着。
Redis 的 Sentinel 就是这种思路:通过运行监控服务器来保证服务的可用性。官网:https://redis.io/topics/sentinel
从 Redis2.8 版本起,提供了一个稳定版本的 Sentinel(哨兵),用来解决高可用的问题。它是一个特殊状态的 redis 实例。
我们会启动一个或者多个 Sentinel 的服务(通过 src/redis-sentinel),它本质上只是一个运行在特殊模式之下的 Redis,Sentinel 通过 info 命令得到被监听 Redis 机器的master,slave 等信息。
为了保证监控服务器的可用性,我们会对 Sentinel 做集群的部署。Sentinel 既监控所有的 Redis 服务,Sentinel 之间也相互监控。
注意:Sentinel 本身没有主从之分,只有 Redis 服务节点有主从之分。
概念梳理:master,slave(redis group),sentinel,sentinel 集合
- 服务下线
Sentinel 默认以每秒钟 1 次的频率向 Redis 服务节点发送 PING 命令。如果在down-after-milliseconds 内都没有收到有效回复,Sentinel 会将该服务器标记为下线(主观下线)。
# sentinel.conf
sentinel down-after-milliseconds
这个时候 Sentinel 节点会继续询问其他的 Sentinel 节点,确认这个节点是否下线,如果多数 Sentinel 节点都认为 master 下线,master 才真正确认被下线(客观下线),这个时候就需要重新选举 master。
- 故障转移
如果 master 被标记为下线,就会开始故障转移流程。既然有这么多的 Sentinel 节点,由谁来做故障转移的事情呢?
故障转移流程的第一步就是在 Sentinel 集群选择一个 Leader,由 Leader 完成故障转移流程。Sentinle 通过 Raft 算法,实现 Sentinel 选举。
Ratf 算法
在分布式存储系统中,通常通过维护多个副本来提高系统的可用性,那么多个节点之间必须要面对数据一致性的问题。Raft 的目的就是通过复制的方式,使所有节点达成一致,但是这么多节点,以哪个节点的数据为准呢?所以必须选出一个 Leader。
大体上有两个步骤:领导选举,数据复制。
Raft 是一个共识算法(consensus algorithm)。比如比特币之类的加密货币,就需要共识算法。Spring Cloud 的注册中心解决方案 Consul 也用到了 Raft 协议。
Raft 的核心思想:先到先得,少数服从多数。
Raft 算法演示:
http://thesecretlivesofdata.com/raft/
总结:
Sentinle 的 Raft 算法和 Raft 论文略有不同。
1、master 客观下线触发选举,而不是过了 election timeout 时间开始选举。
2、Leader 并不会把自己成为 Leader 的消息发给其他 Sentinel。其他 Sentinel 等待 Leader 从 slave 选出 master 后,检测到新的 master 正常工作后,就会去掉客观下线的标识,从而不需要进入故障转移流程。
问题:怎么让一个原来的 slave 节点成为主节点?
- 选出 Sentinel Leader 之后,由 Sentinel Leader 向某个节点发送 slaveof no one命令,让它成为独立节点。
- 然后向其他节点发送 slaveof x.x.x.x xxxx(本机服务),让它们成为这个节点的子节点,故障转移完成。
问题:这么多从节点,选谁成为主节点?
关于从节点选举,一共有四个因素影响选举的结果,分别是断开连接时长、优先级排序、复制数量、进程 id。
如果与哨兵连接断开的比较久,超过了某个阈值,就直接失去了选举权。如果拥有选举权,那就看谁的优先级高,这个在配置文件里可以设置(replica-priority 100),数值越小优先级越高。
如果优先级相同,就看谁从 master 中复制的数据最多(复制偏移量最大),选最多的那个,如果复制数量也相同,就选择进程 id 最小的那个。
Sentinel 的功能总结
- 监控:Sentinel 会不断检查主服务器和从服务器是否正常运行。
- 通知:如果某一个被监控的实例出现问题,Sentinel 可以通过 API 发出通知。
- 自动故障转移(failover):如果主服务器发生故障,Sentinel 可以启动故障转移过程。把某台服务器升级为主服务器,并发出通知。
- 配置管理:客户端连接到 Sentinel,获取当前的 Redis 主服务器的地址。
Sentinel 实战
Sentinel 配置
为了保证 Sentinel 的高可用,Sentinel 也需要做集群部署,集群中至少需要三个Sentinel 实例(推荐奇数个,防止脑裂)。
以 Redis 安装路径/usr/local/soft/redis-5.0.5/为例。
在 204 和 205 的 src/redis.conf 配置文件中添加
slaveof 192.168.8.203 6379
在 203、204、205 创建 sentinel 配置文件(安装后根目录下默认有 sentinel.conf):
cd /usr/local/soft/redis-5.0.5
mkdir logs
mkdir rdbs
mkdir sentinel-tmp
vim sentinel.conf
三台服务器内容相同:
daemonize yes
port 26379
protected-mode no
dir "/usr/local/soft/redis-5.0.5/sentinel-tmp"
sentinel monitor redis-master 192.168.8.203 6379 2
sentinel down-after-milliseconds redis-master 30000
sentinel failover-timeout redis-master 180000
sentinel parallel-syncs redis-master 1
上面出现了 4 个'redis-master',这个名称要统一,并且使用客户端(比如 Jedis)连接的时候名称要正确。
Sentinel 验证
启动 Redis 服务和 Sentinel
cd /usr/local/soft/redis-5.0.5/src
# 启动 Redis 节点
./redis-server ../redis.conf
# 启动 Sentinel 节点
./redis-sentinel ../sentinel.conf
# 或者
./redis-server ../sentinel.conf --sentinel
查看集群状态:
203
204 和 205
模拟 master 宕机,在 203 执行:
redis> shutdown
205 被选为新的 Master,只有一个 Slave 节点
注意看 sentinel.conf 里面的 redis-master 被修改了!
模拟原 master 恢复,在 203 启动 redis-server。它还是 slave,但是 master 又有两个 slave 了。
slave 宕机和恢复省略。
Sentinel 连接使用
Jedis 连接 Sentinel,master name 来自于 sentinel.conf 的配置。
private static JedisSentinelPool createJedisPool() {
String masterName = "redis-master";
Set sentinels = new HashSet();
sentinels.add("192.168.8.203:26379");
sentinels.add("192.168.8.204:26379");
sentinels.add("192.168.8.205:26379");
pool = new JedisSentinelPool(masterName, sentinels);
return pool;
}
Spring Boot 连接 Sentinel
spring.redis.sentinel.master=redis-master
spring.redis.sentinel.nodes=192.168.8.203:26379,192.168.8.204:26379,192.168.8.205:26379
无论是 Jedis 还是 Spring Boot(2.x 版本默认是 Lettuce),都只需要配置全部哨兵的地址,由哨兵返回当前的 master 节点地址。
哨兵机制的不足
主从切换的过程中会丢失数据,因为只有一个 master。只能单点写,没有解决水平扩容的问题。如果数据量非常大,这个时候我们需要多个 master-slave 的 group,把数据分布到不同的 group 中。
问题来了,数据怎么分片?分片之后,怎么实现路由?
4 Redis 分布式方案
如果要实现 Redis 数据的分片,我们有三种方案。第一种是在客户端实现相关的逻辑,例如用取模或者一致性哈希对 key 进行分片,查询和修改都先判断 key 的路由。
第二种是把做分片处理的逻辑抽取出来,运行一个独立的代理服务,客户端连接到这个代理服务,代理服务做请求的转发。
第三种就是基于服务端实现。
-
客户端 Sharding
Jedis 客户端提供了 Redis Sharding 的方案,并且支持连接池。
public class ShardingTest {
public static void main(String[] args) {
JedisPoolConfig poolConfig = new JedisPoolConfig();
// Redis服务器
JedisShardInfo shardInfo1 = new JedisShardInfo("127.0.0.1", 6379);
JedisShardInfo shardInfo2 = new JedisShardInfo("192.168.8.205", 6379);
// 连接池
List infoList = Arrays.asList(shardInfo1, shardInfo2);
ShardedJedisPool jedisPool = new ShardedJedisPool(poolConfig, infoList);
ShardedJedis jedis = null;
try{
jedis = jedisPool.getResource();
for(int i=0; i<100; i++){
jedis.set("k"+i, ""+i);
}
for(int i=0; i<100; i++){
Client client = jedis.getShard("k"+i).getClient();
System.out.println("取到值:"+jedis.get("k"+i)+","+"当前key位于:" + client.getHost() + ":" + client.getPort());
}
}finally{
if(jedis!=null) {
jedis.close();
}
}
}
}
使用 ShardedJedis 之类的客户端分片代码的优势是配置简单,不依赖于其他中间件,分区的逻辑可以自定义,比较灵活。但是基于客户端的方案,不能实现动态的服务增减,每个客户端需要自行维护分片策略,存在重复代码。
第二种思路就是把分片的代码抽取出来,做成一个公共服务,所有的客户端都连接到这个代理层。由代理层来实现请求和转发。
4.2 代理 Proxy
典型的代理分区方案有 Twitter 开源的 Twemproxy 和国内的豌豆荚开源的 Codis。
-
Twemproxy
two-em-proxy
https://github.com/twitter/twemproxy
Twemproxy 的优点:比较稳定,可用性高。
不足:
1、出现故障不能自动转移,架构复杂,需要借助其他组件(LVS/HAProxy +
Keepalived)实现 HA
2、扩缩容需要修改配置,不能实现平滑地扩缩容(需要重新分布数据)。 -
Codis
Codis 是一个代理中间件,用 Go 语言开发的。
功能:客户端连接 Codis 跟连接 Redis 没有区别。
分片原理:Codis 把所有的 key 分成了 N 个槽(例如 1024),每个槽对应一个分组,一个分组对应于一个或者一组 Redis 实例。Codis 对 key 进行 CRC32 运算,得到一个32 位的数字,然后模以 N(槽的个数),得到余数,这个就是 key 对应的槽,槽后面就是 Redis 的实例。比如4个槽:
Codis 的槽位映射关系是保存在 Proxy 中的,如果要解决单点的问题,Codis 也要做集群部署,多个 Codis 节点怎么同步槽和实例的关系呢?需要运行一个 Zookeepe(r 或者 etcd/本地文件)。
在新增节点的时候,可以为节点指定特定的槽位。Codis 也提供了自动均衡策略。
Codis 不支持事务,其他的一些命令也不支持。
不支持的命令 https://github.com/CodisLabs/codis/blob/release3.2/doc/unsupported_cmds.md
获取数据原理(mget):在 Redis 中的各个实例里获取到符合的 key,然后再汇总到 Codis 中。
Codis 是第三方提供的分布式解决方案,在官方的集群功能稳定之前,Codis 也得到了大量的应用。
4.3Redis Cluster
Redis Cluster 是在 Redis 3.0 的版本正式推出的,用来解决分布式的需求,同时也可以实现高可用。跟 Codis 不一样,它是去中心化的,客户端可以连接到任意一个可用节点。
数据分片有几个关键的问题需要解决:
- 数据怎么相对均匀地分片
- 客户端怎么访问到相应的节点和数据
- 重新分片的过程,怎么保证正常服务
- 架构
Redis Cluster 可以看成是由多个 Redis 实例组成的数据集合。客户端不需要关注数据的子集到底存储在哪个节点,只需要关注这个集合整体。
以3主3从为例,节点之间两两交互,共享数据分片、节点状态等信息。
- CentOS 7 单机安装Redis Cluster(3主3从)
为了节省机器,我们直接把6个Redis实例安装在同一台机器上(3主3从),只是使用不同的端口号。
机器IP 192.168.8.207
cd /usr/local/soft/redis-5.0.5
mkdir redis-cluster
cd redis-cluster
mkdir 7291 7292 7293 7294 7295 7296
复制redis配置文件到7291目录
cp /usr/local/soft/redis-5.0.5/redis.conf /usr/local/soft/redis-5.0.5/redis-cluster/7291
修改7291的redis.conf配置文件,内容:
cd /usr/local/soft/redis-5.0.5/redis-cluster/7291
>redis.conf
vim redis.conf
port 7291
daemonize yes
protected-mode no
dir /usr/local/soft/redis-5.0.5/redis-cluster/7291/
cluster-enabled yes
cluster-config-file nodes-7291.conf
cluster-node-timeout 5000
appendonly yes
pidfile /var/run/redis_7291.pid
把7291下的redis.conf复制到其他5个目录。
cd /usr/local/soft/redis-5.0.5/redis-cluster/7291
cp redis.conf ../7292
cp redis.conf ../7293
cp redis.conf ../7294
cp redis.conf ../7295
cp redis.conf ../7296
批量替换内容
cd /usr/local/soft/redis-5.0.5/redis-cluster
sed -i 's/7291/7292/g' 7292/redis.conf
sed -i 's/7291/7293/g' 7293/redis.conf
sed -i 's/7291/7294/g' 7294/redis.conf
sed -i 's/7291/7295/g' 7295/redis.conf
sed -i 's/7291/7296/g' 7296/redis.conf
启动6个Redis节点
cd /usr/local/soft/redis-5.0.5/
./src/redis-server redis-cluster/7291/redis.conf
./src/redis-server redis-cluster/7292/redis.conf
./src/redis-server redis-cluster/7293/redis.conf
./src/redis-server redis-cluster/7294/redis.conf
./src/redis-server redis-cluster/7295/redis.conf
./src/redis-server redis-cluster/7296/redis.conf
是否启动了6个进程
ps -ef|grep redis
创建集群
旧版本中的redis-trib.rb已经废弃了,直接用–cluster命令
注意用绝对IP,不要用127.0.0.1
cd /usr/local/soft/redis-5.0.5/src/
redis-cli --cluster create 192.168.8.207:7291 192.168.8.207:7292 192.168.8.207:7293 192.168.8.207:7294 192.168.8.207:7295 192.168.8.207:7296 --cluster-replicas 1
Redis会给出一个预计的方案,对6个节点分配3主3从,如果认为没有问题,输入yes确认
>>> 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:7295 to 127.0.0.1:7291
Adding replica 127.0.0.1:7296 to 127.0.0.1:7292
Adding replica 127.0.0.1:7294 to 127.0.0.1:7293
>>> Trying to optimize slaves allocation for anti-affinity
[WARNING] Some slaves are in the same host as their master
M: dfdc9c0589219f727e4fd0ad8dafaf7e0cfb4f1c 127.0.0.1:7291
slots:[0-5460] (5461 slots) master
M: 8c878b45905bba3d7366c89ec51bd0cd7ce959f8 127.0.0.1:7292
slots:[5461-10922] (5462 slots) master
M: aeeb7d7076d9b25a7805ac6f508497b43887e599 127.0.0.1:7293
slots:[10923-16383] (5461 slots) master
S: ebc479e609ff8f6ca9283947530919c559a08f80 127.0.0.1:7294
replicates aeeb7d7076d9b25a7805ac6f508497b43887e599
S: 49385ed6e58469ef900ec48e5912e5f7b7505f6e 127.0.0.1:7295
replicates dfdc9c0589219f727e4fd0ad8dafaf7e0cfb4f1c
S: 8d6227aefc4830065624ff6c1dd795d2d5ad094a 127.0.0.1:7296
replicates 8c878b45905bba3d7366c89ec51bd0cd7ce959f8
Can I set the above configuration? (type 'yes' to accept):
注意看slot的分布:
7291 [0-5460] (5461个槽)
7292 [5461-10922] (5462个槽)
7293 [10923-16383] (5461个槽)
集群创建完成
>>> 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:7291)
M: dfdc9c0589219f727e4fd0ad8dafaf7e0cfb4f1c 127.0.0.1:7291
slots:[0-5460] (5461 slots) master
1 additional replica(s)
M: 8c878b45905bba3d7366c89ec51bd0cd7ce959f8 127.0.0.1:7292
slots:[5461-10922] (5462 slots) master
1 additional replica(s)
M: aeeb7d7076d9b25a7805ac6f508497b43887e599 127.0.0.1:7293
slots:[10923-16383] (5461 slots) master
1 additional replica(s)
S: 8d6227aefc4830065624ff6c1dd795d2d5ad094a 127.0.0.1:7296
slots: (0 slots) slave
replicates aeeb7d7076d9b25a7805ac6f508497b43887e599
S: ebc479e609ff8f6ca9283947530919c559a08f80 127.0.0.1:7294
slots: (0 slots) slave
replicates dfdc9c0589219f727e4fd0ad8dafaf7e0cfb4f1c
S: 49385ed6e58469ef900ec48e5912e5f7b7505f6e 127.0.0.1:7295
slots: (0 slots) slave
replicates 8c878b45905bba3d7366c89ec51bd0cd7ce959f8
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
重置集群
重置集群的方式是在每个节点上个执行cluster reset
,然后重新创建集群。
重启集群
使用./src/redis-server ./7291/redis.conf
命令可以将实例重新启动,启动完成之后,自动加入到集群当中,其他节点一样。
关闭集群
./src/redis-cli -c -h 127.0.0.1 -p 7291 shutdown
集群伸缩
1.扩容集群
1.准备新节点
2.加入集群
使用redis-cli 语法:add-node 新节点ip 端口 已存在节点ip 端口
指定主从
使用redis-cli 语法(加入时指定):add-node 新节点ip 端口 已存在节点ip 端口 --cluster-slave --cluster-master-id masterID
使用原生命令 语法:cluster replicate node-id
3.迁移槽和数据
1.槽迁移计划
语法:/redis-cli --cluster reshard 已存在节点ip : 端口
/usr/local/bin/redis-cli --cluster reshard 192.168.204.188:7000
2.迁移数据
执行流程:提示要分配多少槽-》接收节点ID-》all/done
3.添加从节点
2.缩容集群
1.下线迁移槽
语法:redis-cli --cluster reshard --cluster-from 要迁出节点ID --cluster-to 接收槽节点ID --cluster-slots 迁出槽数量 已存在节点ip 端口
/usr/local/bin/redis-cli --cluster reshard --cluster-from a2fdd1359d03acacf2a6e558acbc006639445d53 --cluster-to 1794864d5f8af79e88cfc0f699f02b6341c78b5c --cluster-slots 1366 192.168.0.104 7000
2.忘记节点.关闭节点
语法: redis-cli --cluster del-node 已存在节点IP:端口 要删除的节点ID
/usr/local/bin/redis-cli --cluster del-node 192.168.0.104:7000 8de55e2a7419983184cede9daab5d36ee9da1fa3
连接到客户端
redis-cli -p 7291
redis-cli -p 7292
redis-cli -p 7293
批量写入值
cd /usr/local/soft/redis-5.0.5/redis-cluster/
vim setkey.sh
执行
chmod +x setkey.sh
./setkey.sh
每个节点分布的数据
127.0.0.1:7292> dbsize
(integer) 6683
127.0.0.1:7293> dbsize
(integer) 6665
127.0.0.1:7291> dbsize
(integer) 6652
其他命令,比如添加节点、删除节点,重新分布数据:
redis-cli --cluster help
Cluster Manager Commands:
create host1:port1 ... hostN:portN
--cluster-replicas
check host:port
--cluster-search-multiple-owners
info host:port
fix host:port
--cluster-search-multiple-owners
reshard host:port
--cluster-from
--cluster-to
--cluster-slots
--cluster-yes
--cluster-timeout
--cluster-pipeline
--cluster-replace
rebalance host:port
--cluster-weight
--cluster-use-empty-masters
--cluster-timeout
--cluster-simulate
--cluster-pipeline
--cluster-threshold
--cluster-replace
add-node new_host:new_port existing_host:existing_port
--cluster-slave
--cluster-master-id
del-node host:port node_id
call host:port command arg arg .. arg
set-timeout host:port milliseconds
import host:port
--cluster-from
--cluster-copy
--cluster-replace
help
For check, fix, reshard, del-node, set-timeout you can specify the host and port of any working node in the cluster.
附录:
集群命令
cluster info :打印集群的信息
cluster nodes :列出集群当前已知的所有节点(node),以及这些节点的相关信息。
cluster meet :将 ip 和 port 所指定的节点添加到集群当中,让它成为集群的一份子。
cluster forget :从集群中移除 node_id 指定的节点(保证空槽道)。
cluster replicate :将当前节点设置为 node_id 指定的节点的从节点。
cluster saveconfig :将节点的配置文件保存到硬盘里面。
槽slot命令
cluster addslots [slot …] :将一个或多个槽(slot)指派(assign)给当前节点。
cluster delslots [slot …] :移除一个或多个槽对当前节点的指派。
cluster flushslots :移除指派给当前节点的所有槽,让当前节点变成一个没有指派任何槽的节点。
cluster setslot node :将槽 slot 指派给 node_id 指定的节点,如果槽已经指派给另一个节点,那么先让另一个节点删除该槽>,然后再进行指派。
cluster setslot migrating :将本节点的槽 slot 迁移到 node_id 指定的节点中。
cluster setslot importing :从 node_id 指定的节点中导入槽 slot 到本节点。
cluster setslot stable :取消对槽 slot 的导入(import)或者迁移(migrate)。
键命令
cluster keyslot :计算键 key 应该被放置在哪个槽上。
cluster countkeysinslot :返回槽 slot 目前包含的键值对数量。
cluster getkeysinslot :返回 count 个 slot 槽中的键
问题:Cluster 解决分片的问题,数据怎么分布?
如果是希望数据分布相对均匀的话,我们首先可以考虑哈希后取模。
- 哈希后取模
例如,hash(key)%N,根据余数,决定映射到那一个节点。这种方式比较简单,属于静态的分片规则。但是一旦节点数量变化,新增或者减少,由于取模的 N 发生变化,数据需要重新分布。为了解决这个问题,我们又有了一致性哈希算法。 -
一致性哈希
一致性哈希的原理:把所有的哈希值空间组织成一个虚拟的圆环(哈希环),整个空间按顺时针方向组织。因为是环形空间,0 和 2^32-1 是重叠的。假设我们有四台机器要哈希环来实现映射(分布数据),我们先根据机器的名称或者 IP 计算哈希值,然后分布到哈希环中(红色圆圈)。
现在有 4 条数据或者 4 个访问请求,对 key 计算后,得到哈希环中的位置(绿色圆圈)。沿哈希环顺时针找到的第一个 Node,就是数据存储的节点。
在这种情况下,新增了一个 Node5 节点,不影响数据的分布。
删除了一个节点 Node4,只影响相邻的一个节点
谷歌的 MurmurHash 就是一致性哈希算法。在分布式系统中,负载均衡、分库分表等场景中都有应用。
一致性哈希解决了动态增减节点时,所有数据都需要重新分布的问题,它只会影响到下一个相邻的节点,对其他节点没有影响。但是这样的一致性哈希算法有一个缺点,因为节点不一定是均匀地分布的,特别是在节点数比较少的情况下,所以数据不能得到均匀分布。解决这个问题的办法是引入虚拟节点(Virtual Node)。比如:2 个节点,5 条数据,只有 1 条分布到 Node2,4 条分布到 Node1,不均匀。
Node1 设置了两个虚拟节点,Node2 也设置了两个虚拟节点(虚线圆圈)。
这时候有 3 条数据分布到 Node1,1 条数据分布到 Node2。
Redis 虚拟槽分区
Redis 既没有用哈希取模,也没有用一致性哈希,而是用虚拟槽来实现的。
Redis 创建了 16384 个槽(slot),每个节点负责一定区间的 slot。比如 Node1 负责 0-5460,Node2 负责 5461-10922,Node3 负责 10923-16383。
Redis 的每个 master 节点维护一个 16384 位(2048bytes=2KB)的位序列,比如:
序列的第 0 位是 1,就代表第一个 slot 是它负责;序列的第 1 位是 0,代表第二个 slot不归它负责。
对象分布到 Redis 节点上时,对 key 用 CRC16 算法计算再%16384,得到一个 slot的值,数据落到负责这个 slot 的 Redis 节点上。
查看 key 属于哪个 slot:
···
127.0.0.1:7291> cluster keyslot congzhizhi
(integer) 12625
127.0.0.1:7291>
···
注意:key 与 slot 的关系是永远不会变的,会变的只有 slot 和 Redis 节点的关系。
问题:怎么让相关的数据落到同一个节点上?
比如有些 multi key 操作是不能跨节点的,如果要让某些数据分布到一个节点上,例如用户 2673 的基本信息和金融信息,怎么办?
在 key 里面加入{hash tag}即可。Redis 在计算槽编号的时候只会获取{}之间的字符串进行槽编号计算,这样由于上面两个不同的键,{}里面的字符串是相同的,因此他们可以被计算出相同的槽。
user{2673}base=…
user{2673}fin=…
127.0.0.1:7293> set a{qs}a 1
OK
127.0.0.1:7293> set a{qs}b 1
OK
127.0.0.1:7293> set a{qs}c 1
OK
127.0.0.1:7293> set a{qs}d 1
OK
127.0.0.1:7293> set a{qs}e 1
OK
127.0.0.1:7291> cluster keyslot cong{123}zhizhi
(integer) 5970
127.0.0.1:7291> cluster keyslot cong{123}asdfasdf
(integer) 5970
127.0.0.1:7291> cluster keyslot cong{123}zhizhi
(integer) 5970
127.0.0.1:7291> cluster keyslot cong{123}zhongguo
(integer) 5970
127.0.0.1:7291> cluster keyslot cong{123}dele
(integer) 5970
问题:客户端连接到哪一台服务器?访问的数据不在当前节点上,怎么办?
- 客户端重定向
比如在 7291 端口的 Redis 的 redis-cli 客户端操作:
127.0.0.1:7291> set qs 1
(error) MOVED 13724 127.0.0.1:7293
服务端返回 MOVED,也就是根据 key 计算出来的 slot 不归 7191 端口管理,而是归 7293 端口管理,服务端返回 MOVED 告诉客户端去 7293 端口操作。
这个时候更换端口,用 redis-cli –p 7293 操作,才会返回 OK。或者用./redis-cli -c -p port 的命令(c 代表 cluster)。这样客户端需要连接两次。
Jedis 等客户端会在本地维护一份 slot——node 的映射关系,大部分时候不需要重定向,所以叫做 smart jedis(需要客户端支持)
问题:新增或下线了 Master 节点,数据怎么迁移(重新分配)?
因为 key 和 slot 的关系是永远不会变的,当新增了节点的时候,需要把原有的 slot分配给新的节点负责,并且把相关的数据迁移过来。
添加新节点(新增一个 7297):
redis-cli --cluster add-node 127.0.0.1:7291 127.0.0.1:7297
新增的节点没有哈希槽,不能分布数据,在原来的任意一个节点上执行:
redis-cli --cluster reshard 127.0.0.1:7291
输入需要分配的哈希槽的数量(比如 500),和哈希槽的来源节点(可以输入 all 或者 id)。
问题:只有主节点可以写,一个主节点挂了,从节点怎么变成主节点?
- 高可用和主从切换原理
当 slave 发现自己的 master 变为 FAIL 状态时,便尝试进行 Failover,以期成为新的master。由于挂掉的master可能会有多个slave,从而存在多个slave竞争成为master节点的过程, 其过程如下:
- slave 发现自己的 master 变为 FAIL
- 将自己记录的集群 currentEpoch 加 1,并广播FAILOVER_AUTH_REQUEST 信息
- 其他节点收到该信息,只有 master 响应,判断请求者的合法性,并发送
FAILOVER_AUTH_ACK,对每一个 epoch 只发送一次 ack - 尝试 failover 的 slave 收集 FAILOVER_AUTH_ACK
- 超过半数后变成新 Master
- 广播 Pong 通知其他集群节点。
Redis Cluster 既能够实现主从的角色分配,又能够实现主从切换,相当于集成了Replication 和 Sentinal 的功能。
redis cluster总结
优势
- 无中心架构。
- 数据按照 slot 存储分布在多个节点,节点间数据共享,可动态调整数据分布。
- 可扩展性,可线性扩展到 1000 个节点(官方推荐不超过 1000 个),节点可动
态添加或删除。 - 高可用性,部分节点不可用时,集群仍可用。通过增加 Slave 做 standby 数据副本,能够实现故障自动 failover,节点之间通过 gossip 协议交换状态信息,用投票机制完成 Slave 到 Master 的角色提升。
- 降低运维成本,提高系统的扩展性和可用性。
不足
- Client 实现复杂,驱动要求实现 Smart Client,缓存 slots mapping 信息并及时更新,提高了开发难度,客户端的不成熟影响业务的稳定性。
- 节点会因为某些原因发生阻塞(阻塞时间大于 clutser-node-timeout),被判断下线,这种 failover 是没有必要的。
- 数据通过异步复制,不保证数据的强一致性。
- 多个业务使用同一套集群时,无法根据统计区分冷热数据,资源隔离性较差,容易出现相互影响的情况。