redis cluster 集群 HA 原理和实操(史上最全、面试必备)

文章很长,建议收藏起来慢慢读!疯狂创客圈总目录 语雀版 | 总目录 码云版| 总目录 博客园版 为您奉上珍贵的学习资源 :

  • 免费赠送 经典图书:《Java高并发核心编程(卷1)》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领

  • 免费赠送 经典图书:《Java高并发核心编程(卷2)》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领

  • 免费赠送 经典图书:《Netty Zookeeper Redis 高并发实战》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领

  • 免费赠送 经典图书:《SpringCloud Nginx高并发核心编程》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领

  • 免费赠送 资源宝库: Java 必备 百度网盘资源大合集 价值>10000元 加尼恩领取


SpringCloud 微服务 精彩博文
nacos 实战(史上最全) sentinel (史上最全+入门教程)
SpringCloud gateway (史上最全) 分库分表sharding-jdbc底层原理与实操(史上最全,5W字长文,吐血推荐)

说明

redis cluster是 生存环境常用的组件,很多小伙伴没有玩过,很可惜

本文从原理到实操,都给大家做了一个介绍,后面会 持续完善

实际上,尼恩给大家准备了一键搭建一套redis cluster集群的 docker-compose 编排文件,可以在虚拟机上 体验一下,比单机 安装还简单,
如果有 docker-compose 编排文件 需要或者 需要技术交流,可以来疯狂创客圈 jAVA高并发 社群,一起研究硬核技术

Redis集群高可用常见的三种方式:

Redis高可用常见的有两种方式:

  • Replication-Sentinel模式
  • Redis-Cluster模式
  • 中心化代理模式(proxy模式)

Replication-Sentinel模式

Redis sentinel 是一个分布式系统中监控 redis 主从服务器,并在主服务器下线时自动进行故障转移。

img

Redis sentinel 其中三个特性:

  • 监控(Monitoring):

Sentinel 会不断地检查你的主服务器和从服务器是否运作正常。

  • 提醒(Notification):

当被监控的某个 Redis 服务器出现问题时, Sentinel 可以通过 API 向管理员或者其他应用程序发送通知。

  • 自动故障迁移(Automatic failover):

当一个主服务器不能正常工作时, Sentinel 会开始一次自动故障迁移操作。

哨兵本身也有单点故障的问题,可以使用多个哨兵进行监控,哨兵不仅会监控redis集群,哨兵之间也会相互监控。

每一个哨兵都是一个独立的进程,作为进程,它会独立运行。

img

特点:

  • 1、保证高可用

  • 2、监控各个节点

  • 3、自动故障迁移

缺点:

主从模式,切换需要时间丢数据

没有解决 master 写的压力

Redis-Cluster模式

redis在3.0上加入了 Cluster 集群模式,实现了 Redis 的分布式存储,也就是说每台 Redis 节点上存储不同的数据。

cluster模式为了解决单机Redis容量有限的问题,将数据按一定的规则分配到多台机器,内存/QPS不受限于单机,可受益于分布式集群高扩展性。

RedisCluster 是 Redis 的亲儿子,它是 Redis 作者自己提供的 Redis 集群化方案。

相对于 Codis 的不同,它是去中心化的,如图所示,该集群有三个 Redis 节点组成, 每个节点负责整个集群的一部分数据,每个节点负责的数据多少可能不一样。这三个节点相 互连接组成一个对等的集群,它们之间通过一种特殊的二进制协议相互交互集群信息。

img

如上图,官方推荐,集群部署至少要 3 台以上的master节点,最好使用 3 主 3 从六个节点的模式。

Redis Cluster 将所有数据划分为 16384 的 slots,它比 Codis 的 1024 个槽划分得更为精细,每个节点负责其中一部分槽位。槽位的信息存储于每个节点中,它不像 Codis,它不 需要另外的分布式存储来存储节点槽位信息。

Redis Cluster是一种服务器Sharding技术(分片和路由都是在服务端实现),采用多主多从,每一个分区都是由一个Redis主机和多个从机组成,片区和片区之间是相互平行的。

Redis Cluster集群采用了P2P的模式,完全去中心化。

3 主 3 从六个节点的Redis集群(Redis-Cluster)

Redis 集群是一个提供在多个Redis节点间共享数据的程序集。

下图以三个master节点和三个slave节点作为示例。

redis cluster 集群 HA 原理和实操(史上最全、面试必备)_第1张图片

Redis 集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽。

集群的每个节点负责一部分hash槽,如图中slots所示。

为了使在部分节点失败或者大部分节点无法通信的情况下集群仍然可用,所以集群使用了主从复制模型,每个节点都会有1-n个从节点。

例如master-A节点不可用了,集群便会选举slave-A节点作为新的主节点继续服务。

中心化代理模式(proxy模式)

这种方案,将分片工作交给专门的代理程序来做。代

理程序接收到来自业务程序的数据请求,根据路由规则,将这些请求分发给正确的 Redis 实例并返回给业务程序。

其基本原理是:通过中间件的形式,Redis客户端把请求发送到代理 proxy,代理 proxy 根据路由规则发送到正确的Redis实例,最后 代理 proxy 把结果汇集返回给客户端。

redis代理分片用得最多的就是Twemproxy,由Twitter开源的Redis代理,其基本原理是:通过中间件的形式,Redis客户端把请求发送到Twemproxy,Twemproxy根据路由规则发送到正确的Redis实例,最后Twemproxy把结果汇集返回给客户端。

redis cluster 集群 HA 原理和实操(史上最全、面试必备)_第2张图片

这种机制下,一般会选用第三方代理程序(而不是自己研发),因为后端有多个 Redis 实例,所以这类程序又称为分布式中间件。

这样的好处是,业务程序不用关心后端 Redis 实例,运维起来也方便。虽然会因此带来些性能损耗,但对于 Redis 这种内存读写型应用,相对而言是能容忍的。

Twemproxy 代理分片

Twemproxy 是一个 Twitter 开源的一个 redis 和 memcache 快速/轻量级代理服务器; Twemproxy 是一个快速的单线程代理程序,支持 Memcached ASCII 协议和 redis 协议。

Twemproxy是由Twitter开源的集群化方案,它既可以做Redis Proxy,还可以做Memcached Proxy。

它的功能比较单一,只实现了请求路由转发,没有像Codis那么全面有在线扩容的功能,它解决的重点就是把客户端分片的逻辑统一放到了Proxy层而已,其他功能没有做任何处理。

img

Tweproxy推出的时间最久,在早期没有好的服务端分片集群方案时,应用范围很广,而且性能也极其稳定。

但它的痛点就是无法在线扩容、缩容,这就导致运维非常不方便,而且也没有友好的运维UI可以使用。

Codis代理分片

Codis 是一个分布式 Redis 解决方案, 对于上层的应用来说, 连接到 Codis Proxy 和连接原生的 Redis Server 没有明显的区别 (有一些命令不支持), 上层应用可以像使用单机的 Redis 一样使用, Codis 底层会处理请求的转发, 不停机的数据迁移等工作, 所有后边的一切事情, 对于前面的客户端来说是透明的, 可以简单的认为后边连接的是一个内存无限大的 Redis 服务,

现在美团、阿里等大厂已经开始用codis的集群功能了,

什么是Codis?

Twemproxy不能平滑增加Redis实例的问题带来了很大的不便,于是豌豆荚自主研发了Codis,一个支持平滑增加Redis实例的Redis代理软件,其基于Go和C语言开发,并于2014年11月在GitHub上开源 codis开源地址 。

Codis的架构图:

img

在Codis的架构图中,Codis引入了Redis Server Group,其通过指定一个主CodisRedis和一个或多个从CodisRedis,实现了Redis集群的高可用。

当一个主CodisRedis挂掉时,Codis不会自动把一个从CodisRedis提升为主CodisRedis,这涉及数据的一致性问题(Redis本身的数据同步是采用主从异步复制,当数据在主CodisRedis写入成功时,从CodisRedis是否已读入这个数据是没法保证的),需要管理员在管理界面上手动把从CodisRedis提升为主CodisRedis。

如果手动处理觉得麻烦,豌豆荚也提供了一个工具Codis-ha,这个工具会在检测到主CodisRedis挂掉的时候将其下线并提升一个从CodisRedis为主CodisRedis。

Codis的预分片

Codis中采用预分片的形式,启动的时候就创建了1024个slot,1个slot相当于1个箱子,每个箱子有固定的编号,范围是1~1024。

Codis的分片算法

Codis proxy 代理通过一种算法把要操作的key经过计算后分配到各个组中,这个过程叫做分片。
在这里插入图片描述

在Codis里面,它把所有的key分为1024个槽,每一个槽位都对应了一个分组,具体槽位的分配,可以进行自定义,现在如果有一个key进来,首先要根据CRC32算法,针对key算出32位的哈希值,然后除以1024取余,然后就能算出这个KEY属于哪个槽,然后根据槽与分组的映射关系,就能去对应的分组当中处理数据了。

在这里插入图片描述

CRC全称是循环冗余校验,主要在数据存储和通信领域保证数据正确性的校验手段,CRC校验(循环冗余校验)是数据通讯中最常采用的校验方式。

slot这个箱子用作存放Key,至于Key存放到哪个箱子,可以通过算法“crc32(key)%1024”获得一个数字,这个数字的范围一定是1~1024之间,Key就放到这个数字对应的slot。

例如,如果某个Key通过算法“crc32(key)%1024”得到的数字是5,就放到编码为5的slot(箱子)。

slot和Server Group的关系

1个slot只能放1个Redis Server Group,不能把1个slot放到多个Redis Server Group中。1个Redis Server Group最少可以存放1个slot,最大可以存放1024个slot。

因此,Codis中最多可以指定1024个Redis Server Group。

槽位和分组的映射关系就保存在codis proxy当中

redis主从复制

主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。

what is ?

主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。

  • 前者称为主节点(master),后者称为从节点(slave);

  • 数据的复制是单向的,只能由主节点到从节点。

  • 默认情况下,每台Redis服务器都是主节点;

  • 且一个主节点可以有多个从节点(或没有从节点),但一个从节点只能有一个主节点。

主从复制的作用

主从复制的作用主要包括:

  1. 数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
  2. 故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。
  3. 负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。
  4. 高可用、高并发基石:主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。

开启主从复制的方式

需要注意,主从复制的开启,完全是在从节点发起的;不需要我们在主节点做任何事情。

从节点开启主从复制,有3种方式:

(1)配置文件

在从服务器的配置文件中加入:slaveof

(2)启动命令

redis-server启动命令后加入 --slaveof

(3)客户端命令

Redis服务器启动后,直接通过客户端执行命令:slaveof ,则该Redis实例成为从节点。

上述3种方式是等效的,下面以客户端命令的方式为例,看一下当执行了slaveof后,Redis主节点和从节点的变化。

主从复制实例

准备工作:启动两个节点

实验所使用的主从节点是在一台机器上的不同Redis实例,其中:

  • 主节点监听6379端口,
  • 从节点监听6380端口;
  • 从节点监听的端口号可以在配置文件中修改:

img

启动后可以看到:

img

两个Redis节点启动后(分别称为6379节点和6380节点),默认都是主节点。

建立复制关系

此时在6380节点执行slaveof命令,使之变为从节点:

img

观察效果

下面验证一下,在主从复制建立后,主节点的数据会复制到从节点中。

(1)首先在从节点查询一个不存在的key:

img

(2)然后在主节点中增加这个key:

img

(3)此时在从节点中再次查询这个key,会发现主节点的操作已经同步至从节点:

img

(4)然后在主节点删除这个key:

img

(5)此时在从节点中再次查询这个key,会发现主节点的操作已经同步至从节点:

img

断开复制

通过slaveof 命令建立主从复制关系以后,可以通过slaveof no one断开。需要注意的是,从节点断开复制后,不会删除已有的数据,只是不再接受主节点新的数据变化。

从节点执行slaveof no one后,打印日志如下所示;

img

可以看出断开复制后,从节点又变回为主节点。

断开复制后,主节点打印日志如下:

img

主从复制的核心原理

1 当启动一个 slave node 的时候,它会发送一个 PSYNC 命令给 master node。

2 如果这是 slave node 初次连接到 master node,那么会触发一次 full resynchronization 全量复制。

master node 怎么进行 full resynchronization 全量复制?

此时 master 会启动一个后台线程,开始生成一份 RDB 快照文件,同时还会将从客户端 client 新收到的所有写命令缓存在内存中。

RDB 文件生成完毕后, master 会将这个 RDB 发送给 slave,

slave node 接收到RDB ,干啥呢?

会先写入本地磁盘,然后再从本地磁盘加载到内存中,

3 数据同步阶段完成后,主从节点进入命令传播阶段;在这个阶段master 将自己执行的写命令发送给从节点,从节点接收命令并执行,从而保证主从节点数据的一致性。

4 部分复制。如果slave node跟 master node 有网络故障,断开了连接,会自动重连,连接之后 master node 仅会复制给 slave 部分缺少的数据。

imgredis cluster 集群 HA 原理和实操(史上最全、面试必备)_第3张图片

主从复制的核心流程

主从复制过程大体可以分为3个阶段:

  • 连接建立阶段(即准备阶段)
  • 数据同步阶段
  • 命令传播阶段;

下面分别进行介绍。

连接建立阶段

该阶段的主要作用是在主从节点之间建立连接,为数据同步做好准备。

步骤1:保存主节点信息

从节点服务器内部维护了两个字段,即masterhost和masterport字段,用于存储主节点的ip和port信息。

需要注意的是,slaveof是异步命令,从节点完成主节点ip和port的保存后,向发送slaveof命令的客户端直接返回OK,实际的复制操作在这之后才开始进行。

这个过程中,可以看到从节点打印日志如下:

img

步骤2:建立socket连接

slave 从节点每秒1次调用复制定时函数replicationCron(),如果发现了有主节点可以连接,便会根据主节点的ip和port,创建socket连接。

如果连接成功,则:

  • 从节点:

为该socket建立一个专门处理复制工作的文件事件处理器,负责后续的复制工作,如接收RDB文件、接收命令传播等。

  • 主节点:

接收到从节点的socket连接后(即accept之后),为该socket创建相应的客户端状态,并将从节点看做是连接到主节点的一个客户端,后面的步骤会以从节点向主节点发送命令请求的形式来进行。

这个过程中,从节点打印日志如下:

img

步骤3:发送ping命令

从节点成为主节点的客户端之后,发送ping命令进行首次请求,目的是:检查socket连接是否可用,以及主节点当前是否能够处理请求。

从节点发送ping命令后,可能出现3种情况:

(1)返回pong:说明socket连接正常,且主节点当前可以处理请求,复制过程继续。

(2)超时:一定时间后从节点仍未收到主节点的回复,说明socket连接不可用,则从节点断开socket连接,并重连。

(3)返回pong以外的结果:如果主节点返回其他结果,如正在处理超时运行的脚本,说明主节点当前无法处理命令,则从节点断开socket连接,并重连。

在主节点返回pong情况下,从节点打印日志如下:

img

步骤4:身份验证

如果从节点中设置了masterauth选项,则从节点需要向主节点进行身份验证;没有设置该选项,则不需要验证。

从节点进行身份验证是通过向主节点发送auth命令进行的,auth命令的参数即为配置文件中的master auth的值。

  • 则身份验证通过,复制过程继续;

  • 如果不一致,则从节点断开socket连接,并重连。

步骤5:发送从节点端口信息

身份验证之后,从节点会向主节点发送其监听的端口号(前述例子中为6380),主节点将该信息保存到该从节点对应的客户端的slave_listening_port字段中;

该端口信息除了在主节点中执行info Replication时显示以外,没有其他作用。

数据同步阶段

主从节点之间的连接建立以后,便可以开始进行数据同步,该阶段可以理解为从节点数据的初始化。

具体执行的方式是:从节点向主节点发送psync命令(Redis2.8以前是sync命令),开始同步。

数据同步阶段是主从复制最核心的阶段,根据主从节点当前状态的不同,可以分为全量复制和部分复制。

在Redis2.8以前,从节点向主节点发送sync命令请求同步数据,此时的同步方式是全量复制;

在Redis2.8及以后,从节点可以发送psync命令请求同步数据,此时根据主从节点当前状态的不同,同步方式可能是全量复制或部分复制。后文介绍以Redis2.8及以后版本为例。

  1. 全量复制:用于初次复制或其他无法进行部分复制的情况,将主节点中的所有数据都发送给从节点,是一个非常重型的操作。
  2. 部分复制:用于网络中断等情况后的复制,只将中断期间主节点执行的写命令发送给从节点,与全量复制相比更加高效。需要注意的是,如果网络中断时间过长,导致主节点没有能够完整地保存中断期间执行的写命令,则无法进行部分复制,仍使用全量复制。

全量复制的过程

Redis通过psync命令进行全量复制的过程如下:

(1)从节点判断无法进行部分复制,向主节点发送全量复制的请求;或从节点发送部分复制的请求,但主节点判断无法进行部分复制;

(2)主节点收到全量复制的命令后,执行bgsave,在后台生成RDB文件,并使用一个缓冲区(称为复制缓冲区)记录从现在开始执行的所有写命令

(3)主节点的bgsave执行完成后,将RDB文件发送给从节点;从节点接收完成之后,首先清除自己的旧数据,然后载入接收的RDB文件,将数据库状态更新至主节点执行bgsave时的数据库状态

(4)主节点将前述复制缓冲区中的所有写命令发送给从节点,从节点执行这些写命令,将数据库状态更新至主节点的最新状态

(5)如果从节点开启了AOF,则会触发bgrewriteaof的执行,从而保证AOF文件更新至主节点的最新状态

下面是执行全量复制时,主从节点打印的日志;可以看出日志内容与上述步骤是完全对应的。

主节点的打印日志如下:

img

从节点打印日志如下图所示:

redis cluster 集群 HA 原理和实操(史上最全、面试必备)_第4张图片

其中,有几点需要注意:

  • 从节点接收了来自主节点的89260个字节的数据;

  • 从节点在载入主节点的数据之前要先将老数据清除;

  • 从节点在同步完数据后,调用了bgrewriteaof。

通过全量复制的过程可以看出,全量复制是非常重型的操作:

(1)性能损耗:主节点通过bgsave命令fork子进程进行RDB持久化,该过程是非常消耗CPU、内存(页表复制)、硬盘IO的;

(2)带宽占用:主节点通过网络将RDB文件发送给从节点,对主从节点的带宽都会带来很大的消耗

(3)停服载入:从节点清空老数据、载入新RDB文件的过程是阻塞的,无法响应客户端的命令;如果从节点执行bgrewriteaof,也会带来额外的消耗

题外话:什么是Redis Bgrewriteaof ?

Redis Bgrewriteaof 命令用于异步执行一个 AOF(AppendOnly File) 文件重写操作。

Bgrewriteaof 重写会创建一个当前 AOF 文件的体积优化版本。

即使 Bgrewriteaof 执行失败,也不会有任何数据丢失,因为旧的 AOF 文件在 Bgrewriteaof 成功之前不会被修改。

**注意:**从 Redis 2.4 开始, AOF 重写由 Redis 自行触发, BGREWRITEAOF 仅仅用于手动触发重写操作。

redis Bgrewriteaof 命令基本语法如下:

redis 127.0.0.1:6379> BGREWRITEAOF 

redis2.8 版本之前主从复制流程

redis2.8 版本之前主从复制流程:

img

  • 从服务器连接主服务器,发送SYNC命令;
  • 主服务器接收到SYNC命名后,开始执行BGSAVE命令生成RDB文件并使用缓冲区记录此后执行的所有写命令;
  • 主服务器BGSAVE执行完后,向所有从服务器发送快照文件,并在发送期间继续记录被执行的写命令;
  • 从服务器收到快照文件后丢弃所有旧数据,载入收到的快照;
  • 主服务器快照发送完毕后开始向从服务器发送缓冲区中的写命令;
  • 从服务器完成对快照的载入,开始接收命令请求,并执行来自主服务器缓冲区的写命令;

全量复制的弊端:

场景:(1)新创建的slave,从主机master同步数据。(2)刚宕机一小会的slave,从主机master同步数据。

前者新建的slave则从主机master全量同步数据,这没啥问题。但是后者slave可能只与主机master存在小量的数据差异,要是全量同步肯定没有只同步差异(部分复制)的那点数据性能高

部分复制

由于全量复制在主节点数据量较大时效率太低,因此Redis2.8开始提供部分复制,用于处理网络中断时的数据同步。

部分复制的实现,依赖于三个重要的概念:

(1)offset复制偏移量

  • 主节点和从节点分别维护一个复制偏移量(offset),代表的是主节点向从节点传递的字节数

  • 主节点每次向从节点传播N个字节数据时,主节点的offset增加N;

  • 从节点每次收到主节点传来的N个字节数据时,从节点的offset增加N。

offset复制偏移量的用途

offset用于判断主从节点的数据库状态是否一致:如果二者offset相同,则一致;如果offset不同,则不一致,此时可以根据两个offset找出从节点缺少的那部分数据。

例如,如果主节点的offset是1000,而从节点的offset是500,那么部分复制就需要将offset为501-1000的数据传递给从节点。

而offset为501-1000的数据存储的位置,就是下面要介绍的复制积压缓冲区。

(2)复制积压缓冲区( repl-backlog-buffer )

复制积压缓冲区是由主节点维护的、固定长度的、先进先出(FIFO)队列,默认大小1MB;

当主节点开始有从节点时, master创建一个积压缓冲区,其作用是备份主节点最近收到的redis命令,后续会发送给从节点的数据。

注意,无论主节点有一个还是多个从节点,都只需要一个复制积压缓冲区。

在命令传播阶段,主节点除了将写命令发送给从节点,还会发送一份给复制积压缓冲区,作为写命令的备份;

除了存储写命令,复制积压缓冲区中还存储了其中的每个字节对应的复制偏移量(offset)。

由于复制积压缓冲区定长且是先进先出,所以它保存的是主节点最复制积压缓冲区近执行的写命令;时间较早的写命令会被挤出缓冲区。

在这里插入图片描述

由于该缓冲区长度固定且有限,因此可以备份的写命令也有限,当主从节点offset的差距过大超过缓冲区长度时,将无法执行部分复制,只能执行全量复制。

反过来说,为了提高网络中断时部分复制执行的概率,可以根据需要增大复制积压缓冲区的大小(通过配置repl-backlog-size);

例如如果网络中断的平均时间是60s,而主节点平均每秒产生的写命令(特定协议格式)所占的字节数为100KB,则复制积压缓冲区的平均需求为6MB,保险起见,可以设置为12MB,来保证绝大多数断线情况都可以使用部分复制。

从节点将offset发送给主节点后,主节点根据offset和缓冲区大小决定能否执行部分复制:

  • 如果offset偏移量之后的数据,仍然都在复制积压缓冲区里,则执行部分复制;
  • 如果offset偏移量之后的数据已不在复制积压缓冲区中(数据已被挤出),则执行全量复制。

(3)服务器运行ID(runid)

每个Redis节点(无论主从),在启动时都会自动生成一个随机ID(每次启动都不一样),由40个随机的十六进制字符组成;runid用来唯一识别一个Redis节点。

通过info Server命令,可以查看节点的runid:

img

主从节点初次复制时,主节点将自己的runid发送给从节点,从节点将这个runid保存起来;当断线重连时,从节点会将这个runid发送给主节点;主节点根据runid判断能否进行部分复制:

  • 如果从节点保存的runid与主节点现在的runid相同,说明主从节点之前同步过,主节点会继续尝试使用部分复制(到底能不能部分复制还要看offset和复制积压缓冲区的情况);
  • 如果从节点保存的runid与主节点现在的runid不同,说明从节点在断线前同步的Redis节点并不是当前的主节点,只能进行全量复制。

slavof命令的执行流程

在了解了复制偏移量、复制积压缓冲区、节点运行id之后,

接下来,看看slavof命令的执行流程

在这里插入图片描述

从节点收到slaveof命令之后,首先决定是使用全量复制还是部分复制:

(1)首先,从节点根据当前状态,决定如何调用psync命令:

  • 如果从节点之前未执行过slaveof或最近执行了slaveof no one,则从节点发送命令为psync ? -1,向主节点请求全量复制;
  • 如果从节点之前执行了slaveof,则发送命令为psync {runid} {offset},其中runid为上次复制的主节点的runid,offset为上次复制截止时从节点保存的复制偏移量。

