事件

文件事件

文件事件处理器

构成

套接字,IO多路复用程序,文件事件分派器,事件处理器

尽管多个文件事件可能会并发地出现,但IO多路复用程序总是会将所有产生事件的套接字都放到一个队列里面,然后通过这个队列,以有序( sequentially)、同步( synchronously )、每次一个套接字的方式向文件事件分派器传送套接字。当上一个套接字产生的事件被处理完毕之后(该套接字为事件所关联的事件处理器执行完毕),I/O 多路复用程序才会继续向文件事件分派器传送下一个套接字

复制

一台redis服务器,可以通过slaveof命令或者设置slaveof选项,让一台服务器去复制另外一个服务器的数据

1. 同步(psync)

1.1 完整重同步

从服务器向主服务器发出同步命令,主服务器执行bgsave命令,生成RDB文件,然后在bgsave命令之后,在主服务器中执行的命令,全部放在缓冲池中,当bgsave命令执行完成之后,把RDB文件发送给从服务器,从服务器读取RDB文件,让数据和主服务器执行bgsave命令时的数据保持一致,然后再把缓冲池中的命令,发送到从服务器中,这样,数据就保持了一致了。

1.2 部分重同步

部分重发送是为了解决断线后重复制的情况
当从服务器在断线之后重新连接主服务器,如果条件允许,主服务器可以将在断线之后执行的命令,发送到从服务器上面去,让从服务器执行这些命令,从而保持数据的一致性。

1.2.1 复制偏移量

主服务器和从服务器都会分别维护一个复制偏移量。主服务器向从服务器传播N个字节的数据,就将自己的复制偏移量的值加上N;
从服务器每次收到主服务器传播过来的N个字节的数据时。就将自己的复制偏移量加上N。

假设从服务器断线重新连接,从服务器向主服务器发送psync命令,向主服务器报告当前自身的复制偏移量,然后通过复制积压缓冲区中的命令,来判断是需要执行完整重同步,还是执行部分重同步

1.2.2 复制积压缓冲区

复制积压缓冲区是一个固定长度的 先进先出的队列。
当主服务器进行命令传播时,不仅会把命令发送给从服务器,也会把命令写到复制积压缓冲区中。

  1. 如果offset偏移量之后的数据仍然存在在复制积压缓冲区中,那么执行部门重同步,发送之后的命令给从服务器
  2. 如果offset偏移量之后的数据不存在复制积压缓冲区中,那么执行完整重同步

复制积压缓冲区默认大小为1MB

1.2.3 服务器运行ID

每个redis服务器,都有一个自己的运行ID。运行ID在服务器启动的时候自动生成。主服务器会保存从服务器的运行ID。用运行ID辅助判断,是执行完全重同步还是部分重同步

~~~~~~~~~~~

psync命令的实现

psync命令的调用有两种:

  1. 如果从服务器以前没有复制过任何主服务器,或者之前执行过SLAVEOF no one命令,那么从服务器在开始一次新的复制时将向主服务器发送PsYNc ? -1命令,主动请求主服务器进行完整重同步(因为这时不可能执行部分重同步)。

  2. 相反地,如果从服务器已经复制过某个主服务器,那么从服务器在开始一次新的复制时将向主服务器发送PsYNc 命令:其中 runid是上一次复制的主服务器的运行ID,而offset则是从服务器当前的复制偏移量,接收到这个命令的主服务器会通过这两个参数来判断应该对从服务器执行哪种同步操作。

根据情况,接收到PSYNC命令的主服务器会向从服务器返回以下三种回复的其中一种:

  1. 如果主服务器返回+FULLRESYNC 回复,那么表示主服务器将与从服务器执行完整重同步操作:其中runid是这个主服务器的运行ID,从服务器会将这个ID保存起来,在下一次发送PSYNC命令时使用;而offset则是主服务器当前的复制偏移量,从服务器会将这个值作为自己的初始化偏移量。
  2. 如果主服务器返回+CONTINUE回复,那么表示主服务器将与从服务器执行部分重同步操作,从服务器只要等着主服务器将自己缺少的那部分数据发送过来就可以了。
  3. 如果主服务器返回-ERR回复,那么表示主服务器的版本低于Redis 2.8,它识别不了PsYNC命令,从服务器将向主服务器发送SYNC命令,并与主服务器执行完整同步操作。

