分布式系统——Redis哨兵:自动故障转移与选举原理

文章目录

  • 一、概念简述
    • [1.1 手动恢复主从:为什么不靠谱](#1.1 手动恢复主从:为什么不靠谱)
    • [1.2 哨兵是什么](#1.2 哨兵是什么)
    • [1.3 哨兵自动恢复流程](#1.3 哨兵自动恢复流程)
    • [1.4 哨兵的核心功能](#1.4 哨兵的核心功能)
  • 二、实战搭建
    • [2.1 配置主从节点](#2.1 配置主从节点)
    • [2.2 配置哨兵节点](#2.2 配置哨兵节点)
    • [2.3 模拟主节点故障](#2.3 模拟主节点故障)
    • [2.4 旧主节点恢复](#2.4 旧主节点恢复)
  • 三、故障转移的原理
    • [3.1 主观下线与客观下线](#3.1 主观下线与客观下线)
    • [3.2 选举 leader 哨兵](#3.2 选举 leader 哨兵)
    • [3.3 leader 如何挑选新的主节点](#3.3 leader 如何挑选新的主节点)
    • [3.4 新主确定之后](#3.4 新主确定之后)
  • 四、总结与局限

一、概念简述

  上一篇讲了主从复制,它提高了读的并发和可用性,但留下了一个致命的问题:主节点挂了怎么办? 从节点不会自动晋升为主节点,整个系统就没法写了。哨兵(Sentinel) 就是用自动化的手段解决这个问题的。

1.1 手动恢复主从:为什么不靠谱

  主节点挂了之后,如果靠人工处理,流程是这样的:

  1. 监控系统报警,通知程序员。
  2. 程序员登录上去看看,能不能抢救。
  3. 抢救不了,就从从节点里挑一个作为新的主节点,执行slaveof no one。
  4. 修改其他从节点,通过slaveof让它们认新的主节点。
  5. 告知客户端,让它们连接新的主节点。
  6. 旧主节点修好之后,作为一个新的从节点挂回集群。

  这样的手动修复有两个问题:

  • 繁琐,容易出错:步骤多,一旦出错后果更严重。
  • 慢:即使程序员第一时间响应,也需要一段时间,这段时间系统是不可写的。

1.2 哨兵是什么

  • 哨兵是独立的进程(redis-sentinel),不负责存储数据 ,只负责监控。
  • 哨兵通常不是单个,而是一个集合(后面会解释原因)。
  • 它监控的是 Redis 的数据节点(主节点和从节点),并在主节点出问题时自动处理。

1.3 哨兵自动恢复流程

  1. 哨兵节点不断给 Redis 数据节点发送心跳包(PING),检测是否正常。
  2. 主节点挂了,一个哨兵发现后,还需要多个哨兵认同,才能确认主节点真的挂了,以防误判。
  3. 哨兵们选举出一个 leader,由它来负责这次故障转移。
  4. leader 从从节点中挑选出新的主节点 ,自动控制它执行slaveof no one,并控制其他从节点执行slaveof,改认新的主节点。
  5. 客户端通过哨兵得知新的主节点是谁,之后的写操作就会发给新的主节点。
  6. 旧主节点恢复后,哨兵会自动把它变成新主节点的从节点。

1.4 哨兵的核心功能

  • 监控:持续检查主节点、从节点是否正常。

  • 自动故障转移:主节点挂了,自动选出新主,并调整主从关系。

  • 通知:把故障转移的结果通知给客户端(和管理员)。

  • 细节:客户端其实不会直接写死 Redis 的地址,而是先连哨兵,问它当前的主节点是谁 (sentinel get-master-addr-by-name),所以哨兵还承担了"配置提供者"的角色。

为什么哨兵一个节点也能完成完整的功能,却还要部署多个?

  ① 哨兵本身也可能挂 :如果只有一个哨兵,它挂了,这时候再碰上 Redis 主节点也挂了,就没人做自动恢复了。分布式系统中应当避免出现单点。
  ② 误判的概率高 :网络传输容易延迟、丢包,一个哨兵连不上主节点,不代表主节点真挂了,可能只是这个哨兵到主节点的网络有问题。需要多个哨兵投票 才能降低误判。

哨兵节点为什么最好是奇数个?

哨兵的决策都是靠多数派(超过半数)达成的。3 个哨兵,多数是 2,允许挂 1 个;4 个哨兵,多数是 3,还是只允许挂 1 个。多出来的那一个并没有提升容错能力,反而多了一份开销。所以选奇数个(3、5)就够了。

二、实战搭建

  准备 6 个节点:1 主 2 从 + 3 哨兵 ,使用Docker Compose来搭建。

注意:这里为了演示,把所有节点放在同一台服务器上。实际应用中,所有节点放在一台服务器上是没有意义的(服务器挂了就全挂了),应该分布在不同的主机上。

2.1 配置主从节点

  创建master-slave/docker-compose.yml:

yaml 复制代码
version: '3.7'
services:
  master:
    image: 'redis:5.0.9'
    container_name: redis-master
    restart: always
    command: redis-server --appendonly yes
    ports:
      - 6379:6379
  slave1:
    image: 'redis:5.0.9'
    container_name: redis-slave1
    restart: always
    command: redis-server --appendonly yes --slaveof redis-master 6379
    ports:
      - 6380:6379
  slave2:
    image: 'redis:5.0.9'
    container_name: redis-slave2
    restart: always
    command: redis-server --appendonly yes --slaveof redis-master 6379
    ports:
      - 6381:6379

启动容器:

bash 复制代码
docker-compose up -d
  • 细节:--slaveof redis-master 6379中直接使用了容器名,因为同一个docker网络内,容器之间可以通过名字互相访问。

2.2 配置哨兵节点

  创建redis-sentinel/docker-compose.yml:

yaml 复制代码
version: '3.7'
services:
  sentinel1:
    image: 'redis:5.0.9'
    container_name: redis-sentinel-1
    restart: always
    command: redis-sentinel /etc/redis/sentinel.conf
    volumes:
      - ./sentinel1.conf:/etc/redis/sentinel.conf
    ports:
      - 26379:26379
  sentinel2:
    image: 'redis:5.0.9'
    container_name: redis-sentinel-2
    restart: always
    command: redis-sentinel /etc/redis/sentinel.conf
    volumes:
      - ./sentinel2.conf:/etc/redis/sentinel.conf
    ports:
      - 26380:26379
  sentinel3:
    image: 'redis:5.0.9'
    container_name: redis-sentinel-3
    restart: always
    command: redis-sentinel /etc/redis/sentinel.conf
    volumes:
      - ./sentinel3.conf:/etc/redis/sentinel.conf
    ports:
      - 26381:26379

networks:
  default:
    external:
      name: master-slave_default
  • 注意:networks这一段是让哨兵容器加入主从节点所在的网络(master-slave_default),这样哨兵才能通过容器名访问到redis-master。

  再创建sentinel1.conf、sentinel2.conf、sentinel3.conf三份文件,内容完全相同 ,都放在/root/redis-sentinel/目录中:

bash 复制代码
bind 0.0.0.0
port 26379
sentinel monitor redis-master redis-master 6379 2
sentinel down-after-milliseconds redis-master 1000

逐行解释:

  • bind 0.0.0.0:允许任意地址访问。
  • port 26379:哨兵的端口(Redis 默认是 6379,哨兵默认是 26379)。
  • sentinel monitor <名字> <ip> <port> <quorum>:
    • <名字>:给被监控的主节点起的名字,这里叫redis-master。
    • <ip> <port>:主节点的地址,这里用的是容器名。
    • <quorum>:法定票数。至少有多少个哨兵认为主节点挂了,才会判定为客观下线。这里是 2,通常设为"哨兵总数的一半加一"。
  • sentinel down-after-milliseconds <名字> <毫秒>:多久收不到心跳回复,就认为主节点"主观下线"。这里设为 1000ms,是为了演示效果;实际默认是 30000ms。

细节:

  • 三份配置文件必须是三份独立的文件,不能共用一个。 因为哨兵运行时会改写自己的配置文件(记录自己的 id、发现的从节点和其他哨兵等信息)。
  • 配置里只写了主节点,没有写从节点和其他哨兵,因为哨兵会自动发现 :通过主节点的info命令得到所有从节点,再通过主节点上的发布订阅频道互相发现其他哨兵。
  • 说明:用容器名作为<ip>是演示的写法,Redis 6.2+ 需要额外配置resolve-hostnames yes才支持主机名,生产环境建议直接写 IP。

启动哨兵:

bash 复制代码
docker-compose up -d

2.3 模拟主节点故障

  手动把主节点停掉:

bash 复制代码
docker stop redis-master

  主节点一挂,哨兵节点就开始工作了。在哨兵的目录下用docker-compose logs查看哨兵节点的工作情况:

日志里几个关键的事件,正好对应故障转移的过程:

日志事件 含义
+sdown 主观下线:这个哨兵认为主节点挂了
+odown 客观下线:达到法定票数,确认主节点挂了
+try-failover 开始尝试故障转移
+vote-for-leader/ +elected-leader 哨兵之间投票,选出 leader
+selected-slave leader 选中了某个从节点作为新主
+promoted-slave 该从节点已晋升为主节点
+switch-master 主节点切换完成,通知客户端新主节点的地址

2.4 旧主节点恢复

  再把原来的主节点启动起来,观察它的状态:

bash 复制代码
docker-compose start master
redis-cli -p 6379

  可以发现,旧的主节点恢复后变成了从节点,挂在新的主节点下面,没有"抢回"主节点的位置。这也是哨兵自动完成的,不需要人工干预。

三、故障转移的原理

  哨兵重新选择主节点,大致分三步:判定主节点挂了 → 选出 leader 哨兵 → leader 选出新的主节点。

3.1 主观下线与客观下线

  • 主观下线(SDOWN):一个 哨兵给节点发的心跳包,在down-after-milliseconds内没有收到有效回复,这个哨兵就单方面认为该节点挂了。
  • 客观下线(ODOWN):主观下线只是一个哨兵的看法,可能误判。它会询问其他哨兵,认为主节点挂了的哨兵数量达到了法定票数(quorum),才算客观下线,才能开始故障转移。

  这里的"挂"不一定是进程崩了,只要是无法正常访问,都可以视为是挂了(比如网络不通、进程卡死)。

3.2 选举 leader 哨兵

  确认主节点客观下线后,需要由一个哨兵来执行故障转移,不能多个哨兵各干各的,所以要先选出 leader。

  举个例子:1 号哨兵最先发现主节点客观下线,它就向其他哨兵发起投票请求,给自己投一票;另外两个哨兵也给 1 号哨兵投了一票,1 号哨兵得票过半,成为 leader。本质上是谁反应快、谁先发起,谁就更容易当选。(这里用的是 Raft 算法的简化版)

  • 细节:quorum只决定"能不能判定客观下线";真正执行故障转移,还需要这个 leader 得到==多数(超过哨兵总数一半)哨兵的授权==。这也是哨兵要部署奇数个的原因。

3.3 leader 如何挑选新的主节点

  leader 会去从节点中挑选一个作为新的主节点,先排除掉不健康的节点(已经下线、断线太久的),剩下的按下面三个条件依次比较:

  1. 优先级 :每个 Redis 数据节点在配置文件中都有一个优先级配置(replica-priority,默认 100),值越小优先级越高(设为 0 表示永远不会被选为主节点)。
  2. offset(复制偏移量) :优先级相同,就看同步进度,offset 越大说明数据越新,越容易胜出。
  3. run id :前两项都一样,就看 run id,字典序最小的胜出。run id 是每个 Redis 节点启动时随机生成的一串字符,这里只是为了保证最终能选出唯一的节点。

为什么要看 offset?

因为主从复制是异步的,从节点的数据可能落后于旧主节点。选 offset 最大的,意味着选出的新主丢失的数据最少。

3.4 新主确定之后

  • leader 让被选中的节点执行slaveof no one,晋升为主节点。
  • leader 让其他从节点执行slaveof 新主节点,改认新的主节点。(默认一次只让 1 个从节点去同步,避免新主压力过大,由parallel-syncs配置)
  • 哨兵把新主节点的信息通知给客户端(+switch-master)。
  • 旧主节点恢复后,被设置为新主节点的从节点。

四、总结与局限

  哨兵 = 主从复制 + 自动故障转移 ,解决了主节点挂了需要人工处理的问题,提高了系统的可用性。但它依然有局限:

  • 写能力和容量没有提升:写操作依然只能在一个主节点上,每个节点保存的还是全量数据,受单机内存限制。
  • 切换期间有短暂的不可用:从主节点挂了,到新主节点选出来,这段时间内写操作是失败的。
  • 可能丢数据:主从复制是异步的,旧主节点上还没来得及同步给从节点的数据,在切换后会丢失。

  要解决写能力和存储容量的问题,就需要把数据分片 存储到多组节点上,这就是下一篇要讲的 Redis 集群(Cluster)。

非常感谢您能耐心读完这篇文章。倘若您从中有所收获,还望多多支持呀!🎉

相关推荐
倔强的石头1061 小时前
【Linux指南】进程通信系列(二):匿名管道 —— pipe 的本质、fork 继承与四种读写规则
java·linux·运维
一条破秋裤1 小时前
09-读写多个sys属性文件
linux
2501_933923252 小时前
MySQL CRUD 详解:表数据插入、更新与删除,附生产避坑指南
数据库·mysql
小张成长计划..2 小时前
【Linux】25:线程概念和控制
java·linux·jvm
硅基手札2 小时前
【Linux 内核专栏 18】进程间通信 IPC(下)— Socket·Netlink·fd 三件套·futex·线程同步
linux·运维·驱动开发·单片机·嵌入式硬件·mcu·架构
djjjx.2 小时前
Linux进程篇(二):深入浅出 [进程概念与进程父子关系]:从底层原理到实战应用
linux·运维·服务器
爱喝水的鱼丶4 小时前
SAP-ABAP:S/4 HANA ABAP 新特性入门:与 ECC ABAP 的核心差异、架构优势与学习路径规划
运维·数据库·性能优化·sap·abap·hana·开发交流
贵州诚鑫致达科技有限公司10 小时前
Linux的yum源怎么重启?源不是服务
linux·运维·服务器
余槐i11 小时前
PostgreSQL 函数索引报错 must be immutable:3种绕法与代价
数据库·后端·sql·postgresql·性能优化