Redis复制、哨兵、集群架构

本文详细介绍了Redis的高可用架构,包括复制、哨兵和集群三种模式的特点、配置及实现原理,帮助读者深入理解Redis如何确保系统的稳定性和可用性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Redis高可用架构主要包括:

  • 复制(Replication):扩展系统对于读的能力
  • 哨兵(Sentinel):为服务器提供高可用特性,减少故障停机出现
  • 集群(Cluster):扩展内存容量,增加机器,提高性能读写能力和存储以及提供高可用特性

复制

Redis服务器可以通过执行SLAVEOF命令或者设置slaveof选项,让一个服务器去复制另外一个服务器,被复制的服务器称为主服务器(master),复制的服务器称为从服务器(slave),主从服务器的数据库保存相同的数据。SLAVEOF命令如下:

SLAVEOF <master_ip> <master_port>

设置同步复制后,启动一个slave时,slave会向master发送一个PSYNC命令请求复制数据

  • 如果是第一次连接,那么会进行一次全量复制
  • 如果是重新连接则进行增量复制

全量复制

收到PSYNC命令后,master会fork一个子进程通过BGSAVE命令生成RDB快照,在RDB持久化期间,master会继续接收客户端的请求并缓存修改数据的命令到内存中。持久化进行完毕后,master会把RDB文件发送给slave,slave会把先把RDB文件写入磁盘,然后再加载到内存中。最后master再将之前缓存在内存中的写命令发送给slave。

主从复制(全量复制)流程图

部分复制

在Redis2.8之前只有SYNC命令,不支持部分复制。从Redis2.8版本开始,Redis改用可以支持部分数据复制的命令PSYNC替代SYNC命令,同时支持全量复制和部分复制(断点续传)。

当主从服务器断开重连后,主服务器可以将主从服务器断开连接期间执行的这部分写命令发送给从服务器,以此保证主从服务器的状态一致。部分复制功能由以下三部分构成:

  • 主服务器的复制偏移量(replication offset)和从服务器的复制偏移量
  • 主服务器的复制积压缓冲区(replication backlog)
  • 服务器的运行ID(run ID)

复制偏移量offset

主服务器向从服务器同步N个字节数据,就将自己的offset+N,同样从服务器接收到主服务器同步来的N个字节的数据后,也将offset+N,如果主动服务器处于一致状态,则主从服务器的offset相同,相反,则说明主从服务器并未处于一致的状态

复制积压缓冲区

复制积压缓冲区是主服务器维护的一个固定长度(fixed-size)先进先出(FIFO)队列,默认大小为1MB,当主服务器进行命令传播时,不仅将写命令同步给所有从服务器,还会将写命令写入复制积压缓冲区,因此复制积压缓冲区中是保存了一部分最近传播的写命令,且复制积压缓冲区会为队列中每个字节记录相应的复制偏移量。

当从服务器重新连上主服务器时,从服务器会通过PSYNC命令将自己的复制偏移量offset发送给主服务器,主服务器会根据这个复制偏移量来决定进行何种同步:

  • offset之后的数据存在于复制积压缓冲区,则主服务器对从服务器执行部分同步复制
  • offset之后的数据已经不存在于复制积压缓冲区时,主服务器对从服务器执行全量同步复制

可以通过配置文件的repl_backlog-size去自定义复制积压缓冲区的大小,一般根据断线重连的平均时间*主服务器平均每秒产生的写命令的数据量,在主服务器需要执行大量写命令,或者主从服务器断线后连接重连时间较长建议设置大一点。

服务器运行ID

  • 每个Redis服务器,不论主服务器还是从服务器,都会有自己的运行ID
  • 运行ID在服务器启动时自动生成,由40个随机的十六进制字符组成

当从服务器对主服务器初始复制,主服务器将自己的运行ID发送给从服务器,从服务器将这个运行ID保存起来,当主从服务器断开重连时,从服务器将向当前连接的主服务器发送之前保存的ID,如果ID相同,说明断线之前复制的就是当前连接的主服务器,可尝试增量同步操作,否则必须执行完成全量同步操作。

部分复制、断点续传流程图

PSYNC命令的实现

PSYNC命令的调用方法有两种:

  1. 如果从服务器之前没有复制过任何主服务器或者之前执行过SLAVEOF no one命令,那么从服务器开始一次新的复制时向主服务器发送PSYNC  ?  -1命令,主动请求主服务器进行全量同步(因为这时不可能执行部分重同步)。
  2. 如果从服务器已经复制过某个主服务器,断开重连后的复制时会向主服务器发送 PSYNC <runid> <offset>命令:其中runid是上一次复制的主服务器的运行ID,而offset则是从服务器当前的复制偏移量,接收到命令的主服务器会通过runid和offset判断执行那种复制。

根据情况,接收PSYNC命令的主服务器会向从服务器返回以下三种回复的其中一种:

  1. +FULLRESYNC <runid> <offset>回复,那么表示主从服务器要执行完整重同步操作:其中runid是这个主服务器的运行ID(从服务器会将这个ID保存起来,下次发送PSYNC命令时使用),而offset则是主服务器当前的复制偏移量,从服务器会将这个值作为自己的初始化偏移量。
  2. +CONTINUE回复,那么表示主服务器将与从服务器执行部分重同步操作,从服务器只要等着主服务器将自己缺少的那部分数据发送过来就可以了。
  3. -ERR回复,那么表示主服务器的版本低于Redis 2.8,它识别不了PSYNC命令,从服务器将向主服务器发送SYNC命令,并与主服务器执行完整同步操作。

复制的实现

  1. 在从服务器上执行SLAVEOF <master_ip> <master_port>命令去复制主服务器,同时将主服务器的信息保存到服务器状态redisServer的masterhost和masterport属性中
  2. 从服务器根据ip和port创建连向主服务器的套接字socket连接,socket成功连接主服务器后,从服务器为这个套接字关联一个专门用于处理复制工作的文件事件处理器,负责接收后续的复制工作,比如接收RDB文件、主服务器传播的写命令等;主服务器accept这个套接字连接后,为该套接字创建相应的客户端状态,把从服务器当成一个连接到主服务器的客户端看待
  3. 从服务器成为主服务器的客户端后,会发送一个PING命令,检查套接字的读写状态是否正常,同时检查主服务器能否正常的处理命令请求。
    1.  如果主服务器返回一个命令回复但是从服务器不能在规定时限(timeout)内读取到回复内容,则表示主从服务器连接状态不佳,不能完成复制操作,必须断开并重新创建套接字
    2. 如果主服务器返回一个错误,表示主服务器没办法处理从服务器的命令请求,也必须断开并重新创建套接字
    3. 如果从服务器读取到PONG回复,说明网络连接正常,可以执行复制步骤了
  4. 收到PONG回复后,如果从服务器设置了masterauth,就会进行身份验证,否则不验证
  5. 从服务器执行命令REPLCONF listening-port <port>,发送从服务器的监听端口号,主服务器收到后会将其记录在从服务器对应的客户端状态redisClient的slave_listening_port属性中
  6. 执行复制同步,从服务器向主服务器发送PSYNC命令,执行同步操作
  7. 命令传播,主服务器将执行的写命令发送给从服务器,保证主从一致。
  8. 后续进行增量复制