(2)主节点根据收到的psync命令,及当前服务器状态,决定执行全量复制还是部分复制:

  • 如果主节点版本低于Redis2.8,则返回-ERR回复,此时从节点重新发送sync命令执行全量复制;
  • 如果主节点版本够新,且runid与从节点发送的runid相同,且从节点发送的offset之后的数据在复制积压缓冲区中都存在,则回复+CONTINUE,表示将进行部分复制,从节点等待主节点发送其缺少的数据即可;
  • 如果主节点版本够新,但是runid与从节点发送的runid不同,或从节点发送的offset之后的数据已不在复制积压缓冲区中(在队列中被挤出了),则回复+FULLRESYNC {runid} {offset},表示要进行全量复制,其中runid表示主节点当前的runid,offset表示主节点当前的offset,从节点保存这两个值,以备使用。

重新连接之后的部分复制

部分复制主要是 Redis 针对全量复制的过高开销做出的一种优化措施,使用 psync {runId} {offset} 命令实现。

当从节点正在复制主节点时,如果出现网络闪断或者命令丢失等异常情况时,从节点会向主节点要求补发丢失的命令数据,如果主节点的复制积压缓冲区存在这部分数据,则直接发送给从节点,这样就保证了主从节点复制的一致性。

补发的这部分数据一般远远小于全量数据,所以开销很小。

  1. 当主从节点之间网络出现中断时,如果超过了 repl-timeout 时间,主节点会认为从节点故障并中断复制连接。

  2. 主从连接中断期间主节点依然响应命令,但因复制连接中断命令无法发送给从节点,不过主节点内部存在复制积压缓冲区( repl-backlog-buffer ),依然可以保存最近一段时间的写命令数据,默认最大缓存 1MB。

  3. 当主从节点网络恢复后,从节点会再次连上主节点。

  4. 当主从连接恢复后,由于从节点之前保存了自身已复制的偏移量和主节点的运行ID。因此会把它们作为 psync 参数发送给主节点,要求进行补发复制操作。

  5. 主节点接到 psync 命令后首先核对参数 runId 是否与自身一致,如果一致,说明之前复制的是当前主节点;之后根据参数 offset 在自身复制积压缓冲区查找,如果偏移量之后的数据存在缓冲区中,则对从节点发送 +CONTINUE 响应,表示可以进行部分复制。

  6. 主节点根据偏移量把复制积压缓冲区里的数据发送给从节点,保证主从复制进入正常状态。

命令传播阶段

数据同步阶段完成后,主从节点进入命令传播阶段;

在这个阶段主节点将自己执行的写命令发送给从节点,从节点接收命令并执行,从而保证主从节点数据的一致性。

在命令传播阶段,除了发送写命令,主从节点还维持着心跳机制:PING和REPLCONF ACK。

心跳机制对于主从复制的超时判断、数据安全等有作用。

1.主->从:PING

每隔指定的时间,主节点会向从节点发送PING命令,这个PING命令的作用,主要是为了让从节点进行超时判断。

PING发送的频率由 repl-ping-slave-period 参数控制,单位是秒,默认值是10s。

关于该PING命令究竟是由主节点发给从节点,还是相反,有一些争议;

因为在Redis的官方文档中,对该参数的注释中说明是从节点向主节点发送PING命令,如下图所示:

img

但是通过源码可以看到, PING命令是主节点会向从节点发送.

可能的原因是:代码的迭代和注释的迭代,没有完全同步。 可能早期是 从发给主,后面改成了主发从,而并没有配套修改注释, 就像尼恩的很多代码一样。

2. 从->主:REPLCONF ACK

在命令传播阶段,**从节点会向主节点发送REPLCONF ACK命令,**频率是每秒1次;

命令格式为:REPLCONF ACK {offset},其中offset指从节点保存的复制偏移量。

REPLCONF ACK命令的作用包括:

(1)实时监测主从节点网络状态:该命令会被主节点用于复制超时的判断。此外,在主节点中使用info Replication,可以看到其从节点的状态中的lag值,代表的是主节点上次收到该REPLCONF ACK命令的时间间隔,在正常情况下,该值应该是0或1,如下图所示:

img

(2)检测命令丢失:从节点发送了自身的offset,主节点会与自己的offset对比,如果从节点数据缺失(如网络丢包),主节点会推送缺失的数据(这里也会利用复制积压缓冲区)。

注意,offset和复制积压缓冲区,不仅可以用于部分复制,也可以用于处理命令丢失等情形;区别在于前者是在断线重连后进行的,而后者是在主从节点没有断线的情况下进行的。

(3)辅助保证从节点的数量和延迟:Redis主节点中使用min-slaves-to-write和min-slaves-max-lag参数,来保证主节点在不安全的情况下不会执行写命令;所谓不安全,是指从节点数量太少,或延迟过高。

例如min-slaves-to-write和min-slaves-max-lag分别是3和10,含义是如果从节点数量小于3个,或所有从节点的延迟值都大于10s,则主节点拒绝执行写命令。而这里从节点延迟值的获取,就是通过主节点接收到REPLCONF ACK命令的时间来判断的,即前面所说的info Replication中的lag值。

数据分片(sharding)的基本原理

什么是数据分片?

名词说明:

数据分片(sharding)也叫数据分区

为什么要做数据分片?

全量数据较大的场景下,单节点无法满足要求,需要数据分片

什么是数据分片?

按照分片规则把数据分到若干个shard、partition当中

在这里插入图片描述

range 分片

一种是按照 range 来分,就是每个片,一段连续的数据,这个一般是按比如时间范围/数据范围来的,但是这种一般较少用,因为很容易发生数据倾斜,大量的流量都打在最新的数据上了。

比如,安装数据范围分片,把1到100个数字,要保存在3个节点上

按照顺序分片,把数据平均分配三个节点上

  • 1号到33号数据保存到节点1上
  • 34号到66号数据保存到节点2上
  • 67号到100号数据保存到节点3上

在这里插入图片描述

ID取模分片

此种分片规则将数据分成n份(通常dn节点也为n),从而将数据均匀的分布于各个表中,或者各节点上。

扩容方便。

ID取模分片常用在关系型数据库的设计

具体请参见 秒杀视频的 亿级库表架构设计

hash 哈希分布

使用hash 算法,获取key的哈希结果,再按照规则进行分片,这样可以保证数据被打散,同时保证数据分布的比较均匀

哈希分布方式分为三个分片方式:

  • 哈希取余分片
  • 一致性哈希分片
  • 虚拟槽分片

哈希取余模分片

例如1到100个数字,对每个数字进行哈希运算,然后对每个数的哈希结果除以节点数进行取余,余数为1则保存在第1个节点上,余数为2则保存在第2个节点上,余数为0则保存在第3个节点,这样可以保证数据被打散,同时保证数据分布的比较均匀

比如有100个数据,对每个数据进行hash运算之后,与节点数进行取余运算,根据余数不同保存在不同的节点上

在这里插入图片描述

哈希取余分片是非常简单的一种分片方式

哈希取模分片有一个问题

即当增加或减少节点时,原来节点中的80%的数据会进行迁移操作,对所有数据重新进行分布

哈希取余分片,建议使用多倍扩容的方式,例如以前用3个节点保存数据,扩容为比以前多一倍的节点即6个节点来保存数据,这样只需要适移50%的数据。

数据迁移之后,第一次无法从缓存中读取数据,必须先从数据库中读取数据,然后回写到缓存中,然后才能从缓存中读取迁移之后的数据

img

哈希取余分片优点:

  • 配置简单:对数据进行哈希,然后取余

哈希取余分片缺点:

  • 数据节点伸缩时,导致数据迁移
  • 迁移数量和添加节点数据有关,建议翻倍扩容

一致性哈希分片

一致性哈希原理:

将所有的数据当做一个token环,

token环中的数据范围是0到2的32次方。

然后为每一个数据节点分配一个token范围值,这个节点就负责保存这个范围内的数据。

img

对每一个key进行hash运算,被哈希后的结果在哪个token的范围内,则按顺时针去找最近的节点,这个key将会被保存在这个节点上。

redis cluster 集群 HA 原理和实操(史上最全、面试必备)_第5张图片

一致性哈希分片的节点扩容

在下面的图中:

  • 有4个key被hash之后的值在在n1节点和n2节点之间,按照顺时针规则,这4个key都会被保存在n2节点上

  • 如果在n1节点和n2节点之间添加n5节点,当下次有key被hash之后的值在n1节点和n5节点之间,这些key就会被保存在n5节点上面了

下图的例子里,添加n5节点之后:

  • 数据迁移会在n1节点和n2节点之间进行
  • n3节点和n4节点不受影响
  • 数据迁移范围被缩小很多

同理,如果有1000个节点,此时添加一个节点,受影响的节点范围最多只有千分之2。所以,一致性哈希一般用在节点比较多的时候,节点越多,扩容时受影响的节点范围越少

redis cluster 集群 HA 原理和实操(史上最全、面试必备)_第6张图片

分片方式:哈希 + 顺时针(优化取余)

一致性哈希分片优点:

  • 一致性哈希算法解决了分布式下数据分布问题。比如在缓存系统中,通过一致性哈希算法把缓存键映射到不同的节点上,由于算法中虚拟节点的存在,哈希结果一般情况下比较均匀。
  • 节点伸缩时,只影响邻近节点,但是还是有数据迁移

“但没有一种解决方案是银弹,能适用于任何场景。所以实践中一致性哈希算法有哪些缺陷,或者有哪些场景不适用呢?”

一致性哈希分片缺点:

一致性哈希在大批量的数据场景下负载更加均衡,但是在数据规模小的场景下,会出现单位时间内某个节点完全空闲的情况出现。

虚拟槽分片 (范围分片的变种)

Redis Cluster在设计中没有使用一致性哈希(Consistency Hashing),而是使用数据分片引入哈希槽(hash slot)来实现;

虚拟槽分片是Redis Cluster采用的分片方式.

虚拟槽分片 ,可以理解为范围分片的变种, hash取模分片+范围分片, 把hash值取余数分为n段,一个段给一个节点负责

在这里插入图片描述

虚拟槽分片 (范围分片的变种)

Redis Cluster在设计中没有使用一致性哈希(Consistency Hashing),而是使用数据分片引入哈希槽(hash slot)来实现;

虚拟槽分片是Redis Cluster采用的分片方式.

在该分片方式中:

  • 首先 预设虚拟槽,每个槽为一个hash值,每个node负责一定槽范围。
  • 每一个值都是key的hash值取余,每个槽映射一个数据子集,一般比节点数大

Redis Cluster中预设虚拟槽的范围为0到16383

在这里插入图片描述

虚拟槽分片的映射步骤:

1.把16384槽按照节点数量进行平均分配,由节点进行管理
2.对每个key按照CRC16规则进行hash运算
3.把hash结果对16383进行取余
4.把余数发送给Redis节点
5.节点接收到数据,验证是否在自己管理的槽编号的范围

  • 如果在自己管理的槽编号范围内,则把数据保存到数据槽中,然后返回执行结果
  • 如果在自己管理的槽编号范围外,则会把数据发送给正确的节点,由正确的节点来把数据保存在对应的槽中

需要注意的是:Redis Cluster的节点之间会共享消息,每个节点都会知道是哪个节点负责哪个范围内的数据槽

虚拟槽分布方式中,由于每个节点管理一部分数据槽,数据保存到数据槽中。

当节点扩容或者缩容时,对数据槽进行重新分配迁移即可,数据不会丢失。

3个节点的Redis集群虚拟槽分片结果:

[root@localhost redis-cluster]# docker exec -it redis-cluster_redis1_1 redis-cli --cluster check 172.18.8.164:6001
172.18.8.164:6001 (c4cfd72f...) -> 0 keys | 5461 slots | 1 slaves.
172.18.8.164:6002 (c15a7801...) -> 0 keys | 5462 slots | 1 slaves.
172.18.8.164:6003 (3fe7628d...) -> 0 keys | 5461 slots | 1 slaves.
[OK] 0 keys in 3 masters.
0.00 keys per slot on average.
>>> Performing Cluster Check (using node 172.18.8.164:6001)
M: c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 172.18.8.164:6001
   slots:[0-5460] (5461 slots) master
   1 additional replica(s)
S: a212e28165b809b4c75f95ddc986033c599f3efb 172.18.8.164:6006
   slots: (0 slots) slave
   replicates 3fe7628d7bda14e4b383e9582b07f3bb7a74b469
M: c15a7801623ee5ebe3cf952989dd5a157918af96 172.18.8.164:6002
   slots:[5461-10922] (5462 slots) master
   1 additional replica(s)
S: 5e74257b26eb149f25c3d54aef86a4d2b10269ca 172.18.8.164:6004
   slots: (0 slots) slave
   replicates c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd
S: 8fb7f7f904ad1c960714d8ddb9ad9bca2b43be1c 172.18.8.164:6005
   slots: (0 slots) slave
   replicates c15a7801623ee5ebe3cf952989dd5a157918af96
M: 3fe7628d7bda14e4b383e9582b07f3bb7a74b469 172.18.8.164:6003
   slots:[10923-16383] (5461 slots) master
   1 additional replica(s)
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.

虚拟槽分片特点:

虚拟槽分区巧妙地使用了哈希空间,使用分散度良好的哈希函数把所有数据映射到一个固定范围的整数集合中,整数定义为槽(slot)。槽是集群内数据管理和迁移的基本单位。

槽的范围一般远远大于节点数,比如Redis Cluster槽范围是0~16383。

采用大范围槽的主要目的是为了方便数据拆分和集群扩展,每个节点会负责一定数量的槽。

Redis虚拟槽分区的优点:

  • 解耦数据和节点之间的关系,简化了节点扩容和收缩难度。

  • 节点自身维护槽的映射关系,不需要客户端或者代理服务维护槽分区元数据。

  • 支持节点、槽、键之间的映射查询,用于数据路由,在线伸缩等场景。

  • 无论数据规模大,还是小,Redis虚拟槽分区各个节点的负载,都会比较均衡 。而一致性哈希在大批量的数据场景下负载更加均衡,但是在数据规模小的场景下,会出现单位时间内某个节点完全空闲的情况出现。

Redis集群如何高可用

要实现Redis高可用,前提条件之一,是需要进行Redis的节点集群

集群的必要性

所谓的集群,就是通过添加服务节点的数量,不同的节点提供相同的服务,从而让服务器达到高可用、自动failover的状态。

面试题:单个redis节点,面临哪些问题?

答:

(1)单个redis存在不稳定性。当redis服务宕机了,就没有可用的服务了。

(2)单个redis的读写能力是有限的。单机的 redis,能够承载的 QPS 大概就在上万到几万不等。

对于缓存来说,一般都是用来支撑读高并发、高可用。

单个redis节点,二者都做不到。

Redis集群模式的分类,可以从下面角度来分:

  • 客户端分片
  • 代理分片
  • 服务端分片
  • 代理模式和服务端分片相结合的模式

客户端分片包括:

ShardedJedisPool

ShardedJedisPool是redis没有集群功能之前客户端实现的一个数据分布式方案,

使用shardedJedisPool实现redis集群部署,由于shardedJedisPool的原理是通过一致性哈希进行切片实现的,不同点key被分别分配到不同的redis实例上。

代理分片包括:

  • Codis
  • Twemproxy

服务端分片包括:

  • Redis Cluster

从否中心化来划分

它们还可以用是否中心化来划分

  • 无中心化的集群方案

其中客户端分片、Redis Cluster属于无中心化的集群方案

  • 中心化的集群方案

Codis、Tweproxy属于中心化的集群方案。

是否中心化是指客户端访问多个Redis节点时,是直接访问还是通过一个中间层Proxy来进行操作,直接访问的就属于无中心化的方案,通过中间层Proxy访问的就属于中心化的方案,它们有各自的优劣,下面分别来介绍。

如何学习redis集群

说明:

 (1)redis集群中,每一个redis称之为一个节点。
 (2)redis集群中,有两种类型的节点:主节点(master)、从节点(slave)。
  (3)redis集群,是基于redis主从复制实现。

Docker方式部署redis-cluster步骤

1、redis容器初始化
2、redis容器集群配置

这里引用了别人的一个镜像publicisworldwide/redis-cluster,方便快捷。

redis-cluster的节点端口共分为2种,

  • 一种是节点提供服务的端口,如6379、6001;

  • 一种是节点间通信的端口,固定格式为:10000+6379/10000+6001。

若不想使用host模式,也可以把network_mode去掉,但就要加ports映射。

这里使用host(主机)网络模式,把redis数据挂载到本机目录/data/redis/800*下。

Docker网络

Docker使用Linux桥接技术,在宿主机虚拟一个Docker容器网桥(docker0),Docker启动一个容器时会根据Docker网桥的网段分配给容器一个IP地址,称为Container-IP,同时Docker网桥是每个容器的默认网关。

因为在同一宿主机内的容器都接入同一个网桥,这样容器之间就能够通过容器的Container-IP直接通信。

Docker网桥是宿主机虚拟出来的,并不是真实存在的网络设备,外部网络是无法寻址到的,这也意味着外部网络无法通过直接Container-IP访问到容器。

如果容器希望外部访问能够访问到,可以通过映射容器端口到宿主主机(端口映射),即docker run创建容器时候通过 -p 或 -P 参数来启用,访问容器的时候就通过[宿主机IP]:[容器端口]访问容器。

Docker容器的四类网络模式

Docker网络模式 配置 说明
host模式 –net=host 容器和宿主机共享Network namespace。
container模式 –net=container:NAME_or_ID 容器和另外一个容器共享Network namespace。 kubernetes中的pod就是多个容器共享一个Network namespace。
none模式 –net=none 容器有独立的Network namespace,但并没有对其进行任何网络设置,如分配veth pair 和网桥连接,配置IP等。
bridge模式 –net=bridge (默认为该模式)

桥接模式(default)

Docker容器的默认网络模式为桥接模式,如图所示:
在这里插入图片描述

Docker安装时会创建一个名为docker0的bridge虚拟网桥

bridge模式是docker的默认网络模式,不写–net参数,就是bridge模式。新创建的容器都会自动连接到这个虚拟网桥。

bridge网桥用于同一主机上的docker容器相互通信,连接到同一个网桥的docker容器可以相互通信。

bridge 对宿主机来讲相当于一个单独的网卡设备 ,对于运行在宿主机上的每个容器来说相当于一个交换机,所有容器的虚拟网线的一端都连接到docker0上。

容器通过本地主机进行上网,容器会创建名为veth的虚拟网卡,网卡一端连接到docker0网桥,另一端连接容器,容器就可以通过网桥通过分配的IP地址进行上网。

在这里插入图片描述

docker exec -it rmqbroker-a cat /etc/hosts

[root@localhost ~]# docker exec -it rmqbroker-a cat /etc/hosts
127.0.0.1       localhost
::1     localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
172.30.0.5      c55ea6edcc14

使用docker run -p时,docker实际是在iptables做了DNAT规则,实现端口转发功能。

可以使用iptables -t nat -vnL查看。

 pkts bytes target     prot opt in     out     source               destination
15141  908K RETURN     all  --  br-9a8ffe43b503 *       0.0.0.0/0            0.0.0.0/0
 536K   32M RETURN     all  --  br-e495dc44c56b *       0.0.0.0/0            0.0.0.0/0
    0     0 RETURN     all  --  br-f232a6bcdb94 *       0.0.0.0/0            0.0.0.0/0
    0     0 RETURN     all  --  docker0 *       0.0.0.0/0            0.0.0.0/0
   11   572 DNAT       tcp  --  !br-f232a6bcdb94 *       0.0.0.0/0            0.0.0.0/0            tcp dpt:3306 to:172.19.0.2:3306
    0     0 DNAT       tcp  --  !br-f232a6bcdb94 *       0.0.0.0/0            0.0.0.0/0            tcp dpt:3307 to:172.19.0.3:3306
    0     0 DNAT       tcp  --  !br-f232a6bcdb94 *       0.0.0.0/0            0.0.0.0/0            tcp dpt:3308 to:172.19.0.4:3306
    3   156 DNAT       tcp  --  !br-f232a6bcdb94 *       0.0.0.0/0            0.0.0.0/0            tcp dpt:23306 to:172.19.0.5:23306
    0     0 DNAT       tcp  --  !br-f232a6bcdb94 *       0.0.0.0/0            0.0.0.0/0            tcp dpt:1080 to:172.19.0.5:1080
    0     0 DNAT       tcp  --  !br-e495dc44c56b *       0.0.0.0/0            0.0.0.0/0            tcp dpt:8011 to:172.20.0.2:9555
    8   416 DNAT       tcp  --  !br-e495dc44c56b *       0.0.0.0/0            0.0.0.0/0            tcp dpt:8001 to:172.20.0.2:8001
    0     0 DNAT       tcp  --  !br-e495dc44c56b *       0.0.0.0/0            0.0.0.0/0            tcp dpt:8013 to:172.20.0.3:9555
    0     0 DNAT       tcp  --  !br-e495dc44c56b *       0.0.0.0/0            0.0.0.0/0            tcp dpt:8003 to:172.20.0.3:8003
    0     0 DNAT       tcp  --  !br-e495dc44c56b *       0.0.0.0/0            0.0.0.0/0            tcp dpt:8012 to:172.20.0.4:9555
    0     0 DNAT       tcp  --  !br-e495dc44c56b *       0.0.0.0/0            0.0.0.0/0            tcp dpt:8002 to:172.20.0.4:8002
   20  1040 DNAT       tcp  --  !br-e495dc44c56b *       0.0.0.0/0            0.0.0.0/0            tcp dpt:8848 to:172.20.0.5:8848
    0     0 DNAT       tcp  --  !br-e495dc44c56b *       0.0.0.0/0            0.0.0.0/0            tcp dpt:1082 to:172.20.0.5:1080
    0     0 DNAT       tcp  --  !br-9a8ffe43b503 *       0.0.0.0/0            0.0.0.0/0            tcp dpt:9877 to:172.30.0.2:9876
    0     0 DNAT       tcp  --  !br-9a8ffe43b503 *       0.0.0.0/0            0.0.0.0/0            tcp dpt:9876 to:172.30.0.3:9876
    5   260 DNAT       tcp  --  !br-9a8ffe43b503 *       0.0.0.0/0            0.0.0.0/0            tcp dpt:9001 to:172.30.0.4:9001
    0     0 DNAT       tcp  --  !br-9a8ffe43b503 *       0.0.0.0/0            0.0.0.0/0            tcp dpt:10912 to:172.30.0.5:10912
    0     0 DNAT       tcp  --  !br-9a8ffe43b503 *       0.0.0.0/0            0.0.0.0/0            tcp dpt:10911 to:172.30.0.5:10911
    0     0 DNAT       tcp  --  !br-9a8ffe43b503 *       0.0.0.0/0            0.0.0.0/0            tcp dpt:10922 to:172.30.0.6:10922
    0     0 DNAT       tcp  --  !br-9a8ffe43b503 *       0.0.0.0/0            0.0.0.0/0            tcp dpt:10921 to:172.30.0.6:10921

我们也可以自定义自己的bridge网络,docker文档建议使用自定义bridge网络

创建一个自定义网络, 可以指定子网、IP地址范围、网关等网络配置

docker network create --driver bridge --subnet 172.22.16.0/24 --gateway 172.22.16.1 mynet2

查看docker网络,是否创建成功。

docker network ls

在这里插入图片描述

总之:Docker网络bridge桥接模式,是创建和运行容器时默认模式。这种模式会为每个容器分配一个独立的网卡,桥接到默认或指定的bridge上,同一个Bridge下的容器下可以互相通信的。我们也可以创建自定义bridge以满足个性化的网络需求。

HOST模式

在这里插入图片描述

Docker使用了Linux的Namespaces技术来进行资源隔离,如PID Namespace隔离进程,Mount Namespace隔离文件系统,Network Namespace隔离网络等。

一个Network Namespace提供了一份独立的网络环境,包括网卡、路由、Iptable规则等都与其他的Network Namespace隔离。bridge模式下,一个Docker容器一般会分配一个独立的Network Namespace。

host模式类似于Vmware的桥接模式,与宿主机在同一个网络中,但没有独立IP地址。