由此可见 psync 也有不足之处,当 slave 重启以后 master runid 发生变化,也就意味者 slave 还是会进行全量复制,而在实际的生产中进行 slave 的维护很多时候会进行重启,而正是有由于全量同步需要 master 执行快照,以及数据传输会带不小的影响。因此在 4.0 版本,psync 命令做了改进,我们称之为 psync2

psync2的优化点

Redis 4.0 引入另外一个变量 master_replid 2 来存放同步过的 master 的复制 ID,同时复制 ID 在 slave 上的意义不同于之前的运行 ID,复制 ID 在 master 的意义和之前运行 ID 仍然是一样的,但对于 slave 来说,它保存的复制 ID(即 replid) 表示当前正在同步的 master 的复制 ID 。master_replid 2 则表示前一个 master 的复制 ID(如果它之前没复制过其他的 master,那这个字段没用),这个在主从角色发生改变的时候会用到。

  1. struct redisServer {
  2. ...
  3. /* Replication (`master`) */
  4. char replid[CONFIG_RUN_ID_SIZE+1]; /* My current replication ID. */
  5. char replid2[CONFIG_RUN_ID_SIZE+1]; /* replid inherited from `master`*/
  6. }

slave 在意外关闭前会调用 rdbSaveInfoAuxFields 函数把当前的复制 ID(即关闭前正在复制的 master 的 replid,因为 slave 中的 replid 字段保存的是 master 的复制 ID) 和复制偏移量一起保存到 RDB 文件中,后面该 slave 重启的时候,就可以从 RDB 文件中读取复制 ID 和复制偏移量,然后重连上 masterslave 将这两个值发送给 master,master 会如下判断是否允许 psync:

  1. // 如果 `slave` 发送过来的复制 ID 是当前 `master` 的复制 ID, 说明 `master` 没变过
  2. if (strcasecmp(master_replid, server.replid) &&
  3. // 或者和现在的新 `master` 曾经属于同一 `master`
  4. (strcasecmp(master_replid, server.replid2) ||
  5. // 但同步进度不能比当前 `master` 还快
  6. psync_offset > server.second_replid_offset)) {
  7. ... ...
  8. }
  9. // 判断同步进度是否已经超过范围
  10. if (!server.repl_backlog ||
  11. psync_offset < server.repl_backlog_off ||
  12. psync_offset > (server.repl_backlog_off + server.repl_backlog_histlen)) {
  13. ... ...
  14. }

另外当节点从 slave 提升为 master 后,会保存两个复制 ID(之前角色是 slave 的时候 replid2 没用,现在要派上用场了),分别是 replid 和 replid 2,其他 slave 复制的时候可以根据第二个复制 ID 来进行部分重同步。对应上述代码中第二行判断的情况。

2. 命令传播

主服务器向从服务器发送执行的命令

心跳机制