主从复制的相关参数:

  • master通过BGSAVE命令生成RDB快照发送给salve,如果RDB复制时间超过60秒(repl-time),那么就会认为复制失败,可以适当调大这个参数。
  • master在持久化期间,会将所有新的写指令缓存到内存中,如果这个空间超过了64MB会停止复制,复制失败(client-output-buffer-limit)
  • slave和master分别存储offset,slave每秒发送自己的offset给master,master保存slave的offset,slave 根据master的run id来判断是否需要全量复制
  • slave接收到RDB后,会清空自己的数据,然后重新加载RDB到内存中,如果slave开启了AOF,则会进行AOF重写
  • 全量复制过程中,网络连接断开,则重新连接时,会触发增量复制,master从自己的backlog中获取部分丢失的数据,发送给slave,默认backlog为1MB,master就是根据slave发送的psync中的offset来从backlog中获取数据的

心跳检测

命令传播阶段,从服务器默认以每秒一次的频率向主服务器发送命令

REPLCONF ACK <replication_offset> 

其中:replication_offset是从服务器当前的复制偏移量,主要用于检测:

检测主从服务器的网络连接状态

主从服务器可以通过发送和接收REPLCONF ACK命令来检查两者之间的网络连接是否正常:如果主服务器超过一秒钟没有收到从服务器发来的REPLCONF ACK命令,那么主服务器就知道主从服务器之间的连接出现问题了。
通过向主服务器发送INFO replication命令,在列出的从服务器列表的1ag一栏中,我
们可以看到相应从服务器最后一次向主服务器发送 REPLCONFACK命令距离现在过了多少秒:

127.0.0.1:6379> INFO replication
# Replication
role:master
connected slaves:2
slave0:ip=127.0.0.1,port=12345,state=online,offset=211,1ag=0 #刚刚发送过 REPLCONF ACK命令
slavel:ip=127.0.0.1,port=56789,state=online,offset=197,1ag=15 #15秒之前发送过REPLCONFACK 命令
master_repl_offset:211
repl_backlog_active:1
repl_backlog_size:1048576
rep1_backlog_first_byte_offset:2
repl_backlog_histlen:210

辅助实现 min-slaves 配置选项

Redis的min-slaves-to-write和min-slaves-max-1ag两个选项可以防止主服务器在不安全的情况下执行写命令。比如主服务器提供以下设置:

  • min-slaves-to-write 3
  • min-slaves-max-lag 10

那么在从服务器的数量少于3个,或者三个从服务器的延迟(lag)值都大于或等于10秒时,主服务器将拒绝执行写命令,这里的延迟值就是上面提到的INFO replication命令的lag值。

检测命令丢失

当网络故障,主服务器传播给从服务器的写命令丢失,但是从服务器向发送 REPLCONF ACK命令时,主服务器根据从服务器提交的offset,在复制积压缓冲区里面找到缺少的数据,并将这些数据重新发送给从服务器。

无磁盘化复制

主服务器在内存中直接创建RDB并发送给从服务器,不会再本地落地磁盘。无磁盘diskless方式适合磁盘读写速度慢但网络带宽非常高的环境。

  • repl-diskless-sync no 默认不使用diskless同步方式
  • repl-diskless-sync-delay 5 无磁盘diskless方式在进行数据传递之前会有一个时间的延迟,以便slave端能够进行到待传送的目标队列中,这个时间默认是5秒

生成RDB后等待一段时间,然后开始复制,复制时不接收新的Slave请求

过期key处理

在主从复制中,slave不会过期key,只会等待master过期key,如果master过期了一个key,或通过LRU淘汰了一个key,会模拟一条del命令给slave

基于复制的Redis主从架构

架构图

 特点:

  • master/slave角色,master/slave数据相同
  • 降低master读的压力到slave

问题:

  • 无法保证高可用
  • 没有解决master写的压力

主从架构的优缺点:

优点

  • 高可靠性:采用双击主备架构,能够在主库出现故障时自动进行主备切换,从库提升为主库提供服务,保证服务平稳运行;另一方面,开启数据持久化功能和配置合理的备份策略,能有效的解决数据误操作和数据异常丢失的问题。
  • 读写分离策略:从节点可以扩展主库节点的读能力,有效应对大并发量的读操作。

缺点

  • 故障恢复复杂,如果没有Redis HA 系统(需要开发),当主库节点出现故障时,需要手动将一个从节点晋升为主节点,同时需要通知业务方变更配置,并且需要让其他从库节点去复制新主库节点,整个过程需要人为干预,比较繁琐。
  • 主库的写能力受到单机的限制,可以考虑分片。
  • 主库的存储能力受到单机的限制,可以考虑Pika。
  • 原生复制的弊端在早期的版本中也会比价突出,如:Redis 复制中断后,Slave 会发起 psync ,此时如果同步不成功,则会进行全量同步,主库执行全量备份的同时,可能会造成毫秒或秒级的卡顿。
  • 又由于COW 机制,导致极端情况下的主库内存溢出,程序异常退出或宕机;主库节点生成备份文件导致服务器磁盘IO和CPU 资源消耗;发送数GB 大小的备份文件导致服务器出口带宽暴增,阻塞请求,建议升级到最新版本。

Redis哨兵Sentinel

Sentinel是Redis实现高可用的解决方案:由一个或多个Sentinel实例组成的Sentinel系统可以监视任意多个主从服务器,当被监视的master进入下线状态并超过设定的下线时长,Sentinel自动将其属下的某个slave升级为新的master。之后Sentinel会向其他slave发送复制指令,让它们成为新的master的slave,另外Sentinel还能监视已下线的原master,当它重新上线后将它设置成新的master的slave

