《Redis 设计与实现》读书笔记(三)

多机数据库实现

十五 、复制

从服务器通过命令 slaveof 127.0.0.1 6000 成为主服务器的从服务器。然后执行复制操作,保持自己的状态和主服务器一样

1.理论

同步

成为从服务器后的同步操作:

  1. 从服务器会发送SYNC命令给主服务器,
  2. 主机会执行bgsave命令,并记录当前的偏移量。
  3. bgsave命令执行期间执行的写命令,都会记录到缓冲区
  4. bgsave命令执行成功后,主机发送RDB文件给从机
  5. 从机加载RDB文件
  6. 主机发送缓冲区的命令给从机
  7. 从机执行缓冲区命令

命令传播

当主从机的状态一致后
主机每次执行写命令,都会通过命令传播的方式,发送给从机
从机执行写命令,这样主从的状态又会一致了。

2.8版本前的缺陷

如果主从之间网络断开,这样主机的写命令就不能通过命令传播发给从机了,这时候主从就不一致了。
当主从重新连接上后,在2.8版本前的做法是重新执行一次同步操作。
如果主从断开前执行了很多命令,断开期间期间主机只执行了几条写命令,重新执行一次同步操作,效率会比较慢。更好的做法是只同步断开期间执行的命令给从机就好了。所以为了优化这个缺陷,2.8后新出了PSYNC命令

新版命令

新版增加了PSYNC命令,这个命令支持完整重同步部分重同步
简单来讲就是重连后,主机会判断当前能不能执行部分重同步,如果可以就做,如果不可以,就执行完整重同步。

其他知识

  • 复制偏移量(offset)。
    • 主机和从机都会保存复制偏移量,这个是当前执行过的所有命令的字节数。例如set key value命令的字节数是33。(这个不是简单的把命令转成字符串的,有一定的算法,算法应该和RDB文件的算法一样的。总之就是把一条命令多个参数转成一个字符串。例如 set test 3 命令就占44个字节)
    • 当执行一条新命令,例如偏移量是100,。主机执行完后,就会把自己的偏移量加100
    • 主机命令传播给从机后,从机执行完,也把自己的偏移量加100
    • 这个的作用就是识别主从之间是否一致以及不一致的程度有多少
  • 复制积压缓冲区 (repl_backlog)
    • 这个缓冲区和同步的时候的缓冲区不一样
    • 主机每次执行写命令,就把命令转换成的字符串,存入这个缓冲区
    • 缓冲区采用固定长度,先进先出的队列。
    • 默认缓冲区的大小是1M。通过info replication命令可以查看缓冲区信息,repl_backlog_size:1048576
    • 缓冲区每一个字节,都有自己的偏移量号码对应上面的复制偏移量。
  • 服务器运行ID(run id)
    • 每个redis节点都有自己的运行ID。是40个随机的十六进制字符组成。
    • 主从关系建立后,从机会记录主机的ID
    • 每次从机执行PSYNC都要把主机的ID传输过去,如果主机ID变更,只能使用完整重同步。

info replication

127.0.0.1:6811> info replication
# Replication
role:master #当前节点的角色
connected_slaves:1  #从机数量
slave0:ip=127.0.0.1,port=6801,state=online,offset=2095671,lag=0 #从机1信息
master_repl_offset:2095671  #主机的offset
repl_backlog_active:1  #缓冲区是否可用
repl_backlog_size:1048576 #缓存的大小,默认是1M
repl_backlog_first_byte_offset:1047096 #缓冲区第一个字节的offset
repl_backlog_histlen:1048576 #

在主机执行这个命令,可以查看主从复制的情况,包括有多少个从机,偏移量,缓冲区大小等。

2.过程

