Redis (十五) 哨兵机制

在这里插入图片描述

前言

前面我们学习了关于 Redis 主从复制的问题,主从复制解决的是 Redis 服务器的单点问题,但是解决的主要也是读操作问题,如果 Redis 主节点挂掉了之后,那么整个 Redis 服务就只能进行读操作,而无法进行写操作了。当主节点挂掉之后,只能通过人工的方式,从从节点中选出一个节点作为主节点,然后修改其他从节点的主节点为当前节点才可以,但是既然是需要人为操作的,服务器的工作时间是 7 * 24 小时的,而人不可能全天都可以工作,所以当主节点挂掉之后,一定时间之内是无法完成修复的,也就意味着这一段时间之后是无法进行写操作的,这样造成的损失肯定是很大的。所以为了解决这个问题,就使用了哨兵机制。

什么是哨兵机制

哨兵机制是通过独立的进程来体现的,Redis 哨兵机制就是有一个独立的进程负责监视 Redis 进程,当 Redis 进程出现了异常之后,这个哨兵就会及时的发现问题并且解决这个问题。

在这里插入图片描述
往往哨兵节点的个数应该至少是 3 个,因为如果是一个哨兵节点的话,那么这个哨兵节点挂掉了之后的话,也就没有哨兵对 Redis 节点进行监视了,那么为什么不是 2 个而是三个呢?因为哨兵节点的个数应尽量是奇数个,这里是为了后面哨兵节点选出 leader 更方便,后面为大家详细介绍。

这些哨兵节点是独立于 redis-server 进程运行的进程,这些哨兵节点会对 redis 主节点和从节点进行监视。如果发现从节点挂掉之后,其实影响不大,但是哨兵还是会在日志中记录哪个从节点出现了故障,当我们程序员发现了之后可以对其进行维修。

而如果是主节点出现了故障挂掉之后,哨兵就会发挥他们的作用:

  1. 当一个哨兵节点发现主节点挂掉之后,此时是不够的,因为可能因为网络的问题出现误判的情况,所以这个哨兵就会寻求其他哨兵的意见,如果其他的哨兵也发现了主节点挂掉之后,才会真正的采取措施。
  2. 当证实了主节点挂掉之后,这些哨兵节点就会推举出来一个 leader,然后这个 leader 就会从现有的从节点中挑选出来一个节点作为新的主节点。
  3. 当选出来新的主节点之后,哨兵节点就会控制新的主节点执行 slaveof no one 操作,使之称为一个主节点,然后其他的从节点修改 slaveof 到这个新的主节点之上。
  4. 哨兵节点会自动的通知客户端程序,告诉我们新的主节点是谁,后续客户端再进行写操作的时候就会针对这个新的主节点进行操作了。

总结来说,哨兵的主要功能就是:

  1. 监视
  2. 自动故障转移
  3. 通知

如何实现哨兵机制

按理来说,既然是分布式系统就应该需要多台主机,但是因为我实力有限,只有一台云服务器,所以如何实现我就在一台云服务器上面模拟出来。

那么如何在一台主机上模拟出多台 redis 服务器和哨兵的情景呢?我们可以参照前面主从复制时候配置多台 redis 服务器的做法,但是这样比较麻烦,所以这里我选择的方法是使用 docker 的方式来实现。

什么是 Docker

这里只是简单的为大家说明一下什么是 docker:

Docker是一个开源的应用容器引擎,它允许开发者将应用及其依赖打包到一个可移植的容器中,并发布到任何流行的Linux或Windows操作系统的机器上。这个容器是完全使用沙箱机制,相互之间不会有任何接口。它的主要目的是让开发者能够更轻松地打包、发布和运行他们的应用程序。

docker 也可以认为是一个“轻量级”的虚拟机,但是它又不像虚拟机那样需要吃很多的硬件资源,所以就算在配置比较低的云服务上面也能够构造出几个这样的虚拟环境。

