Redis集群

0 阅读23分钟

redis 集群

1、主从复制

redis主从复制的大体过程 image.png

  1. 从服务器上线后向主服务器发送PSYNC请求;
  2. 主服务器收到同步请求后,开始执行BGSAVE命令,并且开辟一块缓冲区用来存储从现在开始的写命令;
  3. 主服务器BGSAVE执行完成后,将生成的RDB文件发送给从服务器
  4. 从服务器接收到主服务器的RDB文件后开始载入文件,
  5. 主服务器将缓冲区中的写命令发给从服务器,确保两边数据一致

1.1、主从复制同步的模式

redis主从复制 PSYNC,分为两种模式:完整重同步(full resynchronization)和部分重同步(partial resynchronization)

1.1.1、完整重同步

完整重同步:顾名思义,主服务器创建并发送RDB文件,以及向从服务器发送缓冲区的命令来进行同步;适用于初次同步和断线重连后,复制偏移量差别过大无法做部分重同步的场景;

1.1.2、部分重同步

部分重同步:适用于从服务器短暂断线重连的场景,因为此场景下主从服务器数据差异有限,没有必要使用完整重同步。
具体工作原理:
redis设计了三个部分来完成部分重同步:

  • 1、主服务器的复制偏移量(replication offset)和从服务器的复制偏移量
    每次主从同步时,主服务器发送了N个字节的数据,那么主服务器的复制偏移量+N,同理从服务器接收到N个字节数据,从服务器的复制偏移量也+N;redis就是这两个偏移量,来判断主从数据是否一致,以及主服务器需要给从服务器同步多少数据。
  • 2、主服务器的复制积压缓冲区
    复制积压缓冲区就是由主服务器维护的一个固定长度先进先出队列(默认大小为1MB)。 image.png

复制积压缓冲区-队列数据示意图

偏移量NN+1N+2N+3N+4N+5N+6
字节值setk1v1

主服务的复制积压缓冲区的作用在于:从服务器向主服务发送PSYNC命令时,PSYNC run_id offset,主服务器收到PSYNC命令后,发现offset不一致,那么就从复制积压缓冲区中根据offset找到差异的命令,发送给从服务器;如果在复制积压缓冲区没有找到这个offset,那么差异过大,没法做部分重同步,只能做完整重同步。

  • 3、服务器的运行ID
    当redis服务器启动时,redis会自动分配一个唯一ID,作为运行ID
    执行info server命令,可以看下均有个run_id image.png run_id的作用:当主从同步时,PSYNC run_id offset, 主服务器接收到从服务器的run_id,如果与自身的run_id一致,那么说明该从服务器之前的主服务器就是自己,可以尝试部分重同步;如果不一致,那么说明该从服务器的主服务器并非自己,此时只能做完整重同步

1.2、主从复制的过程

image.png 主从复制的详细过程如下:

  1. 步骤一:从服务器设置主服务器的地址和端口
    当从服务器的客户端向从服务器发送SLAVEOF 主服务器IP 主服务器端口命令时,从服务器在收到命令后,会将对应的主服务器IP和端口,保存到masterhost和masterport属性中:
struct redisServer {
    // ……
    // 主服务器IP
    char *masterhost;
    // 主服务器端口
    int masterport;
    // ……
}

SLAVEOF是一个异步命令,从服务器在保存完主服务器的信息后,立马给从服务器的客户端发送OK回执,之后才会开始执行真正的复制工作。

  1. 步骤二:从服务器建立与主服务器的套接字连接
    从服务器向主服务器发起套接字连接,如果连接建立成功,从服务器将为该套接字关联一个专门用于处理复制事宜的文件事件处理器,该处理器负责处理后续的复制工作,例如接口RDB文件、主服务器传播的写命令等;于此同时主服务器在接受从服务器的套接字连接之后,将为该套接字创建多赢的客户端状态,也就相当于从服务器作为主服务器的一个客户端。
  2. 步骤三:从服务器发送PING命令 与主服务的套接字连接成功建立后,从服务器会向主服务器发送一个PING命令。
    此时的PING命令主要有两个作用:①、检查套接字的读写状态是否正常 ②、检查主服务器是否正常处理命令请求。
    而从服务器收到主服务器的PONG命令,无外乎以下三种情况:①、主服务器返回PONG超时,说明主从服务器之间网络不稳定,不具备复制的条件,此时从服务器会触发断开重连主服务器;②、主服务器返回错误信息,此时从服务器也会触发断开重连;③、主服务器在规定时间内返回PONG,那么从服务器则进行下一步操作;
  3. 步骤四:主从服务器进行身份验证
    此步是可选项,如果从服务器设置了masterauth选项,就会做身份验证,如果验证不通过,也会触发断开重连
  4. 步骤五:从服务器向主服务器发送监听端口
    从服务器将执行 replconf listening-port 从服务器的监听端口,主服务器收到后会记录到对应的客户端状态中,唯一的作用就是在主服务器执行info replication时,会打印出来从服务器的端口号。如下图所示:image.png
  5. 步骤六:同步
    从服务器向主服务器发送PSYNC命令,执行同步操作,将自己的数据库更新至与主服务器数据库当前所处的状态。 此处同步可能是完整重同步,也可能是部分重同步;值得注意的是,在执行完同步操作后主服务器也会变成从服务器的客户端。
  6. 步骤七:命令传播
    完成同步操作之后,主从同步就会进入命令传播阶段,这个阶段,主服务器会将自己收到的写命令发送给从服务器,从而来保持主从服务器一致。