PSYNC命令实现

  • PSYNC的调用方法有两种

    1. 从机之前没有成为别人的从机,也就是第一次成为从机。会发送PSYNC ? -1命令。这时候肯定会执行完整重同步
    2. 从机之前成为过别人的从机。会发送命令PSYNC runid是之前的主机的ID,offset是从机当前的offset。
      • 主机收到命令后会判断runid是否和自己的一样,如果不一样,就执行完整重同步
      • 如果一样,判断offset是否小于自己的repl_backlog_first_byte_offset,也就是从机缺失的写命令是否还在缓冲区内
        • 如果不在,就执行完整重同步
        • 如果再,就执行部分重同步
      • 所以,只有当runid没有变更,而且offset小于repl_backlog_first_byte_offset,才会执行部分重同步,否则执行完成重同步
  • 如果可以执行部分重同步,主机会返回+CONTINUE命令,然后发送缺失的写命令给从机

  • 如果需要执行完整重同步,主机会返回+FULLRESYNC命令,然后后面的步骤和同步一样。

主从同步完整流程

  • slaveof命令
    • 执行完slaveof命令后,从机会把主机的ip和端口存在redisServer结构体里面,然后就返回ok了
    • 返回ok后才会执行同步操作,所以是异步的。
  • 从机与主机建立socket连接。这时候从机相当于主机的客户端
  • 从机发送ping命令给主机,主机如果正常返回pong命令。如果主机超时不返回或者返回错误。从机断开连接重试。
  • 身份验证,如果需要从机需要发送auth 密码命令
  • 从机发送端口信息给主机。也就是从机节点的端口。
  • 从机发送PSYNC命令
  • 主机判断执行那种同步,不管是那种同步,主机都会成为从机的客户端,也就是连接从机的端口。
    • 如果是完整重同步,主机记录当前offset,执行bgsave,发送RDB文件给从机,发送offset后面的写命令给从机。
    • 如果是部分重同步,主机发送从机的offset之后的写命令给从机
  • 从机执行写命令,主从状态达到一致
  • 然后进入命令传播阶段,主机执行的所有写命令,都发送给从机,从机执行后,主从状态达到一致。

心跳

从机每隔一秒会向主机发送心跳命令 REPLCONF ACK <replication_offset>
心跳可以实现功能:

  • 检测主从之间的网络状态

  • 辅助实现min-slaves

  • 检测命令丢失

  • 检测主从之间的网络状态

    • 如果主机超过1秒没有收到从机的ack命令,就表名从机网络出现了故障
    • info replication命令可以看到从机上一次ack距离现在的时间,就是lag参数,一般在0-1之间,超过就是有故障了
  • 辅助实现min-slaves

    • min-slaves选项是指在从机数小于min-slaves-to-write,而且全部从机的lag值大于min-slaves-max-lag秒时,主机拒绝执行写命令。
    • 这个功能主要是防止主机的主从复制处于不安全状态
  • 检测命令丢失

    • 假如主机的写命令没有成功传输给从机,例如网络丢失了。这时候从机的offset就会小于主机。通过心跳,主机会发现从机的offset不等于自己,就会补发对应的写命令给从机。
      • 从机通过offset可以避免重复执行相同offset的命令
      • 命令补发这种情况较为容易触发。
        • 例如主机刚执行一条新命令,也把命令传播出去了,但是从机还没有收到,然后心跳过来了,这时候从机offset肯定会小于主机offset。
        • 所以不知道Redis有没有机制可以避免这种情况。例如两次心跳都一样而且offset小于自己,才触发命令补发。

十六、哨兵

哨兵是Redis高可用的一种方案。Redis的架构是一主多从,然后有一个或者多个哨兵进程去监听主服务器的情况。当哨兵认为主服务器已经下线,提升其中一个从服务器为主服务器,然后修改其他从服务器的复制配置。
哨兵的作用类似Mysql的MHA,只是哨兵支持多个,MHA只有一个manager。

1.初始化哨兵Sentinel

哨兵也是一个Redis进程,启动方式是redis-sentinel /config.conf
哨兵进程只能执行哨兵相关的命令,不能执行其他的Redis命令。