sentinel哨兵特性

  1. sentinel哨兵是特殊的redis服务,不提供读写服务,主要用来监控redis实例节点。
  2. 哨兵架构下client端第一次从Sentinel找出redis的主节点,后续就直接访问redis的主节点,当redis的主节点发生变化,哨兵会第一时间感知到,并且将新的redis主节点通知给client端(这里面redis的client端一般都实现了订阅功能,订阅sentinel发布的节点变动消息)
  3. 哨兵至少需要三个实例,来保证自己的健壮性。
  4. 哨兵+Redis主从复制只能保证可用性,不能保证数据0丢失。

Sentinel的使用

通过如下命令启动Sentinel

$ redis-sentinel /path/your/redis-sentinel.conf
或者
$ redis-server /path/your/redis-sentinel.conf --sentinel

Sentinel启动时主要步骤如下:

1.初始化服务器

Sentinel本质上是一个运行在特殊模式下的Redis服务器,启动Sentinel的第一步就是初始化一个普通的Redis服务器

Sentinel的初始化和普通Redis服务器的初始化过程并不完全相同,如:初始化Sentinel时不会通过载入RDB文件或者AOF文件来还原数据库状态

2.使用Sentinel专用代码

启动Sentinel后就是将一部分普通Redis服务器使用的代码替换成Sentinel代码,比如普通Redis使用redis.c/redisCommandTable作为服务器的命令表,而Sentinel使用sentinel.c/sentinelcmds作为服务器命令表

3.初始化Sentinel状态

服务器初始化一个sentinel.c/sentinelState结构,即Sentinel状态,这个结构保存了服务器中所有和Sentinel功能相关的状态(服务器的一般状态仍由redis.h/redisServer结构保存),代码如下:

struct sentinelState{

// 当前纪元,用于实现故障转移
uint64_t current_epoch;

// 保存了所有被这个sentinel监视的主服务器
// 字典的键是主服务器的名字
// 字典的值则是一个指向sentinelRedisInstance 结构的指针
dict *masters;

// 是否进入了TILT模式?
int tilt;

// 目前正在执行的脚本的数量
int running_scripts;

// 进入TILT模式的时间
mstime_t tilt_start_time;

// 最后一次执行时间处理器的时间
mstime_t previous_time;

// 一个FIFO队列,包含了所有需要执行的用户脚本
list *scripts_queue;

} sentinel;

Sentinel会根据被载入的Sentinel配置文件初始化Sentinel状态的masters属性,masters字典中记录了所有被Sentinel监视主服务器的相关信息,其中

  • 字典的键就是被监视主服务器的名字
  • 字典的值就是被监视主服务器对应的sentinel.c/sentinelRedisInstance结构的指针
sentinel monitor master1 127.0.0.1 6379 2
....
sentinel monitor master2 127.0.0.1 6389 5

sentinelRedisInstance代表一个被Sentinel监视的Redis服务器实例,可以是master、slaver或者另外一个Sentinel,结构如下:

typedef struct sentinelRedisInstance{

// 标识值,记录了实例的类型,以及该实例的当前状态
int flags;

// 实例的名字
// 主服务器的名字由用户在配置文件中设置
// 从服务器以及Sentine1的名字由Sentinel 自动设置
// 格式为ip:port,例如"127.0.0.1:26379
char *name;

// 实例的运行ID
char *runid;

// 配置纪元,用于实现故障转移
uint64_t config_epoch;

// 实例的地址,sentinelAddr包括了实例的ip,port
sentinelAddr *addr;

//  SENTINEL down-after-milliseconds 选项设定的值
// 实例无响应多少毫秒之后才会被判断为主观下线(subjectively down)
mstime_t down_after_period;

//  SENTINEL monitor <master-name><IP> <port><quorum>选项中的quorum 参数
// 判断这个实例为客观下线(objectively down)所需的支持投票数量
int quorum;


// SENTINEL parallel-syncs <master-name><number>选项的值
// 在执行故障转移操作时,可以同时对新的主服务器进行同步的从服务器数量
int parallel_syncs;

// SENTINEL failover-timeout <master-name><ms>选项的值
//  刷新故障迁移状态的最大时限
mstime_t failover_timeout;

//主服务器属下从服务器的字典
dict *slaves

//监视master的sentinel字典
dict *sentinels

// ...
} sentinelRedisInstance;

 4. 创建Sentinel连接主服务器网络连接

初始化Sentinel最后一步时创建连向被监视的主服务器master的网络连接,Sentinal将成为master的客户端,可以向master发送命令获取回复,Sentinel会创建两个连向主服务器的异步网络连接

  • 一个命令连接,专门用于向主服务器发送命令,接受命令回复
  • 一个订阅命令,专门订阅主服务器的_sentinel_:hello 频道

Sentinel获取服务器信息

初始化Sentinel服务后,载入的Sentinel配置文件并初始化Sentinel状态的masters属性,创建连接master的两种连接后,通过INFO命令获取master和slave的的相关信息

获取master信息

Sentinel默认会以每十秒一次的频率通过命令连接向master发送INFO命令,分析INFO命令的回复获取master信息,具体如下:

# Server
...
run_id:7611c59dc3a29aa6fa0609f841bb6a1019008a9c //服务器的运行ID
...

# Replication
role:master //服务器角色
...
// 服务器下属的所有slave
slave0: ip=127.0.0.1,port=11111,state=online,offset=43,1ag=0 //slave的ip,port,state,offset,lag
slave1:ip=127.0.0.1,port=22222,state=online,offset=43,1ag=0
slave2:ip=127.0.0.1,port=33333,state=online,offset=43,1ag=0
...
# Other sections

收到master的相关信息就会,Sentinel会对master的sentinelRedisInstance实例结构进行更新,包括运行ID和slaves字典(可能更新或者创建某个节点)

获取slave信息

当有新的salve加入时,Sentinel除了给新的slave创建相应的实例结构外,也会创建连接到从服务器的命令连接和订阅连接

同样Sentinel默认每十秒一次的频率通过命令连接向slave发送INFO命令,分析INFO命令的回复获取slave信息,具体如下:

# Server
. . .
run_id:32be0699dd27b410f7c90dada3a6fab17f97899f //服务器的运行ID
. . .

# Replication
role:slave //服务器的角色
master_host:127.0.0.1 //对应master的ip和端口
master_port:6379
master_link_status:up //对应主从服务器的连接状态
slave_repl_offset:11887 //slave的复制偏移量
slave_priority:100 //slave的优先级

# Other sections

Sentinel向master和slave发送信息

默认情况下,Sentinel每两秒一次的频率通过命令连接向所有被监视的master和slave发送以下格式的命令:

PUBLISH _sentinel_:hello  "<s_ip>,<s_port>,<s_runid>,<s_epoch>,<m_name>,<m_ip>,<m_port>,<m_epoch>"