1.3、主从复制中的心跳检测

在命令传播阶段,从服务器默认会每隔1秒向主服务器发送replication ack <relocation_offset> relocation_offset指的是从服务器的复制偏移量;而发送replication ack的命令如下:

  1. 检测主从服务器之间的网络连接状态
  2. 辅助实现min-slaves选项
  3. 检测命令丢失
1.3.1、检测主从服务器之间的网络连接状态

主从服务器之间通过发送接收replconf ack命令来判断网络是否正常,如果主服务器超过一秒未收到从服务器的replconf ack命令,那么主服务器就认为与该从服务器之间的连接出问题了。
image.png 通过info replication命令,如上图所示,lag就代表多少秒之前,从服务器发过replconf ack命令,lag=0或者1都说明连接正常,如果lag值大于1那么就说明连接异常了

1.3.2、辅助实现min-slaves选项

redis的min-slaves-to-write和min-slaves-max-lag,两个选项可以防止主服务器在不安全的的情况下执行写命令;

min-slaves-to-write 3
min-slaves-max-lag 10
如上设置则表示,如果从服务器的数量小于3个,或者3个从服务器的延迟值(lag)都大于等于10秒时,主服务器就会拒绝写操作

1.3.3、检测命令丢失

replconf ack命令中会带有从服务器的复制偏移量,主服务器如果发现从服务器的复制偏移量小于自己的复制偏移量,那么主服务器会根据从服务器的复制偏移量,到复制积压缓冲区中找到从服务器缺少的数据,重新发送给从服务器。

2、sentinel

2.1、sentinel如何监听redis服务器

image.png 如上图所示为一个处于正常工作状态的sentinel集群部署架构示意图,主要由三部分组成redis client、sentinel、redis server,下面具体介绍下整个集群如何工作:

  1. 启动sentinel服务
// sentinel config配置
port 26379
……
// sentinel监听 名为abcd的redis集群的master节点
sentinel monitor abcd 10.46.31.164 6379 2
……

三台sentinel启动后,此时彼此之间是互相不感知的,但是会分别与监听的redis集群master节点创建命令连接,并且会在master创建一个channel sentinel,三台sentinel均会订阅这个通道,此时sentinel与master节点会有两个连接,分别是用于发送命令和订阅服务。

  1. 获取主从服务器信息
    上一步完成后,此时sentinel只是完成了与master建立了命令连接和订阅连接,仍然不知道slave和sentinel依然互不感知。
    sentinel会向master发送INFO命令,从master回复的报文,就可以知道slave的信息。之后也会与slave建立命令连接和订阅连接,此后以固定频率向redis集群的主从服务器发送INFO命令。
  2. 向主从服务器推送订阅消息 上一步完成后sentinel将slave纳入自己的监控之中,但是sentinel仍然不知道彼此的存在。
    sentinel会以固定频率向其监听的主从服务器发送如下命令
    publish __sentinel__:hello "<s_ip>,<s_port>,<s_runid>,<s_epoch>,<m_name>,<m_ip>,<m_port>,<m_epoch>"
参数含义
s_ipsentinel的IP
s_portsentinel的端口号
s_runidsentinel runid
s_epochsentinel 纪元
m_name主服务器名称
m_ip主服务器IP
m_port主服务器端口号
m_epoch主服务器纪元

sentinel向通道推送上诉命令,其他sentinel因为订阅了该通道,所有它们也能收到该命令,这时候sentinel之间也就互相知道了各自的存在,两两之间创建命令连接。至此整个sentinel和redis主从服务之间的所有连接全部创建完成。

