Loading

Redis主从集群-哨兵机制

主从集群数据同步原理

img

  1. 从节点刚连接到主节点时,主动请求数据同步
  2. 主节点判断是否是第一次同步,如果是,就返回主节点的数据版本信息
  3. 随后,主节点执行bgsave,生成RDB文件,并发送给从节点
  4. 这个过程中以及随后产生的全部命令都会记录到repl_baklog这个缓冲区中,然后一点一点的同步给从节点,这样就保证了主从节点数据的一致性

如何判断从节点同步进度

上面有说到要判断从节点是否是第一次同步,是第一次就来个全量同步,言外之意就是不是第一次就会有增量同步?问题是咋判断从节点的同步进度?

  1. Replication Id:每一个master节点有一个replid,slave节点继承master的replid,如果replid一样,认为它们保存了同一个数据集(同一份数据)
  2. offsetmasterslave都维护一个offset,可以看作它们当前所在的数据集的偏移量,slave的偏移量总会小于等于master,当它等于master,证明它的数据与master完全一致,无需同步

Replication Id用于判断两个节点是否持有同一份数据集,主从集群中的所有节点Replid应该相同,而offset指示着每个节点保存的数据的进度。所以,主节点只需要判断从节点的replidoffset就能知道它是否是第一次同步。事实上,主节点只需判断replid即可,因为第一次同步时,从节点的replid不等于主节点的replid

增量同步

img

repl_baklog大小上限

repl_baklog实际上是一个环形数组的结构

img

也就是说,如果slave宕机的情况下,只要它别落下超过这个环大小的数据量(可以理解为master.offset-slave.offset <= repl_baklog.size),它就可以使用增量同步,只同步它落下的这些。

但如果slave落下的数据超过了环的大小

img

那么,它无法再通过增量同步来从master中同步数据,此时需要一次全量同步

主从复制优化

  1. 开启无磁盘复制,复制时不生成RDB,直接通过内存。(repl-diskless-sync yes)
  2. Redis单节点内存占用不要太高
  3. 提高repl_baklog大小
  4. slave宕机后及时恢复,避免全量同步
  5. 限制单个master的节点数量,采用主-从-从结构,减少master压力

主从替换

img

Redis通过哨兵(Sentinel)集群来监控主从集群的状态,这个并不是阿里的那个Sentinel。当哨兵发现了集群主节点挂了,它会选出一个从节点当作主节点,并通知客户端主节点ip发生变化。

服务状态监控

每个哨兵不断的向集群中的节点发送心跳检测,如果未在指定时间内响应,则该哨兵认为该节点主观下线,如果超过quorum的哨兵都认为一个节点主观下线了,那么此时该节点就被认为客观下线了(quorum的数量最好大于哨兵数量的一半)

img

master选举

  1. 如果slave与master断开时间过长,超过指定值(down-after-millisecond * 10),则该节点无法参与选举
  2. 判断节点的slave-priority值(默认为1),越小优先级越高,0代表不参与选举
  3. 优先级一样的情况下,offset越大优先级越高
  4. 如果offset也一样,节点id越小优先级越高(节点id是唯一的,不会出现到这还选不出的情况了)

所以,master的选举主要看的是人为设置的优先级,在没设置的情况下,看的是谁的数据更新(offset更大)

故障转移

  1. 给选中的节点发送slaveof no one,使它恢复成主节点
  2. 给所有其它节点广播slaveof <新主节点ip>
  3. 哨兵强制改动故障节点的配置文件,添加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命令也是不能用的。

参考:docker-library/redis - issue#287: WARNING: Sentinel was not able to save the new configuration on disk!!!: Device or resource busy

坑2:启动后直接检测到slave下线

实际上就是IP问题,类似的问题还有主机下线后无法重新选举,sentinel节点之间无法同步...

启动后伴随着大量的slave下线信息,并且有很多我们都没见过的ip

img

这种情况就是没有在redis和sentinel的配置文件中指明announce-ipannounce-port这两个值的设置必须是你的主机IP还有docker映射到的本地主机端口

测试结果&选举过程分析

启动并将master下线,查看其中几个sentinel的日志,它检测到了master下线,并在quorum到达2后重新选举了新的master

img

img

img

选举的过程如下:

  1. 三台哨兵先选主,从这里可以看到最终选的主是哨兵3
  2. 哨兵中的主节点进行选举新redis master,从日志里的select-slave可以看出
  3. 主哨兵给重新选择的主节点发送slaveof no one命令
  4. 主哨兵给其它节点广播新的主节点

Sentinel会在连接后、重选主节点后更新各自的配置文件,比如下图,重新选举后主节点已经变成了新的,连接后也生成了一些新的行,主要是生成的myid和探测到的其它的redis节点和sentinel节点。

img

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的读取策略:

  1. MASTER: 从主节点读
  2. MASTER_PREFERRED: 优先从主节点读,主节点不可以从Replica读(也就是slave)
  3. REPLICA:从Replica读
  4. REPLICA_PREFERRED:优先从Replica读,所有的Replica都不可用从主节点读
posted @ 2022-08-23 17:46  yudoge  阅读(1124)  评论(2编辑  收藏  举报