Redis 集群

2023/2/22

接上文:Redis 入门

# 4 主从复制

作用:备份、读写分离

# 4.1 实现方式

假如现在有两个redis,一个6379;一个6380;让6379成为master节点;6380成为slave节点

  1. 命令方式:
    1. redis-6380:slaveof 127.0.0.1 6379;主从关系建立后,slave先删除所有数据再从master同步
    2. 去掉redis-6380:slaveof no one 127.0.0.1 6379
  2. 配置方式:
    1. 配置文件:
      1. slaveof ip port
      2. slave-read-only:是否现在slave只读

# 4.2 全量/部分复制

# 4.2.1 runid和偏移量

runid:

  1. redis每次启动的时候都会有一个随机的id来保障redis的标识,每次重启之后都是不一样的
  2. 查看runid:
redis-cli -p 6979 info server | grep run
run_id:dsfsdf34234wfdsdf23432fdsdf

偏移量:

  • 一个数据写入量的字节,记录写了多少数据。主服务器会把偏移量同步给从服务器,当主从的偏移量一致,则数据是完全同步的
  • 如果主从服务的偏移量大于从服务器,则主从不同步
  • 查看offset:
redis-cli -p 6979 info replication   //查看命令
slave_repl_offset:1978    // 偏移量参数

redis选择选择全量复制还是部分复制:从节点将 runId,offset 发送给主节点后,主节点根据runId,偏移量和复制缓冲区(repl_back_buffer)大小来决定是否执行部分复制

  • 如果runId和master的相同,并且offset 偏移量之后的数据,仍然在复制积压缓冲区的话,使用部分复制
  • 如果runId和master的不同,或者offset 偏移量之后的数据,已经不在积压缓冲区的话,说明数据已经被挤出,所以进行全量复制

复制缓冲区(积压队列,repl_back_buffer):是一个固定长度的循环队列,默认情况下积压队列的大小为 1 MB;每次写操作会往这个缓冲区写,循环写

# 4.2.2 全量复制

19.png

流程:

  • slave 向 master 传递命令 psync? -1 (因为第一次通信不知道master的runid和偏移量,所以传-1)
  • master 向 slave 返回runid 和偏移量(为什么要返回runid给slave呢,因为可能master重启了,但是slave没有重启,slave还是老的runid)
  • slave 保存 master 的信息
  • master 执行 bgsave 生产RDB快照
  • master 做send RDB 操作 向 slave 同步快照信息
  • master 在生成RDB文件及传输过程中执行的命令写到repl_back_buffer,然后把repl_back_buffer也传送也slave
  • slave清空之前的数据
  • slave 加载 RDB文件及数据
  • slave执行repl_back_buffer中的命令

开销:

  • bgsave时间
  • RDB文件网络传输时间
  • 从节点清空数据时间
  • 从节点加载RDB的时间

# 4.2.3 部分复制

20.png

流程:

  • 当网络发生抖动,slave会与master断开
  • master 写命令时,会写一份复制缓冲区的命令
  • 当slave在此连接master时 ,传递命令 psync {offset} {runid} ,告诉 master 自己当前的偏移量是多少
  • master 向 slave 返回CONTINUE 把 缺失的内容 传递过去

# 4.2.4 命令持续复制

当完成了上面的同步之后,主从服务器就会进入命令传播阶段,主节点会持续地把写命令发送给从节点

# 4.2.5 心跳检测

当完成了上面的同步之后,主从之间维护着长连接并彼此发送心跳命令,便以后续持续发送写命令,主从心跳检测如下图所示:

58737ABA-018F-4613-99D3-0A1E11B0AC65.png

主从节点彼此都有心跳检测机制,各自模拟成对方的客户端进行通信,主从心跳检测的规则如下:

  • 主节点默认每隔 10 秒对从节点发送 ping 命令,判断从节点的存活性和连接状态。可通过修改 redis.conf 配置文件里面的 repl-ping-replica-period 参数来控制发送频率
  • 从节点在主线程中每隔 1 秒发送 replconf ack {offset} 命令,给主节点 上报自身当前的复制偏移量,这条命令除了检测主从节点网络之外,还通过发送复制偏移量来保证主从的数据一致

# 4.3 故障处理