概念:进入命令传播阶段后,master与slave与要进行信息交换,使用心跳机制来进行维护,实现双方保持在线。

  • master心跳:
    • 指令:PING
    • 周期:通过repl-ping-slave-period配置,默认时10秒。
    • 作用:判断slave是否在线
    • 查询:可以通过info replication查看,获取slave最后一次的连接间隔,lag是0或者1是正常的,超过1就可能是超时了。
  • slave心跳:

    • 指令:replconf ack [offset]
    • 周期:1秒
    • 作用1:告诉master自己的复制偏移量,获取最新的数据变更指令
    • 作用2:判断master是否在线

      心跳阶段注意事项

  • 当slave多数掉线或者延迟较高时,master为了保障数据的稳定性,将拒绝所有信息同步操作。

    • slave数量小于2个时,master停止写操作,停止数据同步:min-slaves-to-write 2
    • slave延迟都大于8秒时,master停止写操作,停止数据同步:min-slaves-max-lag 8
  • slave数量由slave发送replconf ack命令做确认
  • slave延迟由slave发送replconf ack命令做确认

    sentinel

    sentinel就是一个特殊状态的redis服务器
    启动一个sentinel时,需要执行以下步骤:
  1. 初始化服务器
  2. 将普通的redis服务器使用的代码替换成sentinel专用代码
  3. 初始化sentinel状态
  4. 根据给定的配置文件,初始化sentinel的监视主服务器列表
  5. 创建向主服务器的网络连接
  6. 创建向从服务器的网络连接
  7. 向主服务器和从服务器发送信息
  8. 接收来自主服务器和从服务器的频道信息
  9. 检测主观下线状态(每秒一次的频率向所有与它创建的命令连接的实例发送PING消息,并且通过返回的ping消息回复来判断实例是否在线)
  10. 检查客观下线状态(当Sentinel将一个主服务器判断为主观下线之后,为了确认这个主服务器是否真的下线了,它会向同样监视这一主服务器的其他Sentinel进行询问,看它们是否也认为主服务器已经进入了下线状态(可以是主观下线或者客观下线)。当Sentinel 从其他Sentinel那里接收到足够数量的已下线判断之后,Sentinel就会将从服务器判定为客观下线,并对主服务器执行故障转移操作。)

sentinel是如何发现其他sentinel的呢?
上面描述的第8点:
SUBSCRIBEsentinel: hello
Sentinel对_sentinel
:hello频道的订阅会一直持续到Sentinel 与服务器的连接断开为止。
这也就是说,对于每个与Sentinel连接的服务器,Sentinel既通过命令连接向服务器的
sentinel:hello频道发送信息,又通过订阅连接从服务器的sentinel_:hello频道接收信息。
对于监视同一个服务器的多个Sentinel来说,一个Sentinel 发送的信息会被其他Sentinel接收到,这些信息会被用于更新其他Sentinel对发送信息 Sentinel 的认知,也会被用于更新其他Sentinel对被监视服务器的认知。
举个例子,假设现在有sentinel1、sentinel2、sentinel3三个Sentinel在监视同一个服务器,那么当sentinell向服务器的_sentinel
:hello频道发送一条信息时,所有订阅了sentinel_:hello频道的Sentinel(包括sentinel1自己在内)都会收到这条信息。

当Sentinel通过频道信息发现一个新的Sentinel时,它不仅会为新Sentinel在 sentinels字典中创建相应的实例结构,还会创建一个连向新Sentinel的命令连接,而新Sentinel也同样会创建连向这个Sentinel 的命令连接,最终监视同一主服务器的多个Sentinel将形成相互连接的网络:Sentinel A有连向Sentinel B的命令连接,而Sentinel B也有连向Sentinel A 的命令连接。

如果主服务器master下线了怎么办?

  1. 选举leader sentinel
  • 所有的sentinel服务器都有机会成为leader sentinel
  • 在每次进行leader sentinel选举之后,配置纪元(计数器)都要自增一次(不管是否成功)
  • 每次发现主服务器进入客观下线的sentinel都会要求其他sentinel将自己设置为局部 leader sentinel
  • 当一个Sentinel(源Sentinel)向另一个Sentinel(目标Sentinel)发送SENTINELis-master-down-by-addr命令,并且命令中的runid参数不是*符号而是源Sentinel的运行ID时,这表示源Sentinel要求目标Sentinel将前者设置为后者的局部领头 Sentinel。
  • Sentinel设置局部领头Sentinel的规则是先到先得:最先向目标Sentinel发送设置要求的源Sentinel将成为目标Sentinel的局部领头Sentinel,而之后接收到的所有设置要求都会被目标 Sentinel拒绝。
  • 如果有某个Sentinel被半数以上的Sentinel 设置成了局部领头Sentinel,那么这个Sentinel成为领头 Sentinel。举个例子,在一个由10个Sentinel组成的Sentinel 系统里面,只要有大于等于10/2+1=6个Sentinel将某个Sentinel设置为局部领头Sentinel,那么被设置的那个Sentinel就会成为领头Sentinel。
  1. 故障转移
  • 在已经下线的主服务器属下的所有从服务器中,挑选一个从服务器,并且将它设置为主服务器
  • 让已经下线的主服务器属下的所有从服务器改为新的主服务器的从服务器
  • 将已经下线的主服务器设置为新的主服务器的从服务器,当这个旧的主服务器重新上线时,它就会成为新的主服务器的从服务器

