20210304. 6. Redis 高可用方案 - 拉勾教育
Redis 高可用方案
- 高可用性(High Availability)通常来描述一个系统经过专门的设计,从而减少停工时间,而保持其服务的高度可用性。即 CAP 中的 A 。
- 单机的 Redis 是无法保证高可用性的,当 Redis 服务器宕机后,即使在有持久化的机制下也无法保证不丢失数据。所以我们采用 Redis 多机和集群的方式来保证 Redis 的高可用性。
- 单进程 + 单线程 + 多机 (集群)
主从复制
Redis 支持主从复制功能,可以通过执行 slaveof
( Redis 5 以后改成 replicaof
)或者在配置文件中设置 slaveof
( Redis 5 以后改成 replicaof
)来开启复制功能。
具体情况可以分为:
- 一主一从
- 一主多从
- 传递复制
传递复制:
- 主对外从对内,主可写从不可写
- 主挂了,从不可为主
主从配置
主 Redis 配置
无需特殊配置
从 Redis 配置
修改从服务器上的 redis.conf
文件:
# slaveof <masterip> <masterport>
# 表示当前【从服务器】对应的【主服务器】的IP是192.168.10.135,端口是6379。
replicaof 127.0.0.1 6379
作用
读写分离
- 一主多从,主从同步
- 主负责写,从负责读
- 提升 Redis 的性能和吞吐量
- 主从的数据一致性问题
数据容灾
- 从机是主机的备份
- 主机宕机,从机可读不可写
- 默认情况下主机宕机后,从机不可为主机
- 利用哨兵可以实现主从切换,做到高可用
原理与实现
复制流程
保存主节点信息
当客户端向从服务器发送 slaveof
( replicaof
) 主机地址( 127.0.0.1 ) 端口( 6379 )时:从服务器将主机 ip ( 127.0.0.1 )和端口( 6379 )保存到 redisServer
的 masterhost
和 masterport
中。
Struct redisServer{
char *masterhost; //主服务器ip
int masterport; //主服务器端口
} ;
从服务器将向发送 SLAVEOF
命令的客户端返回 OK
,表示复制指令已经被接收,而实际上复制工作是在 OK
返回之后进行。
建立 Socket 连接
Slaver 与 Master 建立 Socket 连接
Slaver 关联文件事件处理器,该处理器接收 RDB 文件(全量复制)、接收 Master 传播来的写命令(增量复制)
主服务器 accept 到从服务器的 Socket 连接后,创建相应的客户端状态。相当于从服务器是主服务器的 Client 端。
发送 ping 命令
Slaver 向 Master 发送 ping
命令:
- 检测 Socket 的读写状态
- 检测 Master 能否正常处理
Master 的响应:
- 发送
pong
,说明正常 - 返回错误,说明 Master 不正常
- timeout,说明网络超时
权限验证
主从正常连接后,进行权限验证:
- 主未设置密码(
requirepass=""
) ,从也不用设置密码(masterauth=""
) - 主设置密码(
requirepass!=""
),从需要设置密码(masterauth=主的requirepass的值
),或者从通过auth
命令向主发送密码
发送端口信息
在身份验证步骤之后,从服务器将执行命令 REPLCONF listening-port
,向主服务器发送从服务器的监听端口号。
同步数据
Redis 2.8 之后分为全量同步和增量同步,具体的后面详细讲解。
命令传播
当同步数据完成后,主从服务器就会进入命令传播阶段,主服务器只要将自己执行的写命令发送给从服务器,而从服务器只要一直执行并接收主服务器发来的写命令。
同步数据集
- Redis 2.8 以前使用
SYNC
命令同步复制 - Redis 2.8 之后采用
PSYNC
命令替代SYNC
旧版本
Redis 2.8 以前
实现方式
Redis 的同步功能分为同步( sync )和命令传播( command propagate )。
-
同步操作:
- 通过从服务器发送到
SYNC
命令给主服务器 - 主服务器生成 RDB 文件并发送给从服务器,同时发送保存所有写命令给从服务器
- 从服务器清空之前数据并执行解释 RDB 文件
- 保持数据一致(还需要命令传播过程才能保持一致)
- 通过从服务器发送到
-
命令传播操作:
同步操作完成后,主服务器执行写命令,该命令发送给从服务器并执行,使主从保存一致。
缺陷
- 没有全量同步和增量同步的概念,从服务器在同步时,会清空所有数据。
- 主从服务器断线后重新复制,主服务器会重新生成 RDB 文件和重新记录缓冲区的所有命令,并全量同步到从服务器上。
新版本
Redis 2.8 以后
实现方式
在 Redis 2.8 之后使用 PSYNC
命令,具备完整重同步和部分重同步模式。
- Redis 的主从同步,分为 全量同步 和 增量同步
- 只有从机第一次连接上主机是 全量同步
- 断线重连有可能触发 全量同步 也有可能是 增量同步( Master 判断
runid
是否一致) - 除此之外的情况都是 增量同步
全量同步
Redis 的全量同步过程主要分三个阶段:
- 同步快照阶段 : Master 创建并发送快照 RDB 给 Slave , Slave 载入并解析快照。 Master 同时将此阶段所产生的新的写命令存储到缓冲区。
- 同步写缓冲阶段 : Master 向 Slave 同步存储在缓冲区的写操作命令。
- 同步增量阶段 : Master 向 Slave 同步写操作命令。
增量同步
- Redis 增量同步主要指 Slave 完成初始化后开始正常工作时, Master 发生的写操作同步到 Slave 的过程
- 通常情况下, Master 每执行一个写命令就会向 Slave 发送相同的写命令,然后 Slave 接收并执行
心跳检测
在命令传播阶段,从服务器默认会以每秒一次的频率向主服务器发送命令:
replconf ack <replication_offset>
#ack :应答
#replication_offset:从服务器当前的复制偏移量
主要作用有三个:
-
检测主从的连接状态
- 检测主从服务器的网络连接状态
- 通过向主服务器发送
INFO replication
命令,可以列出从服务器列表,可以看出从最后一次向主发送命令距离现在过了多少秒。 lag
(延迟)的值应该在 0 或 1 之间跳动,如果超过 1 则说明主从之间的连接有故障。
-
辅助实现 min-slaves
-
Redis 可以通过配置防止主服务器在不安全的情况下执行写命令
min-slaves-to-write 3 ( min-replicas-to-write 3 ) min-slaves-max-lag 10 ( min-replicas-max-lag 10 )
-
上面的配置表示:从服务器的数量少于 3 个,或者三个从服务器的延迟( lag )值都大于或等于 10 秒时,主服务器将拒绝执行写命令。这里的延迟值就是上面
INFO replication
命令的 lag 值。
-
-
检测命令丢失
- 如果因为网络故障,主服务器传播给从服务器的写命令在半路丢失,那么当从服务器向主服务器发送
REPLCONF ACK
命令时,主服务器将发觉从服务器当前的复制偏移量少于自己的复制偏移量,然后主服务器就会根据从服务器提交的复制偏移量,在复制积压缓冲区里面找到从服务器缺少的数据,并将这些数据重新发送给从服务器。(补发) 网络不断 - 增量同步:网断了,再次连接时
- 如果因为网络故障,主服务器传播给从服务器的写命令在半路丢失,那么当从服务器向主服务器发送
哨兵模式
哨兵( sentinel )是 Redis 的高可用性( High Availability )的解决方案:
- 由一个或多个 sentinel 实例组成,sentinel 集群可以监视一个或多个主服务器和多个从服务器
- 当主服务器进入下线状态时, sentinel 可以将该主服务器下的某一从服务器升级为主服务器继续提供服务,从而保证 Redis 的高可用性
部署方案
搭建配置
略
执行流程
启动并初始化 Sentinel
Sentinel 是一个特殊的 Redis 服务器,不会进行持久化
Sentinel 实例启动后,每个 Sentinel 会创建 2 个连向主服务器的网络连接:
- 命令连接:用于向主服务器发送命令,并接收响应
- 订阅连接:用于订阅主服务器的
_sentinel_:hello
频道
获取主服务器信息
Sentinel 默认每 10s 一次,向被监控的主服务器发送 info
命令,获取主服务器和其下属从服务器的信息。
127.0.0.1:6379> info
# Server
redis_version:5.0.5
os:Linux 3.10.0-229.el7.x86_64 x86_64
run_id:a4e06ab61b4116660aa37b85079ed482b0b695b1
# Replication
role:master
connected_slaves:2
slave0:ip=127.0.0.1,port=6380,state=online,offset=1571684,lag=1
slave1:ip=127.0.0.1,port=6381,state=online,offset=1571551,lag=1
master_replid:366322125dd7dc9bc95ed3467cfec841c112e207
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:1571684
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:523109
repl_backlog_histlen:1048576
获取从服务器信息
当 Sentinel 发现主服务器有新的从服务器出现时, Sentinel 还会向从服务器建立命令连接和订阅连接。在命令连接建立之后, Sentinel 还是默认 10s 一次,向从服务器发送 info
命令,并记录从服务器的信息。
# Server
redis_version:5.0.5
os:Linux 3.10.0-229.el7.x86_64 x86_64
run_id:e289b3286352aaf8cc9f1ac7ebcc6d36131b8321
# Replication
role:slave
master_host:127.0.0.1
master_port:6379
master_link_status:up
master_last_io_seconds_ago:0
master_sync_in_progress:0
slave_repl_offset:1699595
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:366322125dd7dc9bc95ed3467cfec841c112e207
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:1699595
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:651020
repl_backlog_histlen:1048576
向主服务器和从服务器发送消息(以订阅的方式)
默认情况下,Sentinel 每 2s 一次,向所有被监视的主服务器和从服务器所订阅的 _sentinel_:hello
频道上发送消息,消息中会携带 Sentinel 自身的信息和主服务器的信息。
接收来自主服务器和从服务器的频道信息
当 Sentinel 与主服务器或者从服务器建立起订阅连接之后, Sentinel 就会通过订阅连接,向服务器发送以下命令:
subscribe _sentinel_:hello
Sentinel 彼此之间只创建命令连接,而不创建订阅连接,因为 Sentinel 通过订阅主服务器或从服务器,就可以感知到新的 Sentinel 的加入,而一旦新 Sentinel 加入后,相互感知的 Sentinel 通过命令连接来通信就可以了。
检测主观下线状态
- Sentinel 每秒一次向所有与它建立了命令连接的实例(主服务器、从服务器和其他 Sentinel )发送
PING
命令 - 实例在
down-after-milliseconds
毫秒内返回无效回复(除了+PONG
、-LOADING
、-MASTERDOWN
外) - 实例在
down-after-milliseconds
毫秒内无回复(超时) - Sentinel 就会认为该实例主观下线( SDown )
检查客观下线状态
当一个 Sentinel 将一个主服务器判断为主观下线后 Sentinel 会向同时监控这个主服务器的所有其他 Sentinel 发送查询命令:
# 主机发送
SENTINEL is-master-down-by-addr <ip> <port> <current_epoch> <runid>
# 其他 Sentinel 回复
<down_state>< leader_runid >< leader_epoch >
判断它们是否也认为主服务器下线。如果达到 Sentinel 配置中的 quorum
数量的 Sentinel 实例都判断主服务器为主观下线,则该主服务器就会被判定为客观下线( ODown )。
选举 Leader Sentinel
当一个主服务器被判定为客观下线后,监视这个主服务器的所有 Sentinel 会通过选举算法( Raft ),选出一个 Leader Sentinel 去执行 failover (故障转移)操作。
哨兵leader选举
Raft
- Raft 协议是用来解决分布式系统一致性问题的协议。
- Raft 协议描述的节点共有三种状态: Leader , Follower , Candidate 。
term
: Raft 协议将时间切分为一个个的 Term (任期),可以认为是一种 逻辑时间 。- Raft 采用心跳机制触发 Leader 选举
- Raft 协议的定时器采取随机超时时间,这是选举 Leader 的关键
- 在同一个
term
内,先转为 Candidate 的节点会先发起投票,从而获得多数票
选举流程:
- 系统启动后,全部节点初始化为 Follower ,
term
为 0 - 节点如果收到了
RequestVote
或者AppendEntries
,就会保持自己的 Follower 身份 - 节点如果一段时间内没收到
AppendEntries
消息,在该节点的超时时间内还没发现 Leader , Follower 就会转换成 Candidate ,自己开始竞选 Leader - 一旦转化为 Candidate ,该节点立即开始下面几件事情:
- 增加自己的
term
- 启动一个新的定时器
- 给自己投一票
- 向所有其他节点发送
RequestVote
,并等待其他节点的回复
- 增加自己的
- 如果在计时器超时前,节点收到多数节点的同意投票,就转换成 Leader 。同时向所有其他节点发送
AppendEntries
,告知自己成为了 Leader 。 - 每个节点在一个
term
内只能投一票,采取先到先得的策略, Candidate 前面说到已经投给了自己, Follower 会投给第一个收到RequestVote
的节点
Sentinel 的 Leader 选举流程:
- 某 Sentinel 认定 Master 客观下线后,该 Sentinel 会先看看自己有没有投过票,如果自己已经投过票给其他 Sentinel 了,在一定时间内自己就不会成为 Leader
- 如果该 Sentinel 还没投过票,那么它就成为 Candidate
- Sentinel 需要完成几件事情:
- 更新故障转移状态为
start
- 当前
epoch
加 1 ,相当于进入一个新term
,在 Sentinel 中epoch
就是 Raft 协议中的term
- 向其他节点发送
is-master-down-by-addr
命令请求投票。命令会带上自己的epoch
- 给自己投一票(leader、leader_epoch)
- 更新故障转移状态为
- 当其它哨兵收到此命令时,可以同意或者拒绝它成为领导者;(通过判断
epoch
) - Candidate 会不断的统计自己的票数,直到他发现认同他成为 Leader 的票数超过一半而且超过它配置的
quorum
,这时它就成为了 Leader - 其他 Sentinel 等待 Leader 从 Slave 选出 Master 后,检测到新的 Master 正常工作后,就会去掉客观下线的标识
故障转移
当选举出 Leader Sentinel 后, Leader Sentinel 会对下线的主服务器执行故障转移操作,主要有三个步骤:
- 它会将失效 Master 的其中一个 Slave 升级为新的 Master , 并让失效 Master 的其他 Slave 改为复制新的 Master
- 当客户端试图连接失效的 Master 时,集群也会向客户端返回新 Master 的地址,使得集群可以使用现在的 Master 替换失效 Master
- Master 和 Slave 服务器切换后, Master 的
redis.conf
、 Slave 的redis.conf
和sentinel.conf
的配置文件的内容都会发生相应的改变,即, Master 主服务器的redis.conf
配置文件中会多一行replicaof
的配置,sentinel.conf
的监控目标会随之调换
主服务器的选择
哨兵 Leader 根据以下规则从客观下线的主服务器的从服务器中选择出新的主服务器:
- 过滤掉主观下线的节点
- 选择
slave-priority
最高的节点,如果有则返回,没有就继续选择 - 选择出复制偏移量最大的系节点,因为复制偏移量越大则数据复制的越完整,如果有就返回了,没有就继续
- 选择
run_id
最小的节点,因为run_id
越小说明重启次数越少
集群与分区
分区是将数据分布在多个 Redis 实例( Redis 主机)上,以至于每个实例只包含一部分数据。
分区的意义
-
性能的提升
单机 Redis 的网络 I/O 能力和计算资源是有限的,将请求分散到多台机器,充分利用多台机器的计算能力可网络带宽,有助于提高 Redis 总体的服务能力。
-
存储能力的横向扩展
即使 Redis 的服务能力能够满足应用需求,但是随着存储数据的增加,单台机器受限于机器本身的存储容量,将数据分散到多台机器上存储使得 Redis 服务可以横向扩展。
分区的方式
根据分区键(id)进行分区:
范围分区
根据 id 数字的范围分区,比如 1-10000、100001-20000 ..... 90001-100000,每个范围分到不同的 Redis 实例中
id 范围 | Redis 实例 |
---|---|
1-10000 | Redis01 |
100001-20000 | Redis02 |
...... | |
90001-100000 | Redis10 |
- 好处:实现简单,方便迁移和扩展
- 缺陷:
- 热点数据分布不均,性能损失
- 非数字型 key ,比如 uuid 无法使用(可采用雪花算法替代)
hash 分区
利用简单的 hash 算法即可:
Redis实例=hash(key)%N
key:要进行分区的键,比如user_id
N:Redis实例个数(Redis主机)
- 好处:
- 支持任何类型的 key
- 热点分布较均匀,性能较好
- 缺陷:
- 迁移复杂,需要重新计算,扩展较差(利用一致性 hash 环)
client 端分区
对于一个给定的 key ,客户端直接选择正确的节点来进行读写。许多 Redis 客户端都实现了客户端分区( JedisPool ),也可以自行编程实现。
部署方案
客户端选择算法
普通 hash
hash(key)%N
hash:可以采用hash算法,比如CRC32、CRC16等
N:是Redis主机个数
比如:
user_id : u001
hash(u001) : 1844213068
Redis实例=1844213068%3
余数为2,所以选择Redis3。
- 优势:实现简单,热点数据分布均匀
- 缺陷:
- 节点数固定,扩展的话需要重新计算
- 查询时必须用分片的 key 来查,一旦 key 改变,数据就查不出了,所以要使用不易改变的 key 进行分片
一致性 hash
基本概念:普通 hash 是对主机数量取模,而一致性 hash 是对 2^32 ( 4 294 967 296 )取模。我们把 2^32 想象成一个圆,就像钟表一样,钟表的圆可以理解成由 60 个点组成的圆,而此处我们把这个圆想象成由 2^32个点组成的圆,示意图如下:
圆环的正上方的点代表 0 , 0 点右侧的第一个点代表 1 ,以此类推, 2 、 3 、 4 、 5 、 6 …… 直到 2^32-1 ,也就是说 0 点左侧的第一个点代表 2^32-1 。我们把这个由 2 的 32 次方个点组成的圆环称为 hash 环。
假设我们有 3 台缓存服务器,服务器 A 、服务器 B 、服务器 C ,那么,在生产环境中,这三台服务器肯定有自己的 IP 地址,我们使用它们各自的 IP 地址进行哈希计算,使用哈希后的结果对 2^32 取模,可以使用如下公式:
hash(服务器的IP地址) % 2^32
通过上述公式算出的结果一定是一个 0 到 2^32-1 之间的一个整数,我们就用算出的这个整数,代表服务器 A 、服务器 B 、服务器 C ,既然这个整数肯定处于 0 到 2^32-1 之间,那么,上图中的 hash 环上必定有一个点与这个整数对应,也就是服务器 A 、服务器 B 、服务 C 就可以映射到这个环上,如下图:
假设,我们需要使用 Redis 缓存数据,那么我们使用如下公式可以将数据映射到上图中的 hash 环上。
hash(key) % 2^32
映射后的示意图如下,下图中的橘黄色圆形表示数据:
现在服务器与数据都被映射到了 hash 环上,上图中的数据将会被缓存到服务器 A 上,因为从数据的位置开始,沿顺时针方向遇到的第一个服务器就是 A 服务器,所以,上图中的数据将会被缓存到服务器 A 上。如图:
将缓存服务器与被缓存对象都映射到 hash 环上以后,从被缓存对象的位置出发,沿顺时针方向遇到的第一个服务器,就是当前对象将要缓存于的服务器,由于被缓存对象与服务器 hash 后的值是固定的,所以,在服务器不变的情况下,数据必定会被缓存到固定的服务器上,那么,当下次想要访问这个数据时,只要再次使用相同的算法进行计算,即可算出这个数据被缓存在哪个服务器上,直接去对应的服务器查找对应的数据即可。多条数据存储如下:
优点:添加或移除节点时,数据只需要做部分的迁移,比如上图中把 C 服务器移除,则数据 4 迁移到服务器 A 中,而其他的数据保持不变。添加效果是一样的。
hash 环偏移
在介绍一致性哈希的概念时,我们理想化的将 3 台服务器均匀的映射到了 hash 环上。也就是说数据的范围是 2^32/N 。但实际情况往往不是这样的。有可能某个服务器的数据会很多,某个服务器的数据会很少,造成服务器性能不平均。这种现象称为 hash 环偏移。
理论上我们可以通过增加服务器的方式来减少偏移,但这样成本较高,所以我们可以采用虚拟节点的方式,也就是虚拟服务器,如图:
"虚拟节点"是"实际节点"(实际的物理服务器)在 hash 环上的复制品,一个实际节点可以对应多个虚拟节点。
从上图可以看出, A 、 B 、 C 三台服务器分别虚拟出了一个虚拟节点,当然,如果你需要,也可以虚拟出更多的虚拟节点。引入虚拟节点的概念后,缓存的分布就均衡多了,上图中, 1 号、 3 号数据被缓存在服务器 A 中, 5 号、 4 号数据被缓存在服务器 B 中, 6 号、 2 号数据被缓存在服务器 C 中,如果你还不放心,可以虚拟出更多的虚拟节点,以便减小 hash 环偏斜所带来的影响,虚拟节点越多, hash 环上的节点就越多,缓存被均匀分布的概率就越大。
缺点:
-
复杂度高 客户端需要自己处理数据路由、高可用、故障转移等问题
使用分区,数据的处理会变得复杂,不得不对付多个 Redis 数据库和 AOF 文件,不得在多个实例和主机之间持久化你的数据
-
不易扩展
一旦节点的增或者删操作,都会导致 key 无法在 Redis 中命中,必须重新根据节点计算,并手动迁移全部或部分数据。
proxy 端分区
在客户端和服务器端引入一个代理或代理集群,客户端将命令发送到代理上,由代理根据算法,将命令路由到相应的服务器上。常见的代理有 Codis (豌豆荚)和 TwemProxy ( Twitter )。
部署架构
Codis 由豌豆荚于 2014 年 11 月开源,基于 Go 和 C 开发,是近期涌现的、国人开发的优秀开源软件之一。
Codis 3.x 由以下组件组成:
- Codis Server:基于 Redis-3.2.8 分支开发。增加了额外的数据结构,以支持 slot 有关的操作以及数据迁移指令。
- Codis Proxy:客户端连接的 Redis 代理服务, 实现了 Redis 协议。 除部分命令不支持以外,表现的和原生的 Redis 没有区别(就像 Twemproxy)。
- 对于同一个业务集群而言,可以同时部署多个 codis-proxy 实例;
- 不同 codis-proxy 之间由 codis-dashboard 保证状态同步。
- Codis Dashboard:集群管理工具,支持 codis-proxy、codis-server 的添加、删除,以及据迁移等操作。在集群状态发生改变时,codis-dashboard 维护集群下所有 codis-proxy 的状态的一致性。
- 对于同一个业务集群而言,同一个时刻 codis-dashboard 只能有 0 个或者 1 个;
- 所有对集群的修改都必须通过 codis-dashboard 完成。
- Codis Admin:集群管理的命令行工具。
- 可用于控制 codis-proxy、codis-dashboard 状态以及访问外部存储。
- Codis FE:集群管理界面。
- 多个集群实例共享可以共享同一个前端展示页面;
- 通过配置文件管理后端 codis-dashboard 列表,配置文件可自动更新。
- Storage:为集群状态提供外部存储。
- 提供 Namespace 概念,不同集群的会按照不同 product name 进行组织
- 目前仅提供了 Zookeeper、Etcd、Fs 三种实现,但是提供了抽象的 interface 可自行扩展
分片原理
Codis 将所有的 key 默认划分为 1024 个槽位( slot ),它首先对客户端传过来的 key 进行 CRC32 运算计算哈希值,再将 hash 后的整数值对 1024 这个整数进行取模得到一个余数,这个余数就是对应 key 的槽位。
Codis 的槽位和分组的映射关系就保存在 codis proxy 当中。
优点 & 缺点
优点:
- 对客户端透明,与 Codis 交互方式和 Redis 本身交互一样
- 支持在线数据迁移,迁移过程对客户端透明有简单的管理和监控界面
- 支持高可用,无论是 Redis 数据存储还是代理节点
- 自动进行数据的均衡分配
- 最大支持 1024 个 Redis 实例,存储容量海量
- 高性能
缺点:
- 采用自有的 Redis 分支,不能与原版的 Redis 保持同步
- 如果 Codis 的 proxy 只有一个的情况下, Redis 的性能会下降 20% 左右
- 某些命令不支持
官方 Cluster 分区
- Redis 3.0 之后,Redis 官方提供了完整的集群解决方案。
- 方案采用去中心化的方式,包括:sharding(分区)、replication(复制)、failover(故障转移),称为 RedisCluster。
- Redis 5.0 前采用 redis-trib 进行集群的创建和管理,需要 Ruby 支持
- Redis 5.0 可以直接使用 redis-cli 进行集群的创建和管理
部署架构
去中心化
RedisCluster 由多个 Redis 节点组构成,是一个 P2P 无中心节点的集群架构,依靠 Gossip 协议传播的集群。
Gossip 协议
- Gossip 协议是一个通信协议,一种传播消息的方式。起源于:病毒传播
- Gossip 协议基本思想就是:
- 一个节点周期性(每秒)随机选择一些节点,并把信息传递给这些节点。
- 这些收到信息的节点接下来会做同样的事情,即把这些信息传递给其他一些随机选择的节点。
- 信息会周期性的传递给 N 个目标节点。这个 N 被称为 fanout (扇出)
- Gossip 协议包含多种消息,包括
meet
、ping
、pong
、fail
、publish
等等。
命令 | 说明 |
---|---|
meet |
sender 向 receiver 发出,请求 receiver 加入 sender 的集群 |
ping |
节点检测其他节点是否在线 |
pong |
receiver 收到 meet 或 ping 后的回复信息;在 failover 后,新的 Master 也会广播 pong |
fail |
节点 A 判断节点 B 下线后, A 节点广播 B 的 fail 信息,其他收到节点会将 B 节点标记为下线 |
publish |
节点 A 收到 publish 命令,节点 A 执行该命令,并向集群广播 publish 命令,收到 publish 命令的节点都会执行相同的 publish 命令 |
通过 Gossip 协议, Cluster 可以提供集群间状态同步更新、选举自助 failover 等重要的集群功能。
slot
redis-cluster 把所有的物理节点映射到 [0-16383] 个 slot 上,基本上采用平均分配和连续分配的方式。
比如上图中有 5 个主节点,这样在 RedisCluster 创建时, slot 槽可按下表分配:
节点名称 | slot范围 |
---|---|
Redis1 | 0-3270 |
Redis2 | 3271-6542 |
Redis3 | 6543-9814 |
Redis4 | 9815-13087 |
Redis5 | 13088-16383 |
cluster 负责维护节点和 slot 槽的对应关系,value------>slot-------->节点
当需要在 Redis 集群中放置一个 key-value 时, Redis 先对 key 使用 crc16 算法算出一个结果,然后把结果对 16384 求余数,这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽, Redis 会根据节点数量大致均等的将哈希槽映射到不同的节点。
比如:
set name zhaoyun
hash("name")
采用 crc16 算法,得到值:1324203551%16384=15903
根据上表 15903 在 13088-16383 之间,所以 name 被存储在 Redis5 节点。
slot 槽必须在节点上连续分配,如果出现不连续的情况,则 RedisCluster 不能工作,详见容错。
RedisCluster 的优势
- 高性能
- Redis Cluster 的性能与单节点部署是同级别的。
- 多主节点、负载均衡、读写分离
- 高可用
- Redis Cluster 支持标准的 主从复制配置来保障高可用和高可靠。
- failover
- Redis Cluster 也实现了一个类似 Raft 的共识方式,来保障整个集群的可用性。
- 易扩展
- 向 Redis Cluster 中添加新节点,或者移除节点,都是透明的,不需要停机。
- 水平、垂直方向都非常容易扩展。
- 数据分区,海量数据,数据存储
- 原生
- 部署 Redis Cluster 不需要其他的代理或者工具,而且 Redis Cluster 和单机 Redis 几乎完全兼容。
集群搭建
略
分片
不同节点分组服务于相互无交集的分片( sharding ), Redis Cluster 不存在单独的 proxy 或配置服务器,所以需要将客户端路由到目标的分片。
客户端路由
Redis Cluster 的客户端相比单机 Redis 需要具备路由语义的识别能力,且具备一定的路由缓存能力。
moved 重定向
- 每个节点通过通信都会共享 Redis Cluster 中槽和集群中对应节点的关系
- 客户端向 Redis Cluster 的任意节点发送命令,接收命令的节点会根据 CRC16 规则进行 hash 运算与 16384 取余,计算自己的槽和对应节点
- 如果保存数据的槽被分配给当前节点,则去槽中执行命令,并把命令执行结果返回给客户端
- 如果保存数据的槽不在当前节点的管理范围内,则向客户端返回
moved
重定向异常 - 客户端接收到节点返回的结果,如果是
moved
异常,则从moved
异常中获取目标节点的信息 - 客户端向目标节点发送命令,获取命令执行结果
[root@localhost bin]# ./redis-cli -h 127.0.0.1 -p 7001 -c
127.0.0.1:7001> set name:001 zhaoyun
OK
127.0.0.1:7001> get name:001
"zhaoyun"
[root@localhost bin]# ./redis-cli -h 127.0.0.1 -p 7002 -c
127.0.0.1:7002> get name:001
-> Redirected to slot [4354] located at 127.0.0.1:7001
"zhaoyun"
127.0.0.1:7001> cluster keyslot name:001
(integer) 4354
ask 重定向
在对集群进行扩容和缩容时,需要对槽及槽中数据进行迁移
当客户端向某个节点发送命令,节点向客户端返回 moved
异常,告诉客户端数据对应的槽的节点信息
如果此时正在进行集群扩展或者缩空操作,当客户端向正确的节点发送命令时,槽及槽中数据已经被迁移到别的节点了,就会返回 ask
,这就是 ask 重定向机制
- 客户端向目标节点发送命令,目标节点中的槽已经迁移支别的节点上了,此时目标节点会返回
ask
转向给客户端 - 客户端向新的节点发送
Asking
命令给新的节点,然后再次向新节点发送命令 - 新节点执行命令,把命令执行结果返回给客户端
Smart 智能客户端
JedisCluster
- JedisCluster 是 Jedis 根据 RedisCluster 的特性提供的集群智能客户端
- JedisCluster 为每个节点创建连接池,并跟节点建立映射关系缓存( Cluster slots )
- JedisCluster 将每个主节点负责的槽位一一与主节点连接池建立映射缓存
- JedisCluster 启动时,已经知道 key , slot 和 node 之间的关系,可以找到目标节点
- JedisCluster 对目标节点发送命令,目标节点直接响应给 JedisCluster
- 如果 JedisCluster 与目标节点连接出错,则 JedisCluster 会知道连接的节点是一个错误的节点,此时节点返回 moved 异常给 JedisCluster , JedisCluster 会重新初始化 slot 与 node 节点的缓存关系,然后向新的目标节点发送命令,目标命令执行命令并向 JedisCluster 响应
- 如果命令发送次数超过 5 次,则抛出异常
Too many cluster redirection!
JedisPoolConfig config = new JedisPoolConfig();
Set<HostAndPort> jedisClusterNode = new HashSet<HostAndPort>();
jedisClusterNode.add(new HostAndPort("192.168.127.128", 7001));
jedisClusterNode.add(new HostAndPort("192.168.127.128", 7002));
jedisClusterNode.add(new HostAndPort("192.168.127.128", 7003));
jedisClusterNode.add(new HostAndPort("192.168.127.128", 7004));
jedisClusterNode.add(new HostAndPort("192.168.127.128", 7005));
jedisClusterNode.add(new HostAndPort("192.168.127.128", 7006));
JedisCluster jcd = new JedisCluster(jedisClusterNode, config);
jcd.set("name:001","zhangfei");
String value = jcd.get("name:001");
迁移
在 RedisCluster 中每个 slot 对应的节点在初始化后就是确定的。在某些情况下,节点和分片需要变更:
- 新的节点作为 Master 加入
- 某个节点分组需要下线
- 负载不均衡需要调整 slot 分布
此时需要进行分片的迁移,迁移的触发和过程控制由外部系统完成。包含下面 2 种:
- 节点迁移状态设置:迁移前标记源/目标节点。
- key 迁移的原子化命令:迁移的具体步骤。
- 向节点 B 发送状态变更命令,将 B 的对应 slot 状态置为
importing
- 向节点 A 发送状态变更命令,将 A 对应的 slot 状态置为
migrating
- 向 A 发送
migrate
命令,告知 A 将要迁移的 slot 对应的 key 迁移到 B - 当所有 key 迁移完成后,cluster setslot 重新设置槽位
扩容、缩容
略
容灾(failover)
故障检测
- 集群中的每个节点都会定期地(每秒)向集群中的其他节点发送
PING
消息 - 如果在一定时间内( cluster-node-timeout ),发送
ping
的节点 A 没有收到某节点 B 的pong
回应,则 A 将 B 标识为pfail
- A 在后续发送
ping
时,会带上 B 的pfail
信息, 通知给其他节点 - 如果 B 被标记为
pfail
的个数大于集群主节点个数的一半( N/2+1 )时, B 会被标记为fail
, A 向整个集群广播,该节点已经下线 - 其他节点收到广播,标记 B 为
fail
从节点选举
- Raft ,每个从节点,都根据自己对 Master 复制数据的
offset
,来设置一个选举时间,offset
越大(复制数据越多)的从节点,选举时间越靠前,优先进行选举。 - Slave 通过向其他 Master 发送
FAILVOER_AUTH_REQUEST
消息发起竞选, Master 收到后回复FAILOVER_AUTH_ACK
消息告知是否同意。 - Slave 发送
FAILOVER_AUTH_REQUEST
前会将currentEpoch
自增,并将最新的Epoch
带入到FAILOVER_AUTH_REQUEST
消息中,如果自己未投过票,则回复同意,否则回复拒绝 - 所有的 Master 开始 Slave 选举投票,给要进行选举的 Slave 进行投票,如果大部分 Master node ( N/2+1 )都投票给了某个从节点,那么选举通过,那个从节点可以切换成 Master
RedisCluster 失效的判定:
- 集群中半数以上的主节点都宕机(无法投票)
- 宕机的主节点的从节点也宕机了(slot 槽分配不连续)
变更通知
当 Slave 收到过半的 Master 同意时,会成为新的 Master 。此时会以最新的 Epoch
通过 PONG
消息广播自己成为 Master ,让 Cluster 的其他节点尽快的更新拓扑结构( node.conf
)。
主从切换
自动切换
就是上面讲的从节点选举
手动切换
人工故障切换是预期的操作,而非发生了真正的故障,目的是以一种安全的方式(数据无丢失)将当前 Master 节点和其中一个 Slave 节点(执行 cluster-failover 的节点)交换角色:
- 向从节点发送 cluster failover 命令( slaveof no one )
- 从节点告知其主节点要进行手动切换(
CLUSTERMSG_TYPE_MFSTART
) - 主节点会阻塞所有客户端命令的执行(10s)
- 从节点从主节点的 ping 包中获得主节点的复制偏移量
- 从节点复制达到偏移量,发起选举、统计选票、赢得选举、升级为主节点并更新配置
- 切换完成后,原主节点向所有客户端发送
moved
指令重定向到新的主节点
以上是在主节点在线情况下。
如果主节点下线了,则采用 cluster failover force 或 cluster failover takeover 进行强制切换。
副本漂移
我们知道在一主一从的情况下,如果主从同时挂了,那整个集群就挂了。
为了避免这种情况我们可以做一主多从,但这样成本就增加了。
Redis 提供了一种方法叫副本漂移,这种方法既能提高集群的可靠性又不用增加太多的从机。
如图:
- Master1 宕机,则 Slaver11 提升为新的 Master1
- 集群检测到新的 Master1 是单点的(无从机)
- 集群从拥有最多的从机的节点组( Master3 )中,选择节点名称字母顺序最小的从机( Slaver31 )漂移到单点的主从节点组( Master1 )。
具体流程如下(以上图为例):
- 将 Slaver31 的从机记录从 Master3 中删除
- 将 Slaver31 的的主机改为 Master1
- 在 Master1 中添加 Slaver31 为从节点
- 将 Slaver31 的复制源改为 Master1
- 通过 ping 包将信息同步到集群的其他节点