Redis 6.x 高可用集群搭建
Redis 6.x 高可用集群搭建
开始集群搭建
redis集群需要至少要三个master节点,我们这里搭建三个master节点,并且给每个master再搭建一个slave节点,总共6个redis节点,这里用一台机器(可以多台机器部署,修改一下ip地址就可以了)部署6个redis实例,三主三从,搭建集群的步骤如下:
# 查看配置文件去掉注释和空行
cat ./8001/redis.conf | grep -v "^#" | grep -v "^$"
第一步:在第一台机器的/usr/local下创建文件夹redis-cluster,然后在其下面创建6个文件夾如下:
mkdir -p /usr/local/redis6-cluster
mkdir 8001 8002 8003 8004 8005 8006
第二步:把之前的redis.conf配置文件copy到8001下,修改如下内容:
- 1)daemonize yes
- 2)port 8001(分别对每个机器的端口号进行设置)
- 3)dir /usr/local/redis-cluster/8001/(指定数据文件存放位置,必须要指定不同的目录位置,不然会丢失数据)
- 4)cluster-enabled yes(启动集群模式)
- 5)cluster-config-file nodes-8001.conf(集群节点信息文件,这里800x最好和port对应上)
- 6)cluster-node-timeout 5000
- 7)bind 127.0.0.1(去掉bind绑定访问ip信息)
- 8)protected-mode no (关闭保护模式)
- 9)appendonly yes
如果要设置密码需要增加如下配置:
- 10)requirepass xxx (设置redis访问密码)
- 11)masterauth xxx (设置集群节点间访问密码,跟上面一致)
daemonize yes
protected-mode no
bind 0.0.0.0
port 8001
dir /usr/local/redis-cluster/8001/
cluster-enabled yes
cluster-config-file nodes-8001.conf
cluster-node-timeout 5000
appendonly yes
第三步:把修改后的配置文件,
copy到8001-8006,修改第2、3、5项里的端口号,可以用
echo 8001 8002 8003 8004 8005 8006 | xargs -n 1 cp -v /usr/local/redis6-cluster/redis.conf
上面的意思是将/usr/local/redis-cluster/redis.conf文件同时复制到8001-8006 多个文件夹。
-n 1 告诉 xargs 命令每个命令行最多使用一个参数,并发送到 cp 命令中。
cp 用于复制文件。
-v 启用详细模式来显示更多复制细节。
如果是将指定文件复制到当前路径下的所有文件夹:
echo ./* |xargs -n 1 cp -v d
注意千万不能漏写.另外当前路径下有其他文件会被d文件覆盖 此命令慎用 如果文件不是特别多建议使用上面的命令。
批量替换[在vi编辑状态下使用]:
%s/源字符串/目的字符串/g
批量替换多个文件的内容
格式: sed -i "s/查找字段/替换字段/g" `grep 查找字段 -rl 路径`
sed -i "s/oldstring/newstring/g" `grep oldstring -rl yourdir`
example:
sed -i "s/6379/8001/g" `grep 6379 -rl /usr/local/redis-cluster/8001/redis.conf`
第四步:分别启动6个redis实例,然后检查是否启动成功
/usr/local/redis6/bin/redis-server /usr/local/redis6-cluster/8001/redis.conf
查看是否启动成功
ps -ef | grep redis
第五步:用redis-cli创建整个redis集群(redis5以前的版本集群是依靠ruby脚本redis-trib.rb实现)
/usr/local/redis6/bin/redis-cli --cluster create --cluster-replicas 1 192.168.3.235:8001 192.168.3.235:8002 192.168.3.236:8001 192.168.3.236:8002 192.168.3.237:8001 192.168.3.237:8002
代表为每个创建的主服务器节点创建一个从服务器节点
第六步:验证集群:
1)连接任意一个客户端即可:
./redis-cli -c -h 192.168.3.237 -p 8001
提示:-c表示集群模式,指定ip地址和端口号
例如:
/usr/local/redis6/redis-cli -c -h 192.168.3.237 -p 8001
注意这里进入到8002了,redirected。
2)进行验证: cluster info(查看集群信息)、cluster nodes(查看节点列表)
3)进行数据操作验证
4)关闭集群则需要逐个进行关闭,使用命令:
/usr/local/redis/src/redis-cli -c -h 192.168.3.240 -p 8001 shutdown
Redis基础运维操作
- 创建启动redis节点脚本/启动集群脚本
shell
#!/bin/sh
REDIS_ROOT=/usr/local/redis507/
PORT=8001
for i in {1..6}
do
PORT=$((PORT++))
$REDIS_ROOT/bin/redis-server ./$PORT/redis.conf
done
- 创建Redis集群脚本
shell
#!/bin/sh
PORT=8001
/usr/local/redis507/bin/redis-cli --cluster create --cluster-replicas 1 192.168.3.199:$((PORT++)) 192.168.6.172:$((PORT++)) 192.168.6.172:$((PORT++)) 192.168.6.172:$((PORT++)) 192.168.6.172:$((PORT++)) 192.168.6.172:$((PORT++))
- 编写关闭集群脚本
shell
#!/bin/sh
REDIS_ROOT=/usr/local/redis507/
PORT=8001
for i in {1..6}
do
$REDIS_ROOT/bin/redis-cli -c -h 192.168.6.172 -p $((PORT++)) shutdown
done
Redis水平扩展容
增加redis实例
在/usr/local/redis-cluster下创建8007和8008文件夹,并拷贝8001文件夹下的redis.conf文件到8007和8008这两个文件夹下
按之前的方法修改8007、8008中redis.conf参数,修改完成后进行启动
查询启动情况:
这时候客户端连接8001端口的redis实例,查看节点信息,会发现并无8007、8008节点信息
那么开始配置8007为集群主节点
使用add-node命令新增一个主节点8007(master),8007为新增节点,8001为已知存在节点,看到日志最后有“[OK] New node added correctly”提示代表新节点加入成功
/usr/local/redis6/bin/redis-cli --cluster add-node 192.168.3.240:8007 192.168.3.240:8001
我们为新节点手工分配hash槽
使用redis-cli命令为8008分配hash槽,找到集群中的任意一个主节点(8001),对其进行重新分片工作。
/usr/local/redis6/bin/redis-cli --cluster reshard 192.168.3.240:8001
可以看到0-999已经分给8007了,而8001则从1000-5460
这时候我们配置8008为8007的从节点
添加从节点8008到集群中去并查看集群状态
/usr/local/redis6/bin/redis-cli --cluster add-node 192.168.3.240:8008 192.168.3.240:8001
可以看到8008是一个master节点,没有被分配任何的hash槽。
我们需要执行replicate命令来指定当前节点(从节点)的主节点id为哪个,首先需要连接新加的8008节点的客户端,然后使用集群命令进行操作,把当前的8008(slave)节点指定到一个主节点下
/usr/local/redis6/bin/redis-cli -c -h -p 8008
CLUSTER REPLICATE 5d3c8606cabc871bfd3b4a0d2cc4ffba3aba5209
扩展redis集群已经实现,下面进行删除节点
删除8008从节点
用del-node删除从节点8008,指定删除节点ip和端口,以及节点id
/usr/local/redis6/bin/redis-cli --cluster del-node 192.168.3.240:8008 8008ID
删除8007主节点有点复杂,因为有数据,必须把数据迁移好了才能删除
// 先把slot数据迁移到一个master节点上面
/usr/local/redis6/bin/redis-cli --cluster reshard 192.168.3.240:8007
=> 选择一个master节点ID
=> 输入8007的ID
done
// 删除8007节点
/usr/local/redis6/bin/redis-cli --cluster del-node 192.168.3.240:8007 8007ID
Redis集群分片原理及选举流程
集群分片模式
如果Redis只用复制功能做主从,那么当数据量巨大的情况下,单机情况下可能已经承受不下一份数据,更不用说是主从都要各自保存一份完整的数据。在这种情况下,数据分片是一个非常好的解决办法。
Redis的Cluster正是用于解决该问题。它主要提供两个功能:
- 自动对数据分片,落到各个节点上
- 即使集群部分节点失效或者连接不上,依然可以继续处理命令
对于第二点,它的功能有点类似于Sentienl的故障转移,在这里不细说。下面详细了解下Redis的槽位分片原理,在此之前,先了解下分布式简单哈希算法和一致性哈希算法,以帮助理解槽位的作用。
简单哈希算法
假设有三台机,数据落在哪台机的算法为
c = Hash(key) % 3
例如key A的哈希值为4,4%3=1,则落在第二台机。Key ABC哈希值为11,11%3=2,则落在第三台机上。
利用这样的算法,假设现在数据量太大了,需要增加一台机器。A原本落在第二台上,现在根据算法4%4=0,落到了第一台机器上了,但是第一台机器上根本没有A的值。这样的算法会导致增加机器或减少机器的时候,引起大量的缓存击穿,造成雪崩。
缓存击穿:查询的数据在数据库中是存在,但是缓存中热点数据key恰巧失效,所有请求直接怼到数据库导致数据库连接等待甚至宕机
【加锁,双缓存[A,B]{A 缓存的key需要设置过期时间,而B缓存的key永不失效}】JUC (读写锁)
保证一致性: 在更新数据到数据库之后删除缓存在重新写入缓存。
缓存雪崩:查询的时候大面积的key集体失效导致请求直接到了数据库。(随机范围设置过期时间)
// select* from table where id = -1111 (坏人)
缓存穿透:查询的数据在数据库中不存在,也就意味没有命中缓存中key,导致请求全部到数据操作。(布隆过滤器,缓存空数据)
一致性哈希算法
在1997年,麻省理工学院的Karger等人提出了**一致性哈希算法**,为的就是解决分布式缓存的问题。
在**一致性哈希算法**中,整个哈希空间是一个**虚拟圆环**
假设有四个节点Node A、B、C、D,经过ip地址的哈希计算,它们的位置如下
有4个存储对象Object A、B、C、D,经过对Key的哈希计算后,它们的位置如下
对于各个Object,它所真正的存储位置是按顺时针找到的第一个存储节点。例如Object A顺时针找到的第一个节点是Node A,所以Node A负责存储Object A,Object B存储在Node B。
一致性哈希算法大概如此,那么它的**容错性**和**扩展性**如何呢?
假设Node C节点挂掉了,Object C的存储丢失,那么它顺时针找到的最新节点是Node D。也就是说Node C挂掉了,受影响仅仅包括Node B到Node C区间的数据,并且这些数据会转移到Node D进行存储。
同理,假设现在数据量大了,需要增加一台节点Node X。Node X的位置在Node B到Node C直接,那么受到影响的仅仅是Node B到Node X间的数据,它们要重新落到Node X上。
所以一致性哈希算法对于容错性和扩展性有非常好的支持。但一致性哈希算法也有一个严重的问题,就是**数据倾斜**。
如果在分片的集群中,节点太少,并且分布不均,一致性哈希算法就会出现部分节点数据太多,部分节点数据太少。也就是说无法控制节点存储数据的分配。如下图,大部分数据都在A上了,B的数据比较少。
哈希槽
Redis集群(Cluster)并没有选用上面一致性哈希,而是采用了**哈希槽**(SLOT)的这种概念。主要的原因就是上面所说的,一致性哈希算法对于数据分布、节点位置的控制并不是很友好。
首先**哈希槽**其实是两个概念,第一个是**哈希算法**。Redis Cluster的hash算法不是简单的hash(),而是crc16算法,一种校验算法。
另外一个就是**槽位**的概念,空间分配的规则。其实哈希槽的本质和一致性哈希算法非常相似,不同点就是对于哈希空间的定义。一致性哈希的空间是一个圆环,节点分布是基于圆环的,无法很好的控制数据分布。而Redis Cluster的槽位空间是自定义分配的,类似于Windows盘分区的概念。这种分区是可以自定义大小,自定义位置的。
Redis Cluster包含了16384个哈希槽,每个Key通过计算后都会落在具体一个槽位上,而这个槽位是属于哪个存储节点的,则由用户自己定义分配。例如机器硬盘小的,可以分配少一点槽位,硬盘大的可以分配多一点。如果节点硬盘都差不多则可以平均分配。所以哈希槽这种概念很好地解决了一致性哈希的弊端。
另外在**容错性**和**扩展性**上,表象与一致性哈希一样,都是对受影响的数据进行转移。而哈希槽本质上是对槽位的转移,把故障节点负责的槽位转移到其他正常的节点上。扩展节点也是一样,把其他节点上的槽位转移到新的节点上。
但一定要注意的是,对于槽位的转移和分派,Redis集群是不会自动进行的,而是需要人工配置的。所以Redis集群的高可用是依赖于节点的主从复制与主从间的自动故障转移。
带虚拟节点的一致性哈希 (Redis集群):
Redis采用的方案,在一致性哈希基础之上,引入虚拟节点的概念,虚拟节点被称为槽(slot)。Redis集群中,槽的数量为16384。
槽介于数据和节点之间,将节点划分为一定数量的槽,每个槽包含哈希值一定范围内的数据。由原来的hash–>node 变为 hash–>slot–>node。
当增删节点时,该节点所有拥有的槽会被重新分配给其他节点,可以避免在一致性哈希分区中由于某个节点的增删造成数据的严重分布不均。
通信机制
在上面的哨兵方案中,节点被分为数据节点和哨兵节点,哨兵节点也是redis服务,但只作为选举监控使用,只有数据节点会存储数据。而在Redis集群中,所有节点都是数据节点,也都参与集群的状态维护。
在Redis集群中,数据节点提供**两个TCP端口**,在配置防火墙时需要同时开启下面两类端口:
普通端口:即客户端访问端口,如默认的6379;
集群端口:普通端口号加10000,如6379的集群端口为16379,用于集群节点之间的通讯;
集群的节点之间通讯采用**Gossip协议**,节点根据固定频率(每秒10次)定时任务进行判断,当集群状态发生变化,如增删节点、槽状态变更时,会通过节点间通讯同步集群状态,使集群收敛。
集群间发送的Gossip消息有下面**五种消息类型**:
- MEET:在节点握手阶段,对新加入的节点发送meet消息,请求新节点加入当前集群,新节点收到消息会回复PONG消息;
- PING:节点之间互相发送ping消息,收到消息的会回复pong消息。ping消息内容包含本节点和其他节点的状态信息,以此达到状态同步;
- PONG:pong消息包含自身的状态数据,在接收到ping或meet消息时会回复pong消息,也会主动向集群广播pong消息;
- FAIL:当一个主节点判断另一个主节点进入fail状态时,会向集群广播这个消息,接收到的节点会保存该消息并对该fail节点做状态判断;
- PUBLISH:当节点收到publish命令时,会先执行命令,然后向集群广播publish消息,接收到消息的节点也会执行publish命令;
集群选举原理分析
选举流程:
原理分析:
当slave发现自己的master变为FAIL状态时,便尝试进行Failover,以期成为新的master。由于挂掉的master可能会有多个slave,从而存在多个slave竞争成为master节点的过程, 其过程如下:
- 1.slave发现自己的master变为FAIL
- 2.将自己记录的集群currentEpoch加1,并广播FAILOVER_AUTH_REQUEST信息
- 3.其他节点收到该信息,只需要master做出响应,判断请求者的合法性,并发送FAILOVER_AUTH_ACK,对每一个epoch只发送一次ack
- 4.尝试failover的slave收集FAILOVER_AUTH_ACK
- 5.超过半数后变成新Master
- 6.广播Pong通知其他集群节点。
从节点并不是在主节点一进入 FAIL 状态就马上尝试发起选举,而是有一定延迟,一定的延迟确保我们等待FAIL状态在集群中传播,slave如果立即尝试选举,其它masters或许尚未意识到FAIL状态,可能会拒绝投票
延迟计算公式:
DELAY = 500ms + random(0 ~ 500ms) + SLAVE_RANK * 1000ms
1
SLAVE_RANK表示此slave已经从master复制数据的总量的rank。Rank越小代表已复制的数据越新。这种方式下,持有最新数据的slave将会首先发起选举(理论上)。
补充之前的一个问题:
> 8001 get name
redirect to [] ……
跳转重定位
当客户端向一个错误的节点发出了指令,该节点会发现指令的 key 所在的槽位并不归自己管理,这时它会向客户端发送一个特殊的跳转指令携带目标操作的节点地址,告诉客户端去连这个节点去获取数据。客户端收到指令后除了跳转到正确的节点上去操作,还会同步更新纠正本地的槽位映射表缓存,后续所有 key 将使用新的槽位映射表。