sentinel模式下,client也是通过订阅通道的方式来感知redis故障转移带来的节点变化。
具体而言,client连接sentinel,通过redis服务器名称获取到redis的主从服务器信息,从而创建与redis服务器的连接,同时还会与sentinel建立订阅连接,来监听redis服务器主从节点的变化,如果变化client会重建连接。

2.2、sentinel怎么发现故障机器

sentinel会定期向监听的主从服务器发送PING命令,如果redis服务器在规定的时间内,一直都没有给与正确的响应,那么该sentinel认为该服务器有下线的可能,这时候叫做主观下线。因为有可能只是对应redis服务器此时夯住了,或者网络闪断,而非真正的下线,所以一台sentinel检测到下线,并不能代表真正的下线。
那么怎么判断一台redis真正地故障呢?其他也简单,一台不足以判定,那就多台都判定redis故障,那就认为真正地故障了,这种叫做客观下线。sentinel会向其他sentinel发送如下命令询问你们认为这台机器是否故障,如果有超过半数的sentinel都认为下线了那就认为真的下线了。
sentinel is-master-down-by-addr <ip> <port> <current_epoch> <runid>

参数含义
ip被sentinel判定主观下线的主服务器IP
port被sentinel判定主观下线的主服务器端口号
current_epochsentinel当前纪元,用于选举领头sentinel
runid* 或者sentinel的runid,* 代表检测主服务器的客观下线状态,runid则是选举领头sentinel时使用

总结下:sentinel判断服务器是否下线,分两步

  1. 检测是否主观下线
  2. 检查是否客观下线
    其中主要关心主节点,从节点宕机,对于整个业务来说影响不大

2.3、如何故障转移

上一步确认了主节点挂了之后,那么下一步就需要做故障转移。sentinel主要分三步完成这个工作。

  1. 选举领头sentinel
    sentinel会再次向其他sentinel发送is-master-down-by-addr命令, 例如
    sentinel is-master-down-by-addr 127.0.0.1 6379 0 abcdefgsdsfgr
    在超时时间内如果有半数的sentinel同意其为leader,那么该sentinel就会被选举为领头sentinel。
  2. 在从节点中选择新的主节点
    领头的sentinel会从从节点里剔除下线的从服务器,选择最近同步过INFO命令的从服务器,然后再比较从服务器的复制数据游标,选其中数据最新的一台作为新的主节点;
    从服务升级为主服务器,剩余的从服务器与新主节点建立复制关系,老的节点恢复后作为从服务器存在。
  3. 通知client主节点发生变化 通知订阅该redis服务器的client,主节点已经变化,client收到请求后,重新初始化与redis的连接。

sentinel的检查节点是否客观下线以及故障转移期间的领头sentinel的选举,都是利用了raft的思想
通过sentinel monitor <master-name> <ip> <port> <quorum>命令可以动态添加新的redis主从集群,所以新建一组redis,就可以被sentinel所监管到。

3、cluster集群

上述的sentinel机制完美地解决了redis集群故障自动发现转移的问题,但是如果面对一个足够高并发的业务场景,会带来一个新的问题redis主节点的压力会很大甚至超出redis的承受上限,这时候空间换时间,分而治之的思路就可以派上用场了。 image.png sentinel + redis sharding,通过client分别将不同的key路由到不同的redis上,该方案可以基本解决并发高带来redis压力过大的问题,但是有个美中不足的问题,如果增加shard或者缩减shard, 数据散列的规则发生变化,就需要重新迁移数据,这个可预期地比较麻烦。
redis官方给出了更好的方案cluster。

3.1、数据分布方法

cluster是为了节点单个redis集群压力过大的问题,选择多组redis节点分布存储数据,从而达到分摊并发压力,或者说提高了整个集群的并发能力。
一般来说,分布式存储的方案,无外乎两种:顺序分区和哈希分区。

3.1.1、顺序分区

image.png 这种方式在MySQL的分库分表方案中比较场景,例如按门店分库,按时间分表,按订单号的区间段0-10000,10000-20000这类方式做数据拆分,都是采用的这种方案。这种方案最大的弊端就是,容易发生数据倾斜,分布不均衡。

3.1.2、哈希分区

