redis学习笔记四:多机数据库

复制

同步

  1. 从服务器发送sync请求到主服务器
  2. 主服务器生成RDB,并把之后的写命令都扔进一个缓冲区
  3. 主服务器把RDB发给从服务器,从服务器初始化
  4. 主服务器把缓冲区命令发给从服务器,从服务器执行,使得两边数据一致

命令传播

同步是在从服务器创建的时候从主服务器同步数据,命令传播是在主服务器的数据变动的时候把写命令同步给从服务器的行为

问题

断线导致丢失数据的情况要怎么办?

– 同步:重同步

完整重同步和同步是一样,就是用在初始化同步数据

部分重同步则是用来处理断线后重复制的情况

部分重同步用于处理断线后重复制的情况:当从服务器在断线后重新连接到主服务器时,如果条件允许,主服务器可以将断线后这一段时间执行的血命令发送给从服务器,从服务器只接收并执行这些写命令,就可以将从服务器数据库的状态更新至主服务器当前的状态。

  1. 偏移量:主服务器和从服务器分别维护一个复制偏移量,主服务器发N个消息就给自己的偏移量+N,从服务器收到N个就把自己的偏移量+N,这样一对就知道少了啥了
  2. 复制积压缓冲区:一块大小固定的缓冲区,主服务器会把自己收到的写命令和对应的offset都存在里面,对了一下偏移量,如果对应偏移量的数据还在缓冲区就直接把后面的指令给从服务器,不在缓冲区的话就执行完整重同步
  3. 服务器运行ID:从服务器在初始化复制的时候会把对应的主服务器id存下来,从服务器断线重启之后向主服务器发请求,如果id和主服务器id是同一个,就执行部分重同步,不一样(不大理解为啥会不一样)就完整重同步

– 命令传播:心跳检测

在命令传播阶段,从服务器会每秒给主服务器发一个当前offset

  1. 监测连接状态,一秒以后没收到连接可能就有问题了
  2. 如果连接正常的从服务器少于一个数量或者延迟很大的时候,主服务器可以拒接写入
  3. offset也可以用来监测是不是丢命令了

优缺点

优点很明显,就是主从实现读写分离嘛,减少了主服务器的压力

缺点也很明显,主服务器挂了就没了,没有任何的容错能力,而且也没办法支持扩容之类的操作

Sentinel

唉又是那个毛病,开头能不能先介绍一下sentinel是啥

首先假定(好像不是假定)redis的集群模式是master-slave(好像确实就是),如果master挂了,需要再选一个master节点出来,并且把之前master的slave都转移到新的master头上,这个系统就叫做哨兵系统(sentinel)

当然了同时我们就可以利用sentinel系统监控各个节点的特征,获取节点信息配置监控告警等

受不了了怎么还看到了raft,感觉自己在看hdfs和zk,sentinel集群和zk集群感觉其实真的差不多

TBD:

有空自己开一下sentinel模式试试看

简单来说就是sentinel系统本身就是一个节点,但是这个节点和普通redis节点不一样的是它开启了sentinel模式,同时command表也就不支持set之类的操作,而是pong这样的sentinel模式需要的命令

启动连接

sentinel启动的最后一步是向被监视的主服务器建立网络连接,sentinel会建立两种异步网络连接:

  • 一个是命令连接, 这个连接专门用于向主服务器发送命令, 并接收命令回复。
  • 另一个是订阅连接, 这个连接专门用于订阅主服务器的 __sentinel__:hello 频道。

关于订阅连接究竟是为什么要出现,各种地方的解释不一样,有的文档(包括这本Redis设计与实现)说订阅连接是为了防止丢失信息,因为订阅连接是个长连接,所以即使命令连接的返回数据丢了,也可以从订阅连接里收到。另一个说法是订阅连接可以帮助sentinel发现其他sentinel,从而建立Sentinel集群,姑且我们认为两种都有吧

获取slave节点

Sentinel 以每十秒一次的频率向被监视的主服务器发送 INFO 命令,INFO命令会返回改主服务器下从服务器的信息。获取到从服务器的信息后,sentinel就可以向从服务器建立命令连接和订阅连接

建立Sentinel集群

默认情况下,Sentinel每2s一次,向所有被监视的主服务器和从服务器所订阅的sentinel:hello频道上发送消息,消息中会携带Sentinel自身的信息和主服务器的信息。这个信息先叫他频道消息。