一个Docker容器一般会分配一个独立的Network Namespace。但如果启动容器的时候使用host模式,那么这个容器将不会获得一个独立的Network Namespace,而是和宿主机共用一个Network Namespace。容器将不会虚拟出自己的网卡,配置自己的IP等,而是使用宿主机的IP和端口。

容器与主机在相同的网络命名空间下面,使用相同的网络协议栈,容器可以直接使用主机的所有网络接口

在这里插入图片描述

Container模式

在这里插入图片描述

None

获取独立的network namespace,但不为容器进行任何网络配置,之后用户可以自己进行配置,容器内部只能使用loopback网络设备,不会再有其他的网络资源

创建文件目录结构

mkdir -p /home/docker-compose/redis-cluster/conf/{6001,6002,6003,6004,6005,6006}/data

离线环境镜像导入

从有公网的环境拉取镜像,然后导出镜像

  • publicisworldwide/redis-cluster redis-cluster镜像

  • inem0o/redis-trib 集群管理工具:自动执行节点握手,自动操作节点主从配置,自动给主节点分配槽

无公网的环境,上传到到内网环境, 上传镜像到目标虚拟机

然后导入docker,load到docker

docker load   -i  /root/redis-cluster.tar
docker load   -i   /root/redis-trib.tar

导入后看到两个image 镜像:

[root@localhost ~]# docker image ls

publicisworldwide/redis-cluster   latest                         29e4f38e4475        2 years ago         94.9MB
inem0o/redis-trib                 latest                         0f7b910114d5        4 years ago         32MB

redis容器启动

创建容器编排文件

使用docker-compose方式,先创建一个docker-compose.yml文件,容器的ip使用host模式,内容如下:

version: '3.5'
services:
 redis1:
  image: publicisworldwide/redis-cluster
  network_mode: host
  restart: always
  volumes:
   - /home/docker-compose/redis-cluster/conf/6001/data:/data
  environment:
   - REDIS_PORT=6001

 redis2:
  image: publicisworldwide/redis-cluster
  network_mode: host
  restart: always
  volumes:
   - /home/docker-compose/redis-cluster/conf/6002/data:/data
  environment:
   - REDIS_PORT=6002

 redis3:
  image: publicisworldwide/redis-cluster
  network_mode: host
  restart: always
  volumes:
   - /home/docker-compose/redis-cluster/conf/6003/data:/data
  environment:
   - REDIS_PORT=6003

 redis4:
  image: publicisworldwide/redis-cluster
  network_mode: host
  restart: always
  volumes:
   - /home/docker-compose/redis-cluster/conf/6004/data:/data
  environment:
   - REDIS_PORT=6004

 redis5:
  image: publicisworldwide/redis-cluster
  network_mode: host
  restart: always
  volumes:
   - /home/docker-compose/redis-cluster/conf/6005/data:/data
  environment:
   - REDIS_PORT=6005

 redis6:
  image: publicisworldwide/redis-cluster
  network_mode: host
  restart: always
  volumes:
   - /home/docker-compose/redis-cluster/conf/6006/data:/data
  environment:
   - REDIS_PORT=6006

作为参考,如果容器的ip使用BRIDGE模式,docker-compose.yml文件内容如下:

version: '3'

services:
 redis1:
  image: publicisworldwide/redis-cluster
  restart: always
  volumes:
   - /data/redis/6001/data:/data
  environment:
   - REDIS_PORT=6001
  ports:
    - '6001:6001'       #服务端口
    - '16001:16001'   #集群端口

 redis2:
  image: publicisworldwide/redis-cluster
  restart: always
  volumes:
   - /data/redis/6002/data:/data
  environment:
   - REDIS_PORT=6002
  ports:
    - '6002:6002'
    - '16002:16002'

 redis3:
  image: publicisworldwide/redis-cluster
  restart: always
  volumes:
   - /data/redis/6003/data:/data
  environment:
   - REDIS_PORT=6003
  ports:
    - '6003:6003'
    - '16003:16003'

 redis4:
  image: publicisworldwide/redis-cluster
  restart: always
  volumes:
   - /data/redis/6004/data:/data
  environment:
   - REDIS_PORT=6004
  ports:
    - '6004:6004'
    - '16004:16004'

 redis5:
  image: publicisworldwide/redis-cluster
  restart: always
  volumes:
   - /data/redis/6005/data:/data
  environment:
   - REDIS_PORT=6005
  ports:
    - '6005:6005'
    - '16005:16005'

 redis6:
  image: publicisworldwide/redis-cluster
  restart: always
  volumes:
   - /data/redis/6006/data:/data
  environment:
   - REDIS_PORT=6006
  ports:
    - '6006:6006'
    - '16006:16006'

启动服务redis容器

创建文件后,直接启动服务

窗口模式
docker-compose up
后台进程
docker-compose up -d
1234

查看启动的进程

CONTAINER ID        IMAGE                                     COMMAND                  CREATED             STATUS              PORTS                                                                        NAMES
2bdd27191859        publicisworldwide/redis-cluster           "/usr/local/bin/entr…"   18 seconds ago      Up 17 seconds                                                                                    redis-cluster_redis4_1
afdf208c55f3        publicisworldwide/redis-cluster           "/usr/local/bin/entr…"   18 seconds ago      Up 17 seconds                                                                                    redis-cluster_redis1_1
d14d7dbd207f        publicisworldwide/redis-cluster           "/usr/local/bin/entr…"   18 seconds ago      Up 17 seconds                                                                                    redis-cluster_redis5_1
25070ed4a434        publicisworldwide/redis-cluster           "/usr/local/bin/entr…"   18 seconds ago      Up 17 seconds                                                                                    redis-cluster_redis2_1
35e1ff66d2db        publicisworldwide/redis-cluster           "/usr/local/bin/entr…"   18 seconds ago      Up 17 seconds                                                                                    redis-cluster_redis3_1
615bfbf336c0        publicisworldwide/redis-cluster           "/usr/local/bin/entr…"   18 seconds ago      Up 17 seconds                                                                                    redis-cluster_redis6_1

状态为Up,说明服务均已启动,镜像无问题。

注意:以上镜像不能设置永久密码,其实redis一般是内网访问,可以不需密码。

建立redis集群

这里同样使用了另一个镜像inem0o/redis-trib,执行时会自动下载。

离线场景请提前load,或者导入到私有的restry。

使用redis-trib.rb创建redis 集群

上面只是启动了6个redis容器,并没有设置集群,通过下面的命令可以设置集群。

使用 redis-trib.rb create 命令完成节点握手和槽分配过程

docker run --rm -it inem0o/redis-trib create --replicas 1 hostip:6001 hostip:6002 hostip:6003 hostip:6004 hostip:6005 hostip:6006

#hostip  换成 主机的ip

docker run --rm -it inem0o/redis-trib create --replicas 1 172.18.8.164:6001 172.18.8.164:6002 172.18.8.164:6003 172.18.8.164:6004 172.18.8.164:6005 172.18.8.164:6006

–replicas 参数指定集群中每个主节点配备几个从节点,这里设置为1,

redis-trib.rb 会尽可能保证主从节点不分配在同一机器下,因此会重新排序节点列表顺序。

节点列表顺序用于确定主从角色,先主节点之后是从节点。

创建过程中首先会给出主从节点角色分配的计划,并且会生成报告

日志如下:

[root@localhost redis-cluster]# docker run --rm -it inem0o/redis-trib create --replicas 1 172.18.8.164:6001 172.18.8.164:6002 172.18.8.164:6003 172.18.8.164:6004 172.18.8.164:6005 172.18.8.164:6006
>>> Creating cluster
>>> Performing hash slots allocation on 6 nodes...
Using 3 masters:
172.18.8.164:6001
172.18.8.164:6002
172.18.8.164:6003
Adding replica 172.18.8.164:6004 to 172.18.8.164:6001
Adding replica 172.18.8.164:6005 to 172.18.8.164:6002
Adding replica 172.18.8.164:6006 to 172.18.8.164:6003
M: c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 172.18.8.164:6001
   slots:0-5460 (5461 slots) master
M: c15a7801623ee5ebe3cf952989dd5a157918af96 172.18.8.164:6002
   slots:5461-10922 (5462 slots) master
M: 3fe7628d7bda14e4b383e9582b07f3bb7a74b469 172.18.8.164:6003
   slots:10923-16383 (5461 slots) master
S: 5e74257b26eb149f25c3d54aef86a4d2b10269ca 172.18.8.164:6004
   replicates c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd
S: 8fb7f7f904ad1c960714d8ddb9ad9bca2b43be1c 172.18.8.164:6005
   replicates c15a7801623ee5ebe3cf952989dd5a157918af96
S: a212e28165b809b4c75f95ddc986033c599f3efb 172.18.8.164:6006
   replicates 3fe7628d7bda14e4b383e9582b07f3bb7a74b469
Can I set the above configuration? (type 'yes' to accept): yes

注意:出现Can I set the above configuration? (type ‘yes’ to accept): 是要输入yes 不是Y

>>> Nodes configuration updated
>>> Assign a different config epoch to each node
>>> Sending CLUSTER MEET messages to join the cluster
Waiting for the cluster to join...
>>> Performing Cluster Check (using node 172.18.8.164:6001)
M: c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 172.18.8.164:6001
   slots:0-5460 (5461 slots) master
   1 additional replica(s)
S: a212e28165b809b4c75f95ddc986033c599f3efb 172.18.8.164:6006@16006
   slots: (0 slots) slave
   replicates 3fe7628d7bda14e4b383e9582b07f3bb7a74b469
M: c15a7801623ee5ebe3cf952989dd5a157918af96 172.18.8.164:6002@16002
   slots:5461-10922 (5462 slots) master
   1 additional replica(s)
S: 5e74257b26eb149f25c3d54aef86a4d2b10269ca 172.18.8.164:6004@16004
   slots: (0 slots) slave
   replicates c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd
S: 8fb7f7f904ad1c960714d8ddb9ad9bca2b43be1c 172.18.8.164:6005@16005
   slots: (0 slots) slave
   replicates c15a7801623ee5ebe3cf952989dd5a157918af96
M: 3fe7628d7bda14e4b383e9582b07f3bb7a74b469 172.18.8.164:6003@16003
   slots:10923-16383 (5461 slots) master
   1 additional replica(s)
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.

详解redis-trib.rb 的命令

命令说明:
 redis-trib.rb help
Usage: redis-trib   

#创建集群
create          host1:port1 ... hostN:portN  
                  --replicas  #带上该参数表示是否有从,arg表示从的数量
#检查集群
check           host:port
#查看集群信息
info            host:port
#修复集群
fix             host:port
                  --timeout 
#在线迁移slot  
reshard         host:port       #个是必传参数,用来从一个节点获取整个集群信息,相当于获取集群信息的入口
                  --from   #需要从哪些源节点上迁移slot,可从多个源节点完成迁移,以逗号隔开,传递的是节点的node id,还可以直接传递--from all,这样源节点就是集群的所有节点,不传递该参数的话,则会在迁移过程中提示用户输入
                  --to     #slot需要迁移的目的节点的node id,目的节点只能填写一个,不传递该参数的话,则会在迁移过程中提示用户输入。
                  --slots  #需要迁移的slot数量,不传递该参数的话,则会在迁移过程中提示用户输入。
                  --yes         #设置该参数,可以在打印执行reshard计划的时候,提示用户输入yes确认后再执行reshard
                  --timeout   #设置migrate命令的超时时间。
                  --pipeline  #定义cluster getkeysinslot命令一次取出的key数量,不传的话使用默认值为10。
#平衡集群节点slot数量  
rebalance       host:port
                  --weight 
                  --auto-weights
                  --use-empty-masters
                  --timeout 
                  --simulate
                  --pipeline 
                  --threshold 
#将新节点加入集群 
add-node        new_host:new_port existing_host:existing_port
                  --slave
                  --master-id 
#从集群中删除节点
del-node        host:port node_id
#设置集群节点间心跳连接的超时时间
set-timeout     host:port milliseconds
#在集群全部节点上执行命令
call            host:port command arg arg .. arg
#将外部redis数据导入集群
import          host:port
                  --from 
                  --copy
                  --replace

通过客户端命令使用集群

检查集群状态

 docker exec -it redis-cluster_redis1_1 redis-cli --cluster check 172.18.8.164:6001

使用到的命令为: redis-cli --cluster check

结果如下:

[root@localhost redis-cluster]# docker exec -it redis-cluster_redis1_1 redis-cli --cluster check 172.18.8.164:6001
172.18.8.164:6001 (c4cfd72f...) -> 0 keys | 5461 slots | 1 slaves.
172.18.8.164:6002 (c15a7801...) -> 0 keys | 5462 slots | 1 slaves.
172.18.8.164:6003 (3fe7628d...) -> 0 keys | 5461 slots | 1 slaves.
[OK] 0 keys in 3 masters.
0.00 keys per slot on average.
>>> Performing Cluster Check (using node 172.18.8.164:6001)
M: c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 172.18.8.164:6001
   slots:[0-5460] (5461 slots) master
   1 additional replica(s)
S: a212e28165b809b4c75f95ddc986033c599f3efb 172.18.8.164:6006
   slots: (0 slots) slave
   replicates 3fe7628d7bda14e4b383e9582b07f3bb7a74b469
M: c15a7801623ee5ebe3cf952989dd5a157918af96 172.18.8.164:6002
   slots:[5461-10922] (5462 slots) master
   1 additional replica(s)
S: 5e74257b26eb149f25c3d54aef86a4d2b10269ca 172.18.8.164:6004
   slots: (0 slots) slave
   replicates c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd
S: 8fb7f7f904ad1c960714d8ddb9ad9bca2b43be1c 172.18.8.164:6005
   slots: (0 slots) slave
   replicates c15a7801623ee5ebe3cf952989dd5a157918af96
M: 3fe7628d7bda14e4b383e9582b07f3bb7a74b469 172.18.8.164:6003
   slots:[10923-16383] (5461 slots) master
   1 additional replica(s)
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.

redis-cli --cluster命令详解

redis-cli --cluster命令参数详解

redis-cli --cluster help
Cluster Manager Commands:
  create         host1:port1 ... hostN:portN   #创建集群
                 --cluster-replicas       #从节点个数
  check          host:port                     #检查集群
                 --cluster-search-multiple-owners #检查是否有槽同时被分配给了多个节点
  info           host:port                     #查看集群状态
  fix            host:port                     #修复集群
                 --cluster-search-multiple-owners #修复槽的重复分配问题
  reshard        host:port                     #指定集群的任意一节点进行迁移slot,重新分slots
                 --cluster-from           #需要从哪些源节点上迁移slot,可从多个源节点完成迁移,以逗号隔开,传递的是节点的node id,还可以直接传递--from all,这样源节点就是集群的所有节点,不传递该参数的话,则会在迁移过程中提示用户输入
                 --cluster-to             #slot需要迁移的目的节点的node id,目的节点只能填写一个,不传递该参数的话,则会在迁移过程中提示用户输入
                 --cluster-slots          #需要迁移的slot数量,不传递该参数的话,则会在迁移过程中提示用户输入。
                 --cluster-yes                 #指定迁移时的确认输入
                 --cluster-timeout        #设置migrate命令的超时时间
                 --cluster-pipeline       #定义cluster getkeysinslot命令一次取出的key数量,不传的话使用默认值为10
                 --cluster-replace             #是否直接replace到目标节点
  rebalance      host:port                                      #指定集群的任意一节点进行平衡集群节点slot数量 
                 --cluster-weight          #指定集群节点的权重
                 --cluster-use-empty-masters                    #设置可以让没有分配slot的主节点参与,默认不允许
                 --cluster-timeout                         #设置migrate命令的超时时间
                 --cluster-simulate                             #模拟rebalance操作,不会真正执行迁移操作
                 --cluster-pipeline                        #定义cluster getkeysinslot命令一次取出的key数量,默认值为10
                 --cluster-threshold                       #迁移的slot阈值超过threshold,执行rebalance操作
                 --cluster-replace                              #是否直接replace到目标节点
  add-node       new_host:new_port existing_host:existing_port  #添加节点,把新节点加入到指定的集群,默认添加主节点
                 --cluster-slave                                #新节点作为从节点,默认随机一个主节点
                 --cluster-master-id                       #给新节点指定主节点
  del-node       host:port node_id                              #删除给定的一个节点,成功后关闭该节点服务
  call           host:port command arg arg .. arg               #在集群的所有节点执行相关命令
  set-timeout    host:port milliseconds                         #设置cluster-node-timeout
  import         host:port                                      #将外部redis数据导入集群
                 --cluster-from                            #将指定实例的数据导入到集群
                 --cluster-copy                                 #migrate时指定copy
                 --cluster-replace                              #migrate时指定replace
  help           

For check, fix, reshard, del-node, set-timeout you can specify the host and port of any working node in the cluster.

参考的cluster命令

CLUSTER info:打印集群的信息。
CLUSTER nodes:列出集群当前已知的所有节点(node)的相关信息。
CLUSTER meet  :将ip和port所指定的节点添加到集群当中。
CLUSTER addslots  [slot ...]:将一个或多个槽(slot)指派(assign)给当前节点。
CLUSTER delslots  [slot ...]:移除一个或多个槽对当前节点的指派。
CLUSTER slots:列出槽位、节点信息。
CLUSTER slaves :列出指定节点下面的从节点信息。
CLUSTER replicate :将当前节点设置为指定节点的从节点。
CLUSTER saveconfig:手动执行命令保存保存集群的配置文件,集群默认在配置修改的时候会自动保存配置文件。
CLUSTER keyslot :列出key被放置在哪个槽上。
CLUSTER flushslots:移除指派给当前节点的所有槽,让当前节点变成一个没有指派任何槽的节点。
CLUSTER countkeysinslot :返回槽目前包含的键值对数量。
CLUSTER getkeysinslot  :返回count个槽中的键。
CLUSTER setslot  node  将槽指派给指定的节点,如果槽已经指派给另一个节点,那么先让另一个节点删除该槽,然后再进行指派。  
CLUSTER setslot  migrating  将本节点的槽迁移到指定的节点中。  
CLUSTER setslot  importing  从 node_id 指定的节点中导入槽 slot 到本节点。  
CLUSTER setslot  stable 取消对槽 slot 的导入(import)或者迁移(migrate)。 

CLUSTER failover:手动进行故障转移。
CLUSTER forget :从集群中移除指定的节点,这样就无法完成握手,过期时为60s,60s后两节点又会继续完成握手。
CLUSTER reset [HARD|SOFT]:重置集群信息,soft是清空其他节点的信息,但不修改自己的id,hard还会修改自己的id,不传该参数则使用soft方式。

CLUSTER count-failure-reports :列出某个节点的故障报告的长度。
CLUSTER SET-CONFIG-EPOCH:设置节点epoch,只有在节点加入集群前才能设置。

连接redis的某个节点

成功后可连接redis集群中的摸个节点,用以下命令

[root@localhost redis-cluster]# docker exec -it redis-cluster_redis1_1 redis-cli -c -h 172.18.8.164 -p 6001

172.18.8.164:6001>

通过该redis cli 控制台,可以输入redis的操作命令

查看集群信息和节点信息

# 查看集群信息
cluster info
# 查看集群结点信息
cluster nodes

查看集群信息

[root@localhost redis-cluster]# docker exec -it redis-cluster_redis1_1 redis-cli -c -h172.18.8.164 -p 6001
172.18.8.164:6001> cluster info
cluster_state:ok
cluster_slots_assigned:16384
cluster_slots_ok:16384
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:6
cluster_size:3
cluster_current_epoch:6
cluster_my_epoch:1
cluster_stats_messages_ping_sent:2979
cluster_stats_messages_pong_sent:2904
cluster_stats_messages_sent:5883
cluster_stats_messages_ping_received:2899
cluster_stats_messages_pong_received:2979
cluster_stats_messages_meet_received:5
cluster_stats_messages_received:5883

查看集群结点信息

172.18.8.164:6001> cluster nodes
a212e28165b809b4c75f95ddc986033c599f3efb 172.18.8.164:6006@16006 slave 3fe7628d7bda14e4b383e9582b07f3bb7a74b469 0 1634365163922 6 connected
c15a7801623ee5ebe3cf952989dd5a157918af96 172.18.8.164:6002@16002 master - 0 1634365162000 2 connected 5461-10922
5e74257b26eb149f25c3d54aef86a4d2b10269ca 172.18.8.164:6004@16004 slave c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 0 1634365163000 4 connected
8fb7f7f904ad1c960714d8ddb9ad9bca2b43be1c 172.18.8.164:6005@16005 slave c15a7801623ee5ebe3cf952989dd5a157918af96 0 1634365163000 5 connected
3fe7628d7bda14e4b383e9582b07f3bb7a74b469 172.18.8.164:6003@16003 master - 0 1634365164023 3 connected 10923-16383
c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 172.18.8.164:6001@16001 myself,master - 0 1634365163000 1 connected 0-5460

SET/GET

在 6001节点中执行写入和读取,命令如下:

进入容器并连接至集群某个节点

docker exec -it redis-cluster_redis1_1 redis-cli -c -h 172.18.8.164 -p 6001


[root@localhost redis-cluster]# docker exec -it redis-cluster_redis1_1 redis-cli -c -h 172.18.8.164 -p 6001
172.18.8.164:6001>

# 写入数据
set name mrhelloworld
set aaa 111
set bbb 222
# 读取数据
get name
get aaa
get bbb

第一个命令:set name mrhelloworld

172.18.8.164:6001> set name mrhelloworld
-> Redirected to slot [5798] located at 172.18.8.164:6002
OK
172.18.8.164:6002>

set 命令 set name mrhelloworldname 键根据哈希函数运算以后得到的值为 [5798]

当前集群环境的槽分配情况为:[0-5460] 6001节点[5461-10922] 6002节点[10923-16383] 6003节点

该键的存储就被分配到了 6002节点上;

第二个 set 命令 set aaa 111

172.18.8.164:6002>  set aaa  111
OK

再来看第二个 set 命令 set aaa,这里大家可能会有一些疑问,为什么看不到 aaa 键根据哈希函数运算以后得到的值?

因为刚才重定向至 6002节点插入了数据,此时如果还有数据插入,正好键根据哈希函数运算以后得到的值也还在该节点的范围内,那么直接插入数据即可;

第三个 set 命令 set bbb 222

172.18.8.164:6002>  set bbb  222
-> Redirected to slot [5287] located at 172.18.8.164:6001
OK

接着是第三个 set 命令 set bbbbbb 键根据哈希函数运算以后得到的值为 [5287],所以该键的存储就被分配到了 6001 节点上;

第四个命令 get name

172.18.8.164:6001> get name
-> Redirected to slot [5798] located at 172.18.8.164:6002
"mrhelloworld"
172.18.8.164:6002>

第四个命令 get namename 键根据哈希函数运算以后得到的值为 [5798],被重定向至 6002节点读取;

第五个命令 get aaa

172.18.8.164:6002> get aaa
"111"

第六个命令 get bbb

172.18.8.164:6002> get bbb
-> Redirected to slot [5287] located at 172.18.8.164:6001
"222"

第六个命令 get bbbbbb 键根据哈希函数运算以后得到的值为 [5287],被重定向至 6001 节点读取。

客户端连接

来一波客户端连接操作,随便哪个节点,看看可否通过外部访问 Redis Cluster 集群。

至此使用多机环境基于 Docker Compose 搭建 Redis Cluster 就到这里。

Docker Compose 简化了集群的搭建,之前的方式就需要一个个去操作,而 Docker Compose 只需要一个 docker-compose up/down 命令的操作即可。

集群维护

启动两个节点

规划:一个作为主,一个作为从

为新增的节点,创建文件目录结构

mkdir -p /home/docker-compose/redis-cluster-ext/conf/{6007,6008}/data

准备compose编排文件,并且上传到 /home/docker-compose/redis-cluster-ext 目录

version: '3.5'
services:
 redis1:
  image: publicisworldwide/redis-cluster
  network_mode: host
  restart: always
  volumes:
   - /home/docker-compose/redis-cluster-ext/conf/6007/data:/data
  environment:
   - REDIS_PORT=6007

 redis2:
  image: publicisworldwide/redis-cluster
  network_mode: host
  restart: always
  volumes:
   - /home/docker-compose/redis-cluster-ext/conf/6008/data:/data
  environment:
   - REDIS_PORT=6008

启动两个新的redis节点

