这是我参与8月更文挑战的第2天,活动详情查看:8月更文挑战
redis作为单线程高性能的nosql,它的性能非常好,redis使用了Reactor的事件驱动模型,在它的内部,实现了一种高性能的事件调度器AE,今天我们来分析一下ae的代码和运行逻辑。
下面涉及的所有代码,基于redis的6.0版本。 与AE相关的代码,大致都可以在ae.h和ae*.c中找到。
首先从redis的main开始
int main(int argc, char **argv) {
...
//服务启动
initServer();
...
//ae主流程
aeMain(server.el);
...
}
事件类型
redis把事件分为文件事件fileEvent和时间事件TimeEvent两种 文件事件指的是服务器I/O事件,底层使用I/O多路复用来进行套接字的管理和状态的监听 时间事件指的是redis的定时事件和周期性的事件
eventloop初始化
首先来看initServer()中的 aeCreateEventLoop redis会创建一个eventloop,然后把事件不断的注册到eventloop中,然后不断循环检测有无事件触发
void initServer(void) {
...
server.el = aeCreateEventLoop(server.maxclients+CONFIG_FDSET_INCR);
if (server.el == NULL) {
serverLog(LL_WARNING,
"Failed creating the event loop. Error message: '%s'",
strerror(errno));
exit(1);
}
...
//注册周期性时间事件,serverCron
if (aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) {
serverPanic("Can't create event loop timers.");
exit(1);
}
...
}
aeCreateEventLoop函数需要一个参数setsize,值为server.maxclients+CONFIG_FDSET_INCR,server.maxclients是通过配置,CONFIG_FDSET_INCR是个保护性预定义常量
aeEventLoop *aeCreateEventLoop(int setsize) {
...
if (aeApiCreate(eventLoop) == -1) goto err;
/* Events with mask == AE_NONE are not set. So let's initialize the
* vector with it. */
for (i = 0; i < setsize; i++)
eventLoop->events[i].mask = AE_NONE;
return eventLoop;
...
}
aeApiCreate函数中,系统为event分配内存
state->events = zmalloc(sizeof(struct epoll_event)*eventLoop->setsize);
aeCreateEventLoop中,循环为每个event的mask都初始化为AE_NONE,mask取值如下
#define AE_NONE 0 /* No events registered. */
#define AE_READABLE 1 /* Fire when descriptor is readable. */
#define AE_WRITABLE 2 /* Fire when descriptor is writable. */
#define AE_BARRIER 4 //会影响ae对事件的处理顺序,默认顺序是先读后写,后边会提到
那么到这里,ae的eventloop已经初始化完成了,
文件事件
下面我们来看一下文件事件的创建
redis在socket链接函数connSocketConnect中,调用aeCreateFileEvent进行文件事件注册
int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,
aeFileProc *proc, void *clientData);
aeCreateFileEvent在epoll的实现中调用了epoll_ctl函数。Redis会根据该事件对应之前的mask是否为AE_NONE,来决定使用EPOLL_CTL_ADD还是EPOLL_CTL_MOD。
/* If the fd was already monitored for some event, we need a MOD
* operation. Otherwise we need an ADD operation. */
int op = eventLoop->events[fd].mask == AE_NONE ?
EPOLL_CTL_ADD : EPOLL_CTL_MOD;
...
mask |= eventLoop->events[fd].mask; /* Merge old events */
...
if (epoll_ctl(state->epfd,op,fd,&ee) == -1) return -1;
这样一个文件事件就注册完成了
事件主循环流程
接下来我们回到redis的main函数,main函数在 initServer之后调用了aeMain(server.el),aeMain是整个ae循环调度器的主流程入口
void aeMain(aeEventLoop *eventLoop) {
eventLoop->stop = 0;
while (!eventLoop->stop) {
aeProcessEvents(eventLoop, AE_ALL_EVENTS|
AE_CALL_BEFORE_SLEEP|
AE_CALL_AFTER_SLEEP);
}
}
aeMain的逻辑非常简单,只做一件事:循环调用aeProcessEvents,aeProcessEvents也就是整个ae的主流程循环;直接上代码
int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{
...
//调用aeApiPoll,aeApiPoll中调用了epoll_wait,获取就绪的事件
numevents = aeApiPoll(eventLoop, tvp);
...
//逐个处理就绪的事件
for (j = 0; j < numevents; j++) {
int fd = eventLoop->fired[j].fd;
aeFileEvent *fe = &eventLoop->events[fd];
int mask = eventLoop->fired[j].mask;
int fired = 0; /* Number of events fired for current fd. */
//判断AE_BARRIER
int invert = fe->mask & AE_BARRIER;
//如果设置了AE_BARRIER,则改变处理顺序为写->读
if (!invert && fe->mask & mask & AE_READABLE) {
fe->rfileProc(eventLoop,fd,fe->clientData,mask);
fired++;
fe = &eventLoop->events[fd]; /* Refresh in case of resize. */
}
//默认顺序读->写
if (fe->mask & mask & AE_WRITABLE) {
if (!fired || fe->wfileProc != fe->rfileProc) {
//写事件处理器
fe->wfileProc(eventLoop,fd,fe->clientData,mask);
fired++;
}
}
if (invert) {
fe = &eventLoop->events[fd]; /* Refresh in case of resize. */
if ((fe->mask & mask & AE_READABLE) &&
(!fired || fe->wfileProc != fe->rfileProc))
{
//读事件处理器
fe->rfileProc(eventLoop,fd,fe->clientData,mask);
fired++;
}
}
...
// 处理时间事件
if (flags & AE_TIME_EVENTS)
processed += processTimeEvents(eventLoop);
...
}
我们可以看到,在aeProcessEvents中,调用epoll_wait,等待I/O事件的触发,并且逐一处理事件
默认的事件处理顺序是先处理读事件,在处理写事件;如果事件设置了AE_BARRIER,则先处理写事件。
最后看下时间事件
最后我们来看一下周期处理的时间事件processTimeEvents(eventLoop); 周期性的时间事件我们可以回到initServer中发现,在启动server的时候,注册了一个serverCron函数,这个函数是周期性的执行的。
//定义
long long aeCreateTimeEvent(aeEventLoop *eventLoop, long long milliseconds,
aeTimeProc *proc, void *clientData,
aeEventFinalizerProc *finalizerProc);
//注册周期性时间事件,serverCron
if (aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) {
serverPanic("Can't create event loop timers.");
exit(1);
}
static int processTimeEvents(aeEventLoop *eventLoop) {
int processed = 0;
aeTimeEvent *te;
long long maxId;
//链表头
te = eventLoop->timeEventHead;
maxId = eventLoop->timeEventNextId-1;
monotime now = getMonotonicUs();
while(te) {
long long id;
...
if (te->when <= now) {
int retval;
id = te->id;
te->refcount++;
//时间处理器
retval = te->timeProc(eventLoop, id, te->clientData);
te->refcount--;
processed++;
now = getMonotonicUs();
if (retval != AE_NOMORE) {
//赋值下次的执行时间
te->when = now + retval * 1000;
} else {
te->id = AE_DELETED_EVENT_ID;
}
}
//遍历链表
te = te->next;
}
return processed;
}
时间事件的处理非常简单,redis使用了一个事件链表,然后挨个进行遍历并进行处理,没处理完一个事件时间,判断它是否需要继续执行,如果需要则更新when字段,就这么简单
看起来这样效率很低,如果很多时间事件积压在一起的话,性能会很糟糕,但是在函数的注释里,开发者已经做了解释
* Note that's O(N) since time events are unsorted.
* Possible optimizations (not needed by Redis so far, but...):
* 1) Insert the event in order, so that the nearest is just the head.
* Much better but still insertion or deletion of timers is O(N).
* 2) Use a skiplist to have this operation as O(1) and insertion as O(log(N)).
简单说就是
- 我们已经发现了性能不高,但是现在不需要优化
- 如果优化,会改写成skiplist
总结
-
ae使用reactor模式
-
ae将事件分为文件事件和时间事件
-
在initserver中,初始化eventloop,并注册了时间时间serverCron
-
在aeProcessEvents中不断调用io多路复用的相关函数监听fd并逐个处理
-
时间事件使用链表进行实现,每次遍历链表检查事件是否需要执行