# 4.3.1 slave节点故障

没太大影响

# 4.3.2 master节点故障

21.png

手动对某个slave执行,slaveof no one

# 4.4 常见问题

# 4.4.1 读写分离

22.png

问题:

  • 复制数据延迟
  • 读到之前的数据

# 4.4.2 配置不一致

  • 主从maxmemory不一致,例如master设置成4G,slave设置成2G,那么slave加载4G的RDB文件内存不够,会触发slave的淘汰策略
  • 对主节点做了一些数据结构优化参数(例如hash-max-ziplist-entries),导致从节点的内存不够

# 4.4.3 避免全量复制

  • 第一次全量复制无法避免,好的方法就是对redis进行分片,每个redis不要这么大
  • 节点运行id不匹配(例如主节点重启):这个时候从节点发现主节点runid变了,会触发全量复制
    • 故障转移,将slave转化为master
  • rel_back_buffer不足:默认是1M,假如slave节点和master节点偏移量超过了1M,就会触发全量复制
    • 可以通过参数:rel_backlog_size,设置大一些,例如10M

# 4.4.5 避免复制风暴

  • 例如主节点重启,runid变化了,所有slave都需要全量复制
    • 将一个slave上升为master
    • 更换复制拓扑

23.png

# 5 Redis 哨兵 —— sentinel

主从复制,如果master节点挂掉了,需要手动进行以下操作:

  • 对一个slave节点执行slaveof no one,让其成为master节点
  • 然后对其他slave节点执行slave of newmaster
  • 修改代码,把写节点配置成新的master;可以在diamond里配置

更进一步是把这个过程写成一个脚本,redis有解决方案:sentinel

# 5.1 整体架构

39.png 客户端通过sentinel获取redis连接,sentinel知道谁是master谁是slave,客户端不需要知道谁是master;当master节点发生故障时,sentinel自动进行故障转移;

sentinel故障转移流程:

  1. 多个sentinel发现并确认master有问题
  2. 选举一个sentinel作为领导
  3. 选出一个slave作为master
  4. 通知其余slave成为新的master的slave
  5. 通知客户端主从变化
  6. 等待老的master复活成为新master的slave

一套sentinel可以监控多套主从: 41.png

# 5.2 安装与配置

实现如下目标,26379是sentinel默认端口:

42.png

# 5.2.1 redis节点

redis主节点:

启动 redis-server redis-7000.conf
配置 port 7000
daemonize yes
pidfile /.../redis-7000.pid
logfile "7000.log"
dir/....
  • daemonize设置yes:代表开启守护进程模式。在该模式下,redis会在后台运行,除非手动kill该进程;
  • daemonize设置no:当前界面将进入redis的命令行界面,exit强制退出或者关闭连接工具都会导致redis进程退出

redis从节点:

启动 redis-server redis-7001.conf
redis-server redis-7002.conf
配置 port 7001/7002
daemonize yes
pidfile /.../redis-7001/7002.pid
logfile "7001/7002.log"
dir /....
slaveof 127.0.0.1 7000

# 5.2.2 sentinel配置

命令 说明
port ${port}
dir /... sentinel和redis是两个应用,所以可以使用两个工作目录
logfile "${port}.log"
sentinel monitor mymaster 127.0.0.1 7000 2 当前sentinel要监控的master
mymaster:监控的master名称
127.0.0.1 7000:监控的master地址及端口
2:表示几个sentinel认为它有问题了才是真的有问题
sentinel down-after-milliseconds mymaster 30000 master超过多长时间没响应就认为有故障,默认30s
sentinel会向master发送心跳PING来确认master是否存活
sentinel parallel-syncs mymaster 1 故障转移的时候最多几个slave同时同步数据
新的master别切换之后,同时有多少个slave被切换到去连接新master,重新做同步,可以少设置点防止复制风暴
sentinel failover-timeout mymaster 180000 故障转移的超时时间

# 5.2.3 jedis连接sentinel

  • client遍历sentinel节点集合
  • 获取一个可用的sentinel节点,执行:sentinel get-master-addr-by-name masterName
  • sentinel返回master节点地址和端口
  • client向master节点执行 role或者role replication(role或role replication是redis命令,可以判断当前节点的角色),确定是不是master节点
  • client订阅一个频道(订阅的是sentinel里的频道),这个频道里发布有关master节点变更的消息