[root@localhost redis-cluster]# cd  /home/docker-compose/redis-cluster-ext
[root@localhost redis-cluster-ext]# docker-compose  up -d
Creating redis-cluster-ext_redis8_1 ... done
Creating redis-cluster-ext_redis7_1 ... done


添加一个主节点

通过任意容器的shell终端,都可以执行 --cluster add-node 指令,增加一个新的节点,如 6007节点

docker exec -it redis-cluster_redis1_1 redis-cli --cluster  add-node 127.0.0.1:6007 127.0.0.1:6001  


第一个参数为新增加的节点的IP和端口,第二个参数为任意一个已经存在的节点的IP和端口。

[root@localhost redis-cluster-ext]# docker exec -it redis-cluster_redis1_1 redis-cli --cluster  add-node 127.0.0.1:6007 127.0.0.1:6001
>>> Adding node 127.0.0.1:6007 to cluster 127.0.0.1:6001
>>> Performing Cluster Check (using node 127.0.0.1:6001)
M: c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 127.0.0.1:6001
   slots:[0-5460] (5461 slots) master
   1 additional replica(s)
S: a212e28165b809b4c75f95ddc986033c599f3efb 172.18.8.164:6006
   slots: (0 slots) slave
   replicates 3fe7628d7bda14e4b383e9582b07f3bb7a74b469
M: c15a7801623ee5ebe3cf952989dd5a157918af96 172.18.8.164:6002
   slots:[5461-10922] (5462 slots) master
   1 additional replica(s)
S: 5e74257b26eb149f25c3d54aef86a4d2b10269ca 172.18.8.164:6004
   slots: (0 slots) slave
   replicates c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd
S: 8fb7f7f904ad1c960714d8ddb9ad9bca2b43be1c 172.18.8.164:6005
   slots: (0 slots) slave
   replicates c15a7801623ee5ebe3cf952989dd5a157918af96
M: 3fe7628d7bda14e4b383e9582b07f3bb7a74b469 172.18.8.164:6003
   slots:[10923-16383] (5461 slots) master
   1 additional replica(s)
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
>>> Send CLUSTER MEET to node 127.0.0.1:6007 to make it join the cluster.
[OK] New node added correctly.

查看集群信息

此时该新节点已经成为集群的一份子

docker exec -it redis-cluster-ext_redis7_1 redis-cli -c -h 172.18.8.164 -p 6007

cluster nodes
5e74257b26eb149f25c3d54aef86a4d2b10269ca 172.18.8.164:6004@16004 slave c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 0 1634369547601 1 connected
9db28b4a0fffaa5b7266c3fcf30cbb11519073d4 127.0.0.1:6007@16007 myself,master - 0 1634369546000 0 connected
c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 127.0.0.1:6001@16001 master - 0 1634369547902 1 connected 0-5460
3fe7628d7bda14e4b383e9582b07f3bb7a74b469 172.18.8.164:6003@16003 master - 0 1634369546000 3 connected 10923-16383
8fb7f7f904ad1c960714d8ddb9ad9bca2b43be1c 172.18.8.164:6005@16005 slave c15a7801623ee5ebe3cf952989dd5a157918af96 0 1634369546900 2 connected
a212e28165b809b4c75f95ddc986033c599f3efb 172.18.8.164:6006@16006 slave 3fe7628d7bda14e4b383e9582b07f3bb7a74b469 0 1634369546000 3 connected
c15a7801623ee5ebe3cf952989dd5a157918af96 172.18.8.164:6002@16002 master - 0 1634369546000 2 connected 5461-10922

但是该节点没有包含任何的哈希槽,所以没有数据会存到该主节点。

我们可以通过上面的集群重新分片给该节点分配哈希槽,那么该节点就成为了一个真正的主节点了。

添加从节点到集群

跟添加主节点一样添加一个节点6008,然后连接上该节点并执行如下命令

通过任意容器的shell终端,都可以执行 --cluster add-node 指令,增加一个新的节点,如 6007节点

docker exec -it redis-cluster_redis1_1 redis-cli --cluster  add-node 127.0.0.1:6008 127.0.0.1:6007 


第一个参数为新增加的节点的IP和端口,第二个参数为任意一个已经存在的节点的IP和端口。

[root@localhost redis-cluster-ext]# docker exec -it redis-cluster_redis1_1 redis-cli --cluster  add-node 127.0.0.1:6008 127.0.0.1:6007
>>> Adding node 127.0.0.1:6008 to cluster 127.0.0.1:6007
>>> Performing Cluster Check (using node 127.0.0.1:6007)
M: 9db28b4a0fffaa5b7266c3fcf30cbb11519073d4 127.0.0.1:6007
   slots: (0 slots) master
S: 5e74257b26eb149f25c3d54aef86a4d2b10269ca 172.18.8.164:6004
   slots: (0 slots) slave
   replicates c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd
M: c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 127.0.0.1:6001
   slots:[0-5460] (5461 slots) master
   1 additional replica(s)
M: 3fe7628d7bda14e4b383e9582b07f3bb7a74b469 172.18.8.164:6003
   slots:[10923-16383] (5461 slots) master
   1 additional replica(s)
S: 8fb7f7f904ad1c960714d8ddb9ad9bca2b43be1c 172.18.8.164:6005
   slots: (0 slots) slave
   replicates c15a7801623ee5ebe3cf952989dd5a157918af96
S: a212e28165b809b4c75f95ddc986033c599f3efb 172.18.8.164:6006
   slots: (0 slots) slave
   replicates 3fe7628d7bda14e4b383e9582b07f3bb7a74b469
M: c15a7801623ee5ebe3cf952989dd5a157918af96 172.18.8.164:6002
   slots:[5461-10922] (5462 slots) master
   1 additional replica(s)
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
>>> Send CLUSTER MEET to node 127.0.0.1:6008 to make it join the cluster.
[OK] New node added correctly.


测试一下:

[root@localhost redis-cluster-ext]# docker exec -it redis-cluster-ext_redis7_1 redis-cli -c -h 172.18.8.164 -p 6008
172.18.8.164:6008> cluster replicate 9db28b4a0fffaa5b7266c3fcf30cbb11519073d4
OK

设置主从关系

连接6008,成为 6007的从节点

命令的格式

cluster replicate <nodeId>    
    

具体命令如下:

#进入从节点
docker exec -it redis-cluster-ext_redis8_1 redis-cli -c -h 172.18.8.164 -p 6008

cluster replicate 9db28b4a0fffaa5b7266c3fcf30cbb11519073d4

这样就可以指定该节点成为哪个节点的从节点。

查看一下节点信息

172.18.8.164:6008> cluster nodes
a212e28165b809b4c75f95ddc986033c599f3efb 172.18.8.164:6006@16006 slave 3fe7628d7bda14e4b383e9582b07f3bb7a74b469 0 1634369957000 3 connected
9db28b4a0fffaa5b7266c3fcf30cbb11519073d4 127.0.0.1:6007@16007 master - 0 1634369958089 0 connected
5e74257b26eb149f25c3d54aef86a4d2b10269ca 172.18.8.164:6004@16004 slave c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 0 1634369958000 1 connected
4656b8b2e26dd290928f45f9e4e001123c7ae36d 172.18.8.164:6008@16008 myself,slave 9db28b4a0fffaa5b7266c3fcf30cbb11519073d4 0 1634369958000 7 connected
c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 172.18.8.164:6001@16001 master - 0 1634369958591 1 connected 0-5460
8fb7f7f904ad1c960714d8ddb9ad9bca2b43be1c 172.18.8.164:6005@16005 slave c15a7801623ee5ebe3cf952989dd5a157918af96 0 1634369958000 2 connected
3fe7628d7bda14e4b383e9582b07f3bb7a74b469 172.18.8.164:6003@16003 master - 0 1634369957000 3 connected 10923-16383
c15a7801623ee5ebe3cf952989dd5a157918af96 172.18.8.164:6002@16002 master - 0 1634369959092 2 connected 5461-10922

集群重新分片

如果对默认的平均分配不满意,我们可以对集群进行重新分片。

执行如下命令,只需要指定集群中的其中一个节点地址即可,它会自动找到集群中的其他节点。

(如果设置了密码则需要加上 -a ,没有密码则不需要,后面的命令我会省略这个,设置了密码的自己加上就好)。

重新分片的命令的格式:

redis-cli -a <password> --cluster reshard ip:port

重新分片的命令式:

docker exec -it redis-cluster-ext_redis7_1  redis-cli  --cluster reshard 172.18.8.164:6001

输入你想重新分配的哈希槽数量

docker exec -it redis-cluster-ext_redis7_1How many slots do you want to move (from 1 to 16384)?  1024

输入你想接收这些哈希槽的节点ID

What is the receiving node ID?  6007的id

输入想从哪个节点移动槽点,选择all表示所有其他节点,也可以依次输入节点ID,以done结束。

Please enter all the source node IDs.
  Type 'all' to use all the nodes as source nodes for the hash slots.
  Type 'done' once you entered all the source nodes IDs.
Source node #1:   6001的id

输入yes执行重新分片

省略常常的日志,

查看集群信息

此时该新节点已经成为集群的一份子

docker exec -it redis-cluster-ext_redis7_1 redis-cli -c -h 172.18.8.164 -p 6007

cluster nodes
172.18.8.164:6007> cluster nodes
4656b8b2e26dd290928f45f9e4e001123c7ae36d 127.0.0.1:6008@16008 slave 9db28b4a0fffaa5b7266c3fcf30cbb11519073d4 0 1634370982594 8 connected
5e74257b26eb149f25c3d54aef86a4d2b10269ca 172.18.8.164:6004@16004 slave c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 0 1634370983000 1 connected
9db28b4a0fffaa5b7266c3fcf30cbb11519073d4 127.0.0.1:6007@16007 myself,master - 0 1634370981000 8 connected 0-1023
c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 127.0.0.1:6001@16001 master - 0 1634370982594 1 connected 1024-5460
3fe7628d7bda14e4b383e9582b07f3bb7a74b469 172.18.8.164:6003@16003 master - 0 1634370982994 3 connected 10923-16383
8fb7f7f904ad1c960714d8ddb9ad9bca2b43be1c 172.18.8.164:6005@16005 slave c15a7801623ee5ebe3cf952989dd5a157918af96 0 1634370982594 2 connected
a212e28165b809b4c75f95ddc986033c599f3efb 172.18.8.164:6006@16006 slave 3fe7628d7bda14e4b383e9582b07f3bb7a74b469 0 1634370983997 3 connected
c15a7801623ee5ebe3cf952989dd5a157918af96 172.18.8.164:6002@16002 master - 0 1634370982594 2 connected 5461-10922

failover故障转移

auto-failover自动故障转移

当运行中的master节点挂掉了,集群会在该master节点的slave节点中选出一个作为新的master节点。

容器停止

docker-compose stop 是停止yaml包含的所有容器

停止6007

docker-compose stop redis7

[root@localhost redis-cluster-ext]# docker-compose stop redis7
Stopping redis-cluster-ext_redis7_1 ... done

查看集群信息

此时该新节点已经成为集群的一份子

docker exec -it redis-cluster-ext_redis8_1 redis-cli -c -h 172.18.8.164 -p 6008

cluster nodes
[root@localhost redis-cluster-ext]# docker exec -it redis-cluster-ext_redis8_1 redis-cli -c -h 172.18.8.164 -p 6008
172.18.8.164:6008> cluster nodes
a212e28165b809b4c75f95ddc986033c599f3efb 172.18.8.164:6006@16006 slave 3fe7628d7bda14e4b383e9582b07f3bb7a74b469 0 1634371307000 3 connected
9db28b4a0fffaa5b7266c3fcf30cbb11519073d4 127.0.0.1:6007@16007 master,fail - 1634371240604 1634371239000 8 disconnected
5e74257b26eb149f25c3d54aef86a4d2b10269ca 172.18.8.164:6004@16004 slave c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 0 1634371307537 1 connected
4656b8b2e26dd290928f45f9e4e001123c7ae36d 172.18.8.164:6008@16008 myself,master - 0 1634371306000 9 connected 0-1023
c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 172.18.8.164:6001@16001 master - 0 1634371307537 1 connected 1024-5460
8fb7f7f904ad1c960714d8ddb9ad9bca2b43be1c 172.18.8.164:6005@16005 slave c15a7801623ee5ebe3cf952989dd5a157918af96 0 1634371308000 2 connected
3fe7628d7bda14e4b383e9582b07f3bb7a74b469 172.18.8.164:6003@16003 master - 0 1634371308542 3 connected 10923-16383
c15a7801623ee5ebe3cf952989dd5a157918af96 172.18.8.164:6002@16002 master - 0 1634371308542 2 connected 5461-10922

重启6007

docker-compose up -d redis7

查看状态,变成了 6008的从节点

172.18.8.164:6008> cluster nodes
a212e28165b809b4c75f95ddc986033c599f3efb 172.18.8.164:6006@16006 slave 3fe7628d7bda14e4b383e9582b07f3bb7a74b469 0 1634371452000 3 connected
9db28b4a0fffaa5b7266c3fcf30cbb11519073d4 127.0.0.1:6007@16007 slave 4656b8b2e26dd290928f45f9e4e001123c7ae36d 0 1634371452531 9 connected
5e74257b26eb149f25c3d54aef86a4d2b10269ca 172.18.8.164:6004@16004 slave c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 0 1634371453535 1 connected
4656b8b2e26dd290928f45f9e4e001123c7ae36d 172.18.8.164:6008@16008 myself,master - 0 1634371453000 9 connected 0-1023
c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 172.18.8.164:6001@16001 master - 0 1634371452000 1 connected 1024-5460
8fb7f7f904ad1c960714d8ddb9ad9bca2b43be1c 172.18.8.164:6005@16005 slave c15a7801623ee5ebe3cf952989dd5a157918af96 0 1634371453234 2 connected
3fe7628d7bda14e4b383e9582b07f3bb7a74b469 172.18.8.164:6003@16003 master - 0 1634371452000 3 connected 10923-16383
c15a7801623ee5ebe3cf952989dd5a157918af96 172.18.8.164:6002@16002 master - 0 1634371452230 2 connected 5461-10922

manu-failover手动故障转移

有的时候在主节点没有任何问题的情况下,强制手动故障转移也是很有必要的,比如想要升级主节点的Redis进程,我们可以通过故障转移将master其转为slave,再进行升级操作来避免对集群的可用性造成很大的影响。

Redis集群使用 cluster failover 命令来进行故障转移,不过要在被转移的主节点的slave从节点上执行该命令

也就是说,使用redis-cli连接slave节点并执行 cluster failover命令进行转移。

现在,6007 为从, 6008为主,在6007上进行故障转移:

连接6007

docker exec -it redis-cluster-ext_redis7_1 redis-cli -c -h 172.18.8.164 -p 6007

执行cluster failover 的结果:

[root@localhost redis-cluster-ext]# docker exec -it redis-cluster-ext_redis7_1 redis-cli -c -h 172.18.8.164 -p 6007
172.18.8.164:6007> cluster failover
OK
172.18.8.164:6007> cluster nodes
8fb7f7f904ad1c960714d8ddb9ad9bca2b43be1c 172.18.8.164:6005@16005 slave c15a7801623ee5ebe3cf952989dd5a157918af96 0 1634371888000 2 connected
c15a7801623ee5ebe3cf952989dd5a157918af96 172.18.8.164:6002@16002 master - 0 1634371888686 2 connected 5461-10922
5e74257b26eb149f25c3d54aef86a4d2b10269ca 172.18.8.164:6004@16004 slave c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 0 1634371888587 1 connected
c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 127.0.0.1:6001@16001 master - 0 1634371887583 1 connected 1024-5460
9db28b4a0fffaa5b7266c3fcf30cbb11519073d4 127.0.0.1:6007@16007 myself,master - 0 1634371888000 10 connected 0-1023
4656b8b2e26dd290928f45f9e4e001123c7ae36d 127.0.0.1:6008@16008 slave 9db28b4a0fffaa5b7266c3fcf30cbb11519073d4 0 1634371887000 10 connected
3fe7628d7bda14e4b383e9582b07f3bb7a74b469 172.18.8.164:6003@16003 master - 0 1634371887000 3 connected 10923-16383
a212e28165b809b4c75f95ddc986033c599f3efb 172.18.8.164:6006@16006 slave 3fe7628d7bda14e4b383e9582b07f3bb7a74b469 0 1634371889189 3 connected


节点的移除

可以使用如下命令来移除节点

./src/redis-cli --cluster del-node 127.0.0.1:7001 <nodeId>

第一个参数是任意一个节点的地址,

第二个参数是你想要移除的节点ID。

移除6008

 docker exec -it  redis-cluster-ext_redis7_1 redis-cli --cluster  del-node  172.18.8.164:6007 4656b8b2e26dd290928f45f9e4e001123c7ae36d

结果:

[root@localhost redis-cluster-ext]#  docker exec -it  redis-cluster-ext_redis7_1 redis-cli --cluster  del-node  172.18.8.164:6007 4656b8b2e26dd290928f45f9e4e001123c7ae36d
>>> Removing node 4656b8b2e26dd290928f45f9e4e001123c7ae36d from cluster 172.18.8.164:6007
>>> Sending CLUSTER FORGET messages to the cluster...
>>> SHUTDOWN the node.

如果是移除主节点,需要确保这个节点是空的,如果不是空的,则需要将这个节点上的数据重新分配到其他节点上。

Redis Cluster基本架构

数据分片架构

在单个的 redis节点中,我们都知道redis把数据已 k-v 结构存储在内存中,使得 redis 对数据的读写非常之快。

Redis Cluster 是去中心化的,它将所有数据分区存储。也就是说当多个 Redis 节点搭建成集群后,每个节点只负责自己应该管理的那部分数据,相互之间存储的数据是不同的。

Redis Cluster 将全部的键空间划分为16384块,每一块空间称之为槽(slot),又将这些槽及槽所对应的 k-v 划分给集群中的每个主节点负责。

3个节点的Redis集群虚拟槽如下图:

在这里插入图片描述

3个节点的Redis集群虚拟槽分片结果:

[root@localhost redis-cluster]# docker exec -it redis-cluster_redis1_1 redis-cli --cluster check 172.18.8.164:6001
172.18.8.164:6001 (c4cfd72f...) -> 0 keys | 5461 slots | 1 slaves.
172.18.8.164:6002 (c15a7801...) -> 0 keys | 5462 slots | 1 slaves.
172.18.8.164:6003 (3fe7628d...) -> 0 keys | 5461 slots | 1 slaves.
[OK] 0 keys in 3 masters.
0.00 keys per slot on average.
>>> Performing Cluster Check (using node 172.18.8.164:6001)
M: c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 172.18.8.164:6001
   slots:[0-5460] (5461 slots) master
   1 additional replica(s)
S: a212e28165b809b4c75f95ddc986033c599f3efb 172.18.8.164:6006
   slots: (0 slots) slave
   replicates 3fe7628d7bda14e4b383e9582b07f3bb7a74b469
M: c15a7801623ee5ebe3cf952989dd5a157918af96 172.18.8.164:6002
   slots:[5461-10922] (5462 slots) master
   1 additional replica(s)
S: 5e74257b26eb149f25c3d54aef86a4d2b10269ca 172.18.8.164:6004
   slots: (0 slots) slave
   replicates c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd
S: 8fb7f7f904ad1c960714d8ddb9ad9bca2b43be1c 172.18.8.164:6005
   slots: (0 slots) slave
   replicates c15a7801623ee5ebe3cf952989dd5a157918af96
M: 3fe7628d7bda14e4b383e9582b07f3bb7a74b469 172.18.8.164:6003
   slots:[10923-16383] (5461 slots) master
   1 additional replica(s)
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.

key -> slot 的算法选择

key -> slot 的算法选择上,Redis Cluster 选择的算法是 hash(key) mod 16383,即使用CRC16算法对key进行hash,然后再对16383取模,结果便是对应的slot。

hash(key) mod 16383

1 keyhash= hash(key) 
2 slot= keyhash  % 16383

把16384个槽平均分配给节点进行管理,每个节点只能对自己负责的槽进行读写操作

由于每个节点之间都彼此通信,每个节点都知道另外节点负责管理的槽范围

img

客户端访问任意节点时,对数据key按照CRC16规则进行hash运算,然后对运算结果对16383进行取作,如果余数在当前访问的节点管理的槽范围内,则直接返回对应的数据

节点之间的漫游

如果不在当前节点负责管理的槽范围内,则会告诉客户端去哪个节点获取数据,由客户端去正确的节点获取数据

img

节点间的通信架构

集群中会有多个节点,每个节点负责一部分slot以及对应的k-v数据,并且通过直连具体节点的方式与客户端通信。

那么问题来了,你向我这里请求一个key的value,这个key对应的slot并不归我负责,但我又要需要告诉你MOVED到目标节点,我如何知道这个目标节点是谁呢?

Redis Cluster使用Gossip协议维护节点的元数据信息,这种协议是P2P模式的,主要指责就是信息交换。

节点间不停地去交换彼此的元数据信息,那么总会在一段时间后,大家都知道彼此是谁,负责哪些数据,是否正常工作等等。

节点间信息交换是依赖于彼此发出的Gossip消息的。

集群的元数据

Cluster中的每个节点都维护一份在自己看来当前整个集群的元数据,主要包括:

  • 当前集群状态
  • 集群中各节点所负责的slots信息,及其migrate状态
  • 集群中各节点的master-slave状态
  • 集群中各节点的存活状态及不可达投票

P2P方式模式的元数据交互协议

回顾: es的元数据,是怎么管理的

Redis集群内采用的是P2P方式模式,没有主节点。并且采用的是Gossip协议。

Gossip协议工作原理就是节点彼此不断通信交换信息,一段时间后所有的节点都会知道集群完整的信息,这种方式类似流言传播。

gossip 协议(gossip protocol)又称 epidemic 协议(epidemic protocol),是基于流行病传播方式的节点或者进程之间信息交换的协议,在分布式系统中被广泛使用,比如我们可以使用 gossip 协议来确保网络中所有节点的数据一样。

gossip protocol 最初是由施乐公司帕洛阿尔托研究中心(Palo Alto Research Center)的研究员艾伦·德默斯(Alan Demers)于1987年创造的。

从 gossip 单词就可以看到,其中文意思是八卦、流言等意思,我们可以想象下绯闻的传播(或者流行病的传播);gossip 协议的工作原理就类似于这个。

Goosip 协议的信息传播和扩散通常需要由种子节点发起。

整个传播过程可能需要一定的时间,由于不能保证某个时刻所有节点都收到消息,但是理论上最终所有节点都会收到消息,因此它是一个最终一致性协议。

Gossip协议的特点

Gossip协议是一个P2P协议,所有写操作可以由不同节点发起,并且同步给其他副本。

Gossip内组成的网络节点都是对等节点,是非结构化网络。

gossip 协议利用一种随机的方式将信息传播到整个网络中,并在一定时间内使得系统内的所有节点数据一致。

Gossip 其实是一种去中心化思路的分布式协议,解决状态在集群中的传播和状态一致性的保证两个问题。

节点间的通讯消息

Redis集群的Gossip消息

Redis集群使用二进制协议进行节点到节点的数据交换,这更适合于使用很少的带宽和处理时间在节点之间交换信息。

Gossip协议的主要职责就是信息交换。

信息交换的载体就是节点彼此发送的Gossip消息。

Redis集群中每个redis实例(可能一台机部署多个实例)会使用两个Tcp端口,

  • 一个用于给客户端(redis-cli或应用程序等)使用的端口,
  • 另一个是用于集群中实例相互通信的内部总线端口,且第二个端口比第一个端口一定大10000。

内部总线端口通信使用特殊Gossip协议,以便实现集群内部高带宽低时延的数据交换。

所以配置redis实例时只需要指明第一个端口就可以了。

所以,每一个Redis群集的节点都需要打开两个TCP连接,由于这两个连接就需要两个端口,分别是用于为客户端提供服务的常规RedisTCP命令端口(例如6379)以及通过将10000和命令端口相加(10000+6379)而获得的端口,就是集群端口(例如16379)。

命令端口和集群总线端口偏移量是固定的,始终为10000。第二个大号端口用于群集总线,即使用二进制协议的节点到节点通信通道。节点使用群集总线进行故障检测,配置更新,故障转移授权等。