数据结构

  • sentinelState 哨兵状态结构

    • uint64_t current_epoch 当前纪元,用于选取领头羊哨兵
    • dict *masters 监视的主服务器信息,一个哨兵集群可以监视多个主服务器。key是主服务器的名字,例如127.0.0.1::6479 value是sentinelRedisInstance结构
    • tilt 是否进入tilt模式
  • sentinelRedisInstance 哨兵实例结构

    • flags 标志值,表示实例当前的状态,可取值:主服务器,从服务器,主观下线,客观下线
    • char *name 名字 例如127.0.0.1:6379
    • char *runid 运行ID
    • uint64_t config_epoch 配置纪元
    • *addr 地址包括ip和端口
    • down_after_period 实例无响应多久判断为主观下线
    • quorum 判断为客观下线所需的投票数
    • dict slaves 这个主服务器下面的所有从服务器,结构和masters结构一样。
    • dict sentinels 监视这个主服务器的其他哨兵,不包含哨兵自己

哨兵配置

port 6711


#监听的存储redis,TestMaster1是redis名称,127.0.0.1是ip,6702 是端口,1是升级为Master的权重


sentinel monitor mymaster 127.0.0.1 6721 1
sentinel down-after-milliseconds mymaster 3000
sentinel failover-timeout mymaster 10000
daemonize yes
#指定工作目录
dir "/data/redis_demo"


logfile "/data/redis_demo/log/sentinel.log"
#redis主节点密码
sentinel auth-pass mymaster 123456
  • mymaster是主服务器的名字
  • 后面是ip和端口 1是quorum
  • down-after-milliseconds 实例无响应多久判断为主观下线

哨兵启动后,初始化后,就会和主服务器建立连接,有两个:

  • 命令连接。也就是哨兵充当主服务器的客户端。用于向客户端发送PING,发送订阅等命令
  • 订阅连接,会订阅频道__sentinel__:hello,用于接收订阅消息。

2.获取主从服务器信息

建立连接后,哨兵会每10秒向主服务器发送INFO命令。INFO命令会返回主服务器的所有从服务器信息。这样哨兵就能知道主服务器有多少从服务器了。
然后会新建或者更新主服务器的slaves结构
slaves结构的key是从服务器的ip和端口,例如127.0.0.1:7000,value是sentinelRedisInstance数据结构。

当有新的从服务器,哨兵会像和主服务器建立的连接一样,和从服务器也建立两个连接。

然后会每10向从服务器发送INFO命令。

3.获取其他哨兵信息

哨兵会每个2秒向主和从服务器发送订阅消息,频道是__sentinel__:hello,消息是:PUBLISH __sentinel__:hello "s_ip,s_port,s_runid,s_epoch,m_name,m_ip,m_port,m_epoch"

  • s开头的是哨兵自己的信息
  • m开头的是主服务器的信息

假如哨兵1发送了这个消息,因为其他哨兵,例如2和3,都会订阅这个频道,所以它们也能收到这个消息,哨兵1自己也会收到。
所以当它们收到这个信息后:

  • 如果run_id是自己,不处理
  • 如果run_id不是自己,
    • 更新或者新建其他哨兵的数据结构。更新master的sentinels结构,key是哨兵2和3的ip端口,例如127.0.0.1::8000 value是sentinelRedisInstance结构。
    • 和其他哨兵建立连接,只会建立命令连接,不会建立订阅连接。

4.判断主观下线

哨兵会每1秒向其他哨兵和主从服务器发送PING命令。其他服务器会返回:

  • PONG,LOADING MASTERDOWN3种回复
  • 除此之外的其他回复或者超时不回复称为无效回复。

当在down-after-milliseconds时间内,例如是5s,对方连续返回无效回复,例如是5次PING都返回无效回复,哨兵就会把这个服务标记为主观下线,就是把flags值修改为SRI_S_DOWN。

5.判断客观下线

当哨兵判断一个主服务器主观下线后(从服务器不会触发),会向其他哨兵发送命令:

SENTINEL is-master-down-by-addr <ip> <port> <current_epoch> <runid>

分别为主服务器的ip 端口,自己的配置纪元,runid=*号。

其他哨兵接收这个命令后,会返回

  • down_state 下线状态1=下线,0=未下线
  • leader_runid 选举的leader的run_id,
  • leader_epoch 选举的leader的配置纪元

上面的 current_epoch run_id leader_runid leader_epoch都是用于选举领头羊哨兵的,在判断客观下线中没有用。

所以总的来说,哨兵判断一个主服务器下线后,会询问其他哨兵,是否也把这个服务器标记为下线,如果有大于等于quorum参数的哨兵投票说主服务器已下线,哨兵会把主服务器标记为客观下线,也就是把flags标记为SRI_O_DOWN

6.选举领头羊leader

当一个哨兵把主服务器标记为客观下线后,就会进入选领头羊leader环节,在多个哨兵中选择一个领头羊哨兵,来执行故障转移操作。

  • 哨兵1判断主服务器为客观下线后,向所有其他哨兵发送上面的is-master-down-by-addr命令,current_epoch设置为自己的配置纪元,runid是自己的runid
  • 哨兵2收到这条命令后,如果在自己的配置纪元没有选过领头羊,就会返回leader_runid=哨兵1的runid,leader_epoch=哨兵1的配置纪元。如果已经选过领头羊,就会返回选中的领头羊信息
  • 如果超过总哨兵的半数都投票给哨兵1,哨兵1就会成为领头羊

配置纪元问题:

  • 全部哨兵的配置纪元是否需要相同,如果相同,怎么同步?
  • 如果不相同,怎么判断这个配置单元中有没有选过其他人

解答

  • 在哨兵A认识其他哨兵的时候,会传送自己的配置纪元给对方
  • 一开始所有的哨兵的配置纪元都是0
  • 当哨兵看到对方的配置纪元比自己大,就会更新自己的配置纪元为对方的配置纪元
  • 这样当所有哨兵都认识后,所有哨兵的配置纪元都会统一,也就是所有哨兵中最大的那个,例如A是1,B是2,C是3,最后ABC的配置纪元都会设置为3
  • 当哨兵A发起投票的时候,它会先把自己的配置纪元+1,例如变为4,然后要求BC投票。然后计时(例如等待5s)。
  • 当B收到A的投票要求,如果B的配置纪元比自己的大(例如B现在是3),就会认为4是没有投票的配置纪元,就把票投给A,然后设置自己的配置纪元为4.
  • 当B收到C的投票要求,发现自己的配置纪元等于C的配置纪元(例如都是4),因为在配置纪元=4时,B已经把票投给A了。所以B不能投票给C,它会返回A的runid和A的配置纪元
  • A计时结束后(也就是5s后),如果A只收到B的票,但是没有收到C的票(可能C把票投给B了),所以成为领头羊失败。这时A会把配置纪元再+1=5,然后再次要求BC投票,然后再计时

异常情况

  • 如果A的配置纪元是5,C是4,B是3
  • C先发起投票请求,B会投票给C,但是A不会,因为C的纪元比自己小
  • A发起投票请求,B会投票给A,C也会投票给A
  • 所以最终A和C都认为自己成为了领头羊。
  • 可能的解决方法:
    • 方法1:
      • C收到A的返回中会标明A投票给了A,纪元是5
      • C发现A的纪元比自己的纪元大,所以应该停止成为领头羊
    • 方法2:
      • C和A成为领头羊后,向所有节点群发自己成为领头羊的消息,以及自己的纪元
      • 当C发现A成为领头羊,而且纪元比自己大,就自动放弃领头羊

Raft算法视频
配置纪元
Raft算法

7.故障转移