String masterName = "mymaster";
Set<String> sentinelSet = new HashSet<String>();
sentinelSet.add("127.0.0.1:26379");
sentinelSet.add("127.0.0.1:26380");
sentinelSet.add("127.0.0.1:26381");
JedisSentinelPool sentinelPool 
	= new JedisSentinelPool(masterName,sentinelSet,poolConfig,timeout)
Jedis jedis = null;
try {
	jedis = sentinelPool.getResource();
    // jedis命令
} catch (Exception e) {

} finally {
	if (jedis != null) {
    	jedis.close();
    }
}

# 5.3 故障转移原理

# 5.3.1 三个定时任务

  1. 每个sentinel每10秒对master和slave执行info
    1. sentinel从master节点中获取从节点的消息
    2. 目的:确认主从关系
  2. 每个sentinel每2秒通过master节点的channel交换信息:第一个sentinel可以在第一个master节点的频道发布信息,其他sentinel节点会订阅这个频道,从而达到sentinel之间的交流
    1. 频道名称:sentinel:hello
    2. 交互对节点的“看法”和自身信息
    3. 新加入的sentinel节点也会去订阅这个频道,感知其他sentinel的存在
  3. 每个sentinel每1秒对其他sentinel和redis节点(master和slave节点)执行ping,用于故障检测

43.png

# 5.3.2 主观下线和客观下线

  • 主观下线:每个sentinel节点对redis节点失败的“偏见”,每秒对master节点ping,超过配置的时间没有响应,当前sentinel则认为下线了
  • 客观下线:所有sentinel节点对redis节点失败“达成共识”(大于等于quorum)

# 5.3.3 领导者选举(Raft算法)

  • 原因:只要一个sentinel节点就能完成故障转移
  • 选举:通过sentinel is-master-down-by-addr命令都希望成为领导者
    • 每个做主观下线的sentinel节点向其他sentinel节点发送命令,要求将它设置为领导者
    • 收到命令的sentinel节点如果没有同意过其他sentinel,那么将同意该请求,否则拒绝
    • 如果该sentinel节点发现自己的票数已经超过sentinel半数且超过quorum(这里的quorum和sentinel monitor配置的quorum有关系吗?),那么它将成为领导者
    • 如果此过程有多个sentinel节点成为了领导者,那么将等待一段时间重新进行选举

44.png

# 5.3.4 故障转移

  1. 从slave节点中选出一个“合适的”节点作为新的master节点
    1. 选择slave-priority最高的slave节点,如果存在则返回,不存在则继续,一般不设置,大家都是一样的,不过可以考虑把所在机器性能更好的slave设置高一点优先级,master节点的性能就更好。存在则返回,不存在则继续
    2. 选择复制偏移量更大的slave节点(复制的最完整),存在则返回,不存在则继续
    3. 选择runId最小的slave节点(启动最早的slave)
  2. 对上面的slave节点执行slave of no one命令让其成为master节点
  3. 向剩余的slave节点发送命令,让它们成为新master的slave节点,复制规则和parallel-syncs参数有关
  4. 对原来的master节点配置为slave,并保持对其“关注”,当其恢复后命令它去复制新的master节点

# 6 Redis Cluster

# 6.1 为什么需要集群

  • 并发量:主从只有一个master节点可以写,远远无法满足需要
  • 数据量:如果数据量很大,只有一台master,那么复制就会很慢很慢

# 6.2 理论基础

# 6.2.1 顺序分区和哈希分区

顺序分区:

image.png

哈希分区:例如节点取模,hash(key)%3

分布方式 特点 典型产品
哈希分布
数据分散度高
分布与业务无关
无法顺序访问

一致性哈希Memcache
Redis Cluster
其他缓存产品
顺序分布
数据分散易倾斜
分布与业务相关
可顺序访问

BigTable
HBase

# 6.2.2 哈希分布详解

# 6.2.2.1 节点取余分区

image.png