客户端不应尝试与群集总线端口通信,为了保证Redis命令端口的正常使用,请确保在防火墙中打开这两个端口,否则Redis群集节点将无法通信。

请注意,为了让Redis群集正常工作,您需要为每个节点:

1、用于与客户端进行通信的普通客户端通信端口(通常为6379)对所有需要到达群集的客户端以及所有其他群集节点(使用客户端端口进行密钥迁移)都是开放的。

2、集群总线端口(客户端端口+10000)必须可从所有其他集群节点访问。

Redis集群常用的Gossip消息可分为:ping消息、pong消息、meet消息、fail消息:

  • meet消息 会通知接收该消息的节点,发送节点要加入当前集群,接收者进行响应。
  • ping消息 是集群中的节点定期向集群中其他节点(部分或全部)发送的连接检测以及信息交换请求,消息包含发送节点信息以及发送节点知道的其他节点信息。
  • pong消息 是在节点接收到meet、ping消息后回复给发送节点的响应消息,告诉发送方本次通信正常,消息包含当前节点状态。
  • fail消息 是在节点认为集群内另外某一节点下线后向集群内所有节点广播的消息。

节点的握手消息

在集群启动的过程中,有一个重要的步骤是 节点握手 ,其本质就是在一个节点上向其他所有节点发送meet消息,消息中包含当前节点的信息(节点id,负责槽位,节点标识等等),接收方会将发送节点信息存储至本地的节点列表中。

当发送者接到客户端发送的CLUSTER MEET命令时,发送者会向接收者 发送MEET消息,请求接收者加入到发送者当前所处的集群里面

消息体中还会包含与发送节点通信的其他节点信息(节点标识、节点id、节点ip、port等),接收方也会解析这部分内容,如果本地节点列表中不存在,则会主动向新节点发送meet消息。

接收方处理完消息后,也会回复pong消息给发送者节点,发送者也会解析pong消息更新本地存储节点信息。

因此,虽然只是在一个节点向其他所有节点发送meet消息,最后所有节点都会有其他所有节点的信息。

节点之间会相互通信,meet操作是节点之间完成相互通信的基础,meet操作有一定的频率和规则

img

集群内的心跳消息

集群启动后,集群中各节点也会定时往 其他部分节点 发送ping消息,用来检测:

  • 目标节点是否正常/
  • 以此来检测被选中的节点是否在线/
  • 以及发送自己最新的节点负槽位信息。

接收方同样响应pong消息,由发送方更新本地节点信息。

心跳时机:

Redis节点会记录其向每一个节点上一次发出ping和收到pong的时间,心跳发送时机与这两个值有关。

通过下面的方式既能保证及时更新集群状态,又不至于使心跳数过多,集群的周期性执行clusterCron函数,每秒执行10次,100ms执行一次:

  • 每次clusterCron向所有未建立链接的节点发送ping或meet
  • 每1秒(10次当中某次)从所有已知节点中随机选取5个,向其中上次收到pong最久远的一个发送ping
  • 每次Cron向收到pong超过timeout/2的节点发送ping
  • 收到ping或meet,立即回复pong

集群里的每个节点默认每隔一秒钟就会从已知节点列表中随机选出五个节点,然后对这五个节点中最长时间没有发送过PING消息的节点发送PING消息。

除此之外,如果节点A最后一次收到节点B发送的PONG消息的时间,距离当前时间已经超过了节点A的cluster-node-timeout选项设置时长的一半,那么节点A也会向节点B发送PING消息,这可以防止节点A因为长时间没有随机选中节点B作为PING消息的发送对象而导致对节点B的信息更新滞后

serverCron源码如下,感兴趣就看
serverCron{
...
if (server.cluster_enabled) clusterCron();
...
}
 
clusterCron函数执行如下操作:
(1)向其他节点发送MEET消息,将其加入集群;
(2)每1s会随机选择一个节点,发送ping消息;
(3)如果一个节点在超时时间之内仍未收到ping包的响应(cluster-node-timeout配置项指定的时间),则将其
标记为pfail;
(4)检查是否需要进行主从切换,如果需要则执行切换;
(5)检查是否需要进行副本漂移,如果需要,执行副本漂移操作.
 
注意:
a.对于步骤(1),当在一个集群节点A执行CLUSTER MEET ip port命令时,会将“ip:port”指定的节点B加入该集
群中,但该命令执行时只是将B的“ip:port”信息保存到A节点中,然后在clusterCron函数中为A节点“ip:port”
指定的B节点建立连接并发送MEET类型的数据包.
 
b.对于步骤(3),Redis集群中节点的故障状态有两种.一种为pfail(Possible failure),当一个节点A未在
指定时间收到另一个节点B对ping包的响应时,A节点会将B节点标记为pfail。另一种是,当大多数Master节点
确认B为pfail之后,就会将B标记为fail. fail状态的节点才会需要执行主从切换.
/* -----------------------------------------------------------------------------
 * CLUSTER cron job
 * -------------------------------------------------------------------------- */
 
/* This is executed 10 times every second */
/* 集群的周期性执行函数,每秒执行10次,100ms执行一次 */
void clusterCron(void) {
    dictIterator *di;
    dictEntry *de;
    int update_state = 0;
    /* 没有从节点的主节点的个数-光杆司令的个数*/
    int orphaned_masters; /* How many masters there are without ok slaves. */
    /* 所有从节点从属的主节点个数 */
    int max_slaves; /* Max number of ok slaves for a single master. */
    /* 如果myself是从节点,该从节点对应的主节点下有多少个主节点 */
    int this_slaves; /* Number of ok slaves for our master (if we are slave). */
    
    mstime_t min_pong = 0, now = mstime();
    clusterNode *min_pong_node = NULL;
    /* 局部静态变量,表示该函数执行了多少次 */
    static unsigned long long iteration = 0;
    mstime_t handshake_timeout;
    /* 每执行一次,对iteration做加加的操作 */
    iteration++; /* Number of times this function was called so far. */
 
    /* We want to take myself->ip in sync with the cluster-announce-ip option.
     * The option can be set at runtime via CONFIG SET, so we periodically check
     * if the option changed to reflect this into myself->ip. */
    /*
       我们想要将myself->ip设置地与cluster-announce-ip配置中的是一致的.
       这个配置是可以在运行时的时候通过CONFIG SET来改变的,所以我们间断性地
       检测这个选项配置是否是否能够真实地被写入到myself->ip中.
     */
    {
        static char *prev_ip = NULL;
        char *curr_ip = server.cluster_announce_ip;
        int changed = 0;
 
        if (prev_ip == NULL && curr_ip != NULL) changed = 1;
        else if (prev_ip != NULL && curr_ip == NULL) changed = 1;
        else if (prev_ip && curr_ip && strcmp(prev_ip,curr_ip)) changed = 1;
 
        if (changed) {
            if (prev_ip) zfree(prev_ip);
            prev_ip = curr_ip;
 
            if (curr_ip) {
                /* We always take a copy of the previous IP address, by
                 * duplicating the string. This way later we can check if
                 * the address really changed. */
                prev_ip = zstrdup(prev_ip);
                strncpy(myself->ip,server.cluster_announce_ip,NET_IP_STR_LEN);
                myself->ip[NET_IP_STR_LEN-1] = '\0';
            } else {
                myself->ip[0] = '\0'; /* Force autodetection. */
            }
        }
    }
 
    /* The handshake timeout is the time after which a handshake node that was
     * not turned into a normal node is removed from the nodes. Usually it is
     * just the NODE_TIMEOUT value, but when NODE_TIMEOUT is too small we use
     * the value of 1 second. */
    /*  获取握手的超时时间,如果事件太短以至于小于1秒的话,就将其设置成1秒 */
    handshake_timeout = server.cluster_node_timeout;
    if (handshake_timeout < 1000) handshake_timeout = 1000;
 
    /* Update myself flags. */
    /* 更新当前节点的标志 */
    clusterUpdateMyselfFlags();
 
    /* Check if we have disconnected nodes and re-establish the connection.
     * Also update a few stats while we are here, that can be used to make
     * better decisions in other part of the code. */
    
    /* 获取安全迭代器 */
    di = dictGetSafeIterator(server.cluster->nodes);
     /* 初始化stats_pfail_nodes(状态为pfail的节点的个数)为0 */
    server.cluster->stats_pfail_nodes = 0;
    /* 遍历所有集群中的节点,如果有未建立连接的节点,那么发送PING或PONG消息,建立连接 */
    while((de = dictNext(di)) != NULL) {
        /* 获取节点 */
        clusterNode *node = dictGetVal(de);
 
        /* Not interested in reconnecting the link with myself or nodes
         * for which we have no address. */
        /* 对于是自己的节点和没有地址的节点不感兴趣直接跳过 */
        if (node->flags & (CLUSTER_NODE_MYSELF|CLUSTER_NODE_NOADDR)) continue;
        /* 遇到状态是CLUSTER_NODE_PFAIL的节点,对stats_pfail_nodes统计的变量加1,
           CLUSTER_NODE_PFAIL是疑似下线的节点
        */
        if (node->flags & CLUSTER_NODE_PFAIL)
            server.cluster->stats_pfail_nodes++;
 
        /* A Node in HANDSHAKE state has a limited lifespan equal to the
         * configured node timeout. */
        /* 
            如果node节点处于握手状态,但是从建立连接开始到现在已经超时,
            那么从集群中删除该节点,遍历下一个节点
        */
        if (nodeInHandshake(node) && now - node->ctime > handshake_timeout) {
            clusterDelNode(node);
            continue;
        }
        /* 如果节点的连接对象为空 */
        if (node->link == NULL) {
            /* 为节点创建一个连接对象 */
            clusterLink *link = createClusterLink(node);
            /* 
            通过判断tls_cluster的信息来判断是调用connCreateTLS还是调用connCreateSocket
            来创建连接
            */
            link->conn = server.tls_cluster ? connCreateTLS() : connCreateSocket();
            /* 将link设置为link->conn这个连接的私有信息 */
            connSetPrivateData(link->conn, link);
            /* 建立当前节点与node这个节点的连接 */
            if (connConnect(link->conn, node->ip, node->cport, NET_FIRST_BIND_ADDR,
                        clusterLinkConnectHandler) == -1) {
                /* We got a synchronous error from connect before
                 * clusterSendPing() had a chance to be called.
                 * If node->ping_sent is zero, failure detection can't work,
                 * so we claim we actually sent a ping now (that will
                 * be really sent as soon as the link is obtained). */
                /*
                    如果ping_sent【最近一次发送PING的时间】为0,察觉故障无法执行,
                    因此要设置发送PING的时间,当建立连接后会真正的的发送PING命令,
                    如果连接出错,那么跳过该节点.
                */
                
                if (node->ping_sent == 0) node->ping_sent = mstime();
                serverLog(LL_DEBUG, "Unable to connect to "
                    "Cluster Node [%s]:%d -> %s", node->ip,
                    node->cport, server.neterr);
                /* 释放节点,继续循环 */
                freeClusterLink(link);
                continue;
            }
            /* 为node设置连接对象 */
            node->link = link;
        }
    }
    /* 释放安全迭代器 */
    dictReleaseIterator(di);
 
    /* Ping some random node 1 time every 10 iterations, so that we usually ping
     * one random node every second. */
     /*
         在十次执行此函数中有一次会随机PING一些节点,这样我们通常就可以1秒钟能够ping
         到一个随机的节点
     */
    if (!(iteration % 10)) {
        int j;
 
        /* Check a few random nodes and ping the one with the oldest
         * pong_received time. */
        /*
            随机抽查5个节点,向pong_received值最小的发送PING消息
            pong_received【接收到PONG的时间】
         */
        for (j = 0; j < 5; j++) {
            /* 随机抽查一个节点 */
            de = dictGetRandomKey(server.cluster->nodes);
            clusterNode *this = dictGetVal(de);
 
            /* Don't ping nodes disconnected or with a ping currently active. */
            /* 跳过无连接或已经发送过PING的节点 */
            if (this->link == NULL || this->ping_sent != 0) continue;
            /*  跳过myself节点和处于握手状态的节点 */
            if (this->flags & (CLUSTER_NODE_MYSELF|CLUSTER_NODE_HANDSHAKE))
                continue;
            
            /* 需要再研究,这里是什么意思? */
            /* 当min_pong_node为NULL或者min_pong大于当前节点收到的pong的时间的情况下 */
            /* menwen-查找出这个5个随机抽查的节点,接收到PONG回复过去最久的节点 */
            if (min_pong_node == NULL || min_pong > this->pong_received) {
                min_pong_node = this;
                min_pong = this->pong_received;
            }
        }
        /* 如果min_pong_node不为NULL,
           向接收到PONG回复过去最久的节点发送PING消息,判断是否可达
         */
        if (min_pong_node) {
            serverLog(LL_DEBUG,"Pinging node %.40s", min_pong_node->name);
            clusterSendPing(min_pong_node->link, CLUSTERMSG_TYPE_PING);
        }
    }
 
    /* Iterate nodes to check if we need to flag something as failing.
     * This loop is also responsible to:
     * 1) Check if there are orphaned masters (masters without non failing
     *    slaves).
     * 2) Count the max number of non failing slaves for a single master.
     * 3) Count the number of slaves for our master, if we are a slave. */
    /*
        迭代所有的节点,检查是否需要标记某个节点下线的状态:
        (1)检查是否有孤立的主节点(主节点的从节点全部下线);
        (2)计算单个主节点没下线从节点的最大个数;
        (3)如果myself是从节点,计算该从节点的主节点有多少个从节点.
        追加注释:
        (1)孤立的主节点个数用orphaned_masters记录;
        (2)计算单个主节点没下线从节点的最大个数用max_slaves记录;
        (3)如果myself是从节点,计算该从节点的主节点有多少个从节点用this_slaves记录.
    */
    orphaned_masters = 0;
    max_slaves = 0;
    this_slaves = 0;
    di = dictGetSafeIterator(server.cluster->nodes);
    while((de = dictNext(di)) != NULL) {
        /* 迭代所有的节点 */
        clusterNode *node = dictGetVal(de);
        now = mstime(); /* Use an updated time at every iteration. */
        /* 跳过myself节点,无地址NOADDR节点,和处于握手状态的节点 */
        if (node->flags &
            (CLUSTER_NODE_MYSELF|CLUSTER_NODE_NOADDR|CLUSTER_NODE_HANDSHAKE))
                continue;
 
        /* Orphaned master check, useful only if the current instance
         * is a slave that may migrate to another master. */
        /*
           对无从节点的主节点进行判断,仅仅在当前节点是一个可能快要变成另外一个主节点
           的时候有效
        */
        /* 如果myself是从节点并且node节点是主节点并且该主节点不处于下线状态 */
        if (nodeIsSlave(myself) && nodeIsMaster(node) && !nodeFailed(node)) {
            
            /* 获取node主节点有多少个正常的从节点*/
            int okslaves = clusterCountNonFailingSlaves(node);
 
            /* A master is orphaned if it is serving a non-zero number of
             * slots, have no working slaves, but used to have at least one
             * slave, or failed over a master that used to have slaves. */
             /*
              node主节点没有ok的从节点,
              并且node节点负责有槽位,
              并且node节点指定了槽迁移标识
              */
      
            if (okslaves == 0 && node->numslots > 0 &&
                node->flags & CLUSTER_NODE_MIGRATE_TO)
            {
                   /* 孤立的主节点数加1,光杆司令的数量加一 */
                   orphaned_masters++;
            }
            /* 更新一个主节点最多ok从节点的数量 */
            if (okslaves > max_slaves) max_slaves = okslaves;
            /* 如果myself是从节点, 并且从属于当前node主节点,
               更新该从节点的主节点有多少个从节点的值 
            */
            if (nodeIsSlave(myself) && myself->slaveof == node)
                this_slaves = okslaves;
        }
 
        /* If we are not receiving any data for more than half the cluster
         * timeout, reconnect the link: maybe there is a connection
         * issue even if the node is alive. */
        /*
            如果等待PONG回复的时间超过cluster_node_timeout的一半,则重新建立连接.
           即使节点正常,但是它的连接出问题
        */
        /* 计算ping延迟和数据延迟 */
        mstime_t ping_delay = now - node->ping_sent;
        mstime_t data_delay = now - node->data_received;
      
        /* 如果node->link不为NULL(表明是连接着的)
           且server.cluster_node_timeout不为0(表明还没有重连)
           且node->ping_sent不为0(表明节点还在等待PONG回复)
           且node->pong_received小于node->ping_sent(表明节点仍然在等待PONG回复)
           且ping_delay > server.cluster_node_timeout/2(表明等到PONG回复的时间已经超过了 
           timeout/2)
           且data_delay > server.cluster_node_timeout/2(表明现在已经超过timeout/2的时间 
           没有看到数据的传送)
         */
        if (node->link && /* is connected */
            now - node->link->ctime >
            server.cluster_node_timeout && /* was not already reconnected */
            node->ping_sent && /* we already sent a ping */
            node->pong_received < node->ping_sent && /* still waiting pong */
            /* and we are waiting for the pong more than timeout/2 */
            ping_delay > server.cluster_node_timeout/2 &&
            /* and in such interval we are not seeing any traffic at all. */
            data_delay > server.cluster_node_timeout/2)
        {
            /* Disconnect the link, it will be reconnected automatically. */
            /* 释放连接,等待下个周期的自动重连 */
            freeClusterLink(node->link);
        }
 
        /* If we have currently no active ping in this instance, and the
         * received PONG is older than half the cluster timeout, send
         * a new ping now, to ensure all the nodes are pinged without
         * a too big delay. */
 
        /* 如果当前没有发送PING消息,并且在一定时间内也没有收到PONG回复 */
        if (node->link &&
            node->ping_sent == 0 &&
            (now - node->pong_received) > server.cluster_node_timeout/2)
        {
            /*  给node节点发送一个PING消息 */
            clusterSendPing(node->link, CLUSTERMSG_TYPE_PING);
            continue;
        }
 
        /* If we are a master and one of the slaves requested a manual
         * failover, ping it continuously. */
         /*
            如果当前节点是一个主节点且有从节点请求手动故障转移,那么就持续
            地PING它
         */
         /*
            mf_end-如果为0,表示没有正在进行手动的故障转移.否则表示手动故障转移的时间限制.
            如果有从节点手动请求故障转移且当前节点是主节点且当前节点是手动请求故障转移的
            节点且当前节点的节点不为NULL
         */
        if (server.cluster->mf_end &&
            nodeIsMaster(myself) &&
            server.cluster->mf_slave == node &&
            node->link)
        {
            clusterSendPing(node->link, CLUSTERMSG_TYPE_PING);
            continue;
        }
 
        /* Check only if we have an active ping for this instance. */
        /* 
           如果当前还没有发送PING消息,则跳过,
           只有发送了PING消息之后,才会执行以下操作
         */
        if (node->ping_sent == 0) continue;
 
        /* Check if this node looks unreachable.
         * Note that if we already received the PONG, then node->ping_sent
         * is zero, so can't reach this code at all, so we don't risk of
         * checking for a PONG delay if we didn't sent the PING.
         *
         * We also consider every incoming data as proof of liveness, since
         * our cluster bus link is also used for data: under heavy data
         * load pong delays are possible. */
         /*
            检查一下当前的节点是否看起来不可到达.
            要注意一下如果我们之前就有收到过PING,那么node->ping_sent这个字段是0,
            所以不可能到达这个状态.所以我们不想在没有发送PING消息的情况下冒着一定
            的风险去检测PONG回复的延迟.
         */
 
 
        /*取ping_delay和data_delay中较小的值作为节点的延迟 */
        mstime_t node_delay = (ping_delay < data_delay) ? ping_delay :
                                                          data_delay;
        /* 如果节点的延迟超过了配置文件中设置的cluster-node-timeout
         */
        if (node_delay > server.cluster_node_timeout) {
            /* Timeout reached. Set the node as possibly failing if it is
             * not already in this state. */
             /*
             */
            if (!(node->flags & (CLUSTER_NODE_PFAIL|CLUSTER_NODE_FAIL))) {
                serverLog(LL_DEBUG,"*** NODE %.40s possibly failing",
                    node->name);
                node->flags |= CLUSTER_NODE_PFAIL;
                update_state = 1;
            }
        }
    }
    dictReleaseIterator(di);
 
    /* If we are a slave node but the replication is still turned off,
     * enable it if we know the address of our master and it appears to
     * be up. */
    /*
          myself->slaveof【pointer to the master node】
          
          如果myself是从节点
          且server.masterhost为NULL
          且myself->slaveof不为NULL
          且myself->slaveof(mysql对应的从节点的指针)的地址是存在的
          那么设置当前服务器的主节点的地址,即IP和端口号.
    */
    
    if (nodeIsSlave(myself) &&
        server.masterhost == NULL &&
        myself->slaveof &&
        nodeHasAddr(myself->slaveof))
    {
        replicationSetMaster(myself->slaveof->ip, myself->slaveof->port);
    }
 
    /* Abourt a manual failover if the timeout is reached. */
    /* 终止一个超时的手动故障转移操作 */
    manualFailoverCheckTimeout();
    
    /* 如果当前节点是从节点 */
    if (nodeIsSlave(myself)) {
        
        /* 设置手动故障转移的状态 */
        clusterHandleManualFailover();
         
        /* 
          如果当前节点没有被设置成不允许进行故障转移,那么
          调用clusterHandleSlaveFailover执行从节点的自动或手动故障转移
        */
        if (!(server.cluster_module_flags & CLUSTER_MODULE_FLAG_NO_FAILOVER))
            clusterHandleSlaveFailover();
        /* If there are orphaned slaves, and we are a slave among the masters
         * with the max number of non-failing slaves, consider migrating to
         * the orphaned masters. Note that it does not make sense to try
         * a migration if there is no master with at least *two* working
         * slaves. */
        if (orphaned_masters && max_slaves >= 2 && this_slaves == max_slaves)
            clusterHandleSlaveMigration(max_slaves);
    }
    /* 
       如果存在孤立的主节点,并且集群中的某一主节点有超过2个正常的从节点,
       并且该主节点正好是myself节点的主节点
    */
    if (update_state || server.cluster->state == CLUSTER_FAIL)
        /* 更新集群状态 */
        clusterUpdateState();
}
心跳数据
  • Header,发送者自己的信息
    • 所负责slots的信息
    • 主从信息
    • ip port信息
    • 状态信息
  • Gossip,发送者所了解的部分其他节点的信息
    • ping_sent, pong_received
    • ip, port信息
    • 状态信息,比如发送者认为该节点已经不可达,会在状态信息中标记其为PFAIL或FAIL

考虑到频繁地交换信息会加重带宽(集群节点越多越明显)和计算的负担,Redis Cluster内部的定时任务每秒执行10次,每100毫秒一次,每次遍历本地节点列表,对最近一次接受到pong消息时间大于cluster_node_timeout/2的节点立马发送ping消息,此外每秒随机找5个节点,选里面最久没有通信的节点发送ping消息。

同时 ping 消息的消息投携带自身节点信息,消息体只会携带1/10的其他节点信息,避免消息过大导致通信成本过高。

cluster_node_timeout 参数影响发送消息的节点数量,调整要综合考虑故障转移、槽信息更新、新节点发现速度等方面。

一般带宽资源特别紧张时,可以适当调大一点这个参数,降低通信成本。

fail消息

当集群里的节点A将节点B标记为已下线(FAIL)时,节点A将向集群广播一条关于节点B的FAIL消息,所有接收到这条FAIL消息的节点都会将节点B标记为已下线

fail消息演示案例

举个例子,对于包含7000、7001、7002、7003四个主节点的集群来说:

  • 如果主节点7001发现主节点7000已下线,那么主节点7001将向主节点7002和主节点7003 发送FAIL消息,其中FAIL消息中包含的节点名字为主节点7000的名字,以此来表示主节点 7000已下线
  • 当主节点7002和主节点7003都接收到主节点7001发送的FAIL消息时,它们也会将主节 点7000标记为已下线
  • 因为这时集群已经有超过一半的主节点认为主节点7000已下线,所以集群剩下的几个主节点可以判断是否需要将该节点标记为下线,又或者开始对主节点7000进行故障转移

下图展示了节点发送和接收FAIL消息的整个过程

img

