• 《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消息的实现
      1. 正文是两个clusterMsgDataGossip结构的实例
      2. 因为MEET PING PONG3种消息的正文结构一样,所以通过消息头的type来判断是哪种消息
      3. 发送者会从自己已知节点里面随机找两个节点(可以是主或者从)。然后把两个节点的信息保存到两个clusterMsgDataGossip结构里面,有数据
        1. char nodeName[REDIS_CLUSTER_NAMELEN] 节点名称
        2. uint32_t ping_sent 最后一次向该节点发送PING的时间戳
        3. uint32_t pong_received 最后一次从该节点接受PONG消息的时间戳
        4. char ip[16] IP
        5. uint16_t port 该节点端口
        6. uint16_t flags 节点的标识值
      4. 接受者收到这三种消息后,会查看里面的两个Gossip结构,也就是两个其他节点的信息
        1. 如果接受者第一次接触节点,就会向这个节点握手
        2. 如果接受者已接触这个节点,就会更新节点信息
    2. FAIL消息的实现
      1. 消息使用clusterMsgDataFail结构,只有一个变量char nodename[REDIS_CLUSTER_NAMELEN]
      2. 当接受者收到这个消息,就会标识这个节点为下线状态
    3. PUBLISH消息的实现
      1. PUBLISH命令有两个参数,channel和msg,例如publish "channel1" "msg1"
      2. 消息使用clusterMsgDataPublish
        1. uint32_t channel_len channel的长度
        2. uint32_t message_len 消息的长度
        3. unsigned char bulk_data[8] 消息内容,不一定是8字节
        4. 例如上面的例子: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个从节点各自收到一个投票)。那集群就会进入一个新的配置纪元。再次进行选举。
      1. 有点不太明白。怎么进入新的纪元?谁来决定是否进入新的纪元?
      2. 选举算法和哨兵的类似,也是Raft算法
  • 相关阅读:
    Vue 框架怎么实现对象和数组的监听?
    能说下 vue-router 中常用的 hash 和 history 路由模式实现原理吗?
    vue-router 路由模式有几种?
    Vue 组件间通信有哪几种方式?
    v-model 的原理?
    华硕笔记本修复
    linux下制作u盘启动盘
    virtualbox不能启动虚拟机
    ubuntu14.04建立wifi热点
    git中文文件名和中文目录显示乱码
  • 原文地址:https://www.cnblogs.com/Xjng/p/12085114.html
Copyright © 2020-2023  润新知