因为每个sentinel都会向每个监视的服务器发频道消息,所以对于监视同一个服务器的多个sentinel来说,sentinel可以通过频道消息感知到其他sentinel的存在。

当Sentinel通过频道信息发现一个新的Sentinel时,它不仅会为新Sentinel在sentinels字典中创建相应的实例结构,还会创建一个连向新Sentinel的命令连接(但是不创建订阅连接)

感想就是如果还想研究细节可以去再学一遍zk

发现下线

主观下线:

默认情况下,Sentinel每秒一次向所有与它建立了命令连接的实例(包括主服务器、从服务器和其他Sentinel)发送PING命令,并根据回复判断实例是否在线。如果在Sentinel配置文件中的down-after-milliseconds毫秒内,连接向Sentinel返回无效回复,那么Sentinel就会认为该实例主观下线(SDown)

sentinel认为这个服务器主观下线之后,就会去检查它是不是真的下线了,也就是客观下线:

为了确认是否真的下线,这个Sentinel会向同时监控这个主服务器的所有其他Sentinel发送查询命令,判断它们是否也任务主服务器下线(包括主观下线和客观下线)。如果达到Sentinel配置中的quorum数量的Sentinel实例都判断主服务器为主观下线,则该主服务器就会被判定为客观下线

leader sentinel选举

看到了吗熟悉的raft

说起来在这里放个raft的论文网址:https://web.stanford.edu/~ouster/cgi-bin/papers/raft-atc14 以后方便找

稍微简单解释一下,就是判定服务器为客观下线的sentinel节点可以发起选举,选举拿到半数以上赞成票并且超过一个设定阈值(quorum 值这个东西看上去像redis原创的?)就能当leader

至于具体的投票限制平票方案等等还是去看raft吧,反正就是选了个leader sentinel出来

故障转移

  1. 选新master。当然是有一些条件的
    1. 过滤掉下线节点,过滤掉最近5s没有回复sentinel info的节点
    2. 选conf配置里设定优先级高的节点
    3. 优先级一样的话选复制偏移量最大的
  2. 将所有从服务器改为复制新的主服务器。
  3. 将已下线的主服务器设置为新的主服务器的从服务器。

优缺点

优点:

  1. master-slave的运行本身基于主从复制模式,所以该有的还是有
  2. master挂掉就可以自动切换

缺点:

  1. 扩容还是不支持,容量还是依赖master节点机器配置
  2. 不知道算不算缺点的缺点,运行sentinel集群需要消耗额外的资源

集群

