Redis主从集群-哨兵机制
主从集群数据同步原理
- 从节点刚连接到主节点时,主动请求数据同步
- 主节点判断是否是第一次同步,如果是,就返回主节点的数据版本信息
- 随后,主节点执行bgsave,生成RDB文件,并发送给从节点
- 这个过程中以及随后产生的全部命令都会记录到
repl_baklog
这个缓冲区中,然后一点一点的同步给从节点,这样就保证了主从节点数据的一致性
如何判断从节点同步进度
上面有说到要判断从节点是否是第一次同步,是第一次就来个全量同步,言外之意就是不是第一次就会有增量同步?问题是咋判断从节点的同步进度?
Replication Id
:每一个master节点有一个replid
,slave节点继承master的replid
,如果replid
一样,认为它们保存了同一个数据集(同一份数据)offset
:master
和slave
都维护一个offset
,可以看作它们当前所在的数据集的偏移量,slave
的偏移量总会小于等于master
,当它等于master
,证明它的数据与master
完全一致,无需同步
Replication Id
用于判断两个节点是否持有同一份数据集,主从集群中的所有节点Replid
应该相同,而offset
指示着每个节点保存的数据的进度。所以,主节点只需要判断从节点的replid
和offset
就能知道它是否是第一次同步。事实上,主节点只需判断replid
即可,因为第一次同步时,从节点的replid
不等于主节点的replid
增量同步
repl_baklog大小上限
repl_baklog实际上是一个环形数组的结构
也就是说,如果slave宕机的情况下,只要它别落下超过这个环大小的数据量(可以理解为master.offset-slave.offset <= repl_baklog.size
),它就可以使用增量同步,只同步它落下的这些。
但如果slave落下的数据超过了环的大小
那么,它无法再通过增量同步来从master中同步数据,此时需要一次全量同步
主从复制优化
- 开启无磁盘复制,复制时不生成RDB,直接通过内存。(
repl-diskless-sync yes
) - Redis单节点内存占用不要太高
- 提高
repl_baklog
大小 - slave宕机后及时恢复,避免全量同步
- 限制单个master的节点数量,采用主-从-从结构,减少master压力
主从替换
Redis通过哨兵(Sentinel)集群来监控主从集群的状态,这个并不是阿里的那个Sentinel。当哨兵发现了集群主节点挂了,它会选出一个从节点当作主节点,并通知客户端主节点ip发生变化。
服务状态监控
每个哨兵不断的向集群中的节点发送心跳检测,如果未在指定时间内响应,则该哨兵认为该节点主观下线,如果超过quorum
的哨兵都认为一个节点主观下线了,那么此时该节点就被认为客观下线了(quorum
的数量最好大于哨兵数量的一半)
master选举
- 如果slave与master断开时间过长,超过指定值(
down-after-millisecond * 10
),则该节点无法参与选举 - 判断节点的
slave-priority
值(默认为1),越小优先级越高,0代表不参与选举 - 优先级一样的情况下,
offset
越大优先级越高 - 如果
offset
也一样,节点id越小优先级越高(节点id是唯一的,不会出现到这还选不出的情况了)
所以,master
的选举主要看的是人为设置的优先级,在没设置的情况下,看的是谁的数据更新(offset更大)
故障转移
- 给选中的节点发送
slaveof no one
,使它恢复成主节点 - 给所有其它节点广播
slaveof <新主节点ip>
- 哨兵强制改动故障节点的配置文件,添加
slaveof <新主节点id>
(wtf?这是怎么实现的)
Redis主从集群+Sentinel集群搭建
我这里使用docker compose搭建
services:
redis1:
image: redis
ports:
- 63791:6379
volumes:
- ./conf/redis1.conf:/usr/local/etc/redis/redis.conf
- ./redis1/:/data/
command:
/bin/bash -c "redis-server /usr/local/etc/redis/redis.conf"
redis2:
image: redis
ports:
- 63792:6379
volumes:
- ./conf/redis2.conf:/usr/local/etc/redis/redis.conf
- ./redis2/:/data/
command:
/bin/bash -c "redis-server /usr/local/etc/redis/redis.conf"
redis3:
image: redis
ports:
- 63793:6379
volumes:
- ./conf/redis3.conf:/usr/local/etc/redis/redis.conf
- ./redis3/:/data/
command:
/bin/bash -c "redis-server /usr/local/etc/redis/redis.conf"
sentinel1:
depends_on:
- redis1
image: redis
ports:
- 27001:27001
volumes:
- ./s1_conf/:/usr/local/etc/redis/
- ./sentinel1/:/tmp/sentinel
command:
/bin/bash -c "redis-sentinel /usr/local/etc/redis/sentinel.conf"
sentinel2:
depends_on:
- redis1
image: redis
ports:
- 27002:27002
volumes:
- ./s2_conf/:/usr/local/etc/redis/
- ./sentinel2/:/tmp/sentinel
command:
/bin/bash -c "redis-sentinel /usr/local/etc/redis/sentinel.conf"
sentinel3:
depends_on:
- redis1
image: redis
ports:
- 27003:27003
volumes:
- ./s3_conf/:/usr/local/etc/redis/
- ./sentinel3/:/tmp/sentinel
command:
/bin/bash -c "redis-sentinel /usr/local/etc/redis/sentinel.conf"
然后conf
目录下的四个配置文件:
# redis1.conf
# 填你主机ip
slave-announce-ip 172.21.1.202
# 填redis1容器映射到的主机端口
slave-announce-port 63791
protected-mode no
# redis2.conf
slave-announce-ip 172.21.1.202
slave-announce-port 63792
protected-mode no
slaveof 172.21.1.202 63791
# redis3.conf
slave-announce-ip 172.21.1.202
slave-announce-port 63793
protected-mode no
slaveof 172.21.1.202 63791
# s1_conf/sentinel.conf
protected-mode no
port 27001
sentinel announce-ip "172.21.1.202"
sentinel monitor mymaster 172.21.1.202 63791 2
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 60000
dir "/tmp/sentinel"
# s2_conf/sentinel.conf
protected-mode no
port 27002
sentinel announce-ip "172.21.1.202"
sentinel monitor mymaster 172.21.1.202 63791 2
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 60000
dir "/tmp/sentinel"
# s3_conf/sentinel.conf
protected-mode no
port 27003
sentinel announce-ip "172.21.1.202"
sentinel monitor mymaster 172.21.1.202 63791 2
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 60000
dir "/tmp/sentinel"
坑1:Sentinel无法创建新配置文件
报错信息:
WARNING: Sentinel was not able to save the new configuration on disk!!!: Device or resource busy
因为Sentinel会在启动后向自己的配置文件中追加内容,它采用的是先创建一个临时配置文件,然后使用它替换掉原来的配置文件的方式。
如果是使用挂载卷直接挂载文件的方式,docker貌似不允许这样操作,所以会出现这个错误,你可以将配置文件放到单独的目录中,然后将目录挂载到容器。
另外,从其他帖子中看到,好像sed命令也是这个原理,所以这种情况下sed命令也是不能用的。
坑2:启动后直接检测到slave下线
实际上就是IP问题,类似的问题还有主机下线后无法重新选举,sentinel节点之间无法同步...
启动后伴随着大量的slave下线信息,并且有很多我们都没见过的ip
这种情况就是没有在redis和sentinel的配置文件中指明announce-ip
和announce-port
,这两个值的设置必须是你的主机IP还有docker映射到的本地主机端口
测试结果&选举过程分析
启动并将master下线,查看其中几个sentinel的日志,它检测到了master下线,并在quorum到达2后重新选举了新的master
选举的过程如下:
- 三台哨兵先选主,从这里可以看到最终选的主是哨兵3
- 哨兵中的主节点进行选举新redis master,从日志里的
select-slave
可以看出 - 主哨兵给重新选择的主节点发送
slaveof no one
命令 - 主哨兵给其它节点广播新的主节点
Sentinel会在连接后、重选主节点后更新各自的配置文件,比如下图,重新选举后主节点已经变成了新的,连接后也生成了一些新的行,主要是生成的myid和探测到的其它的redis节点和sentinel节点。
RedisTemplate使用集群
配置哨兵模式
因为使用哨兵保证集群的高可用的情况下,主节点的ip是有可能变更的,所以不能在代码中写死,你需要配置哨兵集群的地址,然后RedisTemplate去哨兵集群中找主节点和从节点的ip
spring:
redis:
sentinel:
# 哨兵集群
nodes:
- 172.21.1.202:27001
- 172.21.1.202:27002
- 172.21.1.202:27003
# 集群名
master: mymaster
配置主从读写分离
@Bean
public LettuceClientConfigurationBuilderCustomizer configurationBuilderCustomizer() {
return configBuilder -> configBuilder.readFrom(ReadFrom.REPLICA_PREFERRED);
}
ReadFrom是redis的读取策略:
- MASTER: 从主节点读
- MASTER_PREFERRED: 优先从主节点读,主节点不可以从Replica读(也就是slave)
- REPLICA:从Replica读
- REPLICA_PREFERRED:优先从Replica读,所有的Replica都不可用从主节点读