成为领头羊leader的哨兵将执行主服务器的故障转移工作

  • 从从服务器中选一个成为主服务器
    • 优先选择近期ping后有回应的服务器
    • 优先选择数据较新的从节点
  • 对新的主服务器执行slaveof no one命令 让它成为主服务器
  • 每2秒对新服务器执行INFO命令,查看role是否从slave更新为master
  • 如果成为master,对其他从服务器执行slaveof操作,让它们从新的主服务器复制数据
  • 把旧的主服务器记录下来,等下次它上线,执行slaveof命令,让它从新的主服务器复制

十七、集群

集群通过分片(sharding)来进行数据共享,并提供复制和故障转移功能。

1.节点

一个集群由多个节点组成,一开始这些节点是互相不能感知的。
我们需要通过命令cluster meet <ip> <port>,让节点加入集群。例如在A节点执行meet命令,ip和port是B节点的,这样A和B节点就相互感知了。
通过 cluster nodes命令可以查看当前集群的情况。

127.0.0.1:6812> cluster nodes
1fdfb5833caf8e9cf3b7f1233ce3969e0a324db7 127.0.0.1:6804 master - 0 1572954527331 12 connected 0-1104 5461-5779 11423-12004
72234454d061c86c630e8eb7995e2480fe340b95 127.0.0.1:6803 master - 0 1572954527331 8 connected 12005-16383
  • 分别是 节点ID,IP 端口,角色

启动

节点需要配置 cluster-enabled yes 才会开启集群模式。
集群模式的节点启动后,其他都和单机节点一样的,只会在serverCron函数中增加一个clusterCron函数的调用

集群数据结构

集群增加了3种数据结构

  • clusterNode 集群节点信息,有字段
    • mstime_t ctime 创建时间
    • char name 节点名,也叫节点ID
    • int flags 存储节点的角色(master还是slave)和集群状态(在线或者下线)
    • uint_64_t configEpoch 配置纪元 用于故障转移
    • char ip 节点IP地址
    • int port 节点端口
    • clusterLink link 和其他节点的连接
  • clusterLink 和其他节点的连接,和redisClient结构很像,有字段:
    • mstime_t ctime 创建时间
    • int fd 套接字描述符
    • sds sndbuf 待发送缓冲区
    • sds rcvbuf 已接收缓冲区
    • clusterNode node 这个连接对应的节点信息
  • clusterState 集群状态,有字段:
    • clusterNode myself指向自己的clusterNode结构
    • uint64_t currentEpoch 配置纪元,用于故障转移
    • int state 集群状态 上线还是下线
    • int size 集群中至少处理着一个槽的节点数量。
    • dict *nodes 集群中所有的节点,key是节点名,value是clusterNode对象,也包括节点自己的node实例。

cluster meet命令

  • 客户端向节点A发送Meet命令
  • 节点A创建节点B的ClusterNode对象
  • A节点发送Meet命令给节点B
  • 节点B创建节点A的ClusterNode对象
  • 返回Pong命令
  • 节点A收到Pong命令
  • 节点A返回Ping命令给节点B
  • 节点B收到Ping命令
  • 握手完成

然后节点A和B通过Gossip协议,然自己一直的节点认识彼此。

2.槽指派

Redis集群有16384个槽。数据库中每个键都对应这些槽中的一个。每个节点处理0-16384个槽。
只有当全部槽都有节点处理,集群才会进入上线状态。

槽指派命令

cluster addslots 0 1 2

这里把槽 0 1 2 3个槽指派给当前连接的节点。

槽的数据结构

槽的信息存储在clusterNode结构的unsigned char slots[16384/8]。这是一个二进制字符串列表,只有0 1。 如果是1表示这个下标的槽由当前节点处理。还要个numslots记录处理的槽的总数。
在clusterState结构有个 clusterNode *slots[16384]变量用来存储每个槽对应的节点对象。
这样就能实现通过O1复杂度可以

  • 查找自己是否负责某个槽
  • 某个槽是哪个节点在处理,还是没有节点在处理

执行cluster addslots命令后,当前节点会把自己负责的槽都同步给其他节点。

当机器所有槽都有节点处理,机器就会进入上线状态

