Redis设计 - 事件

前言

Redis服务器是一个事件驱动程序,服务器需要处理以下两类事件:

  • 文件事件(file event): Redis服务器通过套接字与客户端(或者其他Redis服务器)进行连接,而文件事件就是服务器对套接字操作的抽象。服务器与客户端的通信会产生相应的文件事件,而服务器则通过监听,并处理这些事件来完成一系列的网络通信操作。
  • 时间事件(time event):Redis服务器的一些操作(例如serverCron函数)需要在给定的时间点执行,而这些时间事件就是服务器对这类定时操作的抽象。

文件事件

Redis基于Reactor模式开发了自己的网络事件处理器,这些统称为文件事件处理器(file event handler):

  • 文件事件处理器使用 I/O 多路复用(multiplexing)程序来同时监听多个套接字,并根据套接字目前执行的任务来为套接字关联不同的事件处理器。
  • 当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关闭等操作时,与之相对应的文件事件就会产生,这时文件事件处理器就会调用之前关联好的事件处理器来处理这些事情。

虽然文件事件处理器以单线程的方式运行,但是通过使用I/O多路复用程序来监听多个套接字,既实现了高性能的网络通信模型,又能够很好的与Redis的其他模块对接,保持了Redis内部单线程设计的简单性。

文件事件处理器的构成

文件事件处理器由4个部分组成,它们分别是:套接字、I/O多路复用程序、文件事件分派器(dispatcher)以及事件处理器。

文件事件处理器构成

文件事件是对套接字操作的抽象,每当一个套接字准备好执行连接应答、写入、读取关闭等操作时,就会产生一个文件事件。

服务器通常会连接多个套接字,所以文件事件可能会并发的出现。但是I/O多路复用程序会将所有产生事件的套接字都放到一个队列中,以有序、同步、每次一个套接字的方式向文件事件分派器发送套接字。

套接字队列

I/O多路复用的实现

Redis的I/O多路复用程序的所有功能都是通过包装常见的select、epoll、 evport和kqueueq这些I/O多路复用函数库来实现的,每个函数库在Redis中都有一个单独的文件,比如aeselect.c、aeepoll.c、 ae_kqueue.c 等等。

因为Redis为每个I/O多路复用函数库都实现了相同的API,所以底层是可以互换的。Redis定义了选择函数库的规则,在编译时,自动选择系统中性能最高的I/O多路复用函数库来作为Redis的底层实现。

I/O多路复用底层

事件的类型

I/O多路复用程序可以监听多个套接字的READABLE事件和WRITEABLE事件,这两类事件和套接字操作之间对应关系如下:

  • 当套接字可读时(客户端执行write操作,或者close操作),或者有新的应答出现时(客户端执行connect操作),套接字会产生READABLE事件。
  • 当套接字变得可写时(客户端执行read操作),产生WRITEABLE事件。

I/O多路复用程序允许服务器同时监听套接字的 READABLE 事件和 WRITEABLE 事件。如果产生,那么文件事件分派器会优先处理读事件,等到readable事件处理完后,才会处理writeable事件。

API

  • createFileEvent 函数接受一个套接字描述符、一个事件类型、以及一个事件处理器作为参数,作用是将套接字给定的事件,加入到I/O多路复用程序的监听范围之内,并对事件和事件处理器进行关联。

  • deleteFileEvent函数,让I/O多路复用程序取消套接字给定事件的监听,并取消事件和事件之间的关联。

  • getFileEvents 函数接受一个套接字描述符,返回该套接字正在被监听的事件类型。

    • 如果没有任何事件被监听,返回NONE
    • 如果读被监听,返回READABLE
    • 如果写被监听,返回WRITEABLE
    • 如果读写事件都被监听,返回 READABLE | WRITEABLE
  • aeWait函数,在给定的时间内阻塞并等待套接字给定类型的事件发生,当事件成功产生,或者等待超时之后,函数返回。

  • aeApiPoll函数接受一个 timeval结构为参数,并在指定的时间内,阻塞并等待所有被createFileEvent函数设置为监听状态的套接字所产生的文件事件,当至少一个事件产生,或者超时后,函数返回。

  • aeProcessEvents函数是文件时间分派器,它先调用apiPoll函数来等待事件产生,然后遍历所有已产生的事件,并调用相应的 事件处理器 来处理。

