Redis的循环调度器AE解析和代码解读

743 阅读5分钟

这是我参与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)).

简单说就是

  1. 我们已经发现了性能不高,但是现在不需要优化
  2. 如果优化,会改写成skiplist

总结

  1. ae使用reactor模式

  2. ae将事件分为文件事件和时间事件

  3. 在initserver中,初始化eventloop,并注册了时间时间serverCron

  4. 在aeProcessEvents中不断调用io多路复用的相关函数监听fd并逐个处理

  5. 时间事件使用链表进行实现,每次遍历链表检查事件是否需要执行