docker 中包含两个重要的概念:镜像和容器:

  • Docker镜像:Docker镜像是一个只读的模板,包含了运行某个应用所需的所有代码、库、环境变量和配置文件。你可以将Docker镜像看作是一个应用程序的“集装箱”,里面包含了运行该应用程序所需的所有“货物”。
  • Docker容器:Docker容器则是基于Docker镜像创建的运行实例。你可以将Docker容器看作是使用Docker镜像创建的“集装箱货车”,它包含了运行应用程序所需的所有环境和依赖。

docker 中的镜像和容器就类似于可执行程序和进程的关系。

要想使用 docker,首先需要先下载 docker:

root@iZ2ze5bzkbeuwwqowjzo27Z:~# apt list docker.io
Listing... Done
docker.io/jammy-updates,now 24.0.5-0ubuntu1~22.04.1 amd64 [installed]
N: There are 2 additional versions. Please use the '-a' switch to see them.
root@iZ2ze5bzkbeuwwqowjzo27Z:~# apt install docker.io

安装完成之后,输入 docker 显示下面内容说明安装 docker 成功:

在这里插入图片描述
安装完成 docker 之后还需要安装 docker-compose:

apt install docker-compose

当安装完成 docker 和 docker-compose 之后,我们需要先停止之前的 redis-server:

# 停止redis-server
service redis-server stop

# 停止redis-sentinel,如果之前有的话
service redis-sentinel stop

然后通过 docker 获取 redis 镜像:

docker pull redis:redis版本号

在这里插入图片描述
上面拉取到的镜像,里面包含一个精简的 Linux 系统,并且上面安装了 redis,只要基于这个镜像创建一个容器跑起来,此时 redis 服务器就搭建好了。

可以使用 docker images 查看当前存在的 docker 镜像:

root@iZ2ze5bzkbeuwwqowjzo27Z:~# docker images
REPOSITORY   TAG       IMAGE ID       CREATED         SIZE
redis        6.0.16    b0f8d8ac93c5   15 months ago   112MB

使用docker搭建redis哨兵环境

当安装完成 docker 并且 pull 到了 redis 容器之后,我们就可以通过 docker 来搭建 redis 哨兵环境了。

每一个 redis-server 和 redis-sentinel 都是一个作为一个单独的容器,可以一个一个启动它,但是更方便的是使用 docker-compose 来进行容器编排,这样后面就可以通过一个配置文件和一些简单的命令来实现批量的启动和停止操作了。

这里的配置文件和 redis 配置文件的格式不同,这里使用的配置文件的格式是 YML 的格式。YML 格式跟 python 类似,使用缩进来表示成绩关系:

student:
  name:zhangsan
  id:1
  age:18
  score:
    chinese: 97
    math:100

这里我们使用两个 YML 配置文件来批量启动/停止,为什么用两个呢?因为需要保证 redis 节点的启动需要在哨兵节点之前启动,否则哨兵节点先启动的话,redis 节点没启动,那么哨兵就会认为 redis 节点挂了,虽然对于我们的正常流程没有什么影响,但是会生成错误日志,会对我们日常错误排查产生误导。虽然在一个 YML 配置文件中可以做到容器的先后启动,但是操作比较复杂。所以为了保证 redis 节点在哨兵之前启动,就三个 redis 节点使用一个 YML 配置文件,三个哨兵使用一个 YML 配置文件。

先创建出 redis-data 目录作为 redis-server 的工作目录,redis-sentinel 作为哨兵的工作目录:

root@iZ2ze5bzkbeuwwqowjzo27Z:~# mkdir redis-data
root@iZ2ze5bzkbeuwwqowjzo27Z:~# mkdir redis-sentinel

然后在这两个工作目录中分别创建出 docker-compose.yml 文件,这个配置文件名是固定的,这也是前面为什么需要将 redis-server 和 哨兵 节点的工作目录分开。

然后向配置文件中添加内容,首先是 redis-data 配置文件中的内容:

version: '3.7'
services:
  master:
    image: 'redis:6.0.16'
    container_name: redis-master
    restart: always
    command: redis-server --appendonly yes
    ports:
      - 6379:6379
  slave1:
    image: 'redis:6.0.16'
    container_name: redis-slave1
    restart: always
    command: redis-server --appendonly yes --slaveof redis-master 6379
    ports:
      - 6380:6379
  slave2:
    image: 'redis:6.0.16'
    container_name: redis-slave2
    restart: always
    command: redis-server --appendonly yes --slaveof redis-master 6379
    ports:
      - 6381:6379
  • version:版本号,这里我们默认使用3.7
  • services:表示该配置文件需要启动的服务的名字,这里服务的名字是自定义的
  • image:表示该容器是基于哪个镜像创建的
  • container_name:容器的名称
  • restart:表示如果当前容器意味异常情况挂掉了之后是否重启,always表示总是(手动停止容器不会自动重启)
  • command:表示当前 redis 服务器启动的时候需要哪些选项
  • ports:端口映射,因为docker容器就相当于一个轻量的虚拟机,既然是两台不同的主机,那么端口号之间是互不影响的,但是如果想要在容器外访问到容器内的端口号,那么就可以将容器内的端口号映射到宿主机的的端口

在从节点的 command 当中的 slaveof 选项中,本来是需要写主节点的 IP 和端口号,但是这里写的是主节点容器的名称,这是因为当容器启动的时候会自动分配一个 IP,然后具体的 IP 是啥我们也不能事先知道,所以就只能通过指定容器的名称,然后容器启动的时候就会进行类似的域名解析操作,将容器名改为对应的 IP。

配置完成 redis-data 的配置文件之后,我们可以使用 docker-compose up [-d]来启动容器,-d 选项表示后台启动:

root@iZ2ze5bzkbeuwwqowjzo27Z:~/redis-data# docker-compose up -d
Starting redis-slave1 ... done
Starting redis-master ... done
Starting redis-slave2 ... done

在使用 docker-compose up 命令的时候可能会出现这样的问题:

在这里插入图片描述
这是因为我们的 docker 版本和 urllib 的版本不兼容,如果出现这样的问题,可以通过更新 docker 的版本或者使用 pip install 'urllib3<2' 来降低 urllib 的版本。

通过 docker ps -a 查看启动的容器:

root@iZ2ze5bzkbeuwwqowjzo27Z:~/redis-sentinel# docker ps -a
CONTAINER ID   IMAGE          COMMAND                  CREATED          STATUS         PORTS                                       NAMES
062dbc634c89   redis:6.0.16   "docker-entrypoint.s…"   12 minutes ago   Up 9 minutes   0.0.0.0:6379->6379/tcp, :::6379->6379/tcp   redis-master
e18733744700   redis:6.0.16   "docker-entrypoint.s…"   12 minutes ago   Up 9 minutes   0.0.0.0:6381->6379/tcp, :::6381->6379/tcp   redis-slave2
cf5a98de566c   redis:6.0.16   "docker-entrypoint.s…"   12 minutes ago   Up 9 minutes   0.0.0.0:6380->6379/tcp, :::6380->6379/tcp   redis-slave1

这里测试一下主从关系是否构建成功:

在这里插入图片描述

在这里插入图片描述

因为我这里 6379 端口被占用了,所以我就将主节点的宿主机的映射端口改为了 6382。可以看到这里的主从关系是成功形成了。

也可以通过 docker-compose logs 来查看日志:

在这里插入图片描述

然后是 redis-sentinel 哨兵节点的 YML 配置文件内容:

version: '3.7'
services:
  sentinel1:
    image: 'redis:6.0.16'
    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:6.0.16'
    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:6.0.16'
    container_name: redis-sentinel-3
    restart: always
    command: redis-sentinel /etc/redis/sentinel.conf
    volumes:
      - ./sentinel3.conf:/etc/redis/sentinel.conf
    ports:
      - 26381:26379
  • volumes:是配置文件的映射,因为哨兵在运行过程中会对配置文件进行修改,所以每个哨兵节点都需要独立的配置文件。