文件事件处理器

Redis为文件事件编写了多个处理器,这些事件处理器分别用于实现不同需求的网络通信。

  • 为了对连接服务器的各个客户端进行应答,服务器要有与之关联的连接应答处理器
  • 为了接收客户端传过来的命令请求,服务器要为客户端套接字关联命令请求处理器。
  • 为了向客户端返回命令执行结果,服务器要为客户端套接字关联命令回复处理器
  • 当主从服务器进行复制时,需要关联特别为复制功能编写的复制处理器
1. 连接应答处理器

networking.c/acceptTcpHandler函数是Redis的连接应答处理器,用于对连接服务器监听套接字的客户端进行应答。
当Redis服务器进行初始化的时候,程序会将此出处理器和READABLE事件关联起来,当有客户端执行connect函数连接服务器时,套接字就会产生readable事件,引发此处理器执行。

服务器对客户端的请求进行连接应答
2. 命令请求处理器

networking.c/readQueryFromClient函数是Redis的命令请求处理器,它负责读取客户端发送的命令请求内容,具体实现为 unistd.h/read 函数的包装。

当一个客户端通过成功连接到服务器之后,服务器会将客户端套接字的READABLE事件和命令请求处理器关联起来,之后一旦有请求发送,命令处理器就会执行操作。

服务器接收客户端发来的请求
3. 命令回复处理器

networking.c/sendReplyToClient函数是Redis的命令回复处理器,它负责将命令结果通过套接字返给客户端,具体实现为 unistd.h/write函数的包装。

当服务器有命令回复需要传送给客户端时,服务器会将客户端套接字的WRITEABLE事件和命令回复处理器关联起来,一旦客户端准备好接收,就会产生WRITEABLE事件,引发命令回复处理器执行。

服务器向客户端发送命令回复
一次完整的客户端与服务器连接事件

假设Redis服务器正在运作,那么这个服务器监听套接字的READABLE事件,并且关联了连接应答处理器。

  1. 这时有一个Redis客户端像服务器发起连接,那么监听套接字将产生READABLE事件,触发连接应答处理器执行。处理器会对客户端的连接请求进行应答,然后创建客户端套接字,客户端状态,并将客户端套接字的READABLE事件与命令请求处理器进行关联。
  2. 假设之后客户端向服务器发送一个命令请求,那么客户端套接字将产生READABLE事件,引发命令请求处理器执行,处理器读取客户端的命令内容,然后传给相关程序去执行。
  3. 执行命令后产生相应的命令回复,需要传输给客户端。服务器将客户端套接字的WRITEABLE事件与命令回复处理器关联。当客户端尝试读取命令回复时,客户端套接字就会产生WRITEABLE事件,当命令回复处理器将回复内容都写入到套接字之后,服务器就会接触客户端套接字WRITEABLE事件与命令回复处理器的关联关系。
客户端和服务器的通信过程

时间事件

Redis的时间事件分为两类:

  • 定时事件:让程序在指定的时间后执行一次。
  • 周期性时间:让程序每隔指定时间就执行一次。

一个时间事件主要由以下属性组成:

  • id:服务器为时间事件创建一个全局唯一的ID。ID号按照从小到大的顺序递增,新事件的ID号比旧事件的ID号要大。
  • when:毫秒精度的UNIX时间戳,记录了时间事件的到达(arrive)时间。
  • timeProc : 时间事件处理器,一个函数。当时间事件到达时,服务器就会调用相应的处理器来处理事件。

一个时间事件是定时事件还是周期性事件,取决于处理器的返回值,如果返回的是NO_MORE,那么就是定时事件,处理完毕后就被删除了。如果返回的是 非NOMORE 的整数值,那么这个事件为周期性事件:根据此返回值,对 when属性 进行更新,这样就达到了周期性运行的目的。

Redis时间事件的实现

服务器将所有的时间事件都放在一个无序列表中,每当时间事件执行器运行时,它就遍历整个链表,查找所有已到达的时间事件,并调用相应的事件处理器。

时间事件链表
时间事件应用实例:serverCron函数