也来简单介绍一下,集群模式和sentinel模式是两种不一样的模式,集群模式的逻辑就是去中心化,每个节点都和集群内其他节点有连接,集群内部也可以设置主从,比如六个节点的话可以设置三主三从,主挂了从将自动变成主节点,但是和前面的读写分离不大一样,这里从节点主要是拿来做热备的,因为分布式存储本身就解决了吞吐量的问题(个人看书的理解

同时集群和前面两种模式不一样的是,每个主节点保存的东西都是不一样的,也就是终于转变为了分布式存储(可喜可贺)

集群本身的启动,集群的搭建和主从设置都是要在启动的时候通过配置实现的。

连接各个节点的工作可以使用 CLUSTER MEET 命令来完成, 该命令的格式如下:

1
CLUSTER MEET <ip> <port>

我猜哈我猜,在配置里配好集群节点之后,每个节点启动的时候会执行cluster meet,将对应的节点加到自己所属的集群中

启动节点

一个节点就是一个运行在集群模式下的 Redis 服务器, Redis 服务器在启动时会根据 cluster-enabled 配置选项的是否为 yes 来决定是否开启服务器的集群模式。

集群模式下其他都是都是正常运行,同时serverCon会调用clusterCron 函数来执行在集群模式下需要执行的常规操作

集群相关的信息会被保存在 cluster.h/clusterNode 结构, cluster.h/clusterLink 结构, 以及 cluster.h/clusterState 结构里面

clusterNode

每个节点都会使用一个 clusterNode 结构来记录自己的状态, 并为集群中的所有其他节点(包括主节点和从节点)都创建一个相应的clusterNode 结构, 以此来记录其他节点的状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
struct clusterNode {

// 创建节点的时间
mstime_t ctime;

// 节点的名字,由 40 个十六进制字符组成
// 例如 68eef66df23420a5862208ef5b1a7005b806f2ff
char name[REDIS_CLUSTER_NAMELEN];

// 节点标识
// 使用各种不同的标识值记录节点的角色(比如主节点或者从节点),
// 以及节点目前所处的状态(比如在线或者下线)。
int flags;

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

// 节点的 IP 地址
char ip[REDIS_IP_STR_LEN];

// 节点的端口号
int port;

// 保存连接节点所需的有关信息
clusterLink *link;

// ...

};

clusterLink保存的是连接到节点所需要的连接信息,还挺明显的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
typedef struct clusterLink {

// 连接的创建时间
mstime_t ctime;

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

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

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

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

} clusterLink;

clusterState

最后, 每个节点都保存着一个 clusterState 结构, 这个结构记录了在当前节点的视角下, 集群目前所处的状态 —— 比如集群是在线还是下线, 集群包含多少个节点, 集群当前的配置纪元

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typedef struct clusterState {

// 指向当前节点的指针
clusterNode *myself;

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

// 集群当前的状态:是在线还是下线
int state;

// 集群中至少处理着一个槽的节点的数量
int size;

// 集群节点名单(包括 myself 节点)
// 字典的键为节点的名字,字典的值为节点对应的 clusterNode 结构
dict *nodes;

// ...

} clusterState;

数据分片

redis集群引入了哈希槽(hash slot)的概念,Redis 集群有16384 个哈希槽,每个节点负责一部分hash槽,每个key通过对16384取mod来决定自己的数据放在哪个hash槽,这样在节点增加或删除的时候,只要通过改变一部分hash槽的归属就可以实现

请求重定向

作为一个去中心化的东西,自然没有一个路由器专门接收请求根据slot扔给对应的节点,无论Redis 的客户端访问集群中的哪个节点都可以路由到对应的节点上

  1. 客户端算了一下觉得我应该访问的是节点1(比较智能的客户端本地会存一个slot到节点的map)
  2. 对应的slot已经从节点一移走了,但是节点1的clusterNode里存了每个节点和槽对应的关系,所以返回一个MOVED,告诉客户端应该去节点2
  3. 对应的slot正好在从节点1挪到节点2,节点1会返回一个ASK和节点2的地址,客户端接到返回之后去问节点2数据是否在,节点2返回是否

节点间通信

MEET

也就是建立集群的握手

PING PONG

ping:

用于交换节点的元数据。每个节点每秒会向集群中其他节点发送 ping 消息,消息中封装了自身节点状态还有其他部分节点的状态数据,也包括自身所管理的槽信息等等。

这里的部分节点至少包含 3 个其它节点的信息,最多包含 (总节点数 - 2)个其它节点的信息。

pong:

meet和ping协议的响应,同样包含节点状态还有其他部分节点的信息

Gossip协议

看了一眼机制也挺眼熟的(怎么感觉以前学通信学过),通过ping pong每个节点可以交换部分集群节点的数据,如果每个节点都定时挑几个节点去交换一下数据,最后就会获取整个集群的数据。而不是每个节点都要和每个节点建立通信,这样集群量大的时候通信压力也太大了

缺点大概就是信息会有滞后吧

节点多的话虽然集群通信压力可能小,但是信息滞后会导致重定向次数和概率变高,最后压力还是会大

集群扩容和收缩

其他都不重要(才没有懒得去仔细研究呢),主要是slot配置和slot内数据在节点的移动

  1. 扩容之后,需要有老的节点对新节点发cluster meet,让他加入新节点

图是csdn上抄的,https://blog.csdn.net/a745233700/article/details/112691126,看起来画的很清楚

节点故障下线和恢复

和sentinel一样,分为主观下线和客观下线

当故障节点下线后,如果是持有槽的主节点则需要在其从节点中找出一个替换它,从而保证高可用。然后就回到了前面的sentinel模式

备注:如果集群中某个节点的master和slave节点都宕机了,那么集群就会进入fail状态,因为集群的slot映射不完整。如果集群超过半数以上的master挂掉,无论是否有slave,集群都会进入fail状态。

总结

简单来说,主从复制利用读写分离解决的是吞吐量的问题,sentinel模式在其基础上解决了节点宕机导致服务不可用的问题,而集群在此基础上利用分布式存储解决了动态缩扩容的问题