集群中执行命令

  • 客户端发送命令给其中一个节点
  • 计算这个key对应的槽,使用CRC16 校验和算法
  • 槽是否有当前节点处理。检查clusterState.slots[i]是否指向clusterState.self,如果是就是自己处理。
    • 是,执行命令
    • 否,查看槽在哪个节点负责,返回MOVED错误给客户端MOVED 10000 127.0.0.1:6801分别是槽号,处理该槽的IP和port
  • 客户端收到MOVED错误,连接到对应的节点,重试

通过命令cluster keyslot test 可以查看test这个key属于哪个槽.
如果使用-c集群模式启动客户端,MOVED命令会被隐藏。否则会抛出。

数据库实现

集群模式,的数据库实现和单机模式差不多,不同点:

  • 集群模式只有一个数据库,就是0
  • clusterState对象有个变量是 zskiplist *slots_to_keys是个跳跃表对象,保存当前数据库的所有key,以及key的slot,slot是分数的形式。
    • 保存这个信息的好处是
      • 可以快速执行 cluster getkeysinslot <slot> <count> 用于返回指定槽的N个key。这个命令主要用于重新分片

4.重新分片

重新分片就是把N个槽从节点A迁移到节点B。重新分片过程中,集群是一直在线状态的。

重新分片工作一般是使用管理软件redis-trib负责的
步骤是

  1. 对目标节点发送cluster setslot <slot> IMPORTING <source_id>命令,让目标节点做好导入槽的准备
  2. 对源节点发送cluster setslot <slot> MIGRATING <target_id>命令,让源节点做好导出槽的准备。
  3. 对源节点发送cluster getkeysinslot <slot> <count>命令,获取count个属于槽slot的key
  4. 对源节点发送 migrate <target_ip> <target_port> <key_name> 0 <timeout>命令,将对应的key迁移到目标节点。一条命令只能迁移一个key。

数据结构

  • IMPORTING命令
    • 当目标节点接收IMPORTING命令后,会查看clusterState对象的clusterNode *importing_slots_from[16384]变量对应的slot是否指向NULL,如果否,证明节点正在导入这个slot。如果是,将slot执行source_id对应的clusterNode对象
  • MIGRATING命令
    • 当元节点接收MIGRATING命令后,会查看clusterState对象的clusterNode *migrating_slots_to[16384]变量的对应的slot是否执行NULL,如果否,证明节点正在导出这个slot。如果是,将slot执行target_id对应的clusterNode对象

客户端请求

因为迁移的过程,机器是一直上线的,所以就会存在问题:迁移过程中,如果客户端操作迁移中的key,怎么办。解决方法就是引入ASK错误。

在迁移的过程中,迁移的slot依然由源节点负责,所以对这个slot的key的操作依然是对源节点发送命令的。

  • 客户端发送命令给源节点
  • 源节点查看key是否在数据库中。
  • 如果是,执行命令
  • 如果否
    • 判断key对应的槽i是否在迁移。查看migrating_slots_to[i]是否指向clusterNOde对象。
      • 如果是,有可能在目标节点,返回ASK错误ASK 10000 127.0.0.1:6801分别是槽号,处理该槽的IP和port
      • 如果否,返回key不存在
  • 客户端收到ASK命令后,连接到对应的节点
  • 执行命令REDIS_ASKING打开标识
  • 执行命令
  • 目标节点收到命令后
  • 查看slot是否由自己负责
    • 如果是,执行命令
    • 如果否,查看slot是否正在导入查看importing_slots_from[i]是否指向clusterNOde对象。
      • 如果是,判断客户端是否带ASKING标识。
        • 如果是,执行命令
        • 如果否,返回MOVED命令
      • 如果否,返回MOVED命令

ASK命令

  • ASK命令和MOVED命令一样,也可能被隐藏。
  • 客户端只有打开REDIS_ASKING标识,才能执行命令
  • 打开REDIS_ASKING表示只会对下一条命令生效
  • 下一条该slot的命令,还是会发给源节点

