0%

redis-集群

一、主从复制

1、概念

  • 一个master可以拥有多个slave。
  • master的复制是异步非阻塞的。
  • 客户端可以使用wait命令请求同步复制某些特定数据。
  • 正常连接情况下,master通过命令流来保持对slave的数据更新。
  • master与slave之间得到连接断开,slave在尝试与master重连后,会尝试获取与master断开期间丢失的命令流
  • 当断开期间的数据无法重新同步时,slave会请求全量更新,master将以rdb快照方式将全量数据推送给slave,之后再进行增量更新。

2、复制原理

  • 每个master都有一个replicationId用来表示与slave处于同一个主从复制中;
    同时存在一个偏移量,用来记录上次发送给slave的数据,每次复制数据后该偏移量都会增加,即使没有slave链接master,它的offset也会增加。一对Replication ID, offset表示一个版本的数据
  • 当slave连接到master后,会使用psync命令发送自己存储的旧replicationId和offset,master从积压的缓冲区找到对应版本的数据,开始增量复制推送给slave,如果缓冲区没有的话,master会进行全量RBD复制
  • 全量复制,master后台fork一个进行产生一个rdb文件在本地磁盘,将rdb文件传输给slave保存在本地磁盘,slave加载到内存执行,无磁盘复制可以使用repl-diskless-sync 配置参数

3、准备