这里哨兵节点的 YML 配置文件的配置可能会因为 docker 版本或者是 redis 的版本不一样而出现问题,我这里使用的 docker 版本是 26.1.1,redis 版本是 6.0.16,然后通过上面的 yml 文件配置的话就会出现下面的错误日志:

+sentinel sentinel a0ed657c71a3b2e03e98bc839a07754a99bcd48e 172.20.0.7 26379 @ redis-master 172.20.0.4 6379
redis-sentinel-3 | 1:X 06 May 2024 07:45:42.408 # Could not rename tmp config file (Device or resource busy)
redis-sentinel-3 | 1:X 06 May 2024 07:45:42.408 # WARNING: Sentinel was not able to save the new configuration on disk!!!: Device or resource busy

然后我将上面的报错信息进行搜索之后,我找到了这篇文章 https://www.whosebug.com/question/6334134/WARNING-Sentinel-was-not-able-to-save-the-new-configuration-on-disk-Device-or-resource-busy 发现跟我的问题描述的基本一样,但是因为没有学习过 docker,所以并不能完全理解解决方法,所以就没能够及时解决这个问题,后来向一位大佬寻求了帮助之后才知道,在使用 volumes 选项挂载的时候,不能只挂载某个文件,而是需要挂载该文件所在的整个目文件夹。

卷管理:Docker 卷(volumes)提供了一种机制,用于在容器和宿主机之间共享文件系统。当挂载整个文件夹时,Docker 可以更有效地管理这些卷,包括在容器之间共享数据、持久化数据以及跨多个容器和宿主机迁移数据。

所以大家一定要注意,不然又像我一样花了两天还没解决。所以当出现这样的问题的时候,我就对一些目录结构以及 yml 配置文件做出了修改:

这是我的 redis-sentinel 目录的目录结构:

root@iZ2ze5bzkbeuwwqowjzo27Z:~/redis-sentinel# pwd
/root/redis-sentinel
root@iZ2ze5bzkbeuwwqowjzo27Z:~/redis-sentinel# tree
.
├── conf
│   ├── redis-sentinel1
│   │   └── redis.conf
│   ├── redis-sentinel2
│   │   └── redis.conf
│   └── redis-sentinel3
│       └── redis.conf
└── docker-compose.yml

conf 文件夹下是每个哨兵节点的工作目录,工作目录中有一个 conf 文件,conf 文件中的内容就是:

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:6379 表示该哨兵使用的端口号
  • sentinel monitor redis-master redis-master 6379 2:表示当前哨兵监视的redis服务器的容器名称和容器的IP地址和端口号,2表示票数,当有一个哨兵表示监视的主节点挂了,票数就+1,如果票数达到了这个2,就可以肯定当前主节点挂了
  • sentinel down-after-milliseconds redis-master 1000:表示当前哨兵监视的redis节点的容器名称以及该哨兵在向redis节点发送心跳包 n ms之内如果没有响应,那么该哨兵就认为它监视的这个redis节点挂了

初始情况下的三个配置文件中的内容可以是相同的。

yml 配置文件修改之后的内容:

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

volumes 挂载的时候挂载的是每个哨兵节点配置文件所在的整个文件夹,对应的容器中的文件地址也是一个目录地址,修改了 volumes 之后,command 命令参数也是需要修改的,因为哨兵节点启动需要依靠配置文件中的内容,所以这个选项的值就是一个文件,只不过要改为配置文件的地址。

当 YML 配置文件和哨兵节点的配置文件配置完成之后,就可以启动这些哨兵容器了:

root@iZ2ze5bzkbeuwwqowjzo27Z:~/redis-sentinel# docker-compose up -d
Creating network "redis-sentinel_default" with the default driver
Creating redis-sentinel-1 ... done
Creating redis-sentinel-2 ... done
Creating redis-sentinel-3 ... done

但是这里真的启动成功了吗?我们打开日志看看:

redis-sentinel-3 | *** FATAL CONFIG FILE ERROR (Redis 6.0.16) ***
redis-sentinel-3 | Reading the configuration file, at line 3
redis-sentinel-3 | >>> 'sentinel monitor redis-master redis-master 6379 2'
redis-sentinel-3 | Can't resolve master instance hostname.

可以看到打开日志后,会看到这样的问题,不能够解析出监视的 master 的主机名,那么这是为什么呢?

这是因为当我们使用 docker-compose up 的时候,一下启动了 N 个容器,这 N 个容器都处于一个局域网中,而我们前面使用了两次 docker-compose up 命令,也就是说,三个 redis 节点处于一个局域网中,而三个哨兵节点处于一个局域网中,默认情况下这两个局域网是不能够互通的,所以哨兵节点在启动的时候也就无法解析出 redis 节点的 IP 地址。

可以使用 docker network ls 来列出当前 docker 中的局域网:

root@iZ2ze5bzkbeuwwqowjzo27Z:~/redis-sentinel# docker network ls
NETWORK ID     NAME                     DRIVER    SCOPE
7c02f5c7d968   bridge                   bridge    local
a09082e1efba   host                     host      local
0ff981fc2d86   none                     null      local
728c0cb65933   redis-data_default       bridge    local
e0bd5f3ebaa8   redis-sentinel_default   bridge    local

这里局域网的名称就是每个 docker-compose.yml 文件所在的目录的名称+_default。观察发现,redis-data 和 redis-sentinel 所在的局域网的 ID 是不同的,也就是说 redis 节点和 哨兵节点不在同一个局域网中。

那么如何解决呢?我们可以使用 docker-compose 将两组服务给放到同一个局域网中,如何做呢?需要在 YML 配置文件中添加下面的选项:

这里因为 redis-data 中的容器是先启动的,所以就会形成一个局域网,那么我们就需要在 redis-sentinel 目录中的 YML 配置文件中指定要加入的局域网的名称就可以了:

networks:
  default:
    external:
      name: redis-data_default

添加完上面的配置之后,我们关掉之前启动失败的 docker 容器:

docker-compose down

然后重新启动:

root@iZ2ze5bzkbeuwwqowjzo27Z:~/redis-sentinel# docker-compose up -d
Creating redis-sentinel-1 ... done
Creating redis-sentinel-2 ... done
Creating redis-sentinel-3 ... done

查看当前 docker 局域网:

root@iZ2ze5bzkbeuwwqowjzo27Z:~/redis-sentinel# docker network ls
NETWORK ID     NAME                 DRIVER    SCOPE
7c02f5c7d968   bridge               bridge    local
a09082e1efba   host                 host      local
0ff981fc2d86   none                 null      local
728c0cb65933   redis-data_default   bridge    local

可以发现只存在一个局域网,也就是说后面启动的哨兵节点的容器加入了redis-data 所在的局域网。

再查看日志看是否有错误日志:

在这里插入图片描述

没发现错误日志,然后我们在查看哨兵节点的配置文件中的内容:

在这里插入图片描述
可以发现哨兵节点启动之后,里面的配置文件就自行的修改了。

所以通过上面的操作,我们就在一个主机上使用 docker 模拟出了一个主节点两个从节点以及三个哨兵节点的主从模式的情况。

那么接下来我们手动挂掉主节点,看看哨兵会执行什么操作:

在这里插入图片描述
在这里插入图片描述
查看哨兵节点的日志:

在这里插入图片描述

这一部分日志信息是哨兵节点启动时候产生的日志。下面这个就是挂掉监视的主节点之后产生的日志:

在这里插入图片描述

哨兵节点选出新的主节点的过程

