
# 4 主从复制
作用:备份、读写分离
# 4.1 实现方式
假如现在有两个redis,一个6379;一个6380;让6379成为master节点;6380成为slave节点
- 命令方式:
- redis-6380:slaveof 127.0.0.1 6379;主从关系建立后,slave先删除所有数据再从master同步
- 去掉redis-6380:slaveof no one 127.0.0.1 6379
- 配置方式:
- 配置文件:
- slaveof ip port
- slave-read-only:是否现在slave只读
- 配置文件:
# 4.2 全量/部分复制
# 4.2.1 runid和偏移量
runid:
- redis每次启动的时候都会有一个随机的id来保障redis的标识,每次重启之后都是不一样的
- 查看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 全量复制
流程:
- 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 部分复制
流程:
- 当网络发生抖动,slave会与master断开
- master 写命令时,会写一份复制缓冲区的命令
- 当slave在此连接master时 ,传递命令 psync {offset} {runid} ,告诉 master 自己当前的偏移量是多少
- master 向 slave 返回CONTINUE 把 缺失的内容 传递过去
# 4.2.4 命令持续复制
当完成了上面的同步之后,主从服务器就会进入命令传播阶段,主节点会持续地把写命令发送给从节点
# 4.2.5 心跳检测
当完成了上面的同步之后,主从之间维护着长连接并彼此发送心跳命令,便以后续持续发送写命令,主从心跳检测如下图所示:
主从节点彼此都有心跳检测机制,各自模拟成对方的客户端进行通信,主从心跳检测的规则如下:
- 主节点默认每隔 10 秒对从节点发送 ping 命令,判断从节点的存活性和连接状态。可通过修改 redis.conf 配置文件里面的 repl-ping-replica-period 参数来控制发送频率
- 从节点在主线程中每隔 1 秒发送 replconf ack {offset} 命令,给主节点 上报自身当前的复制偏移量,这条命令除了检测主从节点网络之外,还通过发送复制偏移量来保证主从的数据一致
# 4.3 故障处理
# 4.3.1 slave节点故障
没太大影响
# 4.3.2 master节点故障
手动对某个slave执行,slaveof no one
# 4.4 常见问题
# 4.4.1 读写分离
问题:
- 复制数据延迟
- 读到之前的数据
# 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
- 更换复制拓扑
# 5 Redis 哨兵 —— sentinel
主从复制,如果master节点挂掉了,需要手动进行以下操作:
- 对一个slave节点执行slaveof no one,让其成为master节点
- 然后对其他slave节点执行slave of newmaster
- 修改代码,把写节点配置成新的master;可以在diamond里配置
更进一步是把这个过程写成一个脚本,redis有解决方案:sentinel
# 5.1 整体架构
客户端通过sentinel获取redis连接,sentinel知道谁是master谁是slave,客户端不需要知道谁是master;当master节点发生故障时,sentinel自动进行故障转移;
sentinel故障转移流程:
- 多个sentinel发现并确认master有问题
- 选举一个sentinel作为领导
- 选出一个slave作为master
- 通知其余slave成为新的master的slave
- 通知客户端主从变化
- 等待老的master复活成为新master的slave
一套sentinel可以监控多套主从:
# 5.2 安装与配置
实现如下目标,26379是sentinel默认端口:
# 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 三个定时任务
- 每个sentinel每10秒对master和slave执行info
- sentinel从master节点中获取从节点的消息
- 目的:确认主从关系
- 每个sentinel每2秒通过master节点的channel交换信息:第一个sentinel可以在第一个master节点的频道发布信息,其他sentinel节点会订阅这个频道,从而达到sentinel之间的交流
- 频道名称:sentinel:hello
- 交互对节点的“看法”和自身信息
- 新加入的sentinel节点也会去订阅这个频道,感知其他sentinel的存在
- 每个sentinel每1秒对其他sentinel和redis节点(master和slave节点)执行ping,用于故障检测
# 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节点成为了领导者,那么将等待一段时间重新进行选举
# 5.3.4 故障转移
- 从slave节点中选出一个“合适的”节点作为新的master节点
- 选择slave-priority最高的slave节点,如果存在则返回,不存在则继续,一般不设置,大家都是一样的,不过可以考虑把所在机器性能更好的slave设置高一点优先级,master节点的性能就更好。存在则返回,不存在则继续
- 选择复制偏移量更大的slave节点(复制的最完整),存在则返回,不存在则继续
- 选择runId最小的slave节点(启动最早的slave)
- 对上面的slave节点执行slave of no one命令让其成为master节点
- 向剩余的slave节点发送命令,让它们成为新master的slave节点,复制规则和parallel-syncs参数有关
- 对原来的master节点配置为slave,并保持对其“关注”,当其恢复后命令它去复制新的master节点
# 6 Redis Cluster
# 6.1 为什么需要集群
- 并发量:主从只有一个master节点可以写,远远无法满足需要
- 数据量:如果数据量很大,只有一台master,那么复制就会很慢很慢
# 6.2 理论基础
# 6.2.1 顺序分区和哈希分区
顺序分区:
哈希分区:例如节点取模,hash(key)%3
分布方式 | 特点 | 典型产品 |
---|---|---|
哈希分布 | 数据分散度高 分布与业务无关 无法顺序访问 | 一致性哈希Memcache Redis Cluster 其他缓存产品 |
顺序分布 | 数据分散易倾斜 分布与业务相关 可顺序访问 | BigTable HBase |
# 6.2.2 哈希分布详解
# 6.2.2.1 节点取余分区
如果之前redis有3个点,现在扩容成4个点,哈希算法由hash(key)%3变成hash(key)%4,发现大约有80%的请求不在原来的节点上了,就会导致这80%的请求缓存失效,要重新从数据库里取;建议使用翻倍扩容的方法,如果变成6个节点,大约这有50%请求缓存失效,不过这种方法也不好
# 6.2.2.2 一致性哈希分区
规则:
- 确定node1~node2、node2~node4、node4~node3、node3~node1之间的数据范围
- hash(key)%节点数,确定key应该落在哪个区间
- 顺时针取节点,例如key落到了node4~node3,则key的节点为node3
可以看到新增node5,只会影响node1~node5之间的数据
保证最小迁移数据 |
---|
但是有数据倾斜(热点问题),虚拟节点解决数据倾斜问题 |
# 6.2.2.3 虚拟槽分区
16383是redis cluster默认的槽数量
# 6.2.3 meet操作
节点之间可以相互通信,遵守gossip协议,节点之间相互知道自己负责的槽范围
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 扩容
槽迁移计划:
迁移数据:
- 对target节点发送:cluster setslot {slot} import {sourceNodeId}命令,
让目标节点准备导入槽的数据(把源节点中的槽slot导入到目标节点,先让目标节点把槽创建出来) - 对源节点发送:cluster setslot {slot} migrating {targetNodeId}命令,
让源节点准备准备迁出槽的数据(迁移槽数据)- 源节点循环执行cluster getkeysinslot {slot} {count}命令,每次获取count个属于槽的键
- 在源节点上执行migrate {targetIp} {targetPort} key 0 {timeout}命令,把指定key迁移目标节点
- 删除原节点上的槽
- 重复步骤2直到槽下所有的键数据迁移到目标节点
- 向集群内所有主节点发送cluster setslot {slot} node {targetNodeId}命令,通知槽分配给目标节点
在3.0.6版本,支持批量迁移key,pipeline migrate,但是有bug:
如果迁移的key中既有过期的也有没过期的,迁移到新节点都会当成过期的;3.2.8修复
# 6.4.2 缩容
- 下线迁移槽
- 其他节点忘记该节点:cluster forget {downNodeId}
集群中的每一个节点都应该去忘记,不然60s后大家又会检测到它,认为它又活了
- 下线该节点
# 6.5 客户端路由
# 6.5.1 moved重定向(moved异常)
# -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重定向
背景:原来的槽在node1上,但是正在进行扩容,已经迁移到node2上了
# 6.5.3 smart客户端(例如JedisCluster)
- 从集群中选一个可运行的节点,使用cluster slots获取节点和槽的映射关系
- 将cluster slots的结果映射到本地,为每个节点创建JedisPool
- 当客户端要执行命令的时候,直接在本地crc(key)%16383,然后从本地的表中找到具体的节点
- 向具体节点发出命令
(上图中少画中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消息,表明自己已经替换成为主节点 |