在集群的节点数量比较大的情况下,单纯使用Gossip协议来传播节点的已下线信息会给节点的信息更新带来一定延迟,因为Gossip协议消息通常需要一段时间才能传播至整个集群,而发送FAIL消息可以让集群里的所有节点立即知道某个主节点已下线,从而尽快判断是 否需要将集群标记为下线,又或者对下线主节点进行故障转移 (slave提升为新Master)

ping 时的节点选择

这个地方,很复杂,能讲清楚的培训机构,全网不多,大家慢慢看看

Redis集群的Gossip协议需要兼顾信息交换实时性和成本开销。

  • ping 时要携带一些元数据,如果很频繁,可能会加重网络负担。因此,Redis集群内节点通信采用固定频率(定时任务每秒执行10次),一般每个节点每秒会执行 10 次 ping,每次会选择 5 个最久没有通信的其它节点。

  • 当然如果发现某个节点通信延时达到了 cluster_node_timeout / 2,那么立即发送 ping,避免数据交换延时过长导致信息严重滞后。

    比如说,两个节点之间都 10 分钟没有交换数据了,那么整个集群处于严重的元数据不一致的情况,就会有问题。所以 cluster_node_timeout 可以调节,如果调得比较大,那么会降低 ping 的频率。

  • 每次 ping,会带上自己节点的信息,还有就是带上 1/10 其它节点的信息,发送出去,进行交换。至少包含 3 个其它节点的信息,最多包含 总节点数减 2 个其它节点的信息。

因此节点每次选择需要通信的节点列表变得非常重要。通信节点选择过多虽然可以做到信息及时交换但是成本过高。节点选择过少会降低集群内所有节点彼此信息交互频率,从而影响故障判定、新节点发现等需求的速度。

ping 时,通信节点选择的规则如图所示:

img

根据通信节点选择的流程可以看出:

消息交换的成本主要体现在单位时间选择发送消息的节点数量和每个消息携带的数据量。

选择发送消息的节点数量

  • 集群内每个节点维护定时任务默认每秒执行10次,每秒会随机选取5个节点找出最久没有通信的节点发送ping消息,用于保证Gossip信息交换的随机性。每100毫秒都会扫描本地节点列表,如果发现节点最近一次接受pong消息的时间大于 cluster_node_timeout / 2,则立刻发送ping消息,防止该节点信息太长时间未更新。

    根据以上规则得出每个节点每秒需要发送ping消息的数量,由此,根据以上规则得出每个节点/每秒需要发送ping消息的数量:

    5 + 10*num(num=node.pong_received>cluster_node_timeout/2 的节点数)

    所以: cluster_node_timeout参数对消息发送的节点数量影响非常大。

  • 当我们的带宽资源紧张时,可以适当调大这个参数,如从默认15秒改为30秒来降低带宽占用率。

  • 过度调大cluster_node_timeout会影响消息交换的频率从而影响故障转移、槽信息更新、新节点发现的速度。

  • 需要根据业务容忍度和资源消耗进行平衡,同时整个集群消息总交换量也跟节点数成正比。

消息数据量

  • 每个ping消息的数据量体现在消息头和消息体中,其中消息头主要占用 空间的字段是myslots[CLUSTER_SLOTS/8],占用2KB,这块空间占用相对固定。
  • 消息体会携带一定数量的其他节点信息用于信息交换。消息体携带数据量跟集群的节点数息息相关,更大的集群每次消息通信的成本也就更高,因此对于Redis集群来说并不是大而全的集群更好。

redis虚拟槽位为什么是16384(2^14)个?

问题1

redis虚拟槽位为什么是16384(2^14)个?而不是 65535 (2^16)个?

问题2

CRC16算法产生的hash值有16bit,该算法可以产生2^16-=65536个值。换句话说,值是分布在0~65535之间。那作者在做mod运算的时候,为什么不mod 65536,而选择 mod 16384?

分片SLOT的计算公式

SLOT=CRC16.crc16(key.getBytes()) % MAX_SLOT

在这里插入图片描述

对于客户端请求的key,根据公式HASH_SLOT=CRC16(key) mod 16384,计算出映射到哪个分片上,然后Redis会去相应的节点进行操作!

在这里插入图片描述

但是可能这个槽并不归随机找的这个节点管,节点如果发现不归自己管,就会返回一个MOVED ERROR通知,引导客户端去正确的节点访问,这个时候客户端就会去正确的节点操作数据。

CRC16算法产生的hash值有16bit,该算法可以产生2^16-=65536个值。换句话说,值是分布在0~65535之间。那作者在做mod运算的时候,为什么不mod 65536,而选择 mod 16384?

redis节点发送心跳包

在redis节点发送心跳包时需要把所有的槽放到这个心跳包里,以便让节点知道当前集群信息,

imgredis cluster 集群 HA 原理和实操(史上最全、面试必备)_第7张图片

交换的数据信息,由消息体和消息头组成。消息体无外乎是一些节点标识啊,IP啊,端口号啊,发送时间啊。

这里不做展开,我们来看消息头,结构如下

imgredis cluster 集群 HA 原理和实操(史上最全、面试必备)_第8张图片

消息头里面有个myslots的char数组,长度为16383/8,这其实是一个bitmap,每一个位代表一个槽,如果该位为1,表示这个槽是属于这个节点的。在消息头中,最占空间的是myslots[CLUSTER_SLOTS/8]

这块(2的十四次方)的大小是:
16384÷8÷1024=2kb

16384=16k,

在发送心跳包时使用char进行bitmap压缩后是2k(2 * 8 (8 bit) * 1024(1k) = 16K)个char,也就是说使用2k个char的空间,能表达16k的槽数。

虽然使用CRC16算法最多可以分配65535(2^16-1)个槽位,65535=65k,压缩后就是8k(8 * 8 (8 bit) * 1024(1k) =65K),也就是说需要需要8k的心跳包,作者认为这样做不太值得;

集群节点越多,心跳包的消息体内携带的数据越多。

如果节点过1000个,也会导致网络拥堵。

因此redis作者,不建议redis cluster节点数量超过1000个。
那么,对于节点数在1000以内的redis cluster集群,16384个槽位够用了。没有必要拓展到65536个。

并且一般情况下一个redis集群不会有超过1000个master节点,所以16k的槽位是个比较合适的选择。

Redis Cluster的高可用架构

要保证高可用的前提是离不开从节点的,一旦某个主节点因为某种原因不可用后,就需要一个一直默默当备胎的从节点顶上来了。

一般在集群搭建时最少都需要6个实例,其中3个实例做主节点,各自负责一部分槽位,另外3个实例各自对应一个主节点做其从节点,对主节点的操作进行复制(对于主从复制的细节,前面已经进行详细说明)。

完整的redis集群架构图( 请参见演示)

要求: 参见演示, 建议边看视频,边自己画一个,加深理解

3个节点的Redis集群虚拟槽分片结果:

[root@localhost redis-cluster]# docker exec -it redis-cluster_redis1_1 redis-cli --cluster check 172.18.8.164:6001
172.18.8.164:6001 (c4cfd72f...) -> 0 keys | 5461 slots | 1 slaves.
172.18.8.164:6002 (c15a7801...) -> 0 keys | 5462 slots | 1 slaves.
172.18.8.164:6003 (3fe7628d...) -> 0 keys | 5461 slots | 1 slaves.
[OK] 0 keys in 3 masters.
0.00 keys per slot on average.
>>> Performing Cluster Check (using node 172.18.8.164:6001)
M: c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd 172.18.8.164:6001
   slots:[0-5460] (5461 slots) master
   1 additional replica(s)
S: a212e28165b809b4c75f95ddc986033c599f3efb 172.18.8.164:6006
   slots: (0 slots) slave
   replicates 3fe7628d7bda14e4b383e9582b07f3bb7a74b469
M: c15a7801623ee5ebe3cf952989dd5a157918af96 172.18.8.164:6002
   slots:[5461-10922] (5462 slots) master
   1 additional replica(s)
S: 5e74257b26eb149f25c3d54aef86a4d2b10269ca 172.18.8.164:6004
   slots: (0 slots) slave
   replicates c4cfd72f7cbc22cd81b701bd4376fabbe3d162bd
S: 8fb7f7f904ad1c960714d8ddb9ad9bca2b43be1c 172.18.8.164:6005
   slots: (0 slots) slave
   replicates c15a7801623ee5ebe3cf952989dd5a157918af96
M: 3fe7628d7bda14e4b383e9582b07f3bb7a74b469 172.18.8.164:6003
   slots:[10923-16383] (5461 slots) master
   1 additional replica(s)
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.

集群中指定主从关系

集群中指定主从关系不再使用slaveof命令,而是使用cluster replicate命令,参数使用节点id。

Redis Cluster在给主节点添加从节点时,不是使用 slaveof 命令,而是通过在从节点上执行命令 :

cluster replicate masterNodeId 。

通过cluster nodes获得几个主节点的节点id后,执行下面的命令为每个从节点指定主节点:

redis-cli -p 7000 cluster replicate be816eba968bc16c884b963d768c945e86ac51ae
redis-cli -p 7001 cluster replicate 788b361563acb175ce8232569347812a12f1fdb4
redis-cli -p 7002 cluster replicate a26f1624a3da3e5197dde267de683d61bb2dcbf1

failover故障发现与转移

当集群内少量节点出现故障时,通过自动故障转移保证集群可以正常对外提供服务。

作为一个完整的集群,每个负责处理槽的节点应该具有从节点,保证当它出现故障时可以自动进行故障转移。

redis集群自身实现了高可用,Redis Cluster通过ping/pong消息实现故障发现:不需要sentinel

首次启动的节点和被分配槽的节点都是主节点,从节点负责复制主节点槽信息和相关的数据。

Redis Cluster通过ping/pong消息不仅能传递节点与槽的对应消息,也能传递其他状态,比如:节点主从状态,节点故障等

failover故障发现与转移总体过程

Cluster的故障发现也是基于节点通信的。

完整的failover故障发现与转移总体过程,

要求: 参见演示, 建议边看视频,边自己画一个,加深理解

每个节点在本地存储有一个节点列表(其他节点信息),列表中每个 节点元素除了存储其ID、ip、port、状态标识(主从角色、是否下线等等)外,还有最后一次向该节点发送ping消息的时间、最后一次接收到该节点的pong消息的时间以及一个保存其他节点对该节点下线传播的报告链表 。

节点与节点间会定时发送ping消息,彼此响应pong消息,成功后都会更新这个时间。

同时每个节点都有定时任务扫描本地节点列表里这两个消息时间,若发现pong响应时间减去ping发送时间超过cluster-node-timeout配置时间后,便会将本地列表中对应节点的状态标识为PFAIL,认为其有可能下线。

cluster-node-timeout默认15秒,该参数用来设置节点间通信的超时时间

节点间通信(ping)时会携带本地节点列表中部分节点信息,如果其中包括标记为PFAIL的节点.

那么在消息接收方解析到该节点时,会找自己本地的节点列表中该节点元素的下线报告链表,看是否已经存在发送节点对于该故障节点的报告,如果有,就更新接收到发送ping消息节点对于故障节点的报告的时间,如果没有,则将本次报告添加进链表。

下线报告链表的每个元素结构只有两部分内容,一个是报告本地这个故障节点的发送节点信息,一个是本地接收到该报告的时间 (存储该时间是因为故障报告是有有效期的,避免误报) 。

由于每个节点的下线报告链表都存在于各自的信息结构中,所以在浏览本地节点列表中每个节点元素时,可以清晰地知道,有其他哪些节点跟我说,兄弟,你正在看的这个节点我觉的凉凉了。

故障报告的有效期是 cluster-node-timeout * 2

消息接收方解析到PFAIL节点,并且更新本地列表中对应节点的故障报告链表后,会去查看该节点的故障报告链表中有效的报告节点是否超过所有主节点数的一半。

  • 如果没超过,便继续解析ping消息;

  • 如果超过,代表 超过半数的节点认为这个节点可能下线了,当前节点就会将PFAIL节点本地的节点信息中的状态标识标记为FAIL ,然后向集群内广播一条fail消息,集群内的所有节点接收到该fail消息后,会把各自本地节点列表中该节点的状态标识修改为FAIL。

在所有节点对其标记为FAIL后,开始故障转移:该FAIL节点对应的从节点就会发起转正流程。

在转正流程完成后,这个节点就会正式下线,等到其恢复后,发现自己的槽已经被分给某个节点,便会将自己转换成这个节点的从节点并且ping集群内其他节点,其他节点接到恢复节点的ping消息后,便会更新其状态标识。

此外,恢复的节点若发现自己的槽还是由自己负责,就会跟其他节点通信,其他主节点发现该节点恢复后,就会拒绝其从节点的选举,最终清除自己的FAIL状态。

故障发现

故障发现就是通过这种模式来实现,分为:

  • 主观下线
  • 客观下线

故障发现也是通过消息传播机制实现的,主要环节包括:

(1)主观下线(pfail)。

集群中每个节点都会定期向其他节点发送ping消息,接收节点回复pong消息作为响应。如果在cluster-node-timeout时间内通信一直失败,则发送节点会认为接收节点存在故障,把接收节点标记为主观下线(pfail)状态。

相当于 自己认为,别人下线了,

(2)客观下线(fail)

当某个节点判断另一个节点主观下线后,相应的节点状态会跟随消息在集群内传播。

当接受节点发现消息体中含有主观下线的节点状态,且发送节点是主节点时,会在本地找到故障节点的ClusterNode结构,更新下线报告链表。

相当于 大家认为,别人下线了

主观下线

某个节点认为另一个节点不可用,‘偏见’,只代表一个节点对另一个节点的判断,不代表所有节点的认知

主观下线流程:

完整主观下线过程,

要求: 参见演示, 建议边看视频,边自己画一个,加深理解

1.节点1定期发送ping消息给节点2
2.如果发送成功,代表节点2正常运行,节点2会响应PONG消息给节点1,节点1更新与节点2的最后通信时间
3.如果发送失败,则节点1与节点2之间的通信异常判断连接,在下一个定时任务周期时,仍然会与节点2发送ping消息
4.如果节点1发现与节点2最后通信时间超过node-timeout,则把节点2标识为pfail状态

完整的客观下线与主观下线流程,

要求: 参见演示, 建议边看视频,边自己画一个,加深理解

客观下线

当半数以上持有槽的主节点都标记某节点主观下线时,可以保证判断的公平性

集群模式下,只有主节点(master)才有读写权限和集群槽的维护权限,从节点(slave)只有复制的权限

客观下线流程:

完整客观下线过程,

要求: 参见演示, 建议边看视频,边自己画一个,加深理解

1.某个节点接收到其他节点发送的ping消息,如果接收到的ping消息中包含了其他pfail节点,这个节点会将主观下线的消息内容添加到自身的故障列表中,故障列表中包含了当前节点接收到的每一个节点对其他节点的状态信息

当某个节点判断另一个节点主观下线后,相应的节点状态会跟随消息在集群内传播。

当接受节点发现消息体中含有主观下线的节点状态且发送节点是主节点时,会在本地找到故障节点的ClusterNode结构,更新下线报告链表。

  • 集群中的节点每次接收到其他节点的pfail状态,都会尝试触发客观下线。首先统计有效的下线报告数量,当下线报告数量大于槽主节点数量一半时,标记对应故障节点为客观下线状态。
  • 向集群广播一条fail消息,通知所有的节点将故障节点标记为客观下线,fail消息的消息体只包含故障节点的ID。通知故障节点的从节点触发故障转移流程。
struct clusterNode { /* 认为是主观下线的clusterNode结构 */
    list *fail_reports; /* 记录了所有其他节点对该节点的下线报告 */
};

只有负责槽的主节点(master节点,而非slave)参与故障发现决策,

因为集群模式下只有处理槽的主节点才负责读写请求和集群槽等关键信息维护,而从节点只进行master 主节点数据和状态信息的复制。

故障列表的 检查周期为:

集群的node-timeout * 2,保证以前的故障消息不会对周期内的故障消息造成影响,保证客观下线的公平性和有效性

Redis节点failover(故障转移、故障恢复)流程

故障节点变为客观下线后,如果下线节点是持有槽的主节点, 则需要在它的slave 从节点中选出一个替换它,从而保证集群的高可用

谁来承担故障恢复的职责:

下线主节点的所有从节点

下线主节点的所有从节点承担故障恢复的义务,当从节点通过内部定时任务发现自身复制的主节点进入客观下线时,将会触发故障恢复流程:

(1) 资格检查

每个从节点都要检查最后与主节点断线时间,判断是否有资格替换故障的主节点。

如果从节点与主节点断线时间超过cluster-node-time * cluster-slave-validity-factor,则当前从节点不具备故障转移资格。

(2)准备选举时间

当从节点符合故障转移资格后,更新触发故障选举的时间,只有到达该时间后才能执行后续流程。

在多个从节点的场景

这里之所以采用延迟触发机制,主要是通过对多个从节点使用不同的延迟选举时间来支持优先级问题。

复制偏移量越大,说明从节点延迟越低,那么它应该具有更高的优先级来替换故障主节点。

复制偏移量越小,说明从节点延迟越高,那么它应该具有更低的优先级来替换故障主节点。

(3)发起选举

当从节点定时任务检测到达故障选举时间(failover_auth_time)到达后,发起选举流程如下:会先更新配置纪元,再在集群内广播选举消息,并记录已发送过消息的状态,保证该从节点在一个配置纪元内只能发起一次选举。

(4)选举投票

只有持有槽的主节点才会处理故障选举消息,因为每个持有槽的节点在一个配置纪元内都有唯一的一张选票,当接到第一个请求投票的从节点消息时回复FAILOVER_AUTH_ACK消息作为投票,之后相同配置纪元内其他从节点的选举消息将忽略。当从节点收集到N/2+1个持有槽的主节点投票时,从节点可以执行替换主机点操作。

(5)替换主节点

当从节点收集到足够的选票之后,触发替换主节点操作:

  • 当前从节点取消复制变为主节点。
  • 执行clusterDelSlot操作撤销故障主节点负责的槽,并执行clusterAddSlot把这些槽委派给自己。
  • 向集群广播自己的pong消息,通知集群内所有的节点当前从节点变为主节点并接管了故障主节点的槽信息。

资格检查

每个从节点都要检查最后与主节点断线时间,判断是否有资格替换故障的主节点。如果从节点与主节点断线时间超过cluster-node-time * cluster-slave-validity-factor,则当前从节点不具备故障转移资格。

  • 对从节点的资格进行检查,只有通过检查的从节点才可以开始进行故障恢复

  • 每个从节点检查与故障主节点的断线时间

  • 超过cluster-node-timeout * cluster-slave-validity-factor数字,则取消资格

  • cluster-node-timeout默认为15秒,cluster-slave-validity-factor默认值为10

  • 如果这两个参数都使用默认值,则每个节点都检查与故障主节点的断线时间,如果超过150秒,则这个节点就没有成为替换主节点的可能性

准备选举时间

当从节点符合故障转移资格后,更新触发故障选举的时间,只有到达该时间后才能执行后续流程。

这里之所以采用延迟触发机制,主要是通过对多个从节点使用不同的延迟选举时间来支持优先级问题。

复制偏移量越大说明从节点延迟越低,那么它应该具有更高的优先级来替换故障主节点。

  • 复制偏移量越大,说明从节点延迟越低,那么它应该具有更高的优先级来替换故障主节点。

  • 复制偏移量越小,说明从节点延迟越高,那么它应该具有更低的优先级来替换故障主节点。

struct clusterState {
    mstime_t failover_auth_time; /* 记录之前或者下次将要执行故障选举时间 */
    int failover_auth_rank; /* 记录当前从节点排名 */
}

使偏移量最大的从节点具备优先级成为主节点的条件

redis cluster 集群 HA 原理和实操(史上最全、面试必备)_第9张图片

发起选举

当从节点定时任务检测到达故障选举时间(failover_auth_time)到达后,发起选举流程如下:

(1).更新配置纪元:

配置纪元是一个只增不减的整数,每个主节点自身维护一个配置纪元 (clusterNode.configEpoch)标示当前主节点的版本,所有主节点的配置纪元都不相等,从节点会复制主节点的配置纪元。

整个集群又维护一个全局的配 置纪元(clusterState.current Epoch),用于记录集群内所有主节点配置纪元 的最大版本。

执行cluster info命令可以查看配置纪元信息。

只要集群发生重要的关键事件,纪元数就会增加,所以在选从的时候需要选择一个纪元数最大的从。

(2).广播选举消息:

在集群内广播选举消息(FAILOVER_AUTH_REQUEST),并记录已发送过消息的状态,保证该从节点在一个配置纪元内只能发起一次选举。

消息 内容如同ping消息只是将type类型变为FAILOVER_AUTH_REQUEST。

配置纪元的主要作用:
  • 标示集群内每个主节点的不同版本和当前集群最大的版本。
  • 每次集群发生重要事件时,这里的重要事件指出现新的主节点(新加入的或者由从节点转换而来),从节点竞争选举。都会递增集群全局的配置纪元并赋值给相关主节点,用于记录这一关键事件。
  • 主节点具有更大的配置纪元代表了更新的集群状态,因此当节点间进行ping/pong消息交换时,如出现slots等关键信息不一致时,以配置纪元更大的一方为准,防止过时的消息状态污染集群。

配置纪元的应用场景有:新节点加入、槽节点映射冲突检测、从节点投票选举冲突检测。

选举投票

只有持有哈希槽的主节点才能参与投票,每个主节点有一票的权利,如集群内有N个主节点,那么只要有一个从节点获得了N/2+1的选票即认为胜出。

故障主节点也算在投票数内,假设集群内节点规模是3主3从,其中有2个主节点部署在一台机器上,当这台机器宕机时,由于从节点无法收集到 3/2+1个主节点选票将导致故障转移失败。

这个问题也适用于故障发现环 节。因此部署集群时所有主节点最少需要部署在3台物理机上才能避免单点问题。

**投票作废:**每个配置纪元代表了一次选举周期,如果在开始投票之后的 cluster-node-timeout*2时间内从节点没有获取足够数量的投票,则本次选举作废。

从节点对配置纪元自增并发起下一轮投票,直到选举成功为止。

redis cluster 集群 HA 原理和实操(史上最全、面试必备)_第10张图片

替换主节点

当从节点收集到足够的选票之后,触发替换主节点操作:

  • 当前从节点取消复制, 变为主节点。
  • 执行clusterDelSlot操作, 撤销故障主节点负责的槽,并执行clusterAddSlot把这些槽委派给自己。
  • 向集群广播自己的pong消息,通知集群内所有的节点当前,从节点变为主节点并接管了故障主节点的槽信息。

故障转移时间预估

  • 主观下线(pfail)识别时间 = cluster-node-timeout , 如果节点1发现与节点2最后通信时间超过node-timeout,则把节点2标识为pfail状态

  • 主观下线状态消息传播时间 <= cluster-node-timeout/2。消息通信机制对超过cluster-node-timeout/2未通信节点会发起ping消息,消息体在选择包含哪些节点时会优先选取下线状态节点,所以通常这段时间内能够收集到半数以上主节点的pfail报告从而完成故障发现。

  • 从节点转移时间 <= 1000毫秒。由于存在延迟发起选举机制,偏移量最大的从节点会最多延迟1秒发起选举。通常第一次选举就会成功,所以从节点执行转移时间在1秒以内。

  • 根据以上分析可以预估出故障转移时间,如下:

    failover-time(毫秒) <= cluster-node-timeout + cluster-node-timeout/2 + 1000

cluster-node-timeout时间设置,需要平衡:

  • 当节点发现与其他节点最后通信时间超过cluster-node-timeout/2时会直接发送ping消息,适当提高cluster-node-timeout可以降低消息发送频率,减少网络IO的流量

  • 但同时cluster-node-timeout还影响故障转移的速度,因此需要根据自身业务场景兼顾二者的平衡。

故障转移演练

对某一个主节点执行kill -9 {pid}来模拟宕机的情况

客户端高可用

客户端高可用方案,包含:

  • 客户端moved重定向和ask重定向
  • smart智能客户端

客户端moved重定向和ask重定向

moved重定向

1.每个节点通过通信都会共享Redis Cluster中槽和集群中对应节点的关系
2.客户端向Redis Cluster的任意节点发送命令,接收命令的节点会根据CRC16规则进行hash运算与16383取余,计算自己的槽和对应节点
3.如果保存数据的槽被分配给当前节点,则去槽中执行命令,并把命令执行结果返回给客户端
4.如果保存数据的槽不在当前节点的管理范围内,则向客户端返回moved重定向异常
5.客户端接收到节点返回的结果,如果是moved异常,则从moved异常中获取目标节点的信息
6.客户端向目标节点发送命令,获取命令执行结果

