笔者:单大
有任何疑问欢迎关注微信公众号:网易游戏运维平台。(长按识别上图二维码)
微信公众号原文链接:Redis 核心原理:基于事件的处理流程
本文介绍了 Redis 核心原理和架构:基于事件驱动的模型。事件模型是构成 Redis 内核的引擎,Redis 的丰富功能和组件都是构建在这个模型上的。如果你使用过 Redis,那么本文可以为你打开一道进入 Redis 内部世界的门,窥探 Redis 如何构建它的帝国。
本文先对 Redis 使用的事件模型和原理进行介绍,然后按以下主题顺序展开:
最后以一次客户端 SET
命令操作为例子,讲解一个请求在 Redis 内部的流转是如何完成的。
为了方便公众号上进行阅读,帮助读者快速掌握 Redis 核心原理,本文对 Redis 模型进行了简化,去掉了大量的检查和异常处理流程,并且仅在必要的时候通过代码说明。
本文参考的源码基于编写时的最新分支 Redis 5.0.3,实际对照中发现 Redis 的核心逻辑在历史版本迭代中变化不大,也体现了 Redis 的这个核心逻辑的地位。
事件驱动
,顾名思义,只有在发生某些事件
的时候,程序才会有所行动。
事件驱动模型在架构设计领域也称为 Reactor 模式,体现的是一种被动响应的特征。
事件驱动模型通常可以抽象为如下图所示流程:
主程序处于一个阻塞状态的事件循环 (event loop)中等待事件 (event),当有事件发生时,根据事件的属性分发到相应的处理函数进行处理。事件以并发的方式发送到服务处理器 (service handler),服务处理器将事件整合到一个有序队列中(这过程称为 demultiplexes),并分发到具体的请求处理器 (request handler)进行处理。
为了阅读的方便,因为“事件”这个词在中文中较常见,所以下文针对事件模型中的“事件”等专用术语,会进行特定的标识,如:事件循环 (event loop)
,事件 (event)
,处理器 (handler)
等。
Redis 在事件驱动模型下工作,当有来自外部或内部的请求的时候,才会执行相关的流程。
Redis 程序的整个运作都是围绕事件循环 (event loop)
进行的。
事件循环
对于Redis 而言,就像是一台车的引擎一样,提供了整个系统所需的流转动力。所有其他的组件都是基于这个引擎的基础上组合和构建起来的。可以说理解了 Redis 的事件循环
就能了解 Redis 的工作原理的核心。
Redis 事件模型如下图所示:
事件循环 eventloop
同时监控多个事件
,这里的事件
本质上是 Redis 对于连接套接字的抽象。
当套接字变为可读或者可写状态时,就会触发该事件
,把就绪的事件
放在一个待处理事件的队列中,以有序 (sequentially)、同步 (synchronously) 的方式发送给事件处理器
进行处理。这个过程在 Redis 中被称为 Fire
。
Redis 的事件循环
会保存两个列表:events
和fired
列表,前者表示正在监听的事件
,后者表示就绪事件
,可以被进一步执行。
在具体实现时,Redis 采用IO多路复用 (multiplexing) 的方式,封装了操作系统底层 select/epoll等函数,实现对多个套接字 (socket) 的监听,这些套接字就是对应多个不同客户端的连接。
最后由对应的处理器
将处理的结果返回给客户端去。
Redis事件
的来源有两种:文件事件
和时间事件
,限于篇幅问题,本文主要介绍文件事件
的处理流程,时间事件
会在文章最后做简要的说明。
以上就概括了Redis 处理用户请求的大致过程。从这个过程我们可以发现:
Redis 处理所有命令都是顺序执行的,其中包括来自客户端的连接请求。所以当 Redis 在处理一个复杂度高、时间很长的请求(比如 KEYS 命令)的时候,其他客户端的连接都没办法相应。
Redis 内部定时执行的任务也是放在顺序队列中处理,其中也可能包含时间较长的任务,比如自动删除一个过期的大 Key,比如很大 list, hash, set 等。所以有时候会遇到明明业务没有主动操作复杂,但也会出现卡顿的问题。
有利于功能架构实现上更加解耦,模块的可重用性更高。因事件循环
的流程本身和具体的处理逻辑之间是独立的,只要在创建事件的时候关联特定的处理逻辑(事件处理器
),就可以完成一次事件的创建和处理。
根据论文SEDA: An Architecture for Well-Conditioned, Scalable Internet Services 的测试结果显示,相比一个连接分配一个线程的模型, Reactor 模式(固定线程数)在连接数增大的情况下吞吐量不会明显降低,延时也不会也受到显著的影响。
下面开始,会对 Redis 如何实现事件循环
进行说明,会涉及到一些源码的实现部分,如果不感兴趣可以直接跳到第三节看 Redis 怎么利用事件处理模型来处理具体的命令。
Redis 的事件循环
,最直观的理解,就是一个在不断等待事件
的一个无限循环,直到 Redis 程序退出。
Redis 实现事件循环
主要涉及三个源码文件:server.c
, ae.c
, networking.c
。
server.c
的 main()
函数是整个 Redis 程序的开始,我们也从这里开始观察 Redis 的行为。ae.c
实现事件循环和事件的相关功能。networking.c
则负责处理网络IO相关的功能。初始化的过程主要做三个事情:
事件循环
事件循环
简化后的代码如下:(跳过不影响理解)
// 0. 定义服务器主要结构体, 加载服务器配置
struct redisServer server;
initServerConfig();
loadServerConfig();
// 1. 根据配置参数初始化,
initServer()
{
// 1.1 实际创建事件循环
server.el = aeCreateEventLoop();
// 1.2 为事件循环注册一个可读事件,用于响应外部客户端请求
aeCreateFileEvent(server.el, AE_READABLE, acceptTcpHandler)
}
// 2. 执行事件循环,等待连接和命令请求
aeMain(server.el);
初始化过程中被创建的server.el
包含了两个事件的列表,它的结构体实现如下:
typedef struct aeEventLoop
{
aeFileEvent events[AE_SETSIZE]; /* 注册的事件,被 eventloop 监听 */
aeFiredEvent fired[AE_SETSIZE]; /* 有读写操作需要执行的事件(就绪事件) */
} aeEventLoop;
主循环体aeMain()
在ae.c
文件中被实现,简化后的代码如下:
void aeMain(aeEventLoop *eventLoop) {
while (!eventLoop->stop) {
aeProcessEvents(eventLoop, AE_ALL_EVENTS);
}
}
事件循环
主要就是一个while
循环,不断去轮询是否有就绪的事件
需要处理,具体的处理函数是aeProcessEvents
,接下来会有对这个函数有更详细的介绍。
在上述 Redis 在初始化时,程序会创建一个关联了acceptTcpHandler
处理器的可读事件
:
aeCreateFileEvent(server.el, AE_READABLE, acceptTcpHandler)
这个可读事件
注册到事件循环
中,就实现了 Redis 对外提供的服务地址和端口的连接服务。具体的内容下一个小节事件处理器
中介绍。
所有事件
被创建时,都会关联一个处理器 (handler)
,并注册到事件循环
中,事件处理器用于具体的读写操作。
Redis 的常用几个事件处理器有:
acceptTcpHandler()
readQueryFromClient()
sendReplyToClient()
以上处理器均在networking.c
文件下实现,该文件负责 Redis 所有网络 IO 功能的实现。
一个客户端一次正常的连接和命令操作流程,可以通过上述三个处理器完成。
当 Redis 需要监听某个套接字的时候,就会创建一个事件
,并注册到事件循环
中进行监听,Redis 将处理器
以参数的方式关联到事件中。
比如以下是注册一个可读事件
的操作:
aeCreateFileEvent(server.el, fd, AE_READABLE, readQueryFromClient, c)
eventloop
,一个服务器只有一个el
事件
关联的处理器,当事件
就绪后,就会调用此处理器注册完毕后,事件循环
就会将这个事件
(套接字
)加入到监听的范围,当事件
可读时,Redis 就会将这个事件
发送到待处理事件队列中等待处理,等到可读就绪时,会被readQueryFromClient
处理器处理。
可以看到整个过程中事件循环
和不同处理器
之间是解耦的,互不干扰。这样实现提高了代码的简洁和重用。
在 Redis 完成初始化、创建事件循环
后,就会处于等待和处理事件
的状态:无限循环aeProcessEvents()
函数。
这个函数在ae.c
中实现,该文件主要负责事件循环
的实现,在aeProcessEvents()
中具体做了几个事情:
调用IO多路复用函数(select, epoll, evport, kqueue中的其中一种),阻塞等待事件
变成就绪状态或者直到超时,如果有事件就绪,就会将相应事件加入到eventLoop
的待处理事件队列eventLoop->fired
中,然后进入下一个循环。
numevents = aeApiPoll(eventLoop, tvp);
如果在上一步中,发现有numevents
个事件
被触发,就会将就绪队列的事件一个个按顺序进行处理,处理的函数为
for (j = 0; j < numevents; j++) {
aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd];
fe->rfileProc() // 读事件处理
fe->wfileProc() // 写事件处理
}
fe
就是要处理的文件事件 file event
,对应读操作或写操作。至于处理的具体操作,则由创建事件
时自身关联的处理器
决定的,事件循环
不需要关注。
最后一步:如果有时间事件
,则进行时间事件
的处理:
processTimeEvents(eventLoop);
至此,Redis 的事件循环
的机制已经介绍完毕,可以观察到整个事件循环
的逻辑过程都没有涉及具体的命令操作,只需要定义事件
的类型和处理器
即可。可以说这部分就是Reactor 模式体现出来的一个好处:接收事件和处理流程的实现相互解耦。
本章是建立在 Redis 已经完成了初始化工作,主要是创建事件循环
之后,Redis 接受一个客户端操作的完整流程的介绍。如果对初始化过程还有问题,请参考上文。
本章主要分为两个阶段:
第一个阶段:一个外部客户端与 Redis 服务器建立 TCP 连接。
比如我们常用的 Telnet 到 Redis 端口的操作。
➜ ~ telnet 127.0.0.1 6379
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
第二阶段:已经建立连接的客户端,对Redis 发起一次SET
命令的操作。
set a 1
+OK
如图,展示一个新的外部客户端与 Redis 服务器建立连接的过程。
当有客户端连接到 Redis 服务器的时候,注册在事件循环
中的监听服务端口的事件
就会变成读就绪
状态,从而触发这个事件
到待处理事件队列中,准备调用acceptTcpHandler
进行处理。
为在服务器端创建一个对应本次连接的套接字。
把服务端套接字的文件描述符cfd
作为参数,创建client
变量。
为该客户端连接创建并注册一个关联了readQueryFromClient
处理器的可读事件
到事件循环,用于下一步接收并执行命令的工作。
如图展示一个客户端已经完成了连接,对 Redis 服务器发起一次SET
操作后,Redis 处理命令的完整流程。
在上一节中提到,当一个客户端建立连接后,会有一个可读事件
关联到事件循环
,等待接收命令。当有客户端发起一次命令操作后,Redis 就会调用readQueryFromClient
处理器,对用户发送过来的请求,按RESP (REdis Serialization Protocol)进行解析处理后,调用相关的命令进行处理。
调用命令的函数主要做两个事情:(1)查找对应的命令,比如这里的SET
(2)调用该命令关联的函数进行处理,这里就是setCommand
。
setCommand
函数将客户端传进来的参数,变更数据库对应 KEY 的值,然后回复客户端。
回复客户端addReply
函数将返回给客户端的内容,写到客户端变量的输出缓冲client.buf
中,等待发送给客户端。
以上是整个SET
命令的事件
处理,不过在这个时候,返回给用户的回复内容,只存放于服务器的客户端变量输出缓冲中。至于将结果返回给用户的过程,取决于版本,有不同的操作。
在4.0以前,每次的addReply
操作会创建一个写事件
,然后放到事件循环
中执行。
而4.0开始,在每次重新进入一个新的循环之前,就是eventLoop->beforesleep();
这个操作,Redis 会尝试直接发送给客户端,只有当发送的内容超过一定大小,无法一次发送完成的时候,才会去创建一个可写事件
。
有兴趣的读者可以去看下 Redis 作者的这个 commit:
antirez in commit 1c7d87d:
Avoid installing the client write handler when possible.
目的是减少一次系统调用,适用于大部分操作类命令的回复。
可以观察到,整个操作的实现过程,和事件循环
本身没有交集的(没有涉及到ae.c
),开发者只需要关心具体命令的处理逻辑即可。
事件都是来源于外部客户端吗?
这要看怎么定义“外部客户端”了。首先事件
本身分为两种大类:文件事件
和时间事件
。本文主要介绍文件事件
。而文件事件
的产生可以是来源于网络客户端的连接,正如本文所描述的,也可以来自 Redis 集群内部运行需要,会使用一些伪客户端来触发一些文件事件
。
举个例子,当有从节点 (slave/replica) 向主节点 (master) 发起一次同步的时候,在 Redis 就会产生一个需要处理同步数据的事件。不过严格意义上来讲,这个从节点对于主节点 Redis 来说,也属于“外部客户端”。正常情况下,Redis 自身不会主动产生文件事件
。
Redis 是怎么定期更新状态、删除过期KEY的?
读者大概猜到我要引出时间事件这个概念了。Redis 会定期执行服务器的检查,以及一些周期操作,这个周期由参数hz
决定,默认情况下是100毫秒触发一次检查,执行该周期内的时间事件。
时间事件 是 Redis 也是核心流程中重要的一个组成部分,限于篇幅不在这里详细介绍。但有了对事件循环
的认识,要理解时间事件本身也不会太困难。
Redis 源码:是本文主要内容参考。
Redis 官方 README.md:概述核心代码的结构和用途。
Redis Event Library:关于 Redis 的 Event Library
工作原理的官方说明文档。