读书笔记-《Redis设计与实现》-第三部分:多机数据库的实现

第十五章:复制

旧版复制:同步 + 命令传播 (2.8版本以前)

同步:

1. 从服务器发送SYNC命令给主服务器

2. 主服务器收到后执行BGSAVE,并用缓冲区记录此后收到的写命令

3. 主服务器发送RDB文件给从服务器,从服务器接收到后载入RDB文件

4. 主服务器发送缓冲区命令给从服务器

缺点:断线重连后需要重新开始同步。生成RDB文件耗CPU、内存、磁盘IO,传输RDB耗带宽和流量,载入RDB会阻塞。

新版复制:完整重同步 / 部分重同步 (完整重同步与旧版相同)

部分重同步:

1. 从服务器发送PSYNC命令给主服务器

2. 主服务器返回+CONTINUE回复,表示可以执行部分重同步

扫描二维码关注公众号,回复: 8930747 查看本文章

3. 主服务器发送断线期间的写命令给从服务器

部分重同步实现原理:复制偏移量、复制积压缓冲区、运行ID

复制偏移量:每当主服务器向从服务器传播N个字节的数据时,将自己的偏移量加N,从服务器接收后也会将自己的偏移量加N。

复制积压缓冲区:由主服务器维护的一个固定长度的先进先出的队列(默认1MB)。重连后根据偏移量检查缓冲区中是否还有数据,有则将数据发给从服务器,否则进行完整重同步。理论上其大小 = 断线重连时间 * 主每秒产生写命令的协议格式大小,如果希望减小完整重同步发生的概率,可以设得更大一些。

运行ID:服务器启动时自动生成。

PSYNC命令的实现:

1. 从服务器是否为第一次复制 ? 发送 PSYNC?-1 : 发送PSYNC<runid><offset>

2. 主服务器返回 +FULLRESYNC<runid><offset> / +CONTINUE / -ERR

复制的实现:

1. 设置主服务器的地址和端口

2. 建立套接字连接。建立成功后,从服务器为这个套接字关联一个专门用于处理复制工作的文件事件处理器,处理接收RDB文件、传播命令等,主服务器则为套接字创建客户端状态,将从服务器看作一个连接到主服务器的客户端。

3. 发送PING命令。检测套接字的读写状态,检测主服务器是否能处理命令。如果不能正确返回PONG,则断开重连。

4. 身份验证。根据主服务器的requirepass和从服务器的masterauth进行验证。当双方都没有开启 / 都开启并且密码相同时 执行下个步骤。

5. 发送端口信息。从服务器发送REPLCONF listening-port <port-number>,主服务器保存。

6. 同步。主从服务器互为彼此的客户端,才可进行发送命令、回复命令。

7. 命令传播。默认每秒一次进行心跳检测,从服务器向主服务器发送REPLCONF ACK <replication_offset>。(2.8版本以前没有心跳监测和复制积压缓冲区,如果命令在传播中丢失,是无法感知的。为了保证复制时主从的数据一致性,最好使用2.8版本以上的Redis。)

该命令的作用:

- 监测主从服务器的连接状态。通过INFO replication 可以查看最后一次是多久发的,超过1秒则说明出现问题。

- 辅助实现min-slaves选项。从数量少于3 或 3个以上从服务器的延迟都大于等于10秒,则主服务器拒绝执行写命令。(联想:是否可以通过这个准则进行攻击?将请求打到三个指定从服务器上,待测试TODO)

- 监测命令丢失。通过偏移量判断是否丢失命令,丢失则在复制积压缓冲区中补发(与部分重同步操作相同),但是是在没有断线的情况下进行的。

struct redisServer {

    char *masterhost; // 主服务器的地址

    int masterport; // 主服务器的端口

    ...    

}

typedef struct redisClient {

    int slave listening_port // 从服务器的监听端口号

    ...

}

第十八章:发布与订阅

(第十六章中会多次提到订阅与频道,建议优先阅读该章节)

发送的信息不保存在服务器,如果接收方不在线,就会丢失这条信息,所以需要哨兵订阅主从服务器。

一个频道会将消息发送给所有订阅自己的客户端和模式。

struct redisServer {

    dict * pubsub_channels; // 保存所有频道的订阅关系 key是频道 value是客户端链表

    list *pubsub_patterns; // 保存所有模式的订阅关系

}

typedef struct pubsubPattern {

    redisClient *client; // 订阅模式的客户端

    robj *pattern; // 被订阅的模式

}

第十六章:Sentinel(哨兵)

哨兵是Redis高可用的保障,由哨兵组成的系统监视主从服务器,并在主服务器离线时进行故障转移。

启动哨兵:

1. 初始化服务器。哨兵本质上是运行在特殊模式下的Redis服务器,仅使用Redis服务器的部分功能,例如不使用数据库和键值命令、持久化命令、事务命令、脚本命令。