//其中:
//以s_开头的参数记录的是Sentinel本身的信息
//而m_开头的参数记录的则是主服务器的信息
//s_run_id的运行ID,s_epoch是当前配置纪元

这条命令向服务器的_sentinel_:hello频道发送了一条信息,

  • 如果Sentinel正在监视的是master,那么m_开头的参数记录的就是master的信息;
  • 如果Sentinel正在监视的是slave,那么m_开头的参数记录的就是slave正在复制的master的信息。

以下是一条 Sentinel通过PUBLISH命令向主服务器发送的信息示例:

"127.0.0.1,26379,e955b4c85598ef5b5f055bc7ebfd5e828dbed4fa,0,mymaster,127.0.0.1,6379,0"

包括:Sentinel的IP地址为127.0.0.1端口号为26379,运行ID
e955b4c85598ef5b5f055bc7ebfd5e828dbed4fa,当前的配置纪元为0.

主服务器的名字为mymaster,IP地址为127.0.0.1,端口号为6379,当前的配置纪元为0.

接收主从服务器channel信息

当Sentinel与一个master或slave建立起订阅连接之后,Sentinel就会通过订阅连接,向服务器发送订阅命令:

SUBSCRIBE_sentinel_:hello

Sentinel对_sentinel_:hello频道的订阅会一直持续到Sentinel与服务器的连接断开为止。对于每个与Sentinel连接的服务器

  • Sentinel既通过命令连接向服务器的_sentinel_:hello频道发送信息,
  • 又通过订阅连接从服务器的_sentinel_:hello频道接收信息

对于监视同一个服务器的多个Sentinel来说,一个Sentinel发送的信息会被其他Sentinel接收到,用于更新其他Sentinel对发送信息的Sentinel的认知。

更新sentinel字典

Sentinel为主服务器创建的实例结构中的sentinels字典保存了除Sentinel本身外,所有同样监视这个主服务器的其他Sentinel的资料:

  • sentinels字典的键是其中一个Sentinel的名字,格式为ip:port,比如:"127.0.0.1:26379
  • sentinels字典的值则是键所对应Sentinel的实例结构

当一个Sentinel接收到其他 Sentinel发来的信息时,接收到信息Sentinel会从信息中分析并提取出以下两方面参数:

  • 与Setinel有关的参数:发送信息的Sentinel的IP地址、端口号、运行ID和配置纪元。
  • 与主服务器有关的参数:发送信息的Sentinel正在监视的主服务器的名字、IP地址、端口号和配置纪元。

根据参数,接收信息的Sentinel会在自己的Sentinel状态的masters字典中查找相应的主服务器实例结构,检查主服务器实例结构的 sentinels字典中,发送信息Sentinel的实例结构是否存在:

  • 如果发送信息Sentinel的实例结构已经存在,则对发送信息Sentinel的实例结构进行更新。
  • 如果发送信息 Sentinel的实例结构不存在,说明发送信息Sentinel是新加入Sentinel, 接收信息Sentinel会为这个新的Sentinel创建一个新的实例结构,并将这个结构添加sentinels字典里面。
     

创建连向其他 Sentinel的命令连接

当Sentinel通过频道信息发现一个新的Sentinel时,除了新Sentinel在sentinels字典中创建相应的实例结构,两个Sentinel还会相互创建命令连接,最终形成监视同一主服务器的多个Sentinel将形成相互连接的网络

Sentinel检测下线

主观下线

在默认情况下,Sentinel会以每秒一次的频率向所有与它创建了命令连接的实例(包括
主服务器、从服务器、其他Sentinel在内)发送PING命令,并通过实例返回的PING命令回复判断实例是否在线

当收到的回复是PONG 、-LOADING 、-MASTERDOWN三种回复的一种,视为有效回复,如果是其他回复,或者在指定时限内没有返回任何回复,则Sentinel会修改这个实例的实例结构中的flags属性中打开SRI_S_DOWN标识来表示该实例进入主观下线

通过设置sentinel配置文件中的down-after-milliseconds选项,设置指定时限,如下:

sentinel down-after-milliseconds master 40000

表示40000毫秒不仅是Sentinel判断master进入主观下线的时限,同样也是master属下的slave和同样监视master的Sentinel主观下线的时限,不同的Sentinel可以配置不同的下线时限。

客观下线

当Sentinel将一个master判断为主观下线之后,为了确认是否真的下线了,它会向同样监视这个master的其他Sentinel进行询问,当Sentinel 从其他Sentinel那里接收到足够数量(quorum个)的已下线判断(可以是主观下线或者客观下线)之后,Sentinel就会将从服务器判定为客观下线,并对主服务器执行故障转移操作。

Sentinel使用如下命令询问其他 Sentinel是否同意主服务器已下线:

SENTINEL is-master-down-by-addr <ip><port><current_epoch><runid>
ip:被Sentinel判断为主观下线的主服务器的IP地址
port:被Sentinel判断为主观下线的主服务器的端口
current_epoch:Sentinel当前配置纪元,用于选举领头Sentinel
runid:可以是*或者Sentinel的运行id,*表示检测主服务器的客观下线状态,Sentinel的运行id用于选举

当另外一个Sentinel接受到一个Sentinel发送来的SENTINEL is-master-down-by命令时,该Sentinel会检查对应的主服务器是否下线,然后返回三个参数的Multi Bulk回复:

<down_state>:1 主服务器已下线, 0 代表主服务器未下线

<lead_runid> :* 表示命令用于检测服务器下线状态,Sentinel运行ID用于选举领头Sentinel

<leader_epoch>:lead_runid不为*时有效,用于选举领头Sentinel

收到回复后,Sentinel统计同意已下线的输了,判断是否达到客观下线的条件,当达到客观下线,则Sentinel会将主服务器的实例结构的flags属性SRI_O_DOWN标识打开,表示主服务器客观下线

quorum值

通过配置设置如下:

sentinel monitor master 127.0.0.1 6379 2 

表示包括当前Sentinel在内,只要总共有两个Sentinel认为主服务器下线,则Sentinel将主服务器判断为客观下线。不同的Sentinel判断客观下线的条件可以射姿和不同。

领头Sentinel

当一个master被判断为客观下线后,监视这个下线的master的各个Sentinel会进行协商,选举一个领头的Sentinel,由领头Sentinel进行下线master的故障转移操作