5.复制和故障转移

集群里面有

  • 主节点,负责处理槽
  • 从节点,从主节点复制数据,但是不处理槽
    如果主节点故障,集群会自动把其中一个主节点的从节点提升为新主节点。之前复制旧主节点的从节点会重新复制新主节点

消息

集群的节点通过消息来进行交流。
发送消息的节点成为发送者
接收消息的节点成为接收者
消息有5种:

  • MEET消息。执行cluster meet命令后,发送的消息
  • PING消息。 集群内每个节点每隔一秒钟,就会从集群里面随机选出最多5个节点,然后选出最长时间没有发送PING消息的节点,来发送PING消息。(也就是每一秒只会给一个节点发送PING消息)
    • 如果节点A最后一次接受节点B的PONG消息的时间距离现在超过了cluster-node-timeout配置的一半。节点A也会想节点B发送PING消息。
  • PONG消息。当接受者收到MEET消息或者PING消息,为了向发送者确认已收到这条消息,接受者会向发送者发送PONG消息。
    • 另外,节点可以通过向集群广播PONG消息来让别的节点刷新对该节点的认识
  • FAIL 消息 当一个主节点A判断另一个节点B已经进入FAIL状态时,就会广播FAIL消息。接收到这个消息的节点,会立刻把节点B标志为下线
  • PUBLISH消息。当一个节点收到PUBLISH命令时,会执行这个命令,并向集群广播PUBLISH消息
    MEET PING PONG3中消息称为Gossip协议消息。
    一条消息由消息头和正文组成。

消息头
消息头是一个结构,里面包含正文和其他属性

  • uint32_t totlen。消息的长度,包含消息头和正文
  • uint64_t type。消息类型
  • uint6_t count 消息正文包含的节点信息数量。只有在MEET PING PONG三种消息使用
  • uint64_t currentEpoch 发送者所处的配置纪元
  • uint64_t configEpoch 如果发送者是主节点,记录主节点的配置纪元。如果是从节点,记录正在复制的主节点的配置纪元
  • char sender[REDIS_CLUSTER_NAMELEN]。发送者名称,也就是node id
  • unsigned char myslots[REDIS_CLUSTER_SLOTS/8] 。发送者目前的槽指派信息
  • char slaveof[REDIS_CLUSTER_NAMELEN] 如果是从节点,记录主节点的名称。
  • uint16_t port 发送者端口
  • uint16_t flags 发送者标识值
  • char state 集群状态
  • union clusterMsgData data 正文。是个联合对象。消息不同,这里的数据结构不一样。

消息正文

  1. MEET PING PONG消息的实现
    2. 正文是两个clusterMsgDataGossip结构的实例
    3. 因为MEET PING PONG3种消息的正文结构一样,所以通过消息头的type来判断是哪种消息
    4. 发送者会从自己已知节点里面随机找两个节点(可以是主或者从)。然后把两个节点的信息保存到两个clusterMsgDataGossip结构里面,有数据
    5. char nodeName[REDIS_CLUSTER_NAMELEN] 节点名称
    6. uint32_t ping_sent 最后一次向该节点发送PING的时间戳
    7. uint32_t pong_received 最后一次从该节点接受PONG消息的时间戳
    8. char ip[16] IP
    9. uint16_t port 该节点端口
    8. uint16_t flags 节点的标识值
    9. 接受者收到这三种消息后,会查看里面的两个Gossip结构,也就是两个其他节点的信息
    10. 如果接受者第一次接触节点,就会向这个节点握手
    11. 如果接受者已接触这个节点,就会更新节点信息
  2. FAIL消息的实现
    13. 消息使用clusterMsgDataFail结构,只有一个变量char nodename[REDIS_CLUSTER_NAMELEN]
    14. 当接受者收到这个消息,就会标识这个节点为下线状态
  3. PUBLISH消息的实现
    16. PUBLISH命令有两个参数,channel和msg,例如publish "channel1" "msg1"
    17. 消息使用clusterMsgDataPublish
    17. uint32_t channel_len channel的长度
    18. uint32_t message_len 消息的长度
    19. unsigned char bulk_data[8] 消息内容,不一定是8字节
    20. 例如上面的例子:bulk_data存储的是channel1msg1。channel_len =8 message_len =4