哈希分区就是将分区键计算一个hash值,然后按一定规则散列到各个存储节点上。比较常见的有节点取余分区、一致性哈希分区、哈希槽分区等,redis cluster采用的是哈希槽分区方案。

  • 节点取余分区
    实现思路:计算分区键的哈希值 % 节点总数,将数据存储到对应节点上。
    优点:实现简单,大部分的数据库分库分表都采用的这种策略,提前规划足够的容量,例如按会员号分区,提前分为 512个库,或者1024张表。
    缺点:扩缩容的时候,需要重新计算数据的hash值,然后按新的路由规则迁移数据,一般采用翻倍扩容的方式(实际一般按2^n来设置节点数,相对而言比较经济实惠)。 image.png
  • 一致性哈希
    image.png 一致性哈希可以理解为一个0-2^32次方的圆环,给每个节点分配一个数值,然后键存储先计算键的hash值,然后对2^32取模,得出的结果值,按顺时针方向找最近的一个节点存储。
    image.png 如上图所示,一致性哈希最大的优点,扩缩容的时候,只会影响相邻的节点,需要迁移的数据可以控制在很小的范围。
    从上图也可以看到如果想要尽可能减少数据倾斜的影响,那么圆环上面需要尽量多的节点,才能确保数据分布比较平衡,但是现实情况中,不太可能部署那么多节点。为了解决这个问题,引入了一个新的概念,虚拟节点。
    具体而言,所谓虚拟节点就是给每一个物理节点增加多个虚拟节点,然后维护一份虚拟节点和物理节点的映射关系,通过这种方式来实现增加hash环上节点数量的目的,进而降低数据发生倾斜的概率。如下图所示 image.png
  • 哈希槽分区
    实现思路:利用某种hash算法将分区键映射到一定范围内的整数集合中,也称之为槽,每个槽对应一个节点。例如redis cluster将键hash之后对2^14次方(16384)取模,如下图示意 image.png 扩缩容的时候,只需要调整对应槽位的数据即可。

3.2、cluster集群介绍

image.png 上图为一个redis cluster的简单示意图,搭建这么一个集群大致需要如下几个步骤:

  1. 启动redis节点
  2. 加入集群 上诉6个节点拉起来后,只是一个个的孤立节点,彼此之间并没有什么联系,所以需要将这6个节点加到一个集群中。使用如下命令:cluster meet <ip> <port>
  3. 主从配置 目前6个节点尚未有主从复制关系,使用如下命令配置主从复制关系cluster replicate <node-id>(node-id为主节点的node id)
  4. 分配槽位 redis集群总共分为2^14个槽,只有当所有槽位都被分配之后,这时整个redis cluster集群才正式上线,对外提供服务。分配槽位命令如下:cluster addslotes {0..5461},只需要主节点设置即可。
  • 集群间通信
    redis cluster集群之间的节点采用P2P的gossip协议进行通信。gossip协议原理:节点之间彼此不断通信交换信息,一段时间之后所有节点都会知道集群完整的信息。
    gossip协议消息分为以下几类:
  1. meet:用于通知新节点加入,消息发送者通知接受者加入当前集群。
  2. ping:集群中节点与其他节点进行心跳检测,用于检测其他节点是否在线,同时交换其他额外信息。
  3. pong:用于回复meet以及ping信息,表示已收到,能够正常通信。此外pong消息才可以通知整个集群对自身状态进行更新。
  4. fail:当节点判定集群中某个节点下线时,会像集群中其他节点广播fail消息,其他节点收到后,也会把对应节点更新位下线状态。

消息分为消息头和消息体。
消息头:发送节点自身信息,包括节点ID、槽映射、节点标识(主从角色、是否下线)等
消息体:主要用来放其他节点信息,用于信息交换。
一个节点接收到meet/ping消息,先解析消息头,如果发送节点是新节点且消息类型为meet,则加入本地节点列表;如果是已知节点,那么就更新本地的对应节点信息。然后解析消息体,如果消息体中的其他节点数组中包含未知的新节点,则尝试发起与新节点的meet消息;如果是已知节点,则判断下有无疑似下线的节点,是否满足故障转移的条件。
处理完消息头消息体的解析动作后,然后回复pong消息,里面同样携带自己所知的节点信息。
通过上次几次ping-pong消息的交换,最终达到所有节点数据一致的目的。

gossip消息的注意点: image.png 如上图所示,gossip消息中会携带集群中其他节点的信息,如果集群很大的话势必会导致通信的报文很大,更高的性能损耗。

3.3、集群扩缩容