redis cluster 集群 HA 原理和实操(史上最全、面试必备)_第11张图片

需要注意的是:客户端不会自动找到目标节点执行命令

槽命中:直接返回

redis cluster 集群 HA 原理和实操(史上最全、面试必备)_第12张图片

槽不命中:moved异常

redis cluster 集群 HA 原理和实操(史上最全、面试必备)_第13张图片

ask重定向

redis cluster 集群 HA 原理和实操(史上最全、面试必备)_第14张图片

在对集群进行扩容和缩容时,需要对槽及槽中数据进行迁移

当客户端向某个节点发送命令,节点向客户端返回moved异常,告诉客户端数据对应的槽的节点信息

如果此时正在进行集群扩展或者缩空操作,当客户端向正确的节点发送命令时,槽及槽中数据已经被迁移到别的节点了,就会返回ask,这就是ask重定向机制

redis cluster 集群 HA 原理和实操(史上最全、面试必备)_第15张图片

步骤:

1.客户端向目标节点发送命令,目标节点中的槽已经迁移支别的节点上了,此时目标节点会返回ask转向给客户端
2.客户端向新的节点发送Asking命令给新的节点,然后再次向新节点发送命令
3.新节点执行命令,把命令执行结果返回给客户端

moved异常与ask异常的相同点和不同点

两者都是客户端重定向
moved异常:槽已经确定迁移,即槽已经不在当前节点
ask异常:槽还在迁移中

smart智能客户端

使用智能客户端的首要目标:追求性能

从集群中选一个可运行节点,使用Cluster slots初始化槽和节点映射

将Cluster slots的结果映射在本地,为每个节点创建JedisPool,相当于为每个redis节点都设置一个JedisPool,然后就可以进行数据读写操作

读写数据时的注意事项:

每个JedisPool中缓存了slot和节点node的关系
key和slot的关系:对key进行CRC16规则进行hash后与16383取余得到的结果就是槽
JedisCluster启动时,已经知道key,slot和node之间的关系,可以找到目标节点
JedisCluster对目标节点发送命令,目标节点直接响应给JedisCluster
如果JedisCluster与目标节点连接出错,则JedisCluster会知道连接的节点是一个错误的节点
此时JedisCluster会随机节点发送命令,随机节点返回moved异常给JedisCluster
JedisCluster会重新初始化slot与node节点的缓存关系,然后向新的目标节点发送命令,目标命令执行命令并向JedisCluster响应
如果命令发送次数超过5次,则抛出异常"Too many cluster redirection!"

redis cluster 集群 HA 原理和实操(史上最全、面试必备)_第16张图片

开发运维常见的高可用问题

集群完整性

cluster-require-full-coverage默认为yes,即是否集群中的所有节点都是在线状态且16384个槽都处于服务状态时,集群才会提供服务

集群中16384个槽全部处于服务状态,保证集群完整性

当某个节点故障或者正在故障转移时获取数据会提示:(error)CLUSTERDOWN The cluster is down

建议把cluster-require-full-coverage设置为no

带宽消耗

Redis Cluster节点之间会定期交换Gossip消息,以及做一些心跳检测

官方建议Redis Cluster节点数量不要超过1000个,当集群中节点数量过多时,会产生不容忽视的带宽消耗

消息发送频率:节点发现与其他节点最后通信时间超过cluster-node-timeout /2时,会直接发送PING消息

消息数据量:slots槽数组(2kb空间)和整个集群1/10的状态数据(10个节点状态数据约为1kb)

节点部署的机器规模:集群分布的机器越多且每台机器划分的节点数越均匀,则集群内整体的可用带宽越高

带宽优化:

避免使用'大'集群:避免多业务使用一个集群,大业务可以多集群
cluster-node-timeout:带宽和故障转移速度的均衡
尽量均匀分配到多机器上:保证高可用和带宽

Pub/Sub广播

在任意一个cluster节点执行publish,则发布的消息会在集群中传播,集群中的其他节点都会订阅到消息,这样节点的带宽的开销会很大

publish在集群每个节点广播,加重带宽

解决办法:需要使用Pub/Sub时,为了保证高可用,可以单独开启一套Redis Sentinel

Redis集群的一致性保证

Redis集群不能保证强一致性。

一些已经向客户端确认写成功的操作,会在某些不确定的情况下丢失。

  • 主从节点切换导致的不一致性

  • 集群脑裂、网络问题导致的不一致性

主从节点切换导致的不一致性

面试问题:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-joGUu41A-1647667634997)(./基础设施部署图片/redis-1.jpg)]

主从节点切换导致的不一致性原因

产生写操作丢失的第一个原因,是因为主从节点之间使用了异步的方式来同步数据。

一个写操作是这样一个流程:

  • 1)客户端向主节点B发起写的操作

  • 2)主节点B回应客户端写操作成功

  • 3)主节点B向它的从节点B1,B2,B3同步该写操作

从上面的流程可以看出来,主节点B并没有等从节点B1,B2,B3写完之后再回复客户端这次操作的结果。

所以,如果主节点B在通知客户端写操作成功之后,但同步给从节点之前,主节点B故障了,其中一个没有收到该写操作的从节点会晋升成主节点,该写操作就这样永远丢失了。

就像传统的数据库,在不涉及到分布式的情况下,它每秒写回磁盘。为了提高一致性,可以在写盘完成之后再回复客户端,但这样就要损失性能。这种方式就等于Redis集群使用同步复制的方式。

基本上,在性能和一致性之间,需要一个权衡。

如果真的需要,Redis集群支持同步复制的方式,通过WAIT指令来实现,这可以让丢失写操作的可能性降到很低。

但就算使用了同步复制的方式,Redis集群依然不是强一致性的:

在某些复杂的情况下,比如从节点在与主节点失去连接之后被选为主节点,不一致性还是会发生。

WAIT numslaves timeout

起始版本:3.0.0

**时间复杂度:**O(1)

此命令阻塞当前客户端,直到所有以前的写命令都成功的传输和指定的slaves确认。如果超时,指定以毫秒为单位,即使指定的slaves还没有到达,命令任然返回。

命令始终返回之前写命令发送的slaves的数量,无论是在指定slaves的情况还是达到超时。

注意点:

  1. 当’WAIT’返回时,所有之前的写命令保证接收由WAIT返回的slaves的数量。
  2. 如果命令呗当做事务的一部分发送,该命令不阻塞,而是只尽快返回先前写命令的slaves的数量。
  3. 如果timeout是0那意味着永远阻塞。
  4. 由于WAIT返回的是在失败和成功的情况下的slaves的数量。客户端应该检查返回的slaves的数量是等于或更大的复制水平。

一致性(Consistency and WAIT)

WAIT 不能保证Redis强一致:尽管同步复制是复制状态机的一个部分,但是还需要其他条件。

不过,在sentinel和Redis群集故障转移中,WAIT 能够增强数据的安全性。

如果写操作已经被传送给一个或多个slave节点,当master发生故障我们极大概率(不保证100%)提升一个受到写命令的slave节点为master:不管是Sentinel还是Redis Cluster 都会尝试选slave节点中最优(日志最新)的节点,提升为master。

尽管是选择最优节点,但是仍然会有丢失一个同步写操作可能行。

实现细节

因为引入了部分同步,Redis slave节点在ping主节点时会携带已经处理的复制偏移量。 这被用在多个地方:

  1. 检测超时的slaves
  2. 断开连接后的部分复制
  3. 实现WAIT

WAIT实现的案例中,当客户端执行完一个写命令后,针对每一个复制客户端,Redis会为其记录写命令产生的复制偏移量。当执行命令WAIT时,Redis会检测 slaves节点是否已确认完成该操作或更新的操作。

返回值

integer-reply: 当前连接的写操作会产生日志偏移,该命令会返回已处理至该偏移量的slaves的个数。

例子

> SET foo bar
OK
> WAIT 1 0
(integer) 1
> WAIT 2 1000
(integer) 1

在例子中,第一次调用WAIT并没有使用超时设置,并且设置写命令传输到一个slave节点,返回成功。第二次使用时,我们设置了超时值并要求写命令传输到两个节点。 因为只有一个slave节点有效,1秒后WAIT解除阻塞并返回1–传输成功的slave节点数。

集群脑裂、网络问题导致的不一致性

什么是redis的集群脑裂?

redis的集群脑裂是指因为网络问题,导致redis master节点跟redis slave节点和sentinel集群处于不同的网络分区,此时因为sentinel集群无法感知到master的存在,所以将slave节点提升为master节点。此时存在两个不同的master节点,就像一个大脑分裂成了两个。

集群脑裂问题中,如果客户端还在基于原来的master节点继续写入数据,那么新的master节点将无法同步这些数据,当网络问题解决之后,sentinel集群将原先的master节点降为slave节点,此时再从新的master中同步数据,将会造成大量的数据丢失。

具体来说,这种不一致性发生的情况是这样的:

当客户端与少数的节点(至少含有一个主节点)网络联通,但他们与其他大多数节点网络不通。

比如6个节点,A,B,C是主节点,A1,B1,C1分别是他们的从节点,一个客户端称之为Z。

在这里插入图片描述

当网络出问题时,他们被分成2组网络,组内网络联通,但2组之间的网络不通,假设A,C,A1,B1,C1彼此之间是联通的,另一边,B和Z的网络是联通的。

在这里插入图片描述

Z可以继续往B发起写操作,B也接受Z的写操作。

当网络恢复时,如果这个时间间隔足够短,集群仍然能继续正常工作。如果时间比较长,以致B1在大多数的这边被选为主节点,那刚才Z1发给B的写操作都将丢失。

注意,Z1给B发送写操作是有一个限制的,如果时间长度达到了大多数节点那边可以选出一个新的主节点时,少数这边的所有主节点都不接受写操作。

这个时间的配置,称之为节点超时(node timeout)。

节点超时(node timeout)设置:

对集群来说非常重要:

  • 当达到了这个节点超时的时间之后,主节点被认为已经宕机,可以用它的一个从节点来代替。

  • 同样,在节点超时时,如果主节点依然不能联系到其他主节点,它将进入错误状态,不再接受写操作。

在redis.conf中的参数说明:

cluster-node-timeout :

这是集群中的节点能够失联的最大时间,超过这个时间,该节点就会被认为故障。如果主节点超过这个时间还是不可达,则用它的从节点将启动故障迁移,升级成主节点。注意,任何一个节点在这个时间之内如果还是没有连上大部分的主节点,则此节点将停止接收任何请求。

cluster-node-timeout默认15s。这个参数建议不要设置太小或者太大 。

redis集群没有过半机制会有脑裂问题,网络分区导致脑裂后多个主节点对外提供写服务,一旦网络分区恢复,会将其中一个主节点变为从节点,这时会有大量数据丢失。

如果发生了脑裂,就会有cluster-node-timeout 的数据丢失。

Redis集群故障恢复的几个场景

问题1:如果主节点下线?从节点能否自动升为主节点?

答:主节点下线,从节点自动升为主节点。
在这里插入图片描述

问题2:主节点恢复后,主从关系会如何?

主节点恢复后,主节点变为从节点!
在这里插入图片描述

问题3:如果所有某一段插槽的主从节点都宕掉,redis服务是否还能继续?

答:服务是否继续,可以通过redis.conf中的cluster-require-full-coverage参数(默认关闭)进行控制。

主从都宕掉,意味着有一片数据,会变成真空,没法再访问了!

  • 如果无法访问的数据,是连续的业务数据,我们需要停止集群,避免缺少此部分数据,造成整个业务的异常。

此时可以通过配置cluster-require-full-coverage为yes.

当cluster-require-full-coverage为no时,表示当负责一个插槽的主库下线且没有相应的从库进行故障恢复时,集群不可用

  • 如果无法访问的数据,是相对独立的,对于其他业务的访问,并不影响,那么可以继续开启集群体提供服务。此时,可以配置cluster-require-full-coverage为no。

当cluster-require-full-coverage为no时,表示当负责一个插槽的主库下线且没有相应的从库进行故障恢复时,集群仍然可用

数据倾斜与流量倾斜

对于分布式数据库来说,存在倾斜问题是比较常见的

集群倾斜也就是各个节点使用的内存不一致

数据倾斜与流量倾斜原因

1.节点和槽分配不均,如果使用redis-trib.rb工具构建集群,则出现这种情况的机会不多

redis-trib.rb info ip:port查看节点,槽,键值分布
redis-trib.rb rebalance ip:port进行均衡(谨慎使用)

2.不同槽对应键值数量差异比较大

CRC16算法正常情况下比较均匀
可能存在hash_tag
cluster countkeysinslot {slot}获取槽对应键值个数

3.包含bigkey:例如大字符串,几百万的元素的hash,set等

在从节点:redis-cli --bigkeys
优化:优化数据结构

4.内存相关配置不一致

hash-max-ziplist-value:满足一定条件情况下,hash可以使用ziplist
set-max-intset-entries:满足一定条件情况下,set可以使用intset
在一个集群内有若干个节点,当其中一些节点配置上面两项优化,另外一部分节点没有配置上面两项优化
当集群中保存hash或者set时,就会造成节点数据不均匀
优化:定期检查配置一致性

5.请求倾斜:热点key

重要的key或者bigkey
Redis Cluster某个节点有一个非常重要的key,就会存在热点问题

集群倾斜优化:

避免bigkey
避免hot key

hot key出现造成集群访问量倾斜

Hot key,即热点 key,指的是在一段时间内,该 key 的访问量远远高于其他的 redis key, 导致大部分的访问流量在经过 proxy 分片之后,都集中访问到某一个 redis 实例上。hot key 通常在不同业务中,存储着不同的热点信息。比如

  1. 新闻应用中的热点新闻内容;
  2. 活动系统中某个用户疯狂参与的活动的活动配置;
  3. 商城秒杀系统中,最吸引用户眼球,性价比最高的商品信息;

解决方案一:使用本地缓存

在 client 端使用本地缓存,从而降低了redis集群对hot key的访问量,但是同时带来两个问题:

1、如果对可能成为 hot key 的 key 都进行本地缓存,那么本地缓存是否会过大,从而影响应用程序本身所需的缓存开销。

2、如何保证本地缓存和redis集群数据的有效期的一致性。

解决方案二: 利用分片算法的特性,对key进行打散处理

我们知道 hot key 之所以是 hot key,是因为它只有一个key,落地到一个实例上。

所以我们可以给hot key加上前缀或者后缀,把一个hotkey 的数量变成 redis 实例个数N的倍数M,从而由访问一个 redis key 变成访问 N * M 个redis key。
N*M 个 redis key 经过分片分布到不同的实例上,将访问量均摊到所有实例。

big key 造成集群数据量倾斜

big key ,即数据量大的 key ,由于其数据大小远大于其他key,导致经过分片之后,某个具体存储这个 big key 的实例内存使用量远大于其他实例,造成,内存不足,拖累整个集群的使用。

big key 在不同业务上,通常体现为不同的数据,比如:

  1. 论坛中的大型持久盖楼活动;
  2. 聊天室系统中热门聊天室的消息列表;

解决方案:对 big key 进行拆分

对 big key 存储的数据 (big value)进行拆分,变成value1,value2… valueN,

大厂使用什么样的redis集群:

redis 集群方案主要有3类

第一是使用类 codis 的代理模式架构,按组划分,实例之间互相独立;

第二是基于官方的 redis cluster 的服务端分片方案;

第三是:代理模式和服务端分片相结合的模式

  • 基于官方 redis cluster 的服务端分片方案
  • 类 codis 的代理模式架构
  • 代理模式和服务端分片相结合的模式

类 codis 的代理模式架构

redis cluster 集群 HA 原理和实操(史上最全、面试必备)_第17张图片

这套架构的特点:

  • 分片算法:基于 slot hash桶;
  • 分片实例之间相互独立,每组 一个master 实例和多个slave;
  • 路由信息存放到第三方存储组件,如 zookeeper 或etcd
  • 旁路组件探活

使用这套方案的公司:
阿里云: ApsaraCache, RedisLabs、京东、百度等

阿里云

AparaCache 的单机版已开源(开源版本中不包含slot等实现),集群方案细节未知;ApsaraCache

百度 BDRP 2.0

主要组件:
proxy,基于twemproxy 改造,实现了动态路由表;
redis内核: 基于2.x 实现的slots 方案;
metaserver:基于redis实现,包含的功能:拓扑信息的存储 & 探活;
最多支持1000个节点;

slot 方案:
redis 内核中对db划分,做了16384个db; 每个请求到来,首先做db选择;

数据迁移实现:
数据迁移的时候,最小迁移单位是slot,迁移中整个slot 处于阻塞状态,只支持读请求,不支持写请求;
对比 官方 redis cluster/ codis 的按key粒度进行迁移的方案:按key迁移对用户请求更为友好,但迁移速度较慢;这个按slot进行迁移的方案速度更快;

京东proxy

主要组件:
proxy: 自主实现,基于 golang 开发;
redis内核:基于 redis 2.8
configServer(cfs)组件:配置信息存放;
scala组件:用于触发部署、新建、扩容等请求;
mysql:最终所有的元信息及配置的存储;
sentinal(golang实现):哨兵,用于监控proxy和redis实例,redis实例失败后触发切换;

slot 方案实现:
在内存中维护了slots的map映射表;

数据迁移:
基于 slots 粒度进行迁移;
scala组件向dst实例发送命令告知会接受某个slot;
dst 向 src 发送命令请求迁移,src开启一个线程来做数据的dump,将这个slot的数据整块dump发送到dst(未加锁,只读操作)
写请求会开辟一块缓冲区,所有的写请求除了写原有数据区域,同时双写到缓冲区中。
当一个slot迁移完成后,把这个缓冲区的数据都传到dst,当缓冲区为空时,更改本分片slot规则,不再拥有该slot,后续再请求这个slot的key返回moved;
上层proxy会保存两份路由表,当该slot 请求目标实例得到 move 结果后,更新拓扑;

跨机房:跨机房使用主从部署结构;没有多活,异地机房作为slave;

基于官方 redis cluster 的服务端分片方案

redis cluster 集群 HA 原理和实操(史上最全、面试必备)_第18张图片

和上一套方案比,所有功能都集成在 redis cluster 中,路由分片、拓扑信息的存储、探活都在redis cluster中实现;各实例间通过 gossip 通信;这样的好处是简单,依赖的组件少,应对200个节点以内的场景没有问题(按单实例8w read qps来计算,能够支持 200 * 8 = 1600w 的读多写少的场景);但当需要支持更大的规模时,由于使用 gossip协议导致协议之间的通信消耗太大,redis cluster 不再合适;

使用这套方案的有:AWS, 百度贴吧

官方 redis cluster

数据迁移过程:
基于 key粒度的数据迁移;
迁移过程的读写冲突处理:
从A 迁移到 B;

  • 访问的 key 所属slot 不在节点 A 上时,返回 MOVED 转向,client 再次请求B;
  • 访问的 key 所属 slot 在节点 A 上,但 key 不在 A上, 返回 ASK 转向,client再次请求B;
  • 访问的 key 所属slot 在A上,且key在 A上,直接处理;(同步迁移场景:该 key正在迁移,则阻塞)

AWS ElasticCache

ElasticCache 支持主从和集群版、支持读写分离;
集群版用的是开源的Redis Cluster,未做深度定制;

代理模式和服务端分片相结合的模式

p2p和代理的混合模式: 基于redis cluster + twemproxy混合模式

百度贴吧的ksarch-saas:

基于redis cluster + twemproxy 实现;后被 BDRP 吞并;
twemproxy 实现了 smart client 功能;

使用 redis cluster后还加一层 proxy的好处:

  1. 对client友好,不需要client都升级为smart client;(否则,所有语言client 都需要支持一遍)
  2. 加一层proxy可以做更多平台策略;比如在proxy可做 大key、热key的监控、慢查询的请求监控、以及接入控制、请求过滤等;

即将发布的 redis 5.0 中有个 feature,作者计划给 redis cluster加一个proxy。

ksarch-saas 对 twemproxy的改造已开源:
https://github.com/ksarch-saas/r3proxy

总之,大厂使用代理分片的方案,还是更加广泛一些。虽然,代理分片,中间增加一层Proxy进行转发,必然会有一定的性能损耗(理论值20ms),但是那也是非常有限。

知乎为什么没有使用官方 Redis 集群方案

在 2015 年调研过多种集群方案,综合评估多种方案后,最终选择了看起来较为陈旧的 Twemproxy 而不是官方 Redis 集群方案与 Codis,具体原因如下:

1)MIGRATE 造成的阻塞问题:

Redis 官方集群方案使用 CRC16 算法计算哈希值并将 Key 分散到 16384 个 Slot 中,由使用方自行分配 Slot 对应到每个分片中,扩容时由使用方自行选择 Slot 并对其进行遍历,对 Slot 中每一个 Key 执行 MIGRATE 命令进行迁移。

调研后发现,MIGRATE 命令实现分为三个阶段:

a)DUMP 阶段:由源实例遍历对应 Key 的内存空间,将 Key 对应的 Redis Object 序列化,序列化协议跟 Redis RDB 过程一致;

b)RESTORE 阶段:由源实例建立 TCP 连接到对端实例,并将 DUMP 出来的内容使用 RESTORE 命令到对端进行重建,新版本的 Redis 会缓存对端实例的连接;

c)DEL 阶段(可选):如果发生迁移失败,可能会造成同名的 Key 同时存在于两个节点,此时 MIGRATE 的 REPLACE 参数决定是是否覆盖对端的同名 Key,如果覆盖,对端的 Key 会进行一次删除操作,4.0 版本之后删除可以异步进行,不会阻塞主进程。

经过调研,认为这种模式MIGRATE 并不适合知乎的生产环境。

Redis 为了保证迁移的一致性, MIGRATE 所有操作都是同步操作,执行 MIGRATE 时,两端的 Redis 均会进入时长不等的 BLOCK 状态。对于小 Key,该时间可以忽略不计,但如果一旦 Key 的内存使用过大,一个 MIGRATE 命令轻则导致尖刺,重则直接触发集群内的 Failover,造成不必要的切换

同时,迁移过程中访问到处于迁移中间状态的 Slot 的 Key 时,根据进度可能会产生 ASK 转向,此时需要客户端发送 ASKING 命令到 Slot 所在的另一个分片重新请求,请求时延则会变为原来的两倍。

同样,方案调研期间的 Codis 采用的是相同的 MIGRATE 方案,但是使用 Proxy 控制 Redis 进行迁移操作而非第三方脚本(如 redis-trib.rb),基于同步的类似 MIGRATE 的命令,实际跟 Redis 官方集群方案存在同样的问题。

2)缓存模式下高可用方案不够灵活:

还有,官方集群方案的高可用策略仅有主从一种,高可用级别跟 Slave 的数量成正相关,如果只有一个 Slave,则只能允许一台物理机器宕机, Redis 4.2 roadmap 提到了 cache-only mode,提供类似于 Twemproxy 的自动剔除后重分片策略,但是截至目前仍未实现。

3)内置 Sentinel 造成额外流量负载:

另外,官方 Redis 集群方案将 Sentinel 功能内置到 Redis 内,这导致在节点数较多(大于 100)时在 Gossip 阶段会产生大量的 PING/INFO/CLUSTER INFO 流量,根据 issue 中提到的情况,200 个使用 3.2.8 版本节点搭建的 Redis 集群,在没有任何客户端请求的情况下,每个节点仍然会产生 40Mb/s 的流量,虽然到后期 Redis 官方尝试对其进行压缩修复,但按照 Redis 集群机制,节点较多的情况下无论如何都会产生这部分流量,对于使用大内存机器但是使用千兆网卡的用户这是一个值得注意的地方。

4)slot 存储开销:

最后,每个 Key 对应的 Slot 的存储开销,在规模较大的时候会占用较多内存,4.x 版本以前甚至会达到实际使用内存的数倍,虽然 4.x 版本使用 rax 结构进行存储,但是仍然占据了大量内存,从非官方集群方案迁移到官方集群方案时,需要注意这部分多出来的内存。

总之,官方 Redis 集群方案与 Codis 方案对于绝大多数场景来说都是非常优秀的解决方案,但是仔细调研发现并不是很适合集群数量较多且使用方式多样化的知乎,