主服务器选举
不从已经下线和断线状态的从服务器中选举;
不从最后五秒没有回复leader sentinel info消息的从服务器选举;
不从和已经下线的主服务器连接断开超过down-after-milliseconds * 10 毫秒的从服务器选举;
根据从服务器的优先级进行排序,并且选出来其中优先级最高的,就是master
(复制 偏移量最大的从服务器保存着最新的数据)
(运行ID再次排序,选中其中运行ID最小的从服务器为master)
**

集群

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

cluster meet命令

这个命令可以让一个节点加入到一个集群中去
在节点A中向节点B所在的ip 端口 发送cluster meet命令,可以让节点B加入到节点A所在的集群中。过程:

  1. 节点A 为节点B创建一个clusterNode节点中,并且加入到clusterState.nodes字典中
  2. 节点A向节点B发送一条meet消息
  3. 节点B接受到节点A的meet消息,节点B为节点A创建一个clusterNode节点中,并且加入到clusterState,nodes字典中
  4. 节点B向节点A返回一条PONG消息
  5. 节点A接到节点B的PONG消息后,节点A向节点B发送一条PING消息
  6. 节点B接受到节点A的PING消息后,节点A,B握手完成
  7. 节点A会将节点B的信息,通过Gossip协议,广播给集群中的其他节点,其他节点和节点B进行握手

槽指派

redis集群通过分片的方式来保存数据库中的键值对:集群中的整个数据库被分为16384个槽。
一个节点除了会将自己负责处理的槽记录在clusterNode结构的slots属性和numslots属性之外,它还会将自己的slots数组通过消息发送给集群中的其他节点,以此来告知其他节点自己目前负责处理哪些槽。
clusterstate 结构中的slots 数组记录了集群中所有16384个槽的指派信息:

在对数据库中的16384个槽都进行了指派之后,集群就会进入上线状态,这时客户端就可以向集群中的节点发送数据命令了。
当客户端向节点发送与数据库键有关的命令时,接收命令的节点会计算出命令要处理的数据库键属于哪个槽,并检查这个槽是否指派给了自己:

  1. 如果键所在的槽正好就指派给了当前节点,那么节点直接执行这个命令。
  2. 如果键所在的槽并没有指派给当前节点,那么节点会向客户端返回一个MOVED 错误,指引客户端转向( redirect)至正确的节点,并再次发送之前想要执行的命令。

image.png

计算key属于哪个槽

HASH_SLOT=CRC16(key) mod 16384
对每个key值计算CRC16值,然后对16384取模,这样来获取key对应的hash slot。

重新分片

如何完成重新分片?
是由redis集群管理工具 redis-trib来完成的

扩容场景

当资源不够,需要增加节点时,需要对slots进行节点的重复分配。而此时,又不能停止对外服务,解决方案如何?
假设原来有三个节点:A(0至5000),B(5001至10000),C(10001到16383)。现在需要增加第四个节点D节点进来,重新分配如下:

节点 范围
A 0~5000
B 5001~10000
C 10001~13000
D 13000~16383

需要将C节点的slots,从13000~16383的slot迁移到D节点中。redis通过提供一组命令原语完成迁移操作。