redis cluster采取哈希槽分区的方式散列数据,所以集群在做扩缩容时,相对应地就需要需要对应槽的数据做节点迁移。redis通过redis-trib执行重新分片处理,其中在迁移过程中redis通过ask错误,来确保迁移过程集群依旧可以正常提供读写服务。 redis-trib对集群的单个槽重新分片的步骤如下:

  1. redis-trib对目标节点发送cluster setslot <slot> importing <source_id>命令,让目标节点准备好从源节点导入属于槽slot的键值对;
  2. redis-trib对源节点发送cluster setslot <slot> mingrating <target_id>命令,让源节点准备好将属于槽slot的键值对迁移给目标节点;
  3. redis-trib向源节点发送cluster getkeysinslot <slot> <count>命令,获得最多count个属于槽slot的键值对的键名;
  4. 对于步骤3获得的每个键名,redis-trib向源节点发送migrate <target_ip> <target_port> <key_name> 0 <timeout>命令,将选中的键值对迁移至目标节点
  5. 重复步骤3、4直到将槽slot上所有键值对迁移到目标节点;
  6. redis-trib向集群中任意节点发送cluster setslot <slot> node <target_id>命令,将槽slot指派给目标节点,随后这一指派信息会逐步扩展至整个集群。

3.4、客户端请求的要点

3.4.1、MOVE错误

image.png

3.4.2、ASK错误

image.png image.png ASK错误主要发生在集群节点迁移过程中,具体流程如下:

  1. 客户端根据本地缓存的slot-node的映射关系,发送命令给源节点;如果存在键对象则执行执行命令返回;
  2. 如果不存在键值对,判断下是否对应槽正在迁移,如果是那么就返回ASK错误;
  3. 客户端收到ASK错误后,从中提取中目标节点信息,先发送一个asking命令到目前节点打开客户端连接标识,然后再发送键命令执行,如果存在就执行命令,不存在就返回不存在。

asking命令存在的意义:
如3.3中槽迁移的具体过程,只有在槽所属的所有键值对数据全部从源节点迁移至目标节点之后,redis-trib才会槽指派给目标节点。也就是说在迁移过程中,目标节点并没有被指派该槽,所以如果客户端此时访问目标节点时,会发现并没有槽信息,那么按照约定节点就会回复客户端MOVE错误。故而为了规避这个问题,redis设计了asking命令,告知节点下这一次访问是一个特殊的例外,不要按move错误的逻辑处理。

ASK错误和MOVE错误虽然对客户端都是重定向控制,但是两者有着本质区别。ASK重定向说明集群正在对槽slot进行迁移,只是一次临时的重定向,客户端不会缓存槽与节点的映射关系;但是MOVE重定向说明键对应的槽已经明确指派了新节点,同时客户端会缓存槽与节点的映射关系。

3.5、如何故障转移

3.5.1、故障发现
  • 集群中的每个节点都会定期给其他节点发送ping/pong消息,如果未能在规定时间内给予响应,那么就认为该节点可能下线,主观下线;
  • 然后各个节点之间会互相交换消息,如果发现集群中超过半数以上的节点都认为该节点下线了,那么就是真下线,此时为客观下线。(跟sentinel故障发现类似)
  • 如果被认为下线的节点是主节点,那么就需要发现的节点就要向集群中所有节点广播一条某主节点故障的fail消息,收到这条fail消息的节点都会将该节点标记为客观下线。
3.5.2、故障恢复

当故障节点的从节点通过内部定时任务检测到主节点故障已经客观下线时,就会触发故障转移流程:资格检查 -> 准备选举时间 -> 发起选举 -> 选举投票 -> 替换主节点。

  1. 资格检查
    每个从节点检查与主节点的断开时间,时间过长的节点没有资格
  2. 准备选举时间 对剩下的从节点按照复制偏移量来确定发起选举的优先级,偏移量最大的优先发起选举
  3. 发起选举 从节点更新纪元后,向集群中广播选举消息
  4. 选举投票 只有持有哈希槽的主节点才有选举权,如果某个从节点获得了一半以上主节点的认可,那么就认为选举胜出
  5. 替换主节点 当从节点收集到足够选票之后,就开始替换之前主节点:
  • 当前节点取消复制,变成主节点
  • 撤销故障主节点负责的槽,将这些槽指派给自己
  • 向集群中广播一条pong消息,通知其他节点本从节点已经变成主节点并接管了故障主节点的槽信息。

redis cluster与redis sentinel + redis sharding redis cluster 和sentinel + redis sharding这两种集群部署方式,均可以解决单redis并发压力过大的问题,但是各有利弊;

  • cluster
    胜在扩缩容数据迁移时,客户端依旧可以使用;弊端在于所需机器较多,一个最小的cluster集群至少也需要6个节点。
  • sentinel + redis sharding 弊端在于无法平滑无感地扩缩容,迁移数据,优点在于简单,没有最小机器数地约束。

参考文章

  1. 《redis设计与实现》
  2. 总结redis cluster原理+基本使用+运维注意事项
  3. Redis超详细的手动创建Cluster集群步骤
  4. 一致性hash和普通hash和hash槽