设置从节点

cluster replicate <node_id>

通过这个命令,可以让接收命令的节点成为node_id的从节点。
接收命令的节点会:

  • 修改clusterState.myself.slaveof的属性,执行node_id对应的clusterNode对象
  • 修改clusterState.myself.flags的属性,关闭REDIS_NODE_MASTER标志,打开REDIS_NODE_SLAVE标志
  • 调用复制代码,从主节点复制数据。复制的逻辑和单机复制是一样的,所以相当于执行命令slaveof <master_ip> <master_port>
  • 把消息发送给集群所有节点,让所有节点都知道该节点成为node_id的主节点
  • 其他节点收到消息后
    • 修改主节点对应的clusterNode结构的slaves,这是一个custerNode列表,把从节点加入到列表后面
    • 修改主节点对应的clusterNode结构的numslaves,int类型,加一

故障检测

集群内每个节点都会定期向其他节点发送PING消息,目标节点收到ping消息后,返回PONG消息。如果目标节点超时没有返回,发送节点会在该节点的clusterNode结构里面修改flags属性,打开REDIS_NODE_PFAIL标识,标识位疑似下线状态。

例如节点A标记节点B为疑似下线。然后通过PING PONG命令,节点A会把这个信息同步给集群其他节点。
当节点C收到节点A认为节点B疑似下线。节点C会在节点B的clusterNode结构的fail_reports链表里面添加一个clusterNodeFailReport结构,有变量:

  • clusterNode *node 执行报告节点B疑似下线的节点。这里是节点A
  • time 收到下线报告的时间。

当集群里面半数以上负责槽的主节点都将某个节点标记为疑似下线,那么这个节点会被标记为下线,标记的节点会向集群广播FAIL消息,通知其他节点。

例如这里的节点C,它收到了A节点的报告,同时如果他自己PING节点B也是失败,而且集群里面只有ABC3个负责槽的主节点,那么节点C就会标记节点B位下线,并广播FAIL消息。

故障转移

当集群中其中一个主节点,例如节点B被标记为下线

  1. 那节点B的从节点,会有一个成为主节点,例如节点D
  2. 节点D会执行slaveof no one命令,成为新的主节点
  3. 节点D撤销所有对节点B的槽指派,并将这些槽都指派给自己
  4. 节点D向集群广播一条PONG消息。让其他节点知道自己成为了主节点并接管了节点B的所有槽指派
  5. 节点D开始接受和处理客户端的命令请求,转移完成

选举新节点

  1. 配置纪元是一个自增变量,初始值是0
  2. 当集群某个节点开始一次故障转移时,配置纪元的值会加一
  3. 在一个配置纪元中,主节点只有一次投票机会。它会把票投给第一个要求它投票的节点
  4. 当从节点知道自己的主节点已下线后,会广播一条CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST消息,要求其他主节点为它投票
  5. 如果主节点有投票权(它正在负责处理槽),并且没有投过票给其他节点,那它会给第一个要求投票的节点返回CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息
  6. 每个参选的从节点都会受到ACK消息,如果自己收到的ACK消息大于可投票的节点的半数,这个节点就会成为新的主节点。
  7. 如果在一个配置纪元里面,没有从节点收到足够多的票数(例如3个主节点,挂了一个,剩下2个,2个从节点各自收到一个投票)。那集群就会进入一个新的配置纪元。再次进行选举。
    8. 有点不太明白。怎么进入新的纪元?谁来决定是否进入新的纪元?
    9. 选举算法和哨兵的类似,也是Raft算法
posted @ 2019-12-23 17:54  Xjng  阅读(738)  评论(0编辑  收藏  举报