总之,场景不同侧重点也会不一样,方案也需要调整,没有最有,只有最适合。

中小厂使用什么样的redis集群:

既然大厂倾向于选择代理分片模式的集群如Codis,那么中小厂子该如何选择呢?

Codis与Redis Cluster集群方案对比

Codis Redis Cluster
数据库数量 16 1
客户端支持 All Smart Client
Redis版本 3.2.8分支开发 5.0.3
不支持的命令 KEYS等 SELECT、跨节点multi-key命令
Dashboard
可视化客户端
集群结构 代理 类中心化架构 集群管理层与存储层解耦 P2P模型 Gossip协议 去中心化
哈希槽 1024 16384
pipeline 支持 不支持
重新分片时multi-key操作 支持 不支持
主从复制 不负责 负责
可靠 经过线上服务验证,可靠性较高 新推出,坑会比较多,遇到bug之后需要等官网升级
升级 后续升级无法保证 Redis官方推出,后续升级可保证
部署 较复杂 简单

通过以上表格对比,发现Codis和Redis Cluster各有特点,可以根据项目实际需要进行选择。

选择Redis Cluster的场景:

  1. 需要redis的新特性,例如:Stream
  2. 需要更丰富的命令支持
  3. 资源紧张

选择Codis的场景:

  1. Codis支持的命令可满足需求
  2. 资源充裕
  3. 强调可靠性

Redis Cluster没有采用中心化模式的Proxy方案,而是把请求转发逻辑一部分放在客户端,一部分放在了服务端,它们之间互相配合完成请求的处理。

Redis Cluster是在Redis 3.0推出的,但随着Redis的版本迭代,Redis官方的Cluster也越来越稳定,更多人开始采用官方的集群化方案。

Redis Cluster没有了中间的Proxy代理层,那么是如何进行请求的转发呢?

Smart Client客户端路由转发

Redis把请求转发的逻辑放在了Smart Client中,要想使用Redis Cluster,必须升级Client SDK,这个SDK中内置了请求转发的逻辑,所以业务开发人员同样不需要自己编写转发规则,Redis Cluster采用16384个槽位进行路由规则的转发。

总之,对于中小项目来说,选择 Redis Cluster 会更加合理。

对于大型集群来说, 由于200 个使用 3.2.8 版本节点搭建的 Redis 集群,在没有任何客户端请求的情况下,每个节点仍然会产生 40Mb/s 的流量, 所以不建议使用官方的 Redis Cluster ,建议采用 codis、twenproxy 等代理方案。

高可用Redis集群的架构

集群的性能和数据量参考指标

单节点redis推荐的容量 10-20G

单节点redis推荐的并发量 4-5WQPS

选型:哨兵模式

如果系统的缓存大小<10G

建议使用一主多从的哨兵模式。 从节点的数量,根据qps来扩展,比如10WQPS,可以有3-4个从节点。

选型: Redis Cluster模式

如果系统的缓存大小<2000G, 主节点数<200个,建议使用Redis Cluster模式

选型:proxy模式

对于大型集群来说, 由于200 个使用 3.2.8 版本节点搭建的 Redis 集群,在没有任何客户端请求的情况下,每个节点仍然会产生 40Mb/s 的流量, 所以不建议使用官方的 Redis Cluster ,建议采用 codis、twenproxy 等代理方案。

集群缓存击穿解决方案:

缓存击穿是指热点key在某个时间点过期的时候,而恰好在这个时间点对这个Key有大量的并发请求过来,从而大量的请求打到db。

描述:某一个热点 key,在缓存过期的一瞬间,同时有大量的请求打进来,由于此时缓存过期了,所以请求最终都会走到数据库,造成瞬时数据库请求量大、压力骤增,甚至可能打垮数据库。

  1. 设置热点数据永远不过期。
  2. 采用多级缓存架构,热点数据,肯定数据量不大,可以使用 本地缓存
  3. 如果过期则或者在快过期之前更新,如有变化,主动刷新缓存数据,同时也能保障数据一致性

缓存穿透解决方案:

什么是穿透?

缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,进而给数据库带来压力。

要点:访问一个缓存和数据库都不存在的 key,此时会直接打到数据库上,并且查不到数据,没法写缓存,所以下一次同样会打到数据库上。

此时,缓存起不到作用,请求每次都会走到数据库,流量大时数据库可能会被打挂。此时缓存就好像被“穿透”了一样,起不到任何作用。

解决方案

1、**接口校验。**在正常业务流程中可能会存在少量访问不存在 key 的情况,但是一般不会出现大量的情况,所以这种场景最大的可能性是遭受了非法攻击。可以在最外层先做一层校验:用户鉴权、数据合法性校验等,例如商品查询中,商品的ID是正整数,则可以直接对非正整数直接过滤等等。

2、缓存空值。当访问缓存和DB都没有查询到值时,可以将空值写进缓存,但是设置较短的过期时间,该时间需要根据产品业务特性来设置。

3、hashmap 记录存在性,存在去查redis,不存在直接返回。

4、布隆过滤器。使用布隆过滤器存储所有可能访问的 key,不存在的 key 直接被过滤,存在的 key 则再进一步查询缓存和数据库。

布隆过滤器由一个 bitSet 和 一组 Hash 函数(算法)组成,是一种空间效率极高的概率型算法和数据结构,主要用来判断一个元素是否在集合中存在。布隆过滤器占用多少空间,主要取决于 Hash 函数的个数,跟 key 本身的大小无关,这使得其在空间的优势非常大,但是存在一定的误判率。

缓存雪崩保障方案

什么是雪崩?

缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。

描述:大量的热点 key 设置了相同的过期时间,导在缓存在同一时刻全部失效,造成瞬时数据库请求量大、压力骤增,引起雪崩,甚至导致数据库被打挂。缓存雪崩其实有点像“升级版的缓存击穿”,缓存击穿是一个热点 key,缓存雪崩是一组热点 key。

1、过期时间打散。既然是大量缓存集中失效,那最容易想到就是让他们不集中生效。可以给缓存的过期时间时加上一个随机值时间,使得每个 key 的过期时间分布开来,不会集中在同一时刻失效。

在做电商项目的时候,一般是采取不同分类商品,缓存不同周期。在同一分类中的商品,加上一个随机因子。这样能尽可能分散缓存过期时间,而且,热门类目的商品缓存时间长一些,冷门类目的商品缓存时间短一些,也能节省缓存服务的资源。

2、热点数据不过期。该方式和缓存击穿一样,也是要着重考虑刷新的时间间隔和数据异常如何处理的情况。

集群与数据库的数据一致性保障方案:

  • 方案1:biglog同步保障数据一致性
  • 方案2:使用程序方式发送更新消息,保障数据一致性

方案1:biglog同步保障数据一致性的架构:

方案1,可以通过biglog同步,来保障二级缓存的数据一致性,具体的架构如下

在这里插入图片描述

利用 rocketMQ是支持广播消费的,增加消费端即可。

所以,必须设置 rocketMQ 客户端的消费模式,为 广播模式;

@RocketMQMessageListener(topic = "seckillgood", consumerGroup = "UpdateGuava", messageModel = MessageModel.BROADCASTING)

增加一个更新redis缓存的实力,完成redis的更新。

对于更新Guava或者其他1级缓存来说,增加一个实例消费消息,就可以了。

方案2:使用程序方式保障数据一致性的架构

使用程序方式保障数据一致性的架构,可以编写一个通用的2级缓存通用组件,当数据更新的时候,去发送消息,具体的架构如下:

在这里插入图片描述

方案2和方案1 的区别

方案2和方案1 的整体区别不大,只不过 方案2 需要自己写代码(或者中间组件)发送数据的变化通知。 并且可以进行延迟双删的操作,首先删除一次,再发送到延迟队列,再删一次缓存。

方案1 的一个优势:可以和 建立索引等其他的消费者,共用binlog的消息队列。

其他的区别,大家可以自行探索。

Redis持久化导致的高可用问题分析及解决

Redis的持久化配置

redis的 rdb 和 aof 持久化的区别

aof,rdb是两种 redis持久化的机制。用于crash后,redis的恢复。

redis将数据保存在内存中,一旦Redis服务器被关闭,或者运行Redis服务的主机本身被关闭的话,储存在内存里面的数据就会丢失

如果仅仅将redis用作缓存的话,那么这种数据丢失带来的问题并不是非常大,只需要重启机器,然后再次将数据同步到缓存中就可以了

但如果将redis用作DB的话,那么因为一些原因导致数据丢失的情况就不能接受

Redis的持久化就是将储存在内存里面的数据以文件形式保存硬盘里面,这样即使Redis服务端被关闭,已经同步到硬盘里面的数据也不会丢失

除此之外,持久化也可以使Redis服务器重启时,通过载入同步的持久文件来还原之前的数据,或者使用持久化文件来进行数据备份和数据迁移等工作

RDB持久化功能

RDB持久化功能可以将Redis中所有数据生成快照并以二进行文件的形式保存到硬盘里,文件名为.RDB文件

在Redis启动时载入RDB文件,Redis读取RDB文件内容,还原服务器原有的数据库数据

过程如下图所示:

5bc3321a00012c2508280432.jpg

Redis服务端创建RDB文件,有三种方式

  • 使用SAVE命令手动同步创建RDB文件

  • 使用BGSAVE命令异步创建RDB文件

  • 自动创建RDB文件

使用SAVE命令手动同步创建RDB文件

客户端向Redis服务端发送SAVE命令,服务端把当前所有的数据同步保存为一个RDB文件

使用BGSAVE命令异步创建RDB文件

执行BGSAVE命令也会创建一个新的RDB文件

BGSAVE不会造成redis服务器阻塞:在执行BGSAVE命令的过程中,Redis服务端仍然可以正常的处理其他的命令请求

BGSAVE命令执行步骤:

自动创建RDB文件

打开Redis的配置文件/etc/redis.conf

save 900 1save 300 10save 60 10000

自动持久化配置解释:

  • save 900 1表示:如果距离上一次创建RDB文件已经过去的900秒时间内,Redis中的数据发生了1次改动,则自动执行BGSAVE命令
  • save 300 10表示:如果距离上一次创建RDB文件已经过去的300秒时间内,Redis中的数据发生了10次改动,则自动执行BGSAVE命令
  • save 60 10000表示:如果距离上一次创建RDB文件已经过去了60秒时间内,Redis中的数据发生了10000次改动,则自动执行BGSAVE命令
    当三个条件中的任意一个条件被满足时,Redis就会自动执行BGSAVE命令

rdb持久化的特性如下:

fork一个进程,遍历hash table,利用copy on write,把整个db dump保存下来。
save, shutdown, slave 命令会触发这个操作。
粒度比较大,如果save, shutdown, slave 之前crash了,则中间的操作没办法恢复。

AOF的功能

AOF持久化保存数据库的方法是:每当有修改的数据库的命令被执行时,服务器就会将执行的命令写入到AOF文件的末尾。

因为AOF文件里面储存了服务器执行过的所有数据库修改的命令,所以Redis只要重新执行一遍AOF文件里面保存的命令,就可以达到还原数据库的目的

AOF安全性问题

虽然服务器执行一次修改数据库的命令,执行的命令就会被写入到AOF文件,但这并不意味着AOF持久化方式不会丢失任何数据

在linux系统中,系统调用write函数,将一些数据保存到某文件时,为了提高效率,系统通常不会直接将内容写入硬盘里面,而是先把数据保存到硬盘的缓冲区之中。

等到缓冲区被填满,或者用户执行fsync调用和fdatasync调用时,操作系统才会将储存在缓冲区里的内容真正的写入到硬盘里

对于AOF持久化来说,当一条命令真正的被写入到硬盘时,这条命令才不会因为停机而意外丢失

因此,AOF持久化在遭遇停机时丢失命令的数量,取决于命令被写入硬盘的时间

越早将命令写入到硬盘,发生意外停机时丢失的数据就越少,而越迟将命令写入硬盘,发生意外停机时丢失的数据就越多

AOF三种策略

为了控制Redis服务器在遇到意外停机时丢失的数据量,Redis为AOF持久化提供了appendfsync选项,这个选项的值可以是always,everysec或者no

  • appendfsync always:
    总是写入aof文件,并通过事件循环磁盘同步,即使Redis遭遇意外停机时,最多只丢失一事件循环内的执行的数据
  • appendfsync everysec:
    每一秒写入aof文件,并完成磁盘同步,即使Redis遭遇意外停机时,最多只丢失一秒钟内的执行的数据
  • appendfsync no:
    服务器不主动调用fdatasync,由操作系统决定任何将缓冲区里面的命令写入到硬盘里,这种模式下,服务器遭遇意外停机时,丢失的命令的数量是不确定的
AOF三种方式比较

运行速度:

  • always的速度慢,everysec和no都很快, always丢失的数据最少,但是硬盘IO开销很多,一般的SATA硬盘一秒种只能写入几百次数据
  • everysec每秒同步一次数据,如果Redis发生故障,可能会丢失1秒钟的数据
  • no则系统控制,不可控,不知道会丢失多少数据

可见,从持久化角度讲,always是最安全的。

从效率上讲,no是最快的。而redis默认设置进行了折中,选择了everysec。合情合理。

配置文件中AOF相关选项
appendonly   yes                     # 改为yes,开启AOF功能
appendfilename  "appendonly.aof"    # 生成的AOF的文件名
appendfsync everysec                # AOF同步的策略
no-appendfsync-on-rewrite  yes      # AOF重写时,是否做append的操作,yes是不做,在`rewrite`期间的`AOF`有丢失的风险。

配置文件中AOF相关选项
  • 建议把appendfsync选项设定为everysec,进行持久化,这种情况下Redis宕机最多只会丢失一秒钟的数据

  • 如果使用Redis做为缓存时,即使数据丢失也不会造成任何影响,只需要在下次加载时重新从数据源加载就可以了

  • 不要占用100%的内存。一般分配服务器60%到70%的内存给Redis使用,剩余的内存分留给类似fork的操作

aof与rdb持久化的区别:

把写操作指令,持续的写到一个类似日志文件里。(类似于从postgresql等数据库导出sql一样,只记录写操作)
粒度较小,crash之后,只有crash之前没有来得及做日志的操作没办法恢复。

两种区别就是,

  • 一个是持续的用日志记录写操作,crash后利用日志恢复;

  • 一个是平时写操作的时候不触发写,只有手动提交save命令,或者是关闭命令时,才触发备份操作。

选择的标准,就是看系统是愿意牺牲一些性能,换取更高的缓存一致性(aof),还是愿意写操作频繁的时候,不启用备份来换取更高的性能,待手动运行save的时候,再做备份(rdb)。

rdb这个就更有些 eventually consistent的意思了。

AOF重写出现Redis主进程阻塞,应用端响应超时的问题

问题背景

某个业务线使用Redis集群保存用户session数据,数据量大约在4千万-5千万,每天发生3-4次AOF重写,每次时间持续30-40秒,AOF重写期间出现Redis主进程阻塞,应用端响应超时的问题。

环境:Redis 2.8,一主一从。

什么是AOF重写

AOF重写是AOF持久化的一个机制,用来压缩AOF文件。

随着服务器的不断运行,为了记录Redis中数据的变化,Redis会将越来越多的命令写入到AOF文件中,使得AOF文件的体积来断增大

为了让AOF文件的大小控制在合理的范围,redis提供了AOF重写功能,通过这个功能,服务器可以产生一个新的AOF文件:

  • 新的AOF文件记录的数据库数据和原有AOF文件记录的数据库数据完全一样
  • 新的AOF文件会使用尽可能少的命令来记录数据库数据,因此新的AOF文件的体积通常会比原有AOF文件的体积要小得多
  • AOF重写期间,服务器不会被阻塞,可以正常处理客户端发送的命令请求

AOF重写功能就是把Redis中过期的,不再使用的,重复的以及一些可以优化的命令进行优化,重新生成一个新的AOF文件,从而达到减少硬盘占用量和加速Redis恢复速度的目的

在这里插入图片描述

AOF重写的目的

Redis 的rewrite策略,实现AOF文件的减肥,但是结果是幂等的

AOF重写的流程

Redis通过fork一个子进程,重新写一个新的AOF文件,该次重写不是读取旧的AOF文件进行复制,而是读取内存中的Redis数据库,重写一份AOF文件,有点类似于RDB的快照方式。

在子进程进行AOF重写期间,Redis主进程执行的命令会被保存在AOF重写缓冲区里面,这个缓冲区在服务器创建子进程之后开始使用,当Redis执行完一个写命令之后,它会同时将这个写命令发送给 AOF缓冲区和AOF重写缓冲区。如下图:

在这里插入图片描述

具体的步骤如下:

1.无论是执行bgrewriteaof命令手动开启重写,还是自动进行AOF重写,实际上都是执行BGREWRITEAOF命令
2.执行bgrewriteaof命令,Redis会fork一个子进程,
3.子进程对内存中的Redis数据进行回溯,生成新的AOF文件
4.Redis主进程会处理正常的命令操作
5.同时Redis把会新的命令写入到aof_rewrite_buf当中,当bgrewriteaof命令执行完成,新的AOF文件生成完毕,Redis主进程会把aof_rewrite_buf中的命令追加到新的AOF文件中
6.用新生成的AOF文件替换旧的AOF文件

在这里插入图片描述

AOF重写导致主进程阻塞原因分析

当AOF重写子进程完成AOF重写工作之后,它会向父进程发送一个信号,父进程在接收到该信号之后,会调用一个信号处理函数,并执行以下工作:

  • 将AOF重写缓冲区中的所有内容写入到新的AOF文件中,保证新 AOF文件保存的数据库状态和服务器当前状态一致。
  • 对新的AOF文件进行改名,原子地覆盖现有AOF文件,完成新旧文件的替换
  • 继续处理客户端请求命令。

现在问题出现了,同时在执行bgrewriteaof操作和主进程写aof文件的操作,两者都会操作磁盘,

特别需要注意的是:

bgrewriteaof往往会涉及大量磁盘操作,这样就会造成主进程在写aof文件的时候,出现阻塞的情形,导致主进程阻塞。

根因分析与解决方案

这是当时的Redis配置:

127.0.0.1:6379> config get *append*
1) "no-appendfsync-on-rewrite"
2) "no"
3) "appendonly"
4) "yes"
5) "appendfsync"
6) "everysec"

从配置看,原因理论上就很清楚了:

  • 我们的这个Redis实例使用AOF进行持久化(appendonly)
  • appendfsync策略采用的是everysec刷盘。

但是AOF随着时间推移,文件会越来越大,因此,Redis自动启动一个rewrite策略,实现AOF文件的减肥,但是结果是幂等的

  • no-appendfsync-on-rewrite的策略是 no,这就会导致在进行rewrite操作时,appendfsync会写入aof文件而可能被阻塞。

这不是什么新问题,很多开启AOF的业务场景都会遇到这个问题。

解决的办法有这么几个:

  • 将no-appendfsync-on-rewrite设置为yes.

yes表示在日志AOF重写时,不进行aof文件命令追加操作,而只是将命令放在重写缓冲区里,避免与命令的追加造成磁盘IO造成的阻塞。但是在rewrite期间的AOF有丢失的风险。

  • 给当前Redis实例添加slave节点,当前节点设置为master, 然后master节点关闭AOF,slave节点开启AOF。

这样的方式的风险是如果master挂掉,尚没有同步到slave的数据会丢失。

比较折中的方式:

  • 在master节点设置将no-appendfsync-on-rewrite设置为yes,注意,还有后手,就是停止自动aof重写,如何停止,将auto-aof-rewrite-percentage参数设置为0,关闭主动重写

    auto-aof-rewrite-percentage 参数说明

    aof文件增长比例,指当前aof文件比上次重写的增长比例大小。aof重写即在aof文件在一定大小之后,重新将整个内存写到aof文件当中,以反映最新的状态(相当于bgsave)。这样就避免了,aof文件过大而实际内存数据小的问题(频繁修改数据问题).

  • 为了防止AOF文件越来越大,在任务调度配置在凌晨低峰期定时手动执行bgrewriteaof命令完成每日一次的AOF重写

  • 在重写时为了避免硬盘空间不足或者IO使用率高影响重写功能添加了硬盘空间报警和IO使用率报警保障重写的正常进行

why:Redis不能保证100%数据不丢失

Redis能否保证100%数据不丢失,答案是no。

哪怕是在要求最高的持久化配置场景,将appendfsync值设置为always,其实也会产生数据丢失。

尽管,很多博客都讲,将appendfsync值设置为always,Redis能保证100%数据不丢失,可能会打脸了。

图解:redis的事件循环

void aeMain(aeEventLoop *eventLoop) {
    eventLoop->stop = 0;
    while (!eventLoop->stop) {
        if (eventLoop->beforesleep != NULL)
            eventLoop->beforesleep(eventLoop);
        aeProcessEvents(eventLoop, AE_ALL_EVENTS);
    }
}

在这里插入图片描述

flushAppendOnlyFile 的时机分析

一个while循环,我们把这个循环叫做事件循环, 从写盘的角度来说:

  • 第N+1轮循环的第一阶段,调用flushAppendOnlyFile 的,会将aof buffer写到磁盘上。

  • 第N轮循环的第二阶段,将读取到的命令,写入aof buffer,而不是直接落盘

所以:

redis即使在配制appendfsync=always的策略下,还是会可能丢失一个事件循环的aof_buf数据,

异步复制导致的数据丢失

在这里插入图片描述

因为master->slave的数据同步是异步的,所以可能存在部分数据还没有同步到slave,master就宕机了,此时这部分数据就丢失了。

(2)脑裂导致的数据丢失

redis cluster 集群 HA 原理和实操(史上最全、面试必备)_第19张图片

当master所在的机器突然脱离的正常的网络,与其他slave、sentinel失去了连接,但是master还在运行着。

此时sentinel就会认为master宕机了,会开始选举把slave提升为新的master,这个时候集群中就会出现两个master,也就是所谓的脑裂。

此时虽然产生了新的master节点,但是客户端可能还没来得及切换到新的master,会继续向旧的master写入数据。

当网络恢复正常时,旧的master会变成新的master的从节点,自己的数据会清空,重新从新的master上复制数据。

解决方案

Redis提供了这两个配置用来降低数据丢失的可能性

min-slaves-to-write 1 
min-slaves-max-lag 10

上面两行配置的意思是,要求至少有1个slave,数据复制和同步的延迟不能超过10秒,如果不符合这个条件,那么master将不会接收任何请求。

(1)减少异步复制的数据丢失

有了min-slaves-max-lag这个配置,就可以确保,一旦slave复制数据和ack延时太长,就认为master宕机后损失的数据太多了,那么就拒绝写请求,这样可以把master宕机时由于部分数据未同步到slave导致的数据丢失降低到可控范围内。

(2)减少脑裂的数据丢失

如果一个master出现了脑裂,跟其他slave丢了连接,那么上面两个配置可以确保,如果不能继续给指定数量的slave发送数据,而且slave超过10秒没有给自己ack消息,那么就直接拒绝客户端的写请求

这样脑裂后的旧master就不会接受client的新数据,也就避免了数据丢失。

Redis并不能保证数据的强一致性,看官方文档的说明

redis cluster 集群 HA 原理和实操(史上最全、面试必备)_第20张图片

参考文献

https://www.cnblogs.com/zjxiang/p/12484474.html

https://www.cnblogs.com/zjxiang/p/12484474.html

https://blog.csdn.net/crazymakercircle/article/details/116110302

http://www.redis-doc.com/

https://blog.csdn.net/crazymakercircle/article/details/116110302

https://www.cnblogs.com/kismetv/p/9236731.html#t31

https://blog.csdn.net/qq_35044419/article/details/117817563

https://blog.csdn.net/javarrr/article/details/92830952

https://www.cnblogs.com/ExMan/p/14447298.html

https://blog.csdn.net/tr1912/article/details/81265007

http://www.redis.cn/topics/cluster-tutorial.html

http://www.redis.cn/topics/sentinel.html

http://www.redis.cn/topics/replication.html

https://www.cnblogs.com/mrhelloworld/p/docker14.html

http://www.redis.cn/topics/cluster-tutorial.html]

https://my.oschina.net/dabird/blog/4291090

https://my.oschina.net/dabird/blog/4291090

你可能感兴趣的:(java)