Redis事件驱动(aeEventLoop)原理分析
Redis事件驱动(aeEventLoop)原理分析
ivansli关于Redis事件驱动
众所周知,Redis是高性能的、基于内存的、k-v数据库。其强大的功能背后,存在着2种不同类型的事件驱动,包括:
- 文件事件(File event)
- 时间事件(Time event)
文件事件是对相关的 fd 相关操作的封装,时间事件则是对定时任务相关操作的封装。Redis server通过文件事件来进行外部请求的处理与操作,通过时间事件来对系统内部产生的定时任务进行处理。
(本文重点讲解文件事件相关的操作流程以及原理)
文中探讨的原理及源码基于Redis官方 v7.0 版本
Redis事件驱动的相关源码
在Redis源码中,涉及事件驱动相关的源码文件主要有以下几个(以ae作为文件名称前缀):
1 | src |
ae.c
文件事件驱动/时间事件驱动的核心处理逻辑ae.h
文件事件驱动/时间事件驱动结构体、方法签名的定义ae_epoll.c
linux os 文件事件驱动涉及的i/o多路复用实现ae_evport.c
sun os 文件事件驱动涉及的i/o多路复用实现ae_kqueue.c
mac/BSD os 文件事件驱动涉及的os i/o多路复用实现ae_select.c
其他 os 文件事件驱动涉及的i/o多路复用实现(或者说是通用型的,包括Windows)
根据源码中注释(ae.c)可知 ae
的含义为 A simple event-driven
。
1 | /* A simple event-driven programming library. Originally I wrote this code |
一个简单的事件驱动编程库。最初我(作者:antirez)为Jim的事件循环(Jim是Tcl解释器)编写了这段代码,但后来将其转化为库形式以便于重用。
多种i/o多路复用方法的选择
在Redis源码中存在多种i/o多路复用实现方式,如何选择使用哪种i/o多路复用实现呢?源码编译时选择不同的实现方式,即:Redis源码编译成二进制文件的时候来选择对应的实现方式,在源码可以看到蛛丝马迹。
代码文件: ae.c
1 |
从上面代码可知,在编译源码的预处理阶段,根据不同的编译条件(#ifdef/#else/#endif)来判断对应的宏是否定义(#define定义的常量)来加载实现逻辑。以epoll为例,若定义了 HAVE_EPOLL 宏,则加载 “ae_epoll.c” 文件。宏 “HAVE_EVPORT/HAVE_EPOLL/HAVE_KQUEUE” 分别对应不同的系统(或者说是对应的编译器)。
代码文件: config.h
1 |
假设,当前是linux系统,那么 宏__linux__ 又是从哪里来的呢?
Linux环境下主要用gcc编译,借助 gcc -dM -E - < /dev/null
命令从获得相应的变量中可以看到其定义。
1 | root@ivansli ~# gcc -dM -E - < /dev/null | grep __linux |
即:Redis源码会根据编译器来判断应该把源码编译成对应平台(或者是通用平台,性能会有所下降)运行的二进制可执行程序。
核心结构体 aeEventLoop
aeEventLoop 结构体如下所示:
1 | /* State of an event based program 事件驱动程序的状态 */ |
aeEventLoop 结构体核心字段以及相关交互如下图所示:
setsize
文件事件数组大小,等于 server.maxclients+CONFIG_FDSET_INCRevents
文件事件数组,大小等于setsizefired
文件事件就绪的fd数组,大小等于setsizetimeEventHead
时间事件数组,双向链表apidata
这用于获取特定的API数据,指向 aeApiState结构体,不同的i/o多路复用实现包含不同的字段。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
30
31
32// ae_epoll.c
typedef struct aeApiState { /* 在 aeApiCreate 中初始化,linux则在 ae_linux.c 文件 */
int epfd; /* io多路复用fd */
struct epoll_event *events; /* 就绪的事件数组 */
} aeApiState;
// ae_kqueue.c
typedef struct aeApiState {
int kqfd;
struct kevent *events;
/* Events mask for merge read and write event.
* To reduce memory consumption, we use 2 bits to store the mask
* of an event, so that 1 byte will store the mask of 4 events. */
char *eventsMask;
} aeApiState;
// ae_evport.c
typedef struct aeApiState {
int portfd; /* event port */
uint_t npending; /* # of pending fds */
int pending_fds[MAX_EVENT_BATCHSZ]; /* pending fds */
int pending_masks[MAX_EVENT_BATCHSZ]; /* pending fds' masks */
} aeApiState;
// ae_select.c
typedef struct aeApiState {
fd_set rfds, wfds;
/* We need to have a copy of the fd sets as it's not safe to reuse
* FD sets after select(). */
fd_set _rfds, _wfds;
} aeApiState;
aeEventLoop 相关操作方法签名如下所示(文件ae.h):
1 | aeEventLoop *aeCreateEventLoop(int setsize); |
aeEventLoop事件处理核心方法 | 用途 | 调用i/o多路复用方法 | epoll为例,调用方法 |
---|---|---|---|
aeCreateEventLoop | 创建并初始化事件循环 | aeApiCreate | epoll_create() 默认水平触发 |
aeDeleteEventLoop | 删除事件循环 | aeApiFree | - |
aeCreateFileEvent | 创建文件事件 | aeApiAddEvent | epoll_ctl() EPOLL_CTL_ADD EPOLL_CTL_MOD |
aeDeleteFileEvent | 删除文件事件 | aeApiDelEvent | epoll_ctl() EPOLL_CTL_MOD EPOLL_CTL_DEL |
aeProcessEvents | 处理文件事件 | aeApiPoll | epoll_wait() |
aeGetApiName | 获取i/o多路复用的实现名称 | aeApiName | - |
基于epoll的i/o多路复用
客户端与服务端的连接建立过程,如下图所示:
TCP三次握手时,Linux内核会维护两个队列:
- 半连接队列,被称为SYN队列
- 全连接队列,被称为 accept队列
epoll相关处理方法与逻辑如下图所示:
基于epoll的i/o多路复用伪代码框架:
1 | int main(){ |
从上可知,Redis作为Server服务端在启动之后随时随刻监听着相关事件的发生。以linux为例,其处理过程与基于epoll的i/o多路复用伪代码框架基本相似,Redis源码中更多的是通过封装使其得到一个方便使用的库,库的底层包含了多种i/o多路复用实现方式。
aeEventLoop 的执行过程
以epoll为例,简化版的Redis事件驱动交互过程。
图中仅列出了核心方法,如有错误欢迎指正
Redis 针对不同的 fd 注册 AE_READABLE/AE_WRITABLE 类型的回调方法,同时把 fd 添加到 epoll 中。当 fd 关心的事件触发之后,执行对应回调方法(主要针对 可读/可写/时间事件 3种类型的事件进行处理)。Redis 中 epoll 使用的触发方式为 LT
水平触发,意味着数据一次性没有处理完,下次 epoll_wait()
方法还会返回对应fd,直到处理完毕,对于客户端一次性发起批量处理多条命令的操作非常有益,减少对其他指令的阻塞时间。