系统中只有一台redis服务器是不可靠的,容易出现单点故障。Redis 集群有三种模式。
1.主从模式
1.1 主从模式的优缺点
主从模式优点:
- 读写分离,分担主库压力
- 数据备份,提供多个副本
- 高可用基石:从复制是redis哨兵模式和集群模式的基础
- 故障恢复:master故障时,slave可以提供服务
不足: 不具备自动容错和恢复功能。
1.2.主从复制实现原理
Redis 提供了复制(replication)
功能实现master
数据库中的数据更新后,会自动将更新的数据同步到其他slave
数据库上。
复制命令主服务器
向从服务器
发送:
SLAVEOF <master_ip> <master_port>
复制过程主要可以分为3个阶段:
- 连接建立阶段:在主从节点之间建立连接
- 数据同步阶段:执行数据的全量(或增量)复制(复制RDB文件)
- 命令传播阶段:
主服务器
将已执行的命令发送给从服务器
,从服务器
接收命令并执行,从而实现主从节点的数据一致性
1.2 旧版复制实现SYNC
1.2.1 同步sync
slave
向master
发送SYNC
命令;master
收到SYNC
命令,执行BGSAVE
,在后台生成一个RDB文件,并使用一个缓冲记录区记录从现在开始执行的所有写命令
。master
的BGSAVE
执行完,将RDB文件发送给slave
,slave
将数据库状态更新至master
服务器执行BGSAVE
时的状态。master
将缓冲区所有的写命令
发送给slave
,slave
执行写命令
,将数据库状态更新至主服务器当前状态。
1.2.2 命令传播
slave
将自己执行的写命令
,通过命令传播给slave
执行
缺陷
(1) 考虑 slave
断线后复制的情况:slave
向master
发送SYNC
命令,master
将包含上次SYNC以后所有的键的RDB文件发送給slave
,
其中大部分key都是命令传播已执行的,对slave
来说不必要。
(2)SYNC
命令是一个非常耗资源的操作
1.3 新版复制实现PSYNC
新版复制功能用PSYNC
命令代替SYNC
命令。有完整同步和部分同步两种模式。
1.3.1 完整同步模式
完整同步模式用于处理初次复制,与SYNC
复制基本一致。
1.3.2 部分同步模式
部分同步实现依靠三个特性,复制偏移量(replication offset),复制积压缓冲区(replication backlog)和 服务器运行ID。
(1)master的复制偏移量和slave的复制偏移量(replication offset)
执行复制的双方,分别维护一个复制偏移量,master
向slave
传播N个字节数据的时候,offset + N
;
断线后slave
立即重连上master
,slave
向master
发送PSYNC,报告slave
当前的偏移量
(2)master的复制积压缓冲区(replication backlog)
master
维护一个固定长度的先进先出的队列,默认1mb,可修改。
master
进行命令传播时,不仅将写命令
发送给所有slave
,而且将写命令
入队到复制积压缓冲区
。
当slave
发送PSYNC
时,master
根据复制偏移量
来决定何总同步:
offset后的数据存在复制积压缓冲
区,master
对从执行部分同步操作;否则,完整同步。
(3)服务器运行ID
slave
对master
进行初次复制时,master
会将自己的运行id
给slave
;
slave
断线重连后,判断重连后的master
是否是之前的master
,如果是,尝试执行部分同步
1.4 心跳检测
命令传播
阶段,从服务器
每秒一次,向主服务器
发送命令
REPLCONF ACK <replication_offset>
对于主服务器
,REPLCONF ACK有三个作用
(1)检测主从服务器网络连接状态
通过向主从服务发送
INFO replication
命令,在lag
栏,显示了slave
最后一次向master
发送REPLCONF ACK 命令距离现在过了多少秒(一般在0和1)
(2)辅助实现min-slaves配置选项
min-slaves-to-write 3
min-slaves-max_lag 10
slave
数量少于2,或者3个slave
的延迟都大于等于10s, master
拒绝执行写命令。
(3)检测命令丢失
master
检测到后,再次向slave
传播命令。
与部分同步原理相似,区别在于补发缺失数据操作在slave
没有断线情况下执行。
2 Sentinel(哨兵)模式
Sentinel是redis高可用性的解决方案。它专注于对Redis实例(主节点、从节点)的监控,是自动故障转移的可靠组件。
- 监控(Monitoring) : 监控Redis主节点、从节点是否处于预期的工作状态。
- 通知(Notification) :可以把Redis实例的运行故障信息通过API通知其他应用程序。
- 自动故障恢复(Automatic failover) :当主节点运行故障时,哨兵会启动自动故障恢复流程
- 配置中心(Configuration provider) :可以为客户端提供了Redis主从节点的地址信息
哨兵是Redis的一种运行模式,对于每个被sentinel监视的主服务器来说,sentinel会创建两个连向主服务器的异步网络连接
- 命令连接: 向主服务器发送命令
- 订阅链接: 订阅主服务器的
__sentinel__:hello
频道
2.1 获取主服务器信息
Sentinel会默认以每十秒一次
的频率向被监视的主服务器
发送INFO
命令。
主服务器
的INFO
命令回复会包含2个重要信息
-
主服务器
本身信息,如服务器运行ID和服务器角色(主服务器值为master,从服务由slave开头) -
主服务器
下属所有从服务器
信息(Sentinel可以自动发现从服务器)
2.3 获取从服务器信息
新的从服务器
出现时,Sentinel会创建到命令连接
和订阅链接
。
通过命令连接
,Sentinel默认以每十秒一次
的频率向从服务器
发送INFO
命令,更新自己的实例结构。从服务器
回复主要包括以下内容:
- 从服务器的服务器ID
- 从服务角色
- 从服务ip地址及端口号
- 从服务优先级
- 从服务复制偏移量
2.4 向主服务器和从服务器发送信息
Sentinel默认会以每两秒一次
的频率通过命令连接
向所有主从服务器
的__sentinel__:hello
频道发送如下格式命令
PUBLISH __sentinel__:hello
"<s_ip>,<s_port>,<s_runid>,<s_epoch>,<m_name>,<m_ip>,<s_port>,<s_epoch>"
其中s_开头参数表示Sentinel本身信息,m_开头表示主服务器信息
2.5 接收服务器的频道消息
Sentinel与被监视的服务器还有一个订阅连接
,通过订阅连接
向服务器发送命令
SUBSCRIBE __sentinel__:hello
Sentinel对__sentinel__:hello
频道的订阅连接
会一直持续到服务器连接断开。
Sentinel可以通过订阅连接接收服务器的信息,对于监视同一个服务器的多个Sentinel来说,一个Sentinel发送的信息会被其他Sentinel接收。而对于其他Sentinel来说,就可以用这些信息更新对发信息的Sentinel和服务器的认知
但Sentinel从hello频道接收一条消息,分析消息时就会提取信息中Sentinel的ip,port和runid。当runid和自身相同时则是自己发的,消息不做处理。否则就提醒消息中其他Sentinel的信息并更新自身的实例结构。
2.6 创建和其他Sentinel的命令连接
当Sentinel通过频道消息
发现新的Sentinel时,它首先会在自己的sentinels的字典中创建相应的实例结构,还会创建一个连向新Sentinel的命令连接
。同样新的Sentinel也会创建一个连向这个Sentinel的命令连接
。最终监视同一服务器的多个Sentinel形成相互连接的网络。
Sentinel之间只会创建命令连接
,不会创建订阅连接
。
2.7 检查主观下线状态
默认情况下,Sentinel会以每秒一次
的频率向所有与它创建了命令连接
的实例(包括主服务器。从服务器、其他Sentinel)发送PING
命令,并通过实例返回的PING命令回复来判断实例是否在线。
对于PING命令有两种回复
-
有效回复: +PONG、-LOADING、-MASTERDOWN其中一种
-
无效回复:上面三种回复之外的其他回复,或者限定时间内没有回复
Sentinel配置文件中的down-after-milliseconds
选项指定了Sentinel判断实例进入主观下线
所需的时长。
如果实例在配置值的毫秒内连续向Sentinel返回无效回复,那么Sentinel就会修改实例结构,将结构中的flags属性设为SRI_S_DOWN
,将主服务器
表示主观下线
down-after-milliseconds
作用对象为和Sentinel有命令连接的所有实例,包括主服务、从服务器和其他Sentinel
不同的Sentinel设置的主观下线
时长可能不同,因此当一个Sentinel判断主服务主观下线时,其他Sentinel可能还会认为主服务器处于在线状态。
2.8 检查客观下线状态
当一个Sentinel判断主服务器主观下线
后,
询问其他Sentinel的是否下线:
SENTINEL is-master-down-by-addr <ip><port><current_epoch><runid>
判断下线的Sentinel数量达到配置值时(一般一半),Sentinel才会将服务器判断为客观下线
,并对主服务器进行故障转移
。
回复is-master-down-by-addr
命令的参数有:
-
ip,port表示被判定主观下线的主服务器的ip和端口
-
current_epoch表示Sentinel的当前纪元
-
runid 可以是*号和自己的运行id,*表示仅检查主服务的客观下线状态,运行id则用于选举领头Sentinel
当其他Sentinel接收到命令后,就会去检查主服务器
是否下线,然后向源Sentinel回复消息。源Sentinel会主服务器主观下线
的数量达到了配置的客观下线
条件时,Sentinel就会将主服务器
的实例结构的的flags置为SRI_O_DOWN
,表示主服务客观下线
。
不同的Sentinel判断客观下线的条件可能不同,当一个Sentinel将主服务器判断为客观下线时,其他Sentinel可能并不是这么判断。
2.9 故障转移
当一个主服务器
被判断为客观下线
后,所有监视这个主服务器
的Sentinel就会进行协商,选举出一个领头Sentinel
,之后再由这个领头Sentinel
对下线的主服务器
进行故障转移。
2.9.1 选择新的主服务器
SLAVEOF no one命令
领头Sentinel
将下线的主服务器
的所有从服务器
保存到一个列表,按照以下规则,从从服务器
中选出一个状态良好,数据完整的从服务器
,发送SLAVEOF no one
命令,将从服务器
转换为主服务器
。
- 删除列表中所有处于下线或断线状态的
从服务器
(保证在线) - 删除列表中所有超过
5秒
没有回复领头Sentinel
的INFO
命令的从服务器
(保证通信正常) - 删除所有与下线的主服务器断连超过
down-after-milliseconds*10
毫秒的从服务器
(保证数据较新) - 满足条件的
从服务器
不止一个,再按复制偏移量
最大,运行ID
最小排序
INFO命令
领头Sentinel
之后会以每秒一次
的频率向要升级的从服务器
发送INFO
命令,并关注命令回复中的role信息。当服务器的role从slave
变成master
后,表示从服务器
升级成功。
2.9.2 修改从服务器的复制目标
升级成功后,领头Sentinel
紧接着就会向其他从服务器
发送SLAVEOF
命令,让它们去复制新的主服务器。最后领头Sentinel将下线的主服务设为新主服务器的从服务器。
最后,领头Sentinel
将下线的主服务器
设为新主服务器
的从服务器
。
3 集群
redis3.0以后推出的redis cluster 集群方案,redis cluster集群保证了高可用、高性能、高可扩展性。Redis的集群策略有下面几种
- 推特:twemproxy
- 豌豆荚:codis
- 官方:redis cluster
3.1 集群实现
集群由多个节点组成。多个节点之间通过CLUSTER MEET
命令进行连接,从而构造了集群。
节点根据cluster-enabled
配置选项是否为yes
来决定是否开启集群模式。
查看集群节点:
cluster nodes
CLUSTER MEET命令实现
假设节点node1
已经加入集群,需要将节点node2
加入
- 向
node1
发送CLUSTER MEET node2_ip, node2_port命令,表示将node2
加入集群 node1
为node2
创建clusterNode,添加到自己的clusterSate.nodes字典node2
收到node1
的CLUSTER MEET消息,为node1
创建clusterNode,添加到自己的clusterSate.nodes字典node2
向node1
返回PONG消息node1
接收node2
的PONG,知道node2
接收了CLUSTER MEETnode1
发送PINGnode2
接收PING,知道node1
接收了PONG,- 握手完成
3.2 槽指派
集群整个数据库分为16384
个槽,所有槽都有节点处理时,集群处于上线状态;
使用CLUSTER ADDSLOTS
命令,将一个或多个槽指派给节点。
保存和传播槽指派信息
记录节点槽指派信息
slot和numslots 记录节点负责处理那些槽
struct clusterNode{
//slots数组在索引i上的二进制为1,表示节点负责处理槽i,为0则不处理。
unsiged char slots[16384/8=2048];
//节点处理槽数量
int numslots;
}
传播节点槽指派信息
集群的每个节点都会将自己的slots数组通过消息发送给其他节点,其他节点收到后保存到相应节点的clusterNode结构里
记录集群所有槽指派信息
struct clusterState{
clusterNode *slots[16384];
}
3.3 在集群中执行命令
完成槽指派后,集群上线。
客户端向节点发送数据库键有关命令时,接收命令节点会计算键属于哪个槽,并检查这个槽是否分派给了自己
命令:
CLUSTER KEYSLOT <key>
如果属于当前节点,直接执行命令
未分派给当前节点,向客户端返回MOVED
错误,引导客户端redirect到正确节点
计算键属于哪个槽
def slot_nuber(key):
return CRC16(key) & 16383
MOVED错误 客户端根据MOVED错误,重新发送命令。 集群模式的客户端在收到MOVED错误时,并不会打印,而是根据MOVED错误自动进行节点转向,并打印出转向信息:
3.2 重新分片
重新分片操作将任意数量已经分派给某个节点的槽改为指派给另一个节点,相关槽属的键值对也会从源节点
移动到目标节点
由redis集群管理软件redis-trip
执行,执行步骤如下:
步骤一: 对目标节点
发送CLUSTER SETSLOT <slot> IMPORTING <source_id>
步骤二: 对源节点
发送CLUSTER SETSLOT <slot> MIGRATING <target_id>
步骤三: 对目标节点
发送CLUSTER GETKEYSINSLOT <slot> <count>
步骤四: 对步骤三获得的每个键名
,redis-trip对源节点
发送
MIGRATE <target_id> <target_port> <key_name> 0 <timeout>
步骤五: 重复步骤三 ,步骤四,直到源节点
保存的槽slot的键值对被迁移到目标节点
步骤六: redis-trip
向集群中任意节点发送
CLUSTER SETSLOT <slot> NODE <target_id>
,将槽slot指派给目标节点
,这一信息会通过消息发送至整个集群。
ASK错误
迁移过程中在槽没被迁移走之前,发送与数据库键有关的操作,但键被迁走了,发送ASK错误,并引导到正确的节点。在引导到目标节点后执行对应命令之前会先执行一次ASKING命令。
ASKING命令
ASKING
命令的作用是打开发送命令的客户端的REDIS_ASKING
标识。
接到ASK错误的客户端会根据错误提供的IP地址和端口号,转向至正在导入槽的目标节 点,然后首先向目标节点发送一个ASKING
命令,之后再重新发送原本想要执行的命令。
如果客户端不发送ASKING
命令,而直接发送想要执行的命令的话,那么客户端发送的命令将被节点拒绝执行,并返回 MOVED
错误。
REDIS_ASKING
标识是一次性的,打开后,并在执行后,这个标识会被关闭。