Redis学习记录——多机环境:复制、哨兵、集群

160 阅读11分钟

复制

  • slaveof ip port 命令让某台机器成为ip port 服务器的从节点
  • 复制分为同步、命令传输
  • redis 2.8版本后 采用了优化后的同步,解决了旧版同步的低效问题

旧版复制:

  • 通过sync命令实现

旧版复制步骤:

  • 从服务器发送sync命令给主服务器
  • 收到sync的主服务器此时开始执行bgsave,后台生成一个rdb文件,并通过一个缓冲区记录从此刻开始的所有写命令;
  • 当主服务器的bgsave 执行完后,主服务器会将这个rdb文件发送给从服务器,从服务器接受并载入这个rdb文件,将自己的数据库更新至rdb文件的状态。
  • 主服务器将缓冲区的所有内容发送给从服务器,从服务器执行这些写命令,将自己的数据库状态更新到主服务器状态。

命令传输

  • 同步后不是一成不变的,当主服务器有新的写操作后,主从也需要同步,不然又不一致了
  • 主服务器写了后,会将该条写命令发送给从服务器,从服务器执行这条写命令,再次回到一致状态;

旧版复制的缺点

  • 对于初次复制来说,sync能够很好的完成任务,但是对于断线后的重连,旧版复制虽然也能完成任务,但是效率非常低。
  • 比如说就是断线了一会,就几条命令没同步,却要全部重新载入rdb文件,既消耗主服务器的io和cpu、网络 等资源,从服务器载入的时间也很长。

新版复制的实现

  • 使用psync 命令代替了sync
  • 分为完整同步 和增量同步
  • 完整同步 使用在 初次复制、不能使用增量同步的情况,和sync一样
  • 增量同步 使用在 断线重连,而且满足一定的条件,主服务器只需要将断线期间的所有写命令发送给从服务器,从服务器只要执行这些命令即可。

原理:

  • 借助下面三个部分:
    • 主服务器的 复制偏移量 和从服务器的 复制偏移量;
    • 主服务器的 复制积压缓冲区
    • 服务器的 运行id runid

复制偏移量

  • 执行复制的双方,主和从都维护一个复制偏移量;
  • 主服务器 每次传输n个字节,自己的复制偏移量就增加n
  • 从服务器 每次接受到n个字节,自己的复制偏移量也增加n
  • 这样,只要主从 的复制偏移量一致,就可以认为他们在同一个状态;

复制积压缓冲区

  • 本质 固定长度 fifo的队列
  • 主服务器每次写操作,不管是复制还是命令传输,都会写到复制积压缓冲区内;
  • 默认1mb,这个大小很重要
  • 如果这个大小设置太小,会导致断线很短时间也要全部更新,不能利用增量更新这个优点,如果设置太大,浪费内存
  • 一般使用 2* 平均断线重连的时间 * 每s平均写入命令大小;

服务器运行id

  • 每个服务器都有自己的运行id,40个随机16进制字符;
  • 初次复制时,从服务器会将主服务器的runid存起来,后续断线重连后,会将这个id发给主服务器,主服务器判断是否是从自己那复制的

复制的完整步骤

  • 1.从服务器收到slaveof 命令
  • 2.判断自己是否是初次复制,估计使用是否有主服务器的runid吧
  • 2.1 是第一次,给主服务器发送psync ?-1 ,执行完整同步
  • 2.2 不是第一次,发送 psync runid offset
  • 3.主服务器判断:
    • 如果从服务器 复制偏移量 后的数据都在 自己的复制积压缓冲区内,那么就可以采用增量更新,
    • 如果不行,那还是进行完整同步

哨兵

# Redis专题:深入解读哨兵模式

集群

主要学习集群节点、槽指派、命令执行、重新分配、转向、故障转向

节点

  • 一个集群由多个节点组成,通过cluster meet ip port 加入集群
  • redis启动时会根据配置cluster-enabled 来判断是否开启服务器的集群模式
  • 集群模式和单点模式的区别?
    • 集群模式只使用数据库1
    • 集群模式下的客户端会隐藏moved错误,直接转向
    • 单机模式下的客户端会打印moved错误,而且不会转向,因为他不能理解moved错误
  • 每个节点都会使用一个clusterNode结构来记录自己的状态,并为集群中所有的节点都创造一个clusterNode结构

clusterNode

clusterNode:
    ctime; // 节点创建的时间
    char name; // 节点的名字
    int flags; // 节点的标志,比如说主还是从?在线还是下线?
    uint configEpoch; // 节点当前的配置纪元,用于故障转移
    char ip; // 节点ip
    int port; // 节点端口
    clusterLink link; // 保存节点连接的相关信息
    char slots[]; // 二进制位数组,类似bitmap?下面详细介绍
    int numslots;
    clusterNode slaveof; // 如果是 从节点, 那么指向主节点

clusterLink

clusterLink:
    ctime; //连接 建立时间
    int fd; // tcp 连接 套接字的文件描述符
    sds sndbuf; // 输出缓冲区,保存要发给其他节点的信息
    sds rcvbug; // 输入缓存区,保存从其他节点接受到的消息
    clusterNode node; // 连接对应的节点,可以为null

clusterState

这个结构记录了 在当前节点视角下,当前集群的状态,

clusterState:
    clusterNode myself; // 指向自己节点结构的指针
    uint currentEpoch;// 配置纪元,用于实现故障转移
    int state;// 集群状态,是在线还是下线
    int size;// 集群中 至少处理一个槽的 节点数
    dict node;// 一个字典,key 是节点名,value 是对应的结构指针,包括myself
    clusterNode[] slots;// 16384 个clusterNode数组,下面细说

cluster meet 命令的实现:

向节点A发送cluster meet 命令: cluster meet bip bport ,可以将节点b加入a的集群

  • a与b进行握手,如下操作
    • a 为b创建一个clusterNode结构,并加入到clusterState.node里
    • a 向b 发送一条meet消息
    • b收到a的meet消息后,会为a创建一个clusterNode结构,并加入到自己的clusterState.node里
    • b返回a一条pong消息
    • a收到后,返回一条ping,握手完成。类似三次握手吧

槽指派

  • redis 通过分片的方式来保存数据库中的键值对,共16384个槽,每个键都属于0-16383槽内,集群中每个节点都可以出来0-最多16384个槽
  • 当所有的槽都有节点处理时,集群处于上线状态,否则就是下线状态,记录在clusterState.state里
  • 在某个节点执行 cluster addslots 0 1 2 ... 5000 ,可以将这5001个槽 指派给这个节点
  • 节点负责的槽信息记录在clusterNode里的slots 和numslot里
  • slots: 长度为16384/8=2048字节,包含16384个二进制数据,要是该节点负责处理哪个槽,该索引对应的二进制位为1,要不然的话为0,不知道是不是bitmap实现,思路是一致的,这样的话,检查节点是否负责哪个槽,时间复杂度是O(1)
  • numslots 记录该节点处理的槽的数量
  • 节点除了自己记录自己处理的槽 信息外,还会把自己的slots数组发送给集群的其他节点。告诉其他节点,自己负责哪一块。节点收到其他节点发送的该消息后,会在自己的clusterState.nodes 字典找到对应的clustNode结构,更新其中的slots数组
  • clusterState.slots 里有16384个clusterNode 元素,记录 每个槽 对于的 clusterNode节点信息的指针,为啥光使用clusterState.node里的slots数组还不够?
    • 为了知道槽i被谁处理,可能需要遍历clusterState.nodes 字典。复杂度为O(n)
    • 而使用 clusterState.slots后,只需要O(1)的时间复杂度。
  • 那为啥不能只用 clusterState.slots?还需要clusterState.nodes么?
    • 但是如果需要将某个节点所处理的全部槽信息发送出去时,只需要去clusterState.nodes 中寻找就行了 ,不需要遍历 clusterState.slots

cluster addslots 命令实现:

  • 遍历检查 这些槽,哪怕有一个已经被指派,也返回错误
  • 再次遍历,将这些槽指派给自己节点

集群执行命令

  • 计算该键属于哪个槽,并检查这个槽是不是自己处理
  • 如果是,直接执行这个命令
  • 如果不是,返回一个moved错误,并指引客户端转向正确的节点,再次执行该命令

计算键属于哪个槽

计算键的crc-16校验和,然后 & 16383,

检查是否是自己处理

检查自己的clusterState.slots 里的槽i 是否指向自己

moved错误

  • 集群模式下的客户端会隐藏moved错误,直接转向
  • 单机模式下的客户端会打印moved错误,而且不会转向,因为他不能理解moved错误

重新分片

可以在线操作

实现原理:

  • 由redis-trib软件负责,
    1. redis-trib 对目标节点发送cluster setslot 命令,让目标节点准备好从源节点导入属于槽的键值对
    1. redis-trib 对源节点发送cluster setslot 命令,让源节点准备好讲属于槽的键值对发送目标节点
    1. 源节点返回最多count个键值对
  • 4.对于每个键,都向源节点发送一个migrate命令,让该键值对转移到目标节点
  • 5.重复直到完成全部键值对

ask错误

在重新分片的过程中,如果客户端要查询属于这些槽内的键值对,可能会出现ask错误

  • 源节点会首先从自己的数据库里找,如果找到的话,直接执行命令
  • 如果没找到,这个键可能已经迁移到新节点了,返回一个ask错误,并指引客户端转向正确的节点
  • ask错误也是隐藏的

复制

集群的节点也是有主节点、从节点的,主节点负责处理这些槽,从节点复制某个主节点,当主节点下线时,可以顶上

  • cluster replicate nodeid 成为该节点的从节点
  • 收到命令后,会将自己clusterNode 中的slaveof指向 这个主节点
  • 然后发送给集群中的其他节点,最终全部节点都会知道

故障检测

  • 每个节点都会定期向其他节点发送ping消息,如果某个接受到的节点没有在固定时间返回pong,就会将该节点标为疑似下线;会在自己的clusterState.nodes里找到该节点的clusterNode结构,将其flags属性改为pfali
  • 当主节点a从主节点b得知b认为主节点c下线了,a会在自己的clusterState.nodes字典里找到主节点c对应的clusterNode结构,将b的下线报告记录在clusterNode.fail_reports链表里
  • 如果集群中一般的节点都将主节点c报告疑似下线,那么c会变成fail状态,向集群广播

故障转移

  • 下线主节点的所有从节点中,会有一个被选中
  • 被选中的节点执行slaveof no one 命令,成为新的主节点
  • 新节点会将之前主节点处理的槽下掉,指向自己
  • 新的主节点向集群广播一条pong小学,通知自己变成主节点了,并且负责之前下线的槽

选举新的主节点

  • 1.集群的配置纪元 初始值为0,
  • 2.每开始一次故障转移,配置纪元++
  • 3.对于每个配置纪元。每个主节点都有一次投票机会,而第一个向主节点要求投票的从节点将获得这一票
  • 4.当从节点发现自己的主节点下线了,会广播一条消息,要求其他主节点给自己投票
  • 5.如果主节点收到并且还有投票机会,就会返回一条消息,
  • 6.每个参与的从节点会统计自己获得了多少主节点的支持
  • 7.假设集群有n个主节点,当某个从节点统计到自己得到了n/2+1张票,就会广播自己成为新的主节点