2. 使用哨兵专用代码 。例如加载了特殊的命令表,仅包含需要使用的命令。

3. 初始化哨兵状态。初始化 sentinel.c/sentinelState。

4. 初始化哨兵状态的masters属性。根据当前哨兵的配置文件。

5. 创建连向主服务器的网络连接。(因为哨兵与多个实例创建连接,所以使用异步连接。)

- 命令连接。哨兵成为主服务器的客户端,发送命令、接收命令回复。

- 订阅连接。订阅主服务器的 _sentinel_:hello 频道。

struct sentinelState {

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

    dict *masters; // 保存了所有被这个哨兵监视的主服务器
                   // key是名字 value是指向sentinelRedisInstance结构的指针

    int tilt; // 是否处于TILT模式

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

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

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

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

}

typedef struct sentinelRedisInstance {

    int flags; // 标识值,记录实例的类型和状态,可以是主从服务器或哨兵

    char *name; // 名字。主服务器配置文件中设置,从服务器是哨兵设置的ip:port

    char *runid; // 运行ID

    dict *sentinels; // 监视该实例的其他哨兵

    dict *slaves; // 该实例的子服务器

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

    sentinelAddr *addr; // 地址

    mstime_t down_after_period; // 多少毫秒内无响应判断为主观下线

    int quorum; // 多少票判断为客观下线

    int parallel_syncs; // 故障转移时,可同时对新主服务器进行同步的从服务器数量

    mstime_t failover_timeout; // 刷新故障迁移状态的最大时限

    ...

}

typedef struct sentinelAddr {

    char *ip;

    int port;

}

哨兵获取主服务器信息:

10秒1次,向被监视的主服务器发送INFO命令。可获取到主服务器运行ID、从服务器IP、端口号、状态、延迟、偏移量等信息。

而后更新主从服务器的信息。

哨兵获取从服务器信息:

同样的,创建命令连接、订阅连接,10秒1次,发送INFO命令。获取运行ID、角色、IP、端口号、状态、优先级、偏移量。

而后更新从服务器的信息。

哨兵向主从服务器发送信息:

2秒1次,向所有主从服务器的_sentinel_:hello频道发送信息。包括哨兵自己的IP、端口号、运行ID、纪元,主服务器的名字、IP、端口号、运行ID、纪元。

哨兵接收主从服务器的频道信息:

如果是自己发送的,则忽略,否则更新数据。因此,哨兵可通过频道信息感知到其他哨兵的存在。

- 更新sentinels字典

- 创建连向其他哨兵的命令连接

检测主观下线状态:

1秒1次,向所有与自己创建了命令连接的实例发送PING命令。如果在指定时间内无有效回复,则在flags中打开主观下线标识。

这里的指定时间为 down-after-milliseconds 配置,是用来判断除了该哨兵以外所有实例的标准。(不同的哨兵配置可能不同)

检查客观下线状态:

当哨兵判断一个主服务器为主观下线后,会询问其他哨兵是否也认为该主服务器已下线(主观或客观),当哨兵接收到数量大于等于quorum参数的已下线判断后,就将主服务器判断为客观下线。(不同的哨兵配置可能不同)

- 发送 SENTINEL is-master-down-by-addr 命令。包括IP、端口号、当前配置纪元、运行ID。

- 回复命令。回复是否下线、局部leader的运行ID和纪元。

- 接收命令的回复。

哨兵Leader选举:

- 哨兵Leader选举制度是Raft算法的实现;

- 每个发现主服务器进入客观下线的哨兵,都会要求其他哨兵将自己设置为局部Leader;

- 每次选举,无论成功纪元都会加1;

- 在同一个纪元里只能投一次票,谁先申请投给谁;

- 半数以上的哨兵都认为某个哨兵为局部Leader时,该哨兵成为正式Leader;

- 如果指定时间内没有选举出正式Leader,则一段时间后再次选举,直到成功;

(这里可以了解一下分布式一致性算法,包括2PC、3PC、Paxos算法、比特币PoW算法)

https://www.cnblogs.com/binarylei/p/9906044.html

https://blog.csdn.net/yyd19921214/article/details/68953629

故障转移:

1. 选出新的主服务器

- 哨兵Leader将已下线的主服务器的所有从服务器保存到一个list

- 删除不在线的 / 删除最近5秒内没有回复过哨兵Leader INFO命令的 / 删除与已下线服务器断开连接时间过长的(保证数据最新)

- 之后依次根据优先级、偏移量、运行ID筛选。

2. 修改从服务器的复制目标

3. 将旧的服务器变为从服务器

第十七章:集群

分布式数据库方案,通过分片进行数据共享,提供复制、故障转移操作。

节点时运行在集群模式下的Redis服务器。节点只能使用0号数据库。