Redis服务器需要定期对自身的资源和状态进行检查和调整,从而确保服务能够长期,稳定的运行。这些检查和调整操作由severCron函数负责执行,它的主要工作包括:

  • 更新服务器的各类统计信息,例如cpu处理时间,内存占用,数据库占用情况等。
  • 清理数据库中过期的键值对。
  • 关闭和清理连接失效的客户端。
  • 尝试进行AOF或者RDB持久化操作。
  • 主从模式下,还需要对从服务器进行定期同步。
  • 集群模式下,需要对其他节点进行同步和连接测试。

服务器默认serverCron每秒运行10次(次/100ms),用户可以通过修改hz选项来调整serverCron的每秒执行次数。

事件的调度与执行

因为服务器中同时存在文件事件和时间事件两种事件类型,所以服务器必须对两种事件进行调度,何时要处理文件事件,何时处理时间事件。

件的调度和执行由 processEvents函数负责,该函数的伪代码如下:

def aeProcessEvents() {
    // 获取到达时间距离当前时间最接近的时间事件
    time_event = searchNearestTime()
    //计算最接近的时间事件距离到达还有多少秒
    remaind_ms = time_event.when - unix_ts_now()

    //如果事件已经到达,那么remaind_ms重置为0
    if remaind_ms < 0:
        remaind_ms = 0
    
    //根据remain_ms,创建timeval结构
    timeval = create_timeval_with_ms(remaind_ms)

    //阻塞并等待文件事件产生,最大阻塞时间由timeval结构决定
    //如果remaind_ms的值为0,那么aeApiPoll调用后马上返回,不阻塞
    aeApiPoll(timeval)
    
    // 处理所有已产生的文件事件
    processFileEvents()
    
    //处理所有已到达的时间事件
    processTimeEvents();

将aeProcessEvents函数置于循环里面,加上初始化和清理函数,就构成了Redis服务器的主函数,伪代码如下:

def main():
    // 初始化服务器
    init_server()
    // 一直处理事件,直到服务器关闭
    while server_is_not_shutdown():
        aeProcessEvents()
    // 服务器关闭,执行清理操作
    clean_server()

从事件的角度下服务器运行流程:

image.png

以下是事件的调度和执行规则:

  1. aeApiPoll函数的最大阻塞事件由到达事件最接近当前时间的时间事件决定,这个方法既可以避免服务器对时间事件进行频繁的轮询(忙等待),也可以确保aeApiPoll函数不会阻塞过长时间。
  2. 因为文件事件是随机出现的,如果等待并处理完一次文件事件后,仍未有任何时间事件到达,那么服务器将再次等待并处理文件事件(新的循环)。随着时间推移,一旦时间事件来到指定时间,则开始处理时间事件。
  3. 对于文件事件和时间事件的处理都是同步,有序,原子的,服务器不会中途打断,也不会抢占。因此事件都尽可能地减少程序的阻塞时间,并在有需要时主动让出执行权,降低造成事件饥饿的可能性。比如,命令回复处理器在处理一个命令的回复写入到客户端套接字时,如果写入字节数超过了一个预设常量,命令回复处理器就会主动break跳出写入循环,余下的数据下次再写。另外,时间事件也会将耗时非常长的持久化操作放到子线程或者子进程中执行。
  4. 因为时间事件在文件事件之后处理,且事件不会出现抢占,因此时间事件的实际处理时间,要比设定的稍晚。

回顾

  1. Redis服务器是一个事件驱动程序,处理的事件分为时间事件和文件事件两类。
  2. 文件事件处理器基于Reactor模式实现的网络通讯程序。是对套接字操作的抽象,每次套接字变为可应答(acceptable)、可写(writable)或者可读(readable)时,相应的文件事件就会产生。文件事件分为READABLE和WRITABLE两类。
  3. 时间事件分为定时事件和周期性事件:定时事件只执行一次,周期时间循环执行。服务器一般只执行serverCron函数一个时间事件(周期性事件)。
  4. 文件事件和时间事件之间是合作关系,服务器会轮流处理这两种事件,处理过程中不会进行抢占。
  5. 时间事件的实际处理事件通常会比设定的时间晚一些。

你可能感兴趣的:(Redis设计 - 事件)