迁移逻辑

迁移工作,可以使用redis-trib管理软件进行迁移,具体原理如下:
1、对目标节点,即是D节点发送cluster setslot import 命令,让目标节点做好准备接收迁移准备。
2、对源节点,即是C节点,发送cluster setslot migrating命令,让源节点做好准备迁移准备。
3、对源节点,发送cluster getkeysinslot 命令,获取对应slot的最多count个属于slot的key名。
4、对于步骤3中获取的key,向源节点发送命令migrate 0 命令,将被选中的键原子地从源节点迁移到目标节点。
5、重复上述3,4步骤,直到所有key都迁移成功
image.png
image.png

ASK错误

image.png
举个例子,假设节点7002正在向节点7003迁移槽16198,这个槽包含”is”和”love”两个键,其中键”is”还留在节点7002,而键”love”已经被迁移到了节点7003。
如果我们向节点7002发送关于键”is”的命令,那么这个命令会直接被节点7002执行:
127.0.0.1 : 7002> GET "is"
而如果我们向节点7002发送关于键”love”的命令,那么客户端会先被转向至节点7003,然后再次执行命令:
127.0.o . 1 : 7002>GET ""love"
-> Redirected to slot [16198] located at 127.0.0.1:7003"you get the key 'love '"
127.0.0.1 : 7003>

ASK错误和MOVED错误的区别

ASK错误和MOVED错误都会导致客户端转向,它们的区别在于:

  1. 关于槽i的MOVED错误之后,客户端每次遇到关于槽i的命令请求时,都可以直接将命令请求发送至MOVED错误所指向的节点,因为该节点就是目前负责槽i的节点。
  2. 与此相反,ASK错误只是两个节点在迁移槽的过程中使用的一种临时措施:在客户端收到关于槽i的ASK错误之后,客户端只会在接下来的一次命令请求中将关于槽i的命令请求发送至ASK错误所指示的节点,但这种转向不会对客户端今后发送关于槽i的命令请求产生任何影响,客户端仍然会将关于槽i的命令请求发送至目前负责处理槽i的节点,除非ASK错误再次出现。

**

复制

Redis集群中的节点分为主节点( master)和从节点slave ),其中主节点用于处理槽,而从节点则用于复制某个主节点,并在被复制的主节点下线时,代替下线主节点继续处理命令请求。

故障检测

集群中的每个节点都会定期地向集群中的其他节点发送PING消息,以此来检测对方是否在线,如果接收PING消息的节点没有在规定的时间内,向发送PING消息的节点返回PONG消息,那么发送PING消息的节点就会将接收PING消息的节点标记为疑似下线( probable fail,PFAIL)。
举个例子,如果节点7001向节点7000发送了一条PING消息,但是节点7000没有在规定的时间内,向节点7001返回一条PONG消息,那么节点7001就会在自己的clusterstate.nodes字典中找到节点7000所对应的clusterNode结构,并在结构的flags属性中打开REDIS_NODE_PFAIL标识,以此表示节点7000进人了疑似下线状态,
集群中的各个节点会通过互相发送消息的方式来交换集群中各个节点的状态信息,例如某个节点是处于在线状态、疑似下线状态(PFAIL),还是已下线状态( FAIL )。
如果在一个集群里面,半数以上负责处理槽的主节点都将某个主节点x报告为疑似下线,那么这个主节点x将被标记为已下线(FAIL),将主节点x标记为已下线的节点会向集群广播一条关于主节点x的FAIL消息,所有收到这条FAIL消息的节点都会立即将主节点x标记为已下线。

故障转移