选举领头Sentinel的规则如下::

  1. 监视同一个master的多个在线的Sentinel都可以竞争领头Sentinel
  2. 每个发现主服务器进入客观下线的Sentinel都会要求其他Sentinel将自己设置为局部领头Sentinel,这个Sentinel(源)向其他Sentinel(目标)发送 SENTINEL is-master-down-by-addr命令,命令中的runid参数是源Sentinel的运行ID时,这表示要求目标Sentinel将源Sentinel设置为他们的局部领头 Sentinel.
  3. 目标 Sentinel在接收到SENTINEL is-master-down-by-addr命令后,向源 Sentinel 返回一条命令回复,回复中的leader_runid参数和leader_epoch参数分别记录了目标Sentinel的局部领头Sentinel的运行ID和配置纪元。
  4. Sentinel 设置局部领头Sentinel的规则是先到先得:最先向目标Sentinel发送设置要求的源 Sentinel将成为目标Sentinel的局部领头Sentinel,而之后接收到的所有设置都会被拒绝。
  5. 源Sentinel在接收到目标Sentinel返回的命令回复之后,会检查回复中leader_epoch参数的值和自己的配置纪元是否相同,如果相同的话,那么源Sentinel继续取出回复中的leader_runid参数,如果leader_runid参数的值和源 Sentinel的运行ID一致,那么表示目标Sentinel将源 Sentinel设置成了局部领头Sentinel.
  6. 如果有某个Sentinel被半数以上的Sentinel设置成了局部领头Sentinel,那么这个Sentinel 成为领头 Sentinel.举个例子,在一个由10个Sentinel组成的Sentinel系统里面,只要有大于等于10/2+1=6个Sentinel将某个Sentinel设置为局部领头Sentinel,那么被设置的那个Sentinel就会成为领头 Sentinel.
  7. 因为领头Sentinel的产生需要半数以上Sentinel的支持,并且每个Sentinel在每个配置纪元里面只能设置一次局部领头Sentinel,所以在一个配置纪元里面,只会出现一个领头 Sentinel.
  8. 如果在给定时限内,没有一个Sentinel被选举为领头Sentinel,那么各个Sentinel将在一段时间之后再次进行选举,直到选出领头Sentinel为止。

每次进行领头Sentinel选举之后,不论选举是否成功,所有Sentinel的配置纪元(configuration epoch)的值都会自增一次。配置纪元实际上就是一个计数器,并没有什么特别的。
在一个配置纪元里面,所有Sentinel都有一次将某个Sentinel设置为局部领头Sentinel的机会,并且局部领头一旦设置,在这个配置纪元里面就不能再更改。


故障转移

  1. 从已下线的master的从属slave中挑选一个状态良好,数据完整的salve,将其替换为master
    1. 已下线的slave的所有从服务器列表按照如下规则过滤选择slave
    2. 过滤所有下线或者断线状态的slave
    3. 过滤所有最近5秒内没有回复过领头Sentinel的INFO命令的slave
    4. 过滤所有与已下线的master断开连接超过down-after-milliseconds*10毫秒的服务器(即过滤过早也master断开连接的slave)
    5. 领头Sentinel按照slave优先级排序,优先级相同的则按照复制偏移量最大的优先,如果偏移量和优先级一样,则按照运行id排序,id越小优先级越高
    6. 挑选slave后,向这个slave执行SLAVEOF no one命令,之后,领头Sentinel每秒一次向升级后的slave发送请求,查看角色变化是否从slave到 master,将这个slave替换成master
  2. 让已下线的master从属的所有slave改为复制新的master
    • 领头Sentinel通过SLAVEOF new_master_ip new_master_port命令
  3. 将已下线的master设置为新的master的slave,当已下线的master重新上线后就会成为新的master的slave
    • 当就的master上线后,领头Sentinel通过SLAVEOF new_master_ip new_master_port命令

哨兵(redis-sentinel.conf)的相关配置

  • sentinel monitor <master-name> <ip> <redis-port> <quorum>:sentinel去监听地址为ip:port的一个master,这里的master-name可以自定义,quorum是一个数字,指明当有多少个sentinel认为一个master失效时,master才算真正失效。
  • sentinel auth-pass <master-name> <password> 设置连接master和slave时的密码
  • sentinel down-after-milliseconds <master-name> <milliseconds> :超过多少毫秒跟一个Redis实例断了连接,就认为这个redis节点挂了
  • sentinel parallel-syncs <master-name> <numreplicas>:这个配置项指定了在发生failover主备切换时最多可以有多少个slave同时对新的master进行同步,这个数字越小,完成failover所需的时间就越长,但是如果这个数字越大,就意味着越多的slave因为replication而不可用。可以通过将这个值设为 1 来保证每次只有一个slave 处于不能处理命令请求的状态。
  • sentinel failover-timeout <master-name> <milliseconds> :执行故障转移的timeout超时时长

 Sentinel三个特性:

  • 监控(Monitoring): Sentinel 会不断地主服务器和从服务器是否运作正常。
  • 提醒(Notification): 当被监控的某个 Redis 服务器出现问题时, Sentinel 可以通过 API 向管理员或者其他应用程序发送通知。
  • 自动故障迁移(Automatic failover): 当主服务器不能正常工作时, Sentinel 会开始一次自动故障迁移操作。将失效主服务器的其中一个从服务器升级为新的主服务器, 并让其他从服务器改为复制新的主服务器; 当客户端试图连接失效的主服务器时, 集群也会向客户端返回新主服务器的地址, 使得集群可以使用新主服务器代替失效服务器。

故障转移时,判断一个Master Node宕机,需要大部分哨兵统一才行,涉及到分布式选举问题。

特点:

  • 保证高可用
  • 监控各个节点
  • 自动故障迁移

缺点:

  • 主从模式,切换需要时间丢数据
  • 没有解决master写的压力

集群模式

Redis集群是Redis提供的分布式数据库解决方案,集群通过分片sharding来进行数据共享,并提供复制和故障转移功能

集群节点

一个Redis集群通常由多个节点(node)组成,开始时,每个节点都是相互独立的,必须通过如下命令将各个节点连接起来构成一个集群。

CTUSTER MEET <ip> <port>

向一个节点node发送CLUSTER MEET命令,可以让该node节点与ip和port所指定的节点进行握手(handshake),握手成功后,指定的节点就会添加到该node节点所在的集群中。

启动节点

Redis服务器在启动时根据cluster-enabled配置选项决定是否开启服务器的集群模式,yes则开启集群模式,否则是单机模式,成为一个普通的redis服务器。集群中每个节点都会使用一个clusterNode结构来记录自己的状态:

struct clusterNode{

// 创建节点的时间
mstime_t ctime;

// 节点的名字,由40个十六进制字符组成 例如68eef66df23420a5862208ef5b1a7005b806f2ff
char nameREDIS_CLUSTER_NAMELEN;

// 节点标识
// 使用各种不同的标识值记录节点的角色(比如主节点或者从节点),
//  以及节点目前所处的状态(比如在线或者下线)。
int flags;


// 节点当前的配置纪元,用于实现故障转移
uint64_t configEpoch;

// 节点的IP地址
char ipREDIS_IP_STR_LEN;
// 节点的端口号
int port;
//  保存连接节点所需的有关信息
clusterLink *link;

// 二进制数组,共2048个字节,16384个字节位
unsigned char slots [16384/8];

// 记录节点负责处理的槽的数量
int numslots;

// 如果该节点是个从节点,那么指向主节点
struct clusterNode *slaveof
// ...

}


clusterNode结构的link属性是一个clusterLink结构,该结构保存了连接节点所需的有关信息,比如套接字描述符,输入缓冲区和输出缓冲区:

typedef struct clusterLink

//连接的创建时间
mstime_t ctime;

// TCP 套接字描述符
int fd;

//输出缓冲区,保存着等待发送给其他节点的消息(message).
sds sndbuf;

//输入缓冲区,保存着从其他节点接收到的消息。
sds rcvbuf;
//与这个连接相关联的节点,如果没有的话就为NULL
struct clusterNode *node;

} clusterLink;

redisClient 结构和 clusterLink结构的相同和不同之处

相同点:redisClient 结构和clusterLink结构都有自己的套接字描述符和输入、输出缓冲区

不同点:redisClient 结构中的套接字和缓冲区是用于连接客户端的,而clusterLink的是用于连接节点的。

每个节点都保存着一个clusterState结构,记录了在当前节点的视角下,集群目前所处的状态:集群是在线还是下线,集群包含多少个节点,集群当前的配置纪元等

typedef struct clusterState{

// 指向当前节点的指针
clusterNode *myself;

// 集群当前的配置纪元,用于实现故障转移
uint64_t currentEpoch;

// 集群当前的状态:是在线还是下线
int state;

// 集群中至少处理着一个槽的节点的数量
int size;

// 集群节点名单(包括myself节点)
// 字典的键为节点的名字,字典的值为节点对应的clusterNode 结构
dict *nodes;

// 记录集群中所有16384个槽的指派信息
clusterNode *slots[16384];

//保存槽与键的映射
zskiplist * slots_to_keys;
//...

} clusterState;

CLUSTER MEET命令

通过向节点A发送CLUSTER MEET命令,可以将一个节点添加到节点A当前所在的集群里,收到命令后节点A将会和待添加节点B进行握手hadnshake,确认彼此存在,此时

  1. 节点A为节点B创建一个clusterNode结构,并将该结构添加到自己的cluster.nodes字典里
  2. 节点A根据CLUSTER MEET命令的ip和port,向节点B发送一个MEET消息
  3. 节点B收到MEET消息后,会为节点A创建一个cluserNode结构,并添加到自己的cluster.nodes字典,完成后返回一个PONG消息
  4. 节点A收到PONG,确认节点B成功接收MEET,在返回一个PING消息给节点B
  5. 节点B接收PING 消息后,确认节点A接收到了发送的PONG消息,握手完成

之后节点A会将节点B的信息通过Gossip协议传播给集群中的其他节点,让集群其他节点与节点B完成握手,一段时间后,节点B与所有集群中的其他节点完成握手

Redis槽

Redis集群通过分片的方式来保存数据库中的键值对:集群的整个数据库被分为16384个槽slot,每个键都属于这16384个槽的其中一个,集群中的每个节点可以处理0个或最多16384个槽。

当数据库中的16384个槽都有节点在处理时,集群处于上线状态(ok);当数据库中有任何一个槽没有得到处理,那么集群处于下线状态(fail)。

使用CLUSTER MEET命令将7000、7001、7002三个节点连接到了同一个集群里面,此时集群是处于下线状态,因为集群中的三个节点都没有在处理任何槽:

127.0.0.1:7000>CLUSTER INFO
cluster_state:fa1l
cluster_slots_assigned:0
cluster slots_ok:0
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known nodes:3
cluster_size:0
cluster_current_epoch:0
cluster_stats_messages_sent:110
cluster_stata_messages_received:28

槽指派

通过向节点发送 CLUSTER ADDSLOTS命令,可以将一个或多个槽指派(assign)给节点负责:

CLUSTER ADDSLOTS <slot> [slot...]

如下:将槽0至槽5000指派给节点7000负责:

127.0.0.1:7000> CLUSTER ADDSLOTS 0 1 2 3 4...5000
OK
127.0.0.1:7001>CLUSTER ADDSLOTS 5001 5002 5003 5004,··10000
OK
127.0.0.1:7002> CLUSTER ADDSLOTS 10001 10002 10003 10004...16383
OK

当以上三个CLUSTER ADDSLOTS命令都执行完毕之后,数据库中的16384个槽都已经被指派给了相应的节点,集群进入上线状态:

CLUSTER ADDSLOTS命令底层会遍历所有输入的槽,检查他们是否未指派,如果有一个槽已经被指派了节点,返回错误,否则遍历并将slots[i]指向当前的clusterNode结构,并访问当前节点的clusterNode结构的slots数组,将数组在索引i上的二进制位置设置成1

记录节点的槽指派信息

clusterNode结构中的slots属性和 numslot属性记录了节点负责处理哪些槽以及处理槽的数量
slots属性是一个二进制位数组(bit array),以0为起始索引,16383为终止索引,根据索引i上的二进制位的值来判断节点是否负责处理槽i:

  • 如果slots数组在索引i上的二进制位的值为1,那么表示节点负责处理槽i.
  • 如果slots数组在索引i上的二进制位的值为0,那么表示节点不负责处理槽i.
     

可以看出程序检查节点是否负责处理某个槽,或者将某个槽指派个节点负责,复杂度为O(1)。

各个节点会将这些slots信息通过消息发送给集群中的其他节点,告知它们自己负责的槽,其他节点在收到发送的槽信息后,会在自己的clusterState.nodes字典中查找该节点对应的clusterNode结构,对其slots数组进行保存或更新

在clusterState结构中的slots数组记录了集群16384个槽的指派信息,每个数组项都是一个指向clusterNode结构的指针,如果指向NULL,说明该槽i未分派给任何节点

执行命令

在分配槽之后,集群进入上线状态,客户端可以向集群发送命令,具体流程如下图

计算键所在的槽

节点使用以下算法来计算给定键key属于哪个槽:

def slot_number(key):
return CRC16(key)&16383

其中CRC16(key)语句用于计算键key的CRC-16校验和,而&16383语句则用于计算出一个介于0至16383之间的整数作为键key的槽号。使用CLUSTER KEYSLOT<key>命令可以查看一个给定键属于哪个槽:

127.0.0.1:7000> CLUSTER KEYSLOT date
(integer) 2022
127.0.0.1:7000> CLUSTER KEYSLOT msg
(integer) 6257
127.0.0.1:7000> CLUSTER KEYSLOT name
(integer) 5798
127.0.0.1:7000>

判断槽所在的节点

当节点计算出键所属的槽i之后,节点就会检查自己在clusterState.slots数组中的项i,判断键所在的槽是否由自己负责:

  • 如果clusterState.slots[i]等于clusterState.myself,那么说明槽i由当前节点负责,节点可以执行客户端发送的命令。
  • 如果clusterState.slots[i]不等于clusterState.myself,节点会根据clusterState.slots[i]指向的clusterNode结构所记录的节点IP和端口号,向客户端返回MOVED错误,指引客户端转向至正在处理槽i的节点。

当节点发现键所在的槽并非由自己负责处理的时候,节点就会向客户端返回一个MOVED错误,指引客户端转向至正在负责槽的节点(更换套接字),MOVED错误的格式为:

MOVED <slot> <ip>:<port>
其中slot为键所在槽,而ip和port则是负责处理槽slot的节点的IP地址和端

clusterState结构中的slots_to_keys保存了槽与键之间的关系,slots_to_keys跳跃表每个节点的分值(score)都是一个槽号,而每个节点的成员(member)都是一个数据库键,当节点往数据库中添加或者删除键值对时,节点就会添加或者删除slots_to_keys跳跃表键与槽号的关联。

重新分片

Redis集群的重新分片操作可以将任意数量已经指派给某个节点(源节点)的槽改为指派给另一个节点(目标节点),并且相关槽所属的键值对也会从源节点被移动到目标节点。

在重新分片的过程中,集群不需要下线,并且源节点和目标节点都可以继续处理命令请求。当新增或者删除节点时特别有用。

重新分片的实现原理

重新分片操作是由Redis的集群管理软件redis-trib负责执行的,redis-trib则通过向源节点和目标节点发送命令来进行重新分片操作。redis-trib对集群的单个槽slot进行重新分片的步骤如下:

1)redis-trib对目标节点发送如下命令,让目标节点准备好从源节点导入属于槽slot的键值对。

CLUSTER SETSLOT <slot> IMPORTING <sourceid>

2)redis-trib对源节点发送如下命令,让源节点准备好将属于槽slot的键值对迁移至目标节点。

CLUSTER SETSLOT <sIot> MIGRATING <target id>

3)redis-trib向源节点发送如下命令,获得最多count个属于槽slot的键值对的键名(key name)

CLUSTER GETKEYSINSLOT <slot><count>

4)对于步骤3获得的每个键名,redis-trib都向源节点发送如下命令,将被选中的键原子地从源节点迁移至目标节点。

MIGRATE <target_ip> <target_port> <key_name> 0 <timeout>

5)重复执行步骤3和步骤4,直到源节点保存的所有属于槽slot的键值对都被迁移至目标节点为止。

6)redis-trib向集群中的任意一个节点发送如下命令,将槽slot指派给目标节点,这一指派信息会通过消息发送至整个集群,最终集群中的所有节点都会知道槽slot已经指派给了目标节点。

CLUSTER SETSLOT <slot> NODE <target_id>

ACK错误

在重新分片期间迁移一个槽过程中,可能会导致:被迁移槽的一部分键值对保存在源节点里面,而另一部分键值对保存在目标节点里面,此时客户端向源节点发送一个操作键的命令,而这个键恰好属于正在被迁移的槽时,并且这个键不再源节点中,此时源节点向客户端返回一个ASK错误,指引客户端转向正在迁移到的目标节点,再次发送之前的命令。

ASK错误和MOVED错误的区别

ASK错误和MOVED错误都会导致客户端转向,它们的区别在于:

  • MOVED错误代表槽的负责权已经从一个节点转移到了另一个节点:在客户端收到关于槽i的MOVED错误之后,客户端每次遇到关于槽i的命令请求时,都可以直接将命令请求发送至MOVED错误所指向的节点,因为该节点就是目前负责槽i的节点。
  • ASK错误只是两个节点在迁移槽的过程中使用的一种临时措施:在客户端收到关于槽i的ASK错误之后,客户端只会在接下来的一次命令请求中将关于槽i的命令请求发送至ASK错误所指示的节点,但这种转向不会对客户端今后发送关于槽i的命令请求产生任何影响,客户端仍然会将关于槽i的命令请求发送至目前负责处理槽i的节点,除非ASK错误再次出现。

复制和故障转移

Redis集群中的节点分为主节点和从节点,主节点用于处理槽,从节点用于复制主节点,当主节点下线,从节点代替主节点完成命令处理请求

设置从节点

向一个节点发送命令,可以让该节点成为node_id所指定节点的从节点。并开始复制主节点。

CLUSTER REPLICATE  <node_id>

此时该节点的clusterState.myself.slaveof指针指向node_id对应节点的clusterNode结构,并且修改clusterState.myself.flag标识,打开REDIS_NODE_SLAVE标识。

一个节点成为从节点并开始复制主节点后,会通过消息发送给集群中其他节点,主节点的clusterNode结构的slaves属性和numslaves属性会记录从节点信息

故障检测

集群中的每个节点都会定期地向其他节点发送PING消息,来检测对方是否在线,如果接收PING消息的节点没有在规定的时间内返回PONG消息,则标记为疑似下线(probable fail,PFAIL),节点状态可以是:

  • 在线状态、
  • 疑似下线状态(PFAIL)
  • 已下线状态(FAIL)

如果集群中,半数以上的主节点都将某个主节点x报告为PFAIL,那么这个主节点将被标记为已下线FAIL,同时向集群广播,收到广播的节点都会将主节点x标记为FAIL

故障转移

当一个从节点发现自己正在复制的主节点进入了已下线状态时,从节点将开始对下线主节点进行故障转移,以下是故障转移的执行步骤:

  1. 选中下线主节点的一个从节点,执行SLAVEOF no one命令,让其成为新的主节点。
  2. 新的主节点会撤销所有对已下线主节点的槽指派,并将这些槽全部指派给自己。
  3. 新的主节点向集群广播一条PONG消息,让集群中的其他节点知道这个节点已成为主节点,并且接管原节点负责处理的槽。
  4. 新的主节点开始接收和自己负责处理的槽有关的命令请求,故障转移完成。

选举新的主节点