如果之前redis有3个点,现在扩容成4个点,哈希算法由hash(key)%3变成hash(key)%4,发现大约有80%的请求不在原来的节点上了,就会导致这80%的请求缓存失效,要重新从数据库里取;建议使用翻倍扩容的方法,如果变成6个节点,大约这有50%请求缓存失效,不过这种方法也不好

# 6.2.2.2 一致性哈希分区

48.png

规则:

  1. 确定node1~node2、node2~node4、node4~node3、node3~node1之间的数据范围
  2. hash(key)%节点数,确定key应该落在哪个区间
  3. 顺时针取节点,例如key落到了node4~node3,则key的节点为node3

47.png

可以看到新增node5,只会影响node1~node5之间的数据

保证最小迁移数据
但是有数据倾斜(热点问题),虚拟节点解决数据倾斜问题
# 6.2.2.3 虚拟槽分区

49.png

16383是redis cluster默认的槽数量

# 6.2.3 meet操作

节点之间可以相互通信,遵守gossip协议,节点之间相互知道自己负责的槽范围 51.png

A联系到B,A联系到C;那么B和C就相当于通过A知道对方了

# 6.3 安装与配置

# 6.3.1 原生安装

安装流程 配置
1. 配置开启节点
port ${port}
daemonize yes
dir ...
dbfilename "dump-${port}.db"
logfile ...
cluster-enabled yes(标志是否是cluster节点)
cluster-config-file nodes-${port}.conf(cluster节点配置文件)
cluster-require-full-coverage no(是否只要集群内一个节点不可用,整个集群都不再向外提供服务了,默认是yes要改成no)(当cluster-require-full-coverage为no时,表示当负责一个插槽的master下线且没有相应的从库进行故障恢复时,集群仍然可用)
2. meet 6个节点,然后按照第一步的配置执行:
redis-server redis-7000.conf(master)
redis-server redis-7001.conf(master)
redis-server redis-7002.conf(master)
redis-server redis-7003.conf(slave)
redis-server redis-7004.conf(slave)
redis-server redis-7005.conf(slave)

执行meet:
redis-cli -h 127.0.0.1 -p 7000 cluster meet 127.0.0.1 7001
redis-cli -h 127.0.0.1 -p 7000 cluster meet 127.0.0.1 7002
redis-cli -h 127.0.0.1 -p 7000 cluster meet 127.0.0.1 7003
redis-cli -h 127.0.0.1 -p 7000 cluster meet 127.0.0.1 7004
redis-cli -h 127.0.0.1 -p 7000 cluster meet 127.0.0.1 7005
执行完之后,6个节点之间就可以相互感知了
3. 分配槽 redis-cli -h 127.0.0.1 -p 7000 cluster addslots {0...5461}
redis-cli -h 127.0.0.1 -p 7001 cluster addslots {5462...10922}
redis-cli -h 127.0.0.1 -p 7002 cluster addslots {10923...16383}
4. 主从配置 redis-cli -h 127.0.0.1 -p 7003 cluster replicate ${node-id-7000}
redis-cli -h 127.0.0.1 -p 7004 cluster replicate ${node-id-7001}
redis-cli -h 127.0.0.1 -p 7005 cluster replicate ${node-id-7002}

# 6.3.2 Ruby安装(推荐)

  • 下载、编译、安装Ruby
  • 安装ruby客户端
  • 安装redis-trib.rb(redis的ruby安装工具)

原生命令安装:

  • 理解Redis Cluster架构
  • 生产环境不使用

官方工具安装:高效、准确

# 6.4 集群伸缩

# 6.4.1 扩容

槽迁移计划:

54.png

迁移数据:

  1. 对target节点发送:cluster setslot {slot} import {sourceNodeId}命令,让目标节点准备导入槽的数据(把源节点中的槽slot导入到目标节点,先让目标节点把槽创建出来)
  2. 对源节点发送:cluster setslot {slot} migrating {targetNodeId}命令,让源节点准备准备迁出槽的数据(迁移槽数据)
    1. 源节点循环执行cluster getkeysinslot {slot} {count}命令,每次获取count个属于槽的键
    2. 在源节点上执行migrate {targetIp} {targetPort} key 0 {timeout}命令,把指定key迁移目标节点
    3. 删除原节点上的槽
  3. 重复步骤2直到槽下所有的键数据迁移到目标节点
  4. 向集群内所有主节点发送cluster setslot {slot} node {targetNodeId}命令,通知槽分配给目标节点