当一个从节点发现自己正在复制的主节点进入了已下线状态时,从节点将开始对下线主节点进行故障转移,以下是故障转移的执行步骤:
1)复制下线主节点的所有从节点里面,会有一个从节点被选中。
2)被选中的从节点会执行SLAVEOF no one命令,成为新的主节点。
3 )新的主节点会撤销所有对已下线主节点的槽指派,并将这些槽全部指派给自己。
4)新的主节点向集群广播一条PONG消息,这条PONG消息可以让集群中的其他节点立即知道这个节点已经由从节点变成了主节点,并且这个主节点已经接管了原本由已下线节点负责处理的槽。
5)新的主节点开始接收和自己负责处理的槽有关的命令请求,故障转移完成。

选举新的主节点

1)集群的配置纪元是一个自增计数器,它的初始值为0。
2)当集群里的某个节点开始一次故障转移操作时,集群配置纪元的值会被增一。
3)对于每个配置纪元,集群里每个负责处理槽的主节点都有一次投票的机会,而第一个向主节点要求投票的从节点将获得主节点的投票。
4)当从节点发现自己正在复制的主节点进人已下线状态时,从节点会向集群广播一条CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST消息,要求所有收到这条消息、并且具有投票权的主节点向这个从节点投票。
5)如果一个主节点具有投票权(它正在负责处理槽),并且这个主节点尚未投票给其他从节点,那么主节点将向要求投票的从节点返回一条CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,表示这个主节点支持从节点成为新的主节点。
6)每个参与选举的从节点都会接收CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,并根据自己收到了多少条这种消息来统计自己获得了多少主节点的支持。
7)如果集群里有N个具有投票权的主节点,那么当一个从节点收集到大于等于N/2+1张支持票时,这个从节点就会当选为新的主节点。
8)因为在每一个配置纪元里面,每个具有投票权的主节点只能投一次票,所以如果有N个主节点进行投票,那么具有大于等于N/2+1张支持票的从节点只会有一个,这确保了新的主节点只会有一个。
9)如果在一个配置纪元里面没有从节点能收集到足够多的支持票,那么集群进入一个新的配置纪元,并再次进行选举,直到选出新的主节点为止。
这个选举新主节点的方法和第16章介绍的选举领头Sentinel的方法非常相似,因为两者都是基于Raft算法的领头选举( leader election)方法来实现的。

Gossip协议

redis集群中的各个节点通过Gossip协议来交互各自寰宇不同节点的状态信息,其中Gossip协议由meet,ping,pong三种消息构成

  1. MEET消息:当发送者接到客户端发送的CLUSTER MEET命令时,发送者会向接收者发送MEET消息,请求接收者加入到发送者当前所处的集群里面。
  2. PING 消息:集群里的每个节点默认每隔一秒钟就会从已知节点列表中随机选出五个节点,然后对这五个节点中最长时间没有发送过PING消息的节点发送PING消息,以此来检测被选中的节点是否在线。除此之外,如果节点A最后一次收到节点B发送的PONG消息的时间,距离当前时间已经超过了节点A的cluster-node-timeout选项设置时长的一半,那么节点A也会向节点B发送PING消息,这可以防止节点A因为长时间没有随机选中节点B作为PING消息的发送对象而导致对节点B的信息更新滞后。
  3. PONG消息:当接收者收到发送者发来的MEET消息或者PING消息时,为了向发送者确认这条MEET消息或者PING消息已到达,接收者会向发送者返回一条PONG消息。另外,一个节点也可以通过向集群广播自己的 PONG消息来让集群中的其他节点立即刷新关于这个节点的认识,例如当一次故障转移操作成功执行之后,新的主节点会向集群广播一条PONG消息,以此来让集群中的其他节点立即知道这个节点已经变成了主节点,并且接管了已下线节点负责的槽。
  4. FAIL消息:当一个主节点A判断另一个主节点B已经进人FAIL状态时,节点A会向集群广播一条关于节点B的FAIL消息,所有收到这条消息的节点都会立即将节点B标记为已下线。
  5. PUBLISH消息:当节点接收到一个PUBLISH命令时,节点会执行这个命令,并向集群广播一条PUBLISH消息,所有接收到这条PUBLISH消息的节点都会执行相同的PUBLISH命令。