主节点下线,会在从节点选举一个新的主节点,方法如下:

  1. 从节点会向集群广播一条CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST消息,要求所有收到这条消息、并且具有投票权的主节点(负责处理槽)向这个从节点投票。
  2. 如果一个主节尚未投票给其他从节点,那么主节点将向这个从节点返回一条CLUSTERMSG_TYPE FAILOVER_AUTH_ACK消息,表示支持从节点成为新的主节点。
  3. 当一个从节点收集到大于等于N/2+1张支持票时(集群里有N个具有投票权的主节点),这个从节点就会当选为新的主节点。
  4. 如果在一个配置纪元里面没有从节点能收集到足够多的支持票,那么集群进入一个新的配置纪元,并再次进行选举,直到选出新的主节点为止。

集群的配置纪元是一个自增计数器,它的初始值为0,当集群里的某个节点开始一次故障转移操作时,集群配置纪元的值会被+1。对于每个配置纪元,集群里每个负责处理槽的主节点都有一次投票的机会,而第一个向主节点要求投票的从节点将获得主节点的投票。

选举新主节点的方法和选举领头Sentinel的方法非常相似,因为两者都是基于Raft算法的领头选举(leader election)方法来实现的。

消息

集群中的各个节点通过发送和接收消息(message)来进行通信,节点发送的消息主要有五种: 

  • MEET消息:当发送者接到客户端发送的CLUSTER MEET命令时,发送者会向接收者发送MEET消息,请求接收者加入到发送者当前所处的集群里面。
  • PING 消息:集群里的每个节点默认每隔一秒钟就会从已知节点列表中随机选出五个节点,然后对这五个节点中最长时间没有发送过PING消息的节点发送PING消息,以此来检测被选中的节点是否在线。除此之外,如果节点A最后一次收到节点B发送的PONG消息的时间,距离当前时间已经超过了节点A的cluster-node-timeout选项设置时长的一半,那么节点A也会向节点B发送PING消息,这可以防止节点A因为长时间没有随机选中节点B作为PING 消息的发送对象而导致对节点B的信息更新滞后。
  • PONG消息:当接收者收到发送者发来的MEET消息或者PING消息时,为了向发送者确认这条MEET 消息或者PING消息已到达,接收者会向发送者返回一条PONG消息。另外,一个节点也可以通过向集群广播自己的PONG消息来让集群中的其他节点立即刷新于这个节点的认识,例如当一次故障转移操作成功执行之后,新的主节点会向集群广播一条PONG消息,以此来让集群中的其他节点立即知道这个节点已经变成了主节点,并且接管了已下线节点负责的槽。
  • FAIL消息:当一个主节点A判断另一个主节点B已经进人FAIL状态时,节点A会向集群广播一条关于节点B的FAIL消息,所有收到这条消息的节点都会立即将节点B标记为已下线。
  • PUBLISH消息:当节点接收到一个PUBLISH命令时,节点会执行这个命令,并向集群广播一条PUBLISH消息,所有接收到这条PUBLISH消息的节点都会执行相同的PUBLISH命令。


一条消息由消息头(header)和消息正文(data)组成,接下来的内容将首先介绍消息头,然后再分别介绍上面提到的五种不同类型的消息正文。

消息头

节点发送的所有消息都由一个消息头包裹,消息头除了包含消息正文之外,还记录了消
息发送者自身的一些信息,因为这些信息也会被消息接收者用到,所以严格来讲,我们可以
认为消息头本身也是消息的一部分。
每个消息头都由一个cluster.h/clusterMsg结构表示:
 

typedef struct{

// 消息的长度(包括这个消息头的长度和消息正文的长度)
uint32_t totlen;

// 消息的类型
uint16 t type;

// 消息正文包含的节点信息数量
//  只在发送MEET、PING、PONG这三种Gossip协议消息时使用
uint16_t count;

//  发送者所处的配置纪元
uint64_t currentEpoch;

//  如果发送者是一个主节点,那么这里记录的是发送者的配置纪元
// 如果发送者是一个从节点,那么这里记录的是发送者正在复制的主节点的配置纪元
uint64_t configEpoch;

// 发送者的名字(ID)
char senderREDIS_CLUSTER_NAMELEN;

// 发送者目前的槽指派信息
unsigned char myslotsREDIS_CLUSTER_SLOTS/8;

//  如果发送者是一个从节点,那么这里记录的是发送者正在复制的主节点的名字
// 如果发送者是一个主节点,那么这里记录的是REDIS_NODE NULL NAME
// (一个40字节长,值全为0的字节数组)
char slaveofREDIS_CLUSTER_NAMELEN;

//  发送者的端口号
uint16_t port;
//发送者的标识值
uint16_t flags;
// 发送者所处集群的状态
unsigned char state;
//消息的正文(或者说,内容)
union clusterMsgData data;
} clusterMsg;

clusterMsg.data属性指向联合cluster.h/clusterMsgData,这个联合就是
消息的正文:

union clusterMsgData
//  MEET、PING、PONG消息的正文
struct{
1/ 每条MEET、PING、PONG消息都包含两个
//   clusterMsgDataGossip 结构
clusterMsgDataGossip gossip1;
) ping;

//  FAIL 消息的正文
struct{
clusterMsgDataFail about;
} fail;
//   PUBLISH 消息的正文
struct{
clusterMsgDataPublish msg;
} publish;
//   其他消息的正文...

}

clusterMsg结构的currentEpoch、sender、myslots等属性记录了发送者自身
的节点信息,接收者会根据这些信息,在自己的clusterState.nodes字典里找到发送
者对应的clusterNode结构,并对结构进行更新。
举个例子,通过对比接收者为发送者记录的槽指派信息,以及发送者在消息头的
myslots属性记录的槽指派信息,接收者可以知道发送者的槽指派信息是否发生了变化。
又或者说,通过对比接收者为发送者记录的标识值,以及发送者在消息头的flags属性
记录的标识值,接收者可以知道发送者的状态和角色是否发生了变化,例如节点状态由原来
的在线变成了下线,或者由主节点变成了从节点等等。

特点:
1、无中心架构(不存在哪个节点影响性能瓶颈),少了proxy层。
2、数据按照slot存储分布在多个节点,节点间数据共享,可动态调整数据分布。
3、可扩展性,可线性扩展到1000个节点,节点可动态添加或删除。
4、高可用性,部分节点不可用时,集群仍可用。通过增加Slave做备份数据副本
5、实现故障自动failover,节点之间通过gossip协议交换状态信息,用投票机制完成Slave到Master的角色提升。
缺点:
1、资源隔离性较差,容易出现相互影响的情况。
2、数据通过异步复制,不保证数据的强一致性

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值