1
2
3
4
5
6
7
8
9
10
11
12
13
#创建文件夹,拷贝redis配置,方便后续测试
mkdir /usr/local/test-redis
cp /etc/redis/* /

#注释日志输出,以便控制台观看
vi 6379.conf
#logfile /var/log/redis_6379.log

#关闭aof值测试rdb
appendonly no

#关闭后台启动,前台启动方便查看
daemonize no

4、启动redis

1
2
3
redis-server ./6379.conf 
redis-server ./6380.conf
redis-server ./6380.conf

5、slave追随master

1
2
3
4
5
方式一:在进入6380、6381客户端,使用`replicaof host port`命令(5.0之前命令为`slaveof host port`),让从节点追随主节点

方式二:在启动服务端时,使用`redis-server ./6380.conf --replicaof host port`命令,让从节点追随主节点

方式三:在配置文件指定 # replicaof <masterip> <masterport>

从节点控制台日志

主节点控制台日志

6、错误

此时发现客户端宕机,重新启动客户端报如下错误

6.1、问题1:

1
2
3
4
5
WARNING overcommit_memory is set to 0! Background save may fail under low memory condition. To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf and then reboot or run the command 'sysctl vm.overcommit_memory=1' for this to take effect.
4562:M 13 Apr 2021 21:45:46.588 # WARNING you have Transparent Huge Pages (THP) support enabled in your kernel. This will create latency and memory usage issues with Redis. To fix this issue run the command 'echo madvise > /sys/kernel/mm/transparent_hugepage/enabled' as root, and add it to your /etc/rc.local in order to retain the setting after a reboot. Redis must be restarted after THP is disabled (set to 'madvise' or 'never').

警告:overcommit_memory设置为0!在内存不足的情况下,后台保存可能失败。要解决这个问题,添加‘vm’。overcommit_memory = 1' /etc/sysctl.conf,然后重启或执行'sysctl vm. conf '命令。overcommit_memory=1'让它生效。
#警告:你的内核已经启用了透明的大页面(THP)支持。这会造成Redis的延迟和内存使用问题。要解决这个问题,以root权限运行'echo madvise > /sys/kernel/mm/transparent_hugepage/enabled'命令,并将其添加到/etc/rc.本地,以便在重新启动后保留设置。禁用THP后必须重新启动Redis(设置为'madvise'或'never')。

执行sysctl vm.overcommit_memory=1解决以上问题

6.2、问题2:

1
2
3
WARNING you have Transparent Huge Pages (THP) support enabled in your kernel. This will create latency and memory usage issues with Redis. To fix this issue run the command 'echo madvise > /sys/kernel/mm/transparent_hugepage/enabled' as root, and add it to your /etc/rc.local in order to retain the setting after a reboot. Redis must be restarted after THP is disabled (set to 'madvise' or 'never').

您的内核中启用了透明的大页面(THP)支持。这会造成Redis的延迟和内存使用问题。要解决这个问题,以root权限运行'echo madvise > /sys/kernel/mm/transparent_hugepage/enabled'命令,并将其添加到/etc/rc.本地,以便在重新启动后保留设置。禁用THP后必须重新启动Redis(设置为'madvise''never')。

执行echo madvise > /sys/kernel/mm/transparent_hugepage/enabled解决以上问题

6.3、问题3:

1
2
3
The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.

由于/proc/sys/net/core/somaxconn被设置为较低的值128,TCP backlog设置511无法执行。

执行echo 511 > /proc/sys/net/core/somaxconn解决以上问题

6.4、问题4:

1
2
3
4
5
5499:M 13 Apr 2021 21:58:45.198 # Server initialized
5499:M 13 Apr 2021 21:58:45.198 * Loading RDB produced by version 6.0.9
5499:M 13 Apr 2021 21:58:45.198 * RDB age 1191 seconds
5499:M 13 Apr 2021 21:58:45.198 * RDB memory usage when created 1.85 Mb
5499:M 13 Apr 2021 21:58:45.198 # The RDB file contains module data I can't load: no matching module 'MBbloom--'

此时仍无法启动,需要到持久化目录/var/lib/redis/6381下处理rdb文件mv dump.rdb dump.rdb.bak,此时可以正常启动

7、slave升级为master

当主节点宕机时,我们需要手动进行故障转移,将从节点升级为主节点

从节点监控到主节点宕机

从节点客户端执行replicaof no one指令,将自己从slave升级为master

从节点升级为主节点

8、配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# slave在首次接收master数据时,可以在slave中配置同步期间是否接收旧数据的访问,在首次同步之后,旧数据会被删除,然后再主线程加载新数据,此时slave会阻塞
replica-serve-stale-data yes

#从节点是否开启只读模式
replica-read-only yes

#是否采用无磁盘模式传输,no表示走磁盘,yes走网络传输
repl-diskless-sync no

#增量复制,当redis挂掉之后又恢复,主节点可以增量传输数据过来,但是增量的数据与当前的数据存在偏差,从节点可以通过传送offset从主节点再次拉回数据,此配置大小会关系到redis内存维护的队列大小,此操作的成功与否,数据量大于这个值会造成溢出
# repl-backlog-size 1mb

# 如果至少有 3 个 slave ,并且滞后小于 10 秒,则写入将被接受,如果条件不满足master的写操作将被拒绝。
# min-replicas-to-write 3
# min-replicas-max-lag 10

# 如果master节点设置了密码,需要在此处进行配置对应表的访问密码
# masterauth <master-password>

# 配置追随的master节点
# replicaof <masterip> <masterport>

9、总结

1、从节点追随主节点之后,旧数据会被删除,同时非阻塞方式同步主节点数据

2、从节点只能读,不能写(可修改配置改变)

3、主节点出现故障时,需要人工维护升级新的主节点

二、哨兵模式

1、概念

1
2
3
4
5
1.通过连接master获取slave的信息
2.通过psubscribe正则方式订阅发布的信息,从而发现其他哨兵节点
3.监控主从节点是否正常
3.自动故障转移
4.使用投票机制选举master

2、Sentinel相互发现

  • 每个Sentinel会以每两秒一次的频率,通过发布与订阅功能,向被它监视的所有master和slave的 sentinel:hello 频道发送一条信息,信息中包含了该Sentinel的IP 地址、端口号和运行ID (runid)。
  • 每个Sentinel都订阅了被它监视的所有master和slave的sentinel:hello 频道,查找之前未出现过的sentinel(looking for unknown sentinels)。当一个Sentinel 发现一个新的Sentinel时,它会将新的Sentinel添加到一个列表中,这个列表保存了Sentinel已知的,监视同一个主服务器的所有其他Sentinel。
  • Sentinel 发送的信息中还包括完整的主服务器当前配置(configuration)。 如果一个 Sentinel 包含的主服务器配置比另一个 Sentinel 发送的配置要旧, 那么这个 Sentinel 会立即升级到新配置上。
  • 在将一个新Sentinel添加到监视主服务器的列表上面之前,Sentinel会先检查列表中是否已经包含了和要添加的Sentinel拥有相同运行ID或者相同地址(包括IP地址和端口号)的 Sentinel ,如果是的话,Sentinel会先移除列表中已有的那些拥有相同运行ID或者相同地址的Sentinel, 然后再添加新Sentinel。

3、Sentinel对master的故障判定

1)、 Sentinel以每秒钟一次的频率向它所知的master、slave以及其他 Sentinel 实例发送一个 PING 命令,如果距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值,那么这个实例会被 Sentinel 标记为主观下线。一个有效回复可以是:+PONG 、-LOADING 或者 -MASTERDOWN ,其余回复或者没有回复都算是无效回复。
2)、当master标记为ODOWN(主观下线)后,用通过向另一个 Sentinel 发送 SENTINEL is-master-down-by-addr 命令来询问对方是否认为给定的服务器已下线。
2.1)、当有足够数量的 Sentinel(至少要达到配置文件指定的数量quorum)在指定的时间范围内同意这一判断, 那么这个主服务器被标记为客观下线。
2.2)、当没有足够数量的 Sentinel 同意主服务器已经下线, 主服务器的客观下线状态就会被移除。 当主服务器重新向 Sentinel 的 PING 命令返回有效回复时, 主服务器的主观下线状态就会被移除
3)、在一般情况下, 每个 Sentinel 会以每 10 秒一次的频率向它已知的所有master和slave发送 INFO 命令。 当一个master被 Sentinel 标记为客观下线时, Sentinel 向下线master的所有slave发送 INFO 命令的频率会从 10 秒一次改为每秒一次。

4、选举新的master

1
2
3
4
5
首先标记master为ODOWN状态的Sentinel使用以下规则从slave中来选取新的master:
1、被标记为主观下线、已断线、或者最后一次回复 PING 命令的时间大于五秒钟的从服务器都会被淘汰。
2、与失效主服务器连接断开的时长超过 down-after-milliseconds 选项指定的时长十倍的从服务器都会被淘汰。
3、在经历了以上两轮淘汰之后剩下来的从服务器中, 选出复制偏移量(replication offset)最大的那个从服务器作为新的主服务器;
4、如果复制偏移量不可用, 或者从服务器的复制偏移量相同, 那么带有最小运行 ID 的那个从服务器成为新的主服务器。

5、准备

1
2
3
4
5
6
7
#配置文件参考源码目录下/usr/local/redis-6.0.9/sentinel.conf

创建配置文件
vi 26379.conf

port 26379 # 指定当前哨兵端口号
sentinel monitor mymaster 127.0.0.1 6379 2 # 指定哨兵需要监控的主节点ip port 投票达成一致数量

6、配置

1
2
#如果需要监控监控的master设置了密码,需要在此处设置
# sentinel auth-pass <master-name> <password>

7、启动哨兵节点

1
2
启动方式一:redis-server ./26379.conf --sentinel
启动方式二:redis-sentinel ./26381.conf

8、选举新master节点

当master节点宕机,哨兵节点过半以上检测到之后会重新选举新的master节点

slave节点升级为master节点

另一个slave检测到新的master主节点产生并追随

总结

1、哨兵通过主节点获取从节点信息,同时发布订阅方式发现其他哨兵节点信息

2、主节点宕机后,哨兵节点重新选举新的主节点,做到故障转移,不需要人工处理

3、master节点存在已经过期的key,复制到了slave,那当master的“访问过期”和“定期过期”机制没有被触发时,该key没有被删除,客户端链接slave查询该key时出现什么情况?推测:判断过期返回不存在,过期的key不处理,等待master处理后同步del指令

三、分区

1、概念

1.1、不同端的分区

1
2
3
4
5
6
7
8
9
客户端分区:
由客户端根据一定的算法逻辑计算出该key应该与哪一个redis实例交互
用户客户端自己实现逻辑
代理分区:
客户端不需要理会如何分区,请求发送给代理,有代理觉得链接哪一个redis实例
twemproxy、predixy
查询路由:
客户端随机请求任意的redis实例,redis将请求转发给正确的redis节点处理
redis cluster

1.2、优缺点

1
2
3
4
5
6
分区优点
可以让redis管理更大的内存
分布在不同的计算机利用不同计算机的计算能力,使redis的计算能力得到提升
分区的缺点:
多个key不在统一分区无法使用聚合、事务等操作
动态扩容和收缩需要对数据进行再平衡(预分片可以解决这个问题)

1.3、预分片

​ 因为redis实例占用的内存很小,在一台机子提前启用多台redis以分布式方式运行,随着数据不断增加,需要做的只是将redis实例迁移到另外的计算机中,不需要考虑重新分区

1
2
3
4
5
1.新的服务器启动新的redis实例
2.配置为需要迁移的旧redis的slave(此操作可以同步数据)
3.更新分片映射的旧实例ip为新实例ip
4.客户端连接新的redis实例执行SLAVEOF NO ONE命令,将新实例升级为master
5.停止旧的redis实例

2、分区算法

2.1、普通Hash算法(modula)

1
2
3
4
5
比如你有 N 个 `redis`实例,那么如何将一个`key`映射到`redis`上呢,你很可能会采用类似下面的通用方法计算 key的 hash 值,然后均匀的映射到到 N 个 `redis`上:
  `hash(key)%N`
  如果增加一个`redis`,映射公式变成了 `hash(key)%(N+1)`
  如果一个`redis`宕机了,映射公式变成了 `hash(key)%(N-1)`
  在这两种情况下,每一个`redis`管理的数据全部要重新计算移动,几乎所有的缓存都失效了。会导致数据库访问的压力陡增,严重情况,还可能导致数据库宕机。

2.2、随机分配算法(random)

1
随机将数据分发到Redis集群中,Client无法准确地从某台机器获取相对的数据,该做法常用于消息队列中。

2.3、一致性Hash算法(ketama)

  1. 将内存想象成一个环,由于hash值有32位,因此将内存分出2 ^32(0~2 ^32-1)个地址
  2. 将节点的IP+算法确定唯一的哈希值,之后在内存中确定节点的位置
  3. 当保存数据时,根据key进行哈希运算,确定唯一的一个位置
  4. 根据当前key位置顺时针查找最近的node节点进行挂载(在内存中,加法计算快于减法运算,因此采用顺时针查找)

将各个服务器使用Hash进行一个哈希,具体可以选择服务器的ip或主机名作为关键字进行哈希,这样每台机器就能确定其在哈希环上的位置。

假设将中四台服务器使用ip地址哈希后在环空间的位置如下:

将数据key使用相同的函数Hash计算出哈希值,并确定此数据在环上的位置,从此位置沿环顺时针“行走”,第一台遇到的服务器就是其应该定位到的服务器。

假设4个存储对象 Object A、B、C、D,经过对 Key 的哈希计算后,它们的位置如下:

根据一致性哈希算法,数据A会被定为到Node A上,B被定为到Node B上,C被定为到Node C上,D被定为到Node D上。

容错性和可扩展性

假设Node C不幸宕机,可以看到此时对象A、B、D不会受到影响,只有C对象被重定位到Node D。一般的,在一致性哈希算法中,如果一台服务器不可用,则受影响的数据仅仅是此服务器到其环空间中前一台服务器(即沿着逆时针方向行走遇到的第一台服务器)之间数据,其它不会受到影响。

如果在系统中增加一台服务器Node X,如下图所示:

此时对象Object A、B、D不受影响,只有对象C需要重定位到新的Node X 。一般的,在一致性哈希算法中,如果增加一台服务器,则受影响的数据仅仅是新服务器到其环空间中前一台服务器(即沿着逆时针方向行走遇到的第一台服务器)之间数据,其它数据也不会受到影响。

综上所述,一致性哈希算法对于节点的增减都只需重定位环空间中的一小部分数据,具有较好的容错性和可扩展性。

如果这时候新增了一个结点,对原来缓存的一部分数据的访问将会落到新增的结点上,但是这时候结点并没有数据缓存,它将去数据库中查找并缓存,原先已经缓存数据的结点需要通过淘汰算法(LRU)淘汰数据,它并没有从原缓存结点复制数据到新节点中。

虚拟节点

但一致性哈希算法也有一个严重的问题,就是数据倾斜。如果在分片的集群中,节点太少,并且分布不均,一致性哈希算法就会出现部分节点数据太多,部分节点数据太少。也就是说无法控制节点存储数据的分配。如下图,大部分数据都在 A 上了,B 的数据比较少。

节点数越少,越容易出现节点在哈希环上的分布不均匀,导致各节点映射的对象数量严重不均衡(数据倾斜);相反,节点数越多越密集,数据在哈希环上的分布就越均匀。

以删除节点为例,假设删除了Node B节点,原来Node B节点的数据将转移到Node C上,这样Node C的内存使用率会骤增,如果Node B上存在热点数据,Node C会扛不住甚至会可能挂掉,挂掉之后数据又转移给Node D,如此循环会造成所有节点崩溃,也就是缓存雪崩

为了解决雪崩现象和数据倾斜现象,提出了虚拟节点这个概念。就是将真实节点计算多个哈希形成多个虚拟节点并放置到哈希环上,定位算法不变,只是多了一步虚拟节点到真实节点映射的过程

但实际部署的物理节点有限,我们可以用有限的物理节点,虚拟出足够多的虚拟节点(Virtual Node),最终达到数据在哈希环上均匀分布的效果。

如下图,实际只部署了2个节点 Node A/B,每个节点都复制成3倍,结果看上去是部署了6个节点。可以想象,当复制倍数为 2^32 时,就达到绝对的均匀,通常可取复制倍数为32或更高。

这就解决了雪崩的问题,当某个节点宕机后,其数据并没有全部分配给某一个节点,而是被分到了多个节点,数据倾斜的问题也随之解决

实现

一致性哈希算法,既可以在客户端实现,也可以在中间件上实现(如 proxy)。在客户端实现中,当客户端初始化的时候,需要初始化一张预备的 Redis 节点的映射表:hash(key)=> . 这有一个缺点,假设有多个客户端,当映射表发生变化的时候,多个客户端需要同时拉取新的映射表。

另一个种是中间件(proxy)的实现方法,即在客户端和 Redis 节点之间加多一个代理,代理经过哈希计算后将对应某个 key 的请求分发到对应的节点,一致性哈希算法就在中间件里面实现。可以发现,twemproxy 就是这么做的。

2.4、哈希槽

redis 集群(cluster)并没有选用上面一致性哈希,而是采用了哈希槽(slot)的这种概念。主要的原因就是上面所说的,一致性哈希算法对于数据分布、节点位置的控制并不是很友好

首先哈希槽其实是两个概念,第一个是哈希算法。redis clusterhash 算法不是简单的hash(),而是 crc16 算法,一种校验算法。另外一个就是槽位的概念,空间分配的规则。

其实哈希槽的本质和一致性哈希算法非常相似,不同点就是对于哈希空间的定义。一致性哈希的空间是一个圆环,节点分布是基于圆环的,无法很好的控制数据分布。而 redis cluster 的槽位空间是自定义分配的,类似于 windows 盘分区的概念。这种分区是可以自定义大小,自定义位置的。

redis cluster 包含了16384个哈希槽,每个 key 通过计算后都会落在具体一个槽位上,而这个槽位是属于哪个存储节点的,则由用户自己定义分配。例如机器硬盘小的,可以分配少一点槽位,硬盘大的可以分配多一点。如果节点硬盘都差不多则可以平均分配。所以哈希槽这种概念很好地解决了一致性哈希的弊端

当有新节点加入时,它不再需要像一致性Hash算法那样把每个key取出来重新计算hash值,只需要从旧节点中将新节点应该缓存的槽位数据拷贝到新节点中即可。

另外在容错性和扩展性上,表象与一致性哈希一样,都是对受影响的数据进行转移。而哈希槽本质上是对槽位的转移,把故障节点负责的槽位转移到其他正常的节点上。扩展节点也是一样,把其他节点上的槽位转移到新的节点上。

弊端是聚合操作很难实现,并且不支持跨机器事务,但是提供了Hash Tag让用户控制需要计算的Key都集中在一个Redis中。

3、代理分区-TwemProxy

3.1、安装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
git clone git@github.com:twitter/twemproxy.git
cd twemproxy
autoreconf -fvi
./configure --enable-debug=full
make

#此操作是为了之后可以使用service twemproxy start/stop等命令操作
cd /usr/local/twemproxy/scripts
cp nutcracker.init /etc/init.d/twemproxy
cd /etc/init.d/
chmod +x twemproxy

#可以看到需要在这个目录下创建此配置 OPTIONS="-d -c /etc/nutcracker/.yml"
more twemproxy

进入源码目录
cd /usr/local/twemproxy/conf
cp ./* /etc/nutcracker

cd /usr/local/twemproxy/src
cp nutcracker /usr/bin #之后再操作系统任意地方可以使用nutcracker命令

cd /etc/nutcracker
cp nutcracker.yml nutcracker.yml.bak

3.2、配置

1
2
3
4
5
6
7
8
9
10
11
12
13
vim nutcracker.yml

alpha:
listen: 127.0.0.1:22121
hash: fnv1a_64
distribution: ketama
auto_eject_hosts: true
redis: true
server_retry_timeout: 2000
server_failure_limit: 1
servers:
- 127.0.0.1:6379:1 #1指的是权重
- 127.0.0.1:9380:1

3.3、手动启动redis

1
2
3
4
5
6
#直接指定端口启动的话,会将当前目录作为持久目录
mkdir /usr/local/test-redis/6379_data
redis-server --port 6379

mkdir /usr/local/test-redis/6380_data
redis-server --port 6380

3.4、启动twemproxy

1
service twemproxy start

3.5、通过代理连接redis

1
2
#数据分区,不支持keys、watch、MULTI等命令
redis-cli -p 22121

4、代理分区-Predixy

中文文档

4.1、安装

1
2
wget https://github.com/joyieldInc/predixy/releases/download/1.0.5/predixy-1.0.5-bin-amd64-linux.tar.gz
tar xf predixy-1.0.5-bin-amd64-linux.tar.gz

4.2、predixy相关配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#修改predixy配置
cd /usr/local/predixy/predixy-1.0.5/conf
vi predixy.conf

################################### GENERAL ####################################
# 开放predixy绑定的ip端口
Bind 127.0.0.1:7617
# Bind 0.0.0.0:7617
# Bind /tmp/predixy
################################### SERVERS ####################################
# Include cluster.conf
#加载sentinel相关配置
Include sentinel.conf
# Include try.conf

#修改predixy的sentinel配置
vim sentinel.conf
SentinelServerPool {
Databases 16
Hash crc16
HashTag "{}"
Distribution modula
MasterReadPriority 60
StaticSlaveReadPriority 50
DynamicSlaveReadPriority 50
RefreshInterval 1
ServerTimeout 1
ServerFailureLimit 10
ServerRetryTimeout 1
KeepAlive 120
Sentinels {
+ 127.0.0.1:26379 #配置哨兵节点
+ 127.0.0.1:26380 #配置哨兵节点
+ 127.0.0.1:26381 #配置哨兵节点
}
Group mySentinel001 { #配置redis主从分组名
}
Group mySentinel002 { #配置redis主从分组名
}
}

4.3、sentinel配置

1
2
3
4
5
6
7
8
9
10
11
port 26379
sentinel monitor mySentinel001 127.0.0.1 36379 2
sentinel monitor mySentinel002 127.0.0.1 46379 2

port 26380
sentinel monitor mySentinel001 127.0.0.1 36379 2
sentinel monitor mySentinel002 127.0.0.1 46379 2

port 26381
sentinel monitor mySentinel001 127.0.0.1 36379 2
sentinel monitor mySentinel002 127.0.0.1 46379 2

4.4、启动sentinel

1
2
3
redis-server 26379.conf --sentinel
redis-server 26380.conf --sentinel
redis-server 26381.conf --sentinel

4.5、启动redis主从

1
2
3
4
5
6
7
8
mkdir 36379
mkdir 36380
mkdir 46379
mkdir 46380
redis-sever --port 36379
redis-server --port 36380 --replicaof 127.0.0.1 36379
redis-sever --port 46379
redis-server --port 46380 --replicaof 127.0.0.1 46379

4.6、启动predixy

1
2
cd /usr/local/predixy/predixy-1.0.5/bin
./predixy ../conf/predixy.conf

4.7、通过代理连接redis

1
2
#代理分区,因为sentinel监控了两套主从,不支持keys、watch、MULTI等命令,使用单套主从则可以
redis-cli -p 7167

5、查询路由分区-Redis-Cluster

1
2
3
无需`proxy`代理,客户端直接与`redis`集群的每个节点连接,根据同样的`hash`算法计算出`key`对应的`slot`,然后直接在`slot`对应的`redis`节点上执行命令。

在`redis`看来,响应时间是最苛刻的条件,增加一层带来的开销是`redis`不能接受的。因此,`redis`实现了客户端对节点的直接访问,**为了去中心化,节点之间通过`gossip`协议交换互相的状态,以及探测新加入的节点信息**。`redis`集群支持动态加入节点,动态迁移`slot`,以及自动故障转移。

5.1、slot分配

redis集群模式使用公式 CRC16(key) % 16384 来计算键key属于哪个槽, 其中 CRC16(key) 语句用于计算键 keyCRC16 校验和 。集群中的每个节点负责处理一部分哈希槽。

举个例子, 一个集群可以有三个节点, 其中:

  • 节点 A 负责处理 0 号至 5500 号哈希槽。
  • 节点 B 负责处理 5501 号至 11000 号哈希槽。
  • 节点 C 负责处理 11001 号至 16383 号哈希槽。
  • 此时 Redis Client 需要根据一个Key获取对应的 Value 的数据,首先通过 CRC16(key)%16384 计算出 Slot 的值,假设计算的结果是 5000,将这个数据传送给 Redis Cluster,集群接收到以后会到一个对照表中查找这个 Slot=5000 属于那个缓存节点。发现属于“节点 A ”负责,于是顺着红线的方向调用节点 A中存放的 Key-Value 的内容并且返回给 Redis Client

这种将哈希槽分布到不同节点的做法使得用户可以很容易地向集群中添加或者删除节点。 比如说:

  • 如果用户将新节点 D 添加到集群中, 那么集群只需要将节点 A 、B 、 C 中的某些槽移动到节点 D 就可以了。
  • 如果用户要从集群中移除节点 A , 那么集群只需要将节点 A 中的所有哈希槽移动到节点 B 和节点 C , 然后再移除空白(不包含任何哈希槽)的节点 A 就可以了。

因为将一个哈希槽从一个节点移动到另一个节点不会造成节点阻塞,且成本很低, 所以无论是添加新节点还是移除已存在节点, 又或者改变某个节点包含的哈希槽数量, 都不会造成集群下线

5.2、数据结构

Redis Cluster中的每个节点都保存了集群的配置信息,并且存储在clusterState中,结构如下:

上图的各个变量语义如下:

  • clusterState 记录了从集群中某个节点视角,来看集群配置状态;
  • currentEpoch 表示整个集群中最大的版本号,集群信息每变更一次,改版本号都会自增。
  • nodes 是一个列表,包含了本节点所感知的,集群所有节点的信息(clusterNode),也包含自身的信息。
  • clusterNode 记录了每个节点的信息,其中包含了节点本身的版本 Epoch;自身的信息描述:节点对应的数据分片范围(slot)、为master时的slave列表、为slave时的master等。

每个节点包含一个全局唯一的NodeId

当集群的数据分片信息发生变更(数据在节点间迁移时),Redis Cluster仍然保持对外服务。

当集群中某个master出现宕机时,Redis Cluster 会自动发现,并触发故障转移的操作。会将master的某个slave晋升为新的 master

由此可见,每个节点都保存着Node视角的集群结构。它描述了数据的分片方式,节点主备关系,并通过Epoch作为版本号实现集群结构信息的一致性,同时也控制着数据迁移和故障转移的过程。

5.3、节点通信

Redis Cluster中,这个配置信息交互通过Redis Cluster Bus来完成(独立端口)。Redis Cluster Bus上交互的信息结构如下:

clusterMsg 中的type指明了消息的类型,配置信息的一致性主要依靠PING/PONG。每个节点向其他节点频繁的周期性的发送PING/PONG消息。对于消息体中的Gossip部分,包含了sender/receiver 所感知的其他节点信息,接受者根据这些Gossip 跟新对集群的认识。

对于大规模的集群,如果每次PING/PONG 都携带着所有节点的信息,则网络开销会很大。此时Redis Cluster 在每次PING/PONG,只包含了随机的一部分节点信息。由于交互比较频繁,短时间的几次交互之后,集群的状态也会达成一致。

5.4、一致性

Cluster 结构不发生变化时,各个节点通过gossip 协议在几轮交互之后,便可以得知Cluster的结构信息,达到一致性的状态。但是当集群结构发生变化时(故障转移/分片迁移等),优先得知变更的节点通过Epoch变量,将自己的最新信息扩散到Cluster,并最终达到一致。

clusterNodeEpoch描述的单个节点的信息版本;
clusterStatecurrentEpoch 描述的是集群信息的版本,它可以辅助Epoch 的自增生成。因为currentEpoch 是维护在每个节点上的,在集群结构发生变更时,Cluster 在一定的时间窗口控制更新规则,来保证每个节点的currentEpoch都是最新的。
更新规则如下:

当某个节点率先知道了变更时,将自身的currentEpoch 自增,并使之成为集群中的最大值。再用自增后的currentEpoch 作为新的Epoch 版本;

  • 当某个节点收到了比自己大的currentEpoch时,更新自己的currentEpoch
  • 当收到的Redis Cluster Bus 消息中的某个节点的Epoch > 自身的时,将更新自身的内容;
  • Redis Cluster Bus 消息中,包含了自己没有的节点时,将其加入到自身的配置中。

上述的规则保证了信息的更新都是单向的,最终朝着Epoch更大的信息收敛。同时Epoch也随着currentEpoch的增加而增加,最终将各节点信息趋于稳定。

为了使得集群在一部分节点下线或者无法与集群的大多数(majority)节点进行通讯的情况下, 仍然可以正常运作, Redis 集群对节点使用了主从复制功能: 集群中的每个节点都有 1 个至 N 个复制品(replica), 其中一个复制品为主节点(master), 而其余的 N-1 个复制品为从节点(slave)。

集群间节点支持主从关系,复制的逻辑基本复用了单机版的实现。不过还是有些地方需要注意。

  • 首先集群间节点建立主从关系不再使用原有的SLAVEOF命令和SLAVEOF配置,而是通过cluster replicate命令,这保证了主从节点需要先完成握手,才能建立主从关系。
  • 集群是不能组成链式主从关系的,也就是说从节点不能有自己的从节点。不过对于集群外的没开启集群功能的节点,redis并不干预这些节点去复制集群内的节点,但是在集群故障转移时,这些集群外的节点,集群不会处理。
  • 集群内节点想要复制另一个节点,需要保证本节点不再负责任何slot,不然redis也是不允许的。
  • 集群内的从节点在与其他节点通信的时候,传递的消息中数据分布表和epochmaster的值。

集群主节点出现故障,发生故障转移,其他主节点会把故障主节点的从节点自动提为主节点,原来的主节点恢复后,自动成为新主节点的从节点

这里先说明,把一个master和它的全部slave描述为一个group,故障转移是以group为单位的,集群故障转移的方式跟sentinel的实现很类似。

5.5、均衡集群

在集群运行过程中,有的masterslave宕机,导致了该master成为孤儿masterorphaned masters),而有的master有很多slave

此处孤儿master的定义是那些本来有slave,但是全部离线的master,对于那些原来就没有slavemaster不能认为是孤儿master

redis集群支持均衡slave功能,官方称为Replica migration,而我觉得均衡集群的slave更好理解该概念。集群能把某个slave较多的group上的slave迁移到那些孤儿master上,该功能通过cluster-migration-barrier参数配置,默认为1。

slave在每次定时任务都会检查是否需要迁移slave,即把自己变成孤儿masterslave。 满足以下条件,slave就会成为孤儿masterslave

  • 自己所在的groupslave最多的group
  • 目前存在孤儿master
  • 自己所在的groupslave数目至少超过2个,只有自己一个的话迁移到其他group,自己原来的groupmaster又成了孤儿master
  • 自己所在的groupslave数量大于cluster-migration-barrier配置。
  • group内的其他slave基于memcmp比较node id,自己的node id最小。这个可以防止多个slave并发复制孤儿master,从而原来的group失去过多的slave
5.5.1、优势
  1. 去中心化,集群最大可增加1000个节点,性能随节点增加而线性扩展。
  2. 解耦 数据节点 之间的关系,简化了节点 扩容收缩 难度。
  3. 节点自身 维护槽的 映射关系,不需要 客户端 或者 代理服务 维护 槽分区元数据
5.5.2、劣势
  1. key 批量操作 支持有限。类似 msetmget 操作,目前只支持对具有相同 slot 值的key 执行 批量操作。对于 映射为不同 slot 值的key 由于执行 mgetmget 等操作可能存在于多个节点上,因此不被支持。
  2. 只支持 key同一节点上事务操作,当多个 key 分布在 不同 的节点上时 无法 使用事务功能。
  3. key 作为 数据分区 的最小粒度,不能将一个 大的键值 对象如 hashlist 等映射到 不同的节点
  4. 不支持多数据库空间单机下的Redis可以支持 16 个数据库(db0 ~ db15),集群模式下只能使用一个 数据库空间,即 db0。
  5. 复制结构 只支持一层,从节点 只能复制 主节点,不支持 嵌套树状复制 结构。

5.6、集群搭建

5.6.1、脚本启动

​ 进入redis源码目录下/utils/create-cluster中执行create-cluster脚本

cd /usr/local/redis-6.0.9/utils/create-cluster

5.6.2、配置

vim create-cluster

1
2
3
4
5
6
7
8
9
# Settings
BIN_PATH="../../src/"
CLUSTER_HOST=127.0.0.1
PORT=30000
TIMEOUT=2000
NODES=6 #总的节点数量
REPLICAS=1 #每个master对应的slave数量
PROTECTED_MODE=yes
ADDITIONAL_OPTIONS=""

​ 由以上配置可以看出,总节点数为6,每个master对应的slave数量为1,因此根据此配置启动后集群为三主三从模式;后续如果有需要可以修改此配置

5.6.3、启动集群实例

./create-cluster start

5.6.4、脚本分配slot(hash槽位)

./create-cluster create

5.6.5、客户端-连接

客户端在初始化的时候只需要知道一个节点的地址即可,客户端会先尝试向这个节点执行命令,比如“get key”,如果key所在的slot刚好在该节点上,则能够直接执行成功。如果slot不在该节点,则节点会返回MOVED错误,同时把该slot对应的节点告诉客户端。客户端可以去该节点执行命令。

普通客户端模式-连接

redis-cli -p 30001

​ key分配的slot不在当前连接的redis server时,服务端返回错误提示,让客户端自己进行跳转 (error) MOVED 12706 127.0.0.1:30003

集群客户端模式-连接

redis-cli -c -p 30001

​ key分配的slot不在当前连接的redis server时,集群模式客户端会帮助我们进行重定向跳转 Redirected to slot [12706] located at 127.0.0.1:30003

5.6.6、注意
1
2
	不管是`普通客户端模式`还是`集群客户端模式`去连接服务,如果key不在一个slot仍无法使用事务等指令
​ 解决同类业务数据不在同个 redis哈希槽的问题,在key上加上{tag}来标识某个key,会计算第一次出现'{'到第一次出现'}'之间的subString内容的hash值,如果该内容为空,则计算整个key;这种方式是解决分区key不同分区的通用解决方案
5.6.7、停止实例

​ 停止所有正在运行的redis-cluster实例 ./create-cluster stop

5.6.7、还原实例

​ 清除配置、日志、持久化文件 ./create-cluster clean

5.6.8、手动启动

修改redis配置,之后启动redis时加载此配置 redis-server ./6379.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
################################ REDIS CLUSTER  ###############################
#redis.conf相关集群配置
#配置为cluster模式
cluster-enabled yes

#集群节点配置信息,包括nodeid,集群信息。此文件非常关键,要确保故障转移或者重启的时候此文件还在,所以如果在docker环境下要外挂到外部存储
cluster-config-file nodes-6379.conf

#节点连接超时,如果集群规模小,都在同一个网络环境下,可以配置的短些,更快的做故障转移
cluster-node-timeout 2000

#慢查询日志,用于性能分析,生产环境可设置为1000(毫秒)
slowlog-log-slower-than 1000

#保存慢查询的队列长度 ,设置为1000
slowlog-max-len 1000

#设置为0,默认为10如果master slave都挂掉,slave跟master失联又超过这个数值*timeout的数值,就不会发起选举了。
#如果设置为0,就是永远都会尝试发起选举,尝试从slave变为mater
cluster-slave-validity-factor 10

#设置为no,默认为yes,故障发现到自动完成转移期间整个集群是不可用状态,对于大多数业务无法容忍这种情况
#因此要设置为no,当主节点故障时只影 响它负责槽的相关命令执行,不会影响其他主节点的可用性
cluster-require-full-coverage yes
5.6.9、手动分配slot(hash槽)

​ 使用此方式可以自己指定参与集群的redis节点,设置slave节点数,适合真实环境下多服务器实例的集群搭建

1
redis-cli --cluster create 127.0.0.1:30001 127.0.0.1:30002 127.0.0.1:30003 127.0.0.1:30004 127.0.0.1:30005 127.0.0.1:30006 --cluster-replicas 1
5.6.10、移动slot
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#实行重新分片命令,可任意连接一个redis实例进行操作
redis-cli --cluster reshard 127.0.0.1:30001

>>> Performing Cluster Check (using node 127.0.0.1:30001)
M: 1ba79d08eacd99fa3791d1824907a3e3e136cf06 127.0.0.1:30001
slots:[0-5460] (5461 slots) master
1 additional replica(s)
S: d23ba04b97a59fa4bf3400510feb128ef0694520 127.0.0.1:30004
slots: (0 slots) slave
replicates 1ba79d08eacd99fa3791d1824907a3e3e136cf06
S: 40b2868d28ac09031df1caa9847066eadebfa4f7 127.0.0.1:30006
slots: (0 slots) slave
replicates e5bb13b4ea84f4fd5f23a944ae59a6768a34be28
S: ddeb75d4235f26ce86fe30d3aa9edffde92d5a31 127.0.0.1:30005
slots: (0 slots) slave
replicates e3e285582cabc5360d74e99c778867526cecb2a1
M: e3e285582cabc5360d74e99c778867526cecb2a1 127.0.0.1:30002
slots:[5461-10922] (5462 slots) master
1 additional replica(s)
M: e5bb13b4ea84f4fd5f23a944ae59a6768a34be28 127.0.0.1:30003
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.
#想要移动多少槽位?这里指定了2000
How many slots do you want to move (from 1 to 16384)? 2000
# 想要移动到哪个节点?这里指定了30002的节点id
What is the receiving node ID? e3e285582cabc5360d74e99c778867526cecb2a1
Please enter all the source node IDs.
Type 'all' to use all the nodes as source nodes for the hash slots.
Type 'done' once you entered all the source nodes IDs.
Source node #1: 1ba79d08eacd99fa3791d1824907a3e3e136cf06
Source node #2: done
#略
Moving slot 1998 from 1ba79d08eacd99fa3791d1824907a3e3e136cf06
Moving slot 1999 from 1ba79d08eacd99fa3791d1824907a3e3e136cf06
# 是否执行表重新分片计划?yes
Do you want to proceed with the proposed reshard plan (yes/no)? yes
#略
Moving slot 1997 from 127.0.0.1:30001 to 127.0.0.1:30002:
Moving slot 1998 from 127.0.0.1:30001 to 127.0.0.1:30002:
Moving slot 1999 from 127.0.0.1:30001 to 127.0.0.1:30002:

如下图所示,有2000个槽位从30001的实例迁移到了30002的实例中

5.6.11、集群客户端-帮助
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
[root@centos-3 create-cluster]# redis-cli --cluster help
Cluster Manager Commands:
create host1:port1 ... hostN:portN
--cluster-replicas <arg>
check host:port
--cluster-search-multiple-owners
info host:port
fix host:port
--cluster-search-multiple-owners
--cluster-fix-with-unreachable-masters
reshard host:port
--cluster-from <arg>
--cluster-to <arg>
--cluster-slots <arg>
--cluster-yes
--cluster-timeout <arg>
--cluster-pipeline <arg>
--cluster-replace
rebalance host:port
--cluster-weight <node1=w1...nodeN=wN>
--cluster-use-empty-masters
--cluster-timeout <arg>
--cluster-simulate
--cluster-pipeline <arg>
--cluster-threshold <arg>
--cluster-replace
add-node new_host:new_port existing_host:existing_port
--cluster-slave
--cluster-master-id <arg>
del-node host:port node_id
call host:port command arg arg .. arg
--cluster-only-masters
--cluster-only-replicas
set-timeout host:port milliseconds
import host:port
--cluster-from <arg>
--cluster-copy
--cluster-replace
backup host:port backup_directory
help

For check, fix, reshard, del-node, set-timeout you can specify the host and port of any working node in the cluster.