typedef struct clusterState {

    clusterNode *myself; // 指向自己的指针

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

    int state; // 状态,上下线

    int size; // 节点数量,不包括不处理任何槽的节点

    dict *nodes; // 键为节点名字,值为对应的clusterNode结构

    clusterNode *slots[16384]; // 每个数组项都是一个指向clusterNode结构的指针

    zskiplist *slots_to keys; // 跳表中每个结点的分值是一个槽号,成员是一个数据库键

    clusterNode *importing_slots_from[16384]; // 正在从其他节点导入的槽

    clusterNode *migrating_slots_to[16384]; // 正在迁移至其他节点的槽

    ...

}

struct clusterNode {

    mstime_t ctime; // 创建时间

    char name[REIDS_CLUSTER_NAMELEN]; // 名字,40个十六进制的字符组成

    int flags; // 标识,包括角色、状态

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

    char ip[REDIS_IP_STR_LEN]; // ip地址

    int port; // 端口号

    clusterLink *link; // 连接节点所需信息

    unsigned char slots[16384/8] // 包含16384个二进制的数组,记录这个节点处理那些槽

    int numslots; // 处理槽的数量

    int numslaves; // 正在复制这个主节点的从节点数量

    struct clusterNode **slaves; // 数组,每个数组项指向一个正在复制这个主节点的从节点

    ...

}

typedef struct clusterLink {

    mstime_t ctime; // 创建时间

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

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

    sds rcvbuf; // 输入缓冲区,保存着从其他节点接收到的消息

    struct clusterNode *node; // 与这个连接相关联的节点,没有为NULL

}

节点A将节点B添加到集群里面:A与B进行握手,而后通过Gossip协议传播给集群中的其他节点,让其他节点与B握手。

Redis集群中,整个数据库被分为16384个槽,键值对保存在某个槽上,每个节点可处理0-16384个槽。

每个槽都有节点在处理时,集群处于上线状态,否则为下线状态。

一个节点除了将自己负责的槽记录起来之外,还会通过消息告知其他节点自己负责了哪些槽。

集群上线后,客户端发送命令,当前节点先计算该键属于哪个槽(槽分配算法)。如果该槽由自己处理(比较clusterState.slots数组项和clusterState.myself),则执行命令,否则,返回一个MOVED错误,指引客户端转向正确的节点,再执行。

重新分片:

- 由集群管理软件Redis-trib负责执行;

- 正在分片的槽被访问时触发ASK错误,需要访问两次;

节点分为主从节点,其故障转移与复制功能与哨兵类似,均为Raft算法的实现。

消息:

- 各节点通过发送/接受消息来进行通信。

- 获取到的节点信息后,根据是/否存在于接受者的已知节点列表,对列表进行更新/新增。

- 集群中每个节点默认1秒就会发送PING消息,包括上一次收到PONG消息时间已超过设定时长的,以及从已知节点中随机选中的5个节点,其中最长时间没被发送PING消息的。

typedef struct clusterMsg {

    uint32_t totlen; // 长度,头+正文

    uint16_t type; // 类型

    uint16_t count; // 节点信息数量,只在发送MEET、PING、PONG三种Gossip协议时使用

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

    uint64_t configEpoch; // 如果发送者为主节点,则为发送者纪元;否则为正在复制的主节点的纪元

    char sender[REDIS_CLUSTER_NAMELEN]; // 名字,即id

    unsigned char myslots[REDIS_CLUSTER_SLOTS/8]; // 发槽指派信息

    char slaveof[REDIS_CLUSTER_NAMELEN]; // 名字,逻辑同configEpoch,为主节点时是一个常量

    uint16_t port; // 端口号

    uint16_t flags; // 标识

    unsigned char state; // 集群状态

    union clusterMsgData data; // 正文

}

union clusterMsgData {

    struct {
        clusterMsgDataGossip gossip[1]; // MEET、PING、PONG使用
    }

    struct {
        clusterMsgDataFail about; // FAIL使用,只包含一个nodename
    }

    struct {
        clusterMsgDataPublish msg; // PUBLISH使用
    }

    ...

}

typedef struct clusterMsgDataGossip {
    
    char nodename[REDIS_CLUSTER_NAMELEN]; // 节点名字

    uint32_t ping_sent; // 最后一次向该节点发送PING消息的时间戳

    uint32_t pong_received; // 最后一次从该节点接受到PONG消息的时间戳

    char ip[16]; // ip

    uint16_t port; // 端口号

    uint16_t flags; // 标识

}

typedef struct clusterMsgDataPublish {

    uint32_t channel_len; // 频道长度

    uint32_t message_len; // 消息长度

    unsigned char bulk_data[8]; // 对其其他消息结构,长度不一定为8

}
发布了25 篇原创文章 · 获赞 12 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/qq_25498677/article/details/86607262