当我们挂掉主redis节点之后,就会有哨兵节点发现主redis节点挂掉了,所以就生成了一个 sdown 日志,sdown 是 subject down 主管下线的意思,也就是此时并不能完全认为主redis节点挂掉了,所以这时哨兵节点之间就会进行通信,进行投票 quorum 2/2 用来记录此时投票情况,当票数达到我们前面配置的法定票数了之后,就会生成 odown(object down)客观下线的日志。当确定了主redis节点挂掉之后,哨兵节点就会进行故障转移的操作,也就是 try-failover master redis-master 172.20.0.4 6379 这条日志,这里记录了需要故障转移的主节点的名字、IP和端口号,那么哨兵节点如何进行故障转移操作呢?首先这些哨兵节点之间会进行选 leader 的操作,每个哨兵节点都会进行投票,哪个节点的票数最多,那么这个节点就是 leader,这是投票过程:

在这里插入图片描述
这里也就说明了前面为什么配置哨兵节点的时候,为啥哨兵节点的个数要为奇数,因为为奇数的情况才更容易分出 leader,而哨兵节点为偶数的话,就容易出现相同票数的情况,就不容易分出 leader。

当选出 leader 哨兵节点之后,就会由这个 leader 在剩下的从节点中选出一个新的节点作为新的主节点:

在这里插入图片描述
选出新的主节点之后,哨兵节点会负责在这个新的主节点中执行 slaveof no one 使之称为一个主节点,然后在负责将这个挂掉的主节点和剩下的从点的所属主节点改为这个新的主节点。

在剩下的从节点是如何挑选出节点作为新的主节点的呢?

  1. 优先级:在每个 redis 节点的配置文件中,都会由一个优先级设置,也就是 slave-priority,优先级越高的节点,就会称为新的主节点
  2. offset:如果优先级相同的话,就会看 offset,offset 越大的就会称为新的主节点,offset 表示的是同步的主节点的进度,同步的数据越多,也就更应该成为主节点、
  3. run id:当 redis 节点的优先级和 offset 都相同的话,那么这几个从节点哪个作为新的主节点其实就没区别了,而 run id 是每个节点启动的时候随机生成的,所以根据 run id 来决定也就相当于随机选取

总结

  1. 哨兵节点不能之后一个,否则哨兵节点挂了之后,也会影响系统的可用性
  2. 哨兵节点的个数最好为奇数个,为了方便后面选出 leader,得票更容易超过半数
  3. 哨兵节点不负责存储数据,存储数据还是在 redis 节点上进行
  4. 哨兵 + 主从复制的问题是“提高可用性”,不能解决“极端情况下数据丢失”的问题
  5. 哨兵 + 主从复制不能提高数据的存储容量,当我们需要存储的数据的容量接近或者超过物理内存的时候,这样的结构也是无法胜任的

而为了解决存储数据太多超过物理内存的情况就需要使用到集群。

相关推荐

  1. 哨兵机制Redis Sentinel)常见面试题

    2024-06-08 13:34:02       8 阅读

最近更新

  1. TCP协议是安全的吗?

    2024-06-08 13:34:02       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-06-08 13:34:02       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-06-08 13:34:02       18 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-06-08 13:34:02       20 阅读

热门阅读

  1. 从入门到精通:基础IO

    2024-06-08 13:34:02       8 阅读
  2. 使用Docker运行不同版本的Node.js

    2024-06-08 13:34:02       11 阅读
  3. 40.任务调度线程池

    2024-06-08 13:34:02       9 阅读
  4. mybatis使用笔记

    2024-06-08 13:34:02       10 阅读
  5. Python怎么加载包:深入解析Python包加载机制

    2024-06-08 13:34:02       10 阅读
  6. Python基础语法(五):循环语句

    2024-06-08 13:34:02       11 阅读
  7. GPT-4o:OpenAI的最新篇章与深度探索

    2024-06-08 13:34:02       9 阅读
  8. 离线html文件及资源文件夹转换为单个mhtml文件

    2024-06-08 13:34:02       9 阅读
  9. 36、Flink 的 WindowAssigner之滑动窗口示例

    2024-06-08 13:34:02       10 阅读
  10. 59、约数个数

    2024-06-08 13:34:02       7 阅读