在3.0.6版本,支持批量迁移key,pipeline migrate,但是有bug:

55.png

如果迁移的key中既有过期的也有没过期的,迁移到新节点都会当成过期的;3.2.8修复

# 6.4.2 缩容

  • 下线迁移槽

56.png

  • 其他节点忘记该节点:cluster forget {downNodeId}

57.png

集群中的每一个节点都应该去忘记,不然60s后大家又会检测到它,认为它又活了

  • 下线该节点

# 6.5 客户端路由

# 6.5.1 moved重定向(moved异常)

58.png

# -c,会自动重定向
redis-cli -c -p 7000
127.0.0.1:7000 > set hello world
OK
127.0.0.1:7000 > set php best
-> Redirected to slot[9244]located at 127.0.0.1:7001
OK
127.0.0.1:7000 > get php
"best"
# 不使用-c
redis-cli -p 7000
127.0.0.1:7000 > set php best
(error) MOVED 9244 127.0.0.1:7001

加入-c参数,支持自动的请求重定向,redis-cli接收到moved之后,会自动重定向到对应的节点执行命令

# 6.5.2 ask重定向

59.png

背景:原来的槽在node1上,但是正在进行扩容,已经迁移到node2上了

# 6.5.3 smart客户端(例如JedisCluster)

  1. 从集群中选一个可运行的节点,使用cluster slots获取节点和槽的映射关系
  2. 将cluster slots的结果映射到本地,为每个节点创建JedisPool
  3. 当客户端要执行命令的时候,直接在本地crc(key)%16383,然后从本地的表中找到具体的节点
  4. 向具体节点发出命令

60.png

(上图中少画中ask重定向)

# 6.6 批量操作mget/mset的实现

# 6.6.1 串行mget/mset

本地方法,不用mget/mset,用循环实现

# 6.6.2 串行IO

在客户端对key进行分组,然后执行

# 6.6.3 并行IO

对key分组后,使用多线程去获取结果

# 6.6.4 hash_tag

对key加相同的前缀,那么这些key就很大可能落在同一个节点上

(就是让相同业务的key落到同一个节点上;Hash Tag:允许用key的部分字符串来计算hash;当一个key包含 {} 的时候,就不对整个key做hash,而仅对 {} 包括的字符串做hash,{}可以是()、[],这个是可以配置的)

方案 优点 缺点
串行化 编程简单 大量keys请求延迟严重
串行IO 编程简单,少量节点满足需求 大量节点延迟严重
并行IO 利用并行特性,延迟取决于最慢的节点 编程复杂,超时定位问题难
hash_tag 性能最高 读写增加tag维护成本,容易出现数据倾斜

# 6.7 故障转移

redis cluster不用sentinel,自己实现了故障转移

# 6.7.1 故障发现

节点之间的ping/pong消息,节点之间不止沟通槽信息,还可以监控节点的主从状态,节点的故障等

主观下线:某个节点认为另外一个节点不可用,A节点定时去ping B节点,A节点收到回复后会记录回复时间,当A节点再去ping B节点的时候,如果发现上次的回复时间于当前时间的差超过cluster-node-timeout,则标记该节点pfail;

客观下线:当半数以上持有槽的主节点都标记某节点主观下线;

当C节点收到A节点的Pong消息,这个消息中携带者故障节点B,这个时候C节点会判断是否认有大于半数的master认为B节点下线,如果是则通知集群内所有节点B节点客观下线,还会通知故障节点的从节点触发故障转移流程

# 6.7.2 故障恢复

检查从节点资格 每个从节点与故障master的断线时间,如果超过cluster-node-timeoutcluster-slave-validity-factor(默认15s默认10s = 150s),则取消成为新master的资格
从节点选举 就是从节点发起选举的延迟,偏移量约大,越早发起选举,约有机会成为新的master
选举投票 (其他master)投票超过半数,成为新master
替换主节点
当前从节点取消复制,变成主节点(slaveof no one)
执行clusterDelSlot撤销故障主节点负责的槽,并执行clusterAddSlot把这些槽分配给自己
向集群广播自己的pong消息,表明自己已经替换成为主节点