redis 本身是一个单线程程序,但是其并发能力在开源的非关系型数据库中却名列前茅,这是如何做到的呢?
一个单线程程序,如何能并发处理上万请求?redis 给出的答案是 IO 复用 + 非阻塞 IO 这对神兵利器。
在 linux 平台下,常见的 IO 复用手段有:select、poll、epoll 三种,一般在并发很大的情况下 epoll 的性能最佳,select 和 pool 都有其局限性(select 有监听描述符个数限制,poll 虽然没有个数限制,但是在处理大量文件描述符时,因为其需要遍历整个监听 list,性能也欠佳)。
今天我们就来学习一下 redis 高并发的立足之本: eventloop 机制。本文以 epoll 来分析 redis 中 IO 复用的实现方法。涉及到的代码有 src/ae.h,src/ae.c,src/ae_epoll.c.

eventloop

eventloop 根据平台提供的 IO 复用库,选择最合适的 backend 来实现 eventloop 相关接口。对应的检测代码为:

#ifdef HAVE_EVPORT
#include "ae_evport.c"
#else
#ifdef HAVE_EPOLL
#include "ae_epoll.c"
#else
#ifdef HAVE_KQUEUE
#include "ae_kqueue.c"
#else
#include "ae_select.c"
#endif
#endif
#endif

注意其 include 的是 .c 文件,而一般我们都是 include .h 文件的。后面我们会看到类似 ae_epoll.c 文件里面的函数实现都是
static 的,所以必须要被 include 到 ae.c 中才可以通过编译、链接

再来看一下 eventloop 的定义:

// 网络事件, 这个结构体中没有保存 fd,因为 aeFileEvent 在 eventloop 的 events
// 下标即为其对应的 fd
typedef struct aeFileEvent {
    // 监听事件类型掩码,
    // 值可以是 AE_READABLE 或 AE_WRITABLE ,
    // 或者 AE_READABLE | AE_WRITABLE
    int mask; /* one of AE_(READABLE|WRITABLE) */

    // 读事件处理器
    aeFileProc *rfileProc;

    // 写事件处理器
    aeFileProc *wfileProc;

    // 多路复用库的私有数据
    void *clientData;

} aeFileEvent;

// timer 
typedef struct aeTimeEvent {

    // 时间事件的唯一标识符
    long long id;

    // 事件的到达时间
    long when_sec;
    long when_ms;

    // 事件处理函数
    aeTimeProc *timeProc;

    // 事件释放函数
    aeEventFinalizerProc *finalizerProc;

    // 多路复用库的私有数据
    void *clientData;

    // 指向下个时间事件结构,形成链表
    struct aeTimeEvent *next;

} aeTimeEvent;

typedef struct aeFiredEvent {

    // 已就绪文件描述符
    int fd;

    // 事件类型掩码,
    // 值可以是 AE_READABLE 或 AE_WRITABLE 或 AE_READABLE | AE_WRITABLE
    int mask;

} aeFiredEvent;

typedef struct aeEventLoop {

    // 目前已注册的最大描述符
    int maxfd;

    // 目前已追踪的最大描述符
    int setsize;

    // 用于生成时间事件 id
    long long timeEventNextId;

    // 最后一次执行时间事件的时间
    time_t lastTime;

    // 已注册的文件事件
    aeFileEvent *events;

    // 已就绪的文件事件
    aeFiredEvent *fired;

    // 时间事件
    aeTimeEvent *timeEventHead;

    // 事件处理器的开关
    int stop;

    // 多路复用库的私有数据,存储特定实现(如epool)所需要的数据
    void *apidata;

    // 在处理事件前要执行的函数
    aeBeforeSleepProc *beforesleep;

} aeEventLoop;

构造和析构

// 创建一个初始容量为 setsize 的 eventloop
aeEventLoop *aeCreateEventLoop(int setsize) {
  aeEventLoop *eventLoop;
  int i;

  // 创建事件状态结构
  if ((eventLoop = zmalloc(sizeof(*eventLoop))) == NULL)
    goto err;

  // 初始化文件事件结构和已就绪文件事件结构数组
  eventLoop->events = zmalloc(sizeof(aeFileEvent) * setsize);
  eventLoop->fired = zmalloc(sizeof(aeFiredEvent) * setsize);
  if (eventLoop->events == NULL || eventLoop->fired == NULL)
    goto err;
  // 设置数组大小
  eventLoop->setsize = setsize;
  // 初始化执行最近一次执行时间
  eventLoop->lastTime = time(NULL);

  // 初始化时间事件结构
  eventLoop->timeEventHead = NULL;
  eventLoop->timeEventNextId = 0;

  eventLoop->stop = 0;
  eventLoop->maxfd = -1;
  eventLoop->beforesleep = NULL;
  // backend 执行其对应的初始化操作
  if (aeApiCreate(eventLoop) == -1)
    goto err;

  // 初始化监听事件, 开始 mask 都设置为 AE_NONE,代表没有监听
  for (i = 0; i < setsize; i++)
    eventLoop->events[i].mask = AE_NONE;

  // 返回事件循环
  return eventLoop;

err:
  if (eventLoop) {
    zfree(eventLoop->events);
    zfree(eventLoop->fired);
    zfree(eventLoop);
  }
  return NULL;
}

void aeDeleteEventLoop(aeEventLoop *eventLoop) {
  // backend 析构
  aeApiFree(eventLoop);
  zfree(eventLoop->events);
  zfree(eventLoop->fired);
  zfree(eventLoop);
}

向 eventloop 添加、删除文件事件

// 向 eventloop 中添加一个文件描述符,监听读或者写
int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,
                      aeFileProc *proc, void *clientData) {
  if (fd >= eventLoop->setsize) {
    errno = ERANGE;
    return AE_ERR;
  }

  if (fd >= eventLoop->setsize)
    return AE_ERR;

  // 取出文件事件结构
  aeFileEvent *fe = &eventLoop->events[fd];

  // 监听指定 fd 的指定事件,aeApiAddEvent 由特定的库实现
  if (aeApiAddEvent(eventLoop, fd, mask) == -1)
    return AE_ERR;

  // 设置文件事件类型,以及事件的处理器
  // 如果 mask == AE_READABLE | AE_WRITABLE,则读写事件设定为同一个处理函数
  fe->mask |= mask;
  if (mask & AE_READABLE)
    fe->rfileProc = proc;
  if (mask & AE_WRITABLE)
    fe->wfileProc = proc;

  // 私有数据
  fe->clientData = clientData;

  // 如果有需要,更新事件处理器的最大 fd
  if (fd > eventLoop->maxfd)
    eventLoop->maxfd = fd;

  return AE_OK;
}

void aeDeleteFileEvent(aeEventLoop *eventLoop, int fd, int mask) {
  if (fd >= eventLoop->setsize)
    return;

  // 取出文件事件结构
  aeFileEvent *fe = &eventLoop->events[fd];

  // 未设置监听的事件类型,直接返回
  if (fe->mask == AE_NONE)
    return;

  // 计算新掩码
  fe->mask = fe->mask & (~mask);
  if (fd == eventLoop->maxfd && fe->mask == AE_NONE) {
    /* Update the max fd */
    int j;

    for (j = eventLoop->maxfd - 1; j >= 0; j--)
      if (eventLoop->events[j].mask != AE_NONE)
        break;
    eventLoop->maxfd = j;
  }

  // 取消对给定 fd 的给定事件的监视
  aeApiDelEvent(eventLoop, fd, mask);
}

其实 aeEventLoop 中的 events 和 fired 都一定程度上浪费了内存。因为我们并不会监听每一个文件描述符(比如0,1,2),
但是其却要分配足够容纳 setsize 个 aeFileEvent 的数组,好在一个进程也只有一个 aeEventLoop ,而一个 aeFileEvent 也并没有占用太多内存。

添加、删除、查找 timer

long long aeCreateTimeEvent(aeEventLoop *eventLoop, long long milliseconds,
                            aeTimeProc *proc, void *clientData,
                            aeEventFinalizerProc *finalizerProc) {
  // 更新时间计数器, 生成唯一 id
  long long id = eventLoop->timeEventNextId++;

  // 创建时间事件结构
  aeTimeEvent *te;

  te = zmalloc(sizeof(*te));
  if (te == NULL)
    return AE_ERR;

  // 设置 ID
  te->id = id;

  // 设定处理事件的时间
  aeAddMillisecondsToNow(milliseconds, &te->when_sec, &te->when_ms);
  // 设置事件处理器
  te->timeProc = proc;
  te->finalizerProc = finalizerProc;
  // 设置私有数据
  te->clientData = clientData;

  // 将新事件放入表头, 乱序
  te->next = eventLoop->timeEventHead;
  eventLoop->timeEventHead = te;

  return id;
}

int aeDeleteTimeEvent(aeEventLoop *eventLoop, long long id) {
  aeTimeEvent *te, *prev = NULL;

  // 遍历链表
  te = eventLoop->timeEventHead;
  while (te) {

    // 发现目标事件,删除
    if (te->id == id) {

      if (prev == NULL)
        eventLoop->timeEventHead = te->next;
      else
        prev->next = te->next;

      // 执行清理处理器
      if (te->finalizerProc)
        te->finalizerProc(eventLoop, te->clientData);

      // 释放时间事件
      zfree(te);

      return AE_OK;
    }
    prev = te;
    te = te->next;
  }

  return AE_ERR; /* NO event with the specified ID found */
}

// 因为在 redis 中,timer 事件并没有按照其触发事件排序,所以查找一个马上要发生的事件的事件复杂读是 O(N)
// 很多项目类似 nginx 管理 timer 都是使用一个红黑树来做的
static aeTimeEvent *aeSearchNearestTimer(aeEventLoop *eventLoop) {
  aeTimeEvent *te = eventLoop->timeEventHead;
  aeTimeEvent *nearest = NULL;

  while (te) {
    if (!nearest || te->when_sec < nearest->when_sec ||
        (te->when_sec == nearest->when_sec && te->when_ms < nearest->when_ms))
      nearest = te;
    te = te->next;
  }
  return nearest;
}

处理 event

// 处理到期的 timer
static int processTimeEvents(aeEventLoop *eventLoop) {
  int processed = 0;
  aeTimeEvent *te;
  long long maxId;
  time_t now = time(NULL);

  // 如果发生了 clock skews,我们立刻处理所有的 timer,因为实践表明:
  // 比起来可能 delay 某个定时事件一个不确定的事件,提前触发这个事件带来的问题往往更小
  if (now < eventLoop->lastTime) {
    te = eventLoop->timeEventHead;
    while (te) {
      te->when_sec = 0;
      te = te->next;
    }
  }
  // 更新最后一次处理时间事件的时间
  eventLoop->lastTime = now;

  // 遍历链表
  // 执行那些已经到达的事件
  te = eventLoop->timeEventHead;
  maxId = eventLoop->timeEventNextId - 1;
  while (te) {
    long now_sec, now_ms;
    long long id;

    // 跳过无效事件
    if (te->id > maxId) {
      te = te->next;
      continue;
    }

    // 获取当前时间
    aeGetTime(&now_sec, &now_ms);

    // 如果当前时间等于或等于事件的执行时间,那么说明事件已到达,执行这个事件
    if (now_sec > te->when_sec ||
        (now_sec == te->when_sec && now_ms >= te->when_ms)) {
      int retval;

      id = te->id;
      // 执行事件处理器,并获取返回值
      retval = te->timeProc(eventLoop, id, te->clientData);
      processed++;
      // 如果定时事件返回 AE_NOMORE 代表可以删除这个事件,否则返回值作为一个
      // interval,将 timer 重新添加到 eventloop
      if (retval != AE_NOMORE) {
        aeAddMillisecondsToNow(retval, &te->when_sec, &te->when_ms);
      } else {
        aeDeleteTimeEvent(eventLoop, id);
      }

      // 因为执行事件之后,事件列表可能已经被改变了,因此需要从头遍历 timer 链表
      te = eventLoop->timeEventHead;
    } else {
      te = te->next;
    }
  }
  return processed;
}

/* Process every pending time event, then every pending file event
 * (that may be registered by time event callbacks just processed).
 *
 * Without special flags the function sleeps until some file event
 * fires, or when the next time event occurs (if any).
 *
 * If flags is 0, the function does nothing and returns.
 *
 * if flags has AE_ALL_EVENTS set, all the kind of events are processed.
 *
 * if flags has AE_FILE_EVENTS set, file events are processed.
 *
 * if flags has AE_TIME_EVENTS set, time events are processed.
 *
 * if flags has AE_DONT_WAIT set the function returns ASAP until all
 * the events that's possible to process without to wait are processed.
 *
 * The function returns the number of events processed.
 */
int aeProcessEvents(aeEventLoop *eventLoop, int flags) {
  int processed = 0, numevents;

  /* Nothing to do? return ASAP */
  if (!(flags & AE_TIME_EVENTS) && !(flags & AE_FILE_EVENTS))
    return 0;

  // 因为 redis 中,等待事件事件也是通过 backend 进行 wait 实现的,所以即使 flags 并不要求我们处理 file event 我们
  // 也会调用 aeApiPoll
  if (eventLoop->maxfd != -1 ||
      ((flags & AE_TIME_EVENTS) && !(flags & AE_DONT_WAIT))) {
    int j;
    aeTimeEvent *shortest = NULL;
    struct timeval tv, *tvp;

    // 获取最近的时间事件
    if (flags & AE_TIME_EVENTS && !(flags & AE_DONT_WAIT))
      shortest = aeSearchNearestTimer(eventLoop);

    if (shortest) {
      // 如果时间事件存在的话
      // 那么根据最近可执行时间事件和现在时间的时间差来决定文件事件的阻塞时间
      long now_sec, now_ms;

      // 计算最近的时间事件还要多久才能达到
      aeGetTime(&now_sec, &now_ms);
      tvp = &tv;
      tvp->tv_sec = shortest->when_sec - now_sec;
      if (shortest->when_ms < now_ms) {
        tvp->tv_usec = ((shortest->when_ms + 1000) - now_ms) * 1000;
        tvp->tv_sec--;
      } else {
        tvp->tv_usec = (shortest->when_ms - now_ms) * 1000;
      }

      // 时间差小于 0 ,说明事件已经可以执行了,将秒和毫秒设为 0 (不阻塞)
      if (tvp->tv_sec < 0)
        tvp->tv_sec = 0;
      if (tvp->tv_usec < 0)
        tvp->tv_usec = 0;
    } else {

      // 根据 flags 是否设置了 AE_DONT_WAIT 决定阻塞的时间
      if (flags & AE_DONT_WAIT) {
        tv.tv_sec = tv.tv_usec = 0;
        tvp = &tv;
      } else {
        tvp = NULL; /* wait forever */
      }
    }

    numevents = aeApiPoll(eventLoop, tvp);
    for (j = 0; j < numevents; j++) {
      // 从已就绪数组中获取事件
      aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd];

      int mask = eventLoop->fired[j].mask;
      int fd = eventLoop->fired[j].fd;
      int rfired = 0;

      // 因为存在调用之前的事件处理函数的时候,可能会删除某个已经处于 active 的事件
      // 所以 redis 使用 fe->mask & mask 来检查是否这个 fired event 是否仍然合法
      // 但是其实在一些极限情况下还是有问题的,比如:
      // 1. fileEvent1 根据删除了 fileEvent2(这个 event 在本次 loop 中也是 fired),fileEvent2 对应的 fd 是10
      // 2. fileEvent1 在删除 fielEvent2 之后又打开了一个文件,其 fd 是10(这是很可能的,因为文件描述符会从最小的可用描述符开始使用)
      // 3. fileEvent 将这个新 fileEvent3 注册到了 eventloop
      // 这种情况下,redis 会处理这个新的 fileEvent3,但是其压根不处于 active,不过好在这种情况下
      // 只要是非阻塞套接字,就不会又什么大问题, 只会多一次调用而已
      if (fe->mask & mask & AE_READABLE) {
        // rfired 确保读/写事件只能执行其中一个
        rfired = 1;
        fe->rfileProc(eventLoop, fd, fe->clientData, mask);
      }
      // 写事件
      if (fe->mask & mask & AE_WRITABLE) {
        if (!rfired || fe->wfileProc != fe->rfileProc)
          fe->wfileProc(eventLoop, fd, fe->clientData, mask);
      }

      processed++;
    }
  }

  // 处理 timer
  if (flags & AE_TIME_EVENTS)
    processed += processTimeEvents(eventLoop);

  // 返回处理的 file event 和 time evnet 的总数
  return processed;
}

// 基本上所有的事件驱动程序,其主线程(进程) 都是无限循环的调用 eventloop 的 poll 接口
void aeMain(aeEventLoop *eventLoop) {

  eventLoop->stop = 0;

  while (!eventLoop->stop) {

    // redis 的 eventloop 提供了一个 beforesleep 的 hook,可以在
    // 执行事件循环之前做一些额外的工作
    if (eventLoop->beforesleep != NULL)
      eventLoop->beforesleep(eventLoop);

    // 开始处理事件
    aeProcessEvents(eventLoop, AE_ALL_EVENTS);
  }
}

backend(epoll)

现在来看一下 redis 对 epoll 的封装(epoll 的接口我们就不详细介绍了):

// epoll private data
typedef struct aeApiState {

    // epoll 句柄
    int epfd;

    // 事件槽, 用于 epoll_wait
    struct epoll_event *events;

} aeApiState;

static int aeApiCreate(aeEventLoop *eventLoop) {

    aeApiState *state = zmalloc(sizeof(aeApiState));

    if (!state) return -1;

    // 初始化事件槽空间
    state->events = zmalloc(sizeof(struct epoll_event)*eventLoop->setsize);
    if (!state->events) {
        zfree(state);
        return -1;
    }

    // 创建 epoll 实例, 根据 manual 其实 epoll_create 的参数没有什么实际作用
    state->epfd = epoll_create(1024); 
    if (state->epfd == -1) {
        zfree(state->events);
        zfree(state);
        return -1;
    }

    // 赋值给 eventLoop
    eventLoop->apidata = state;
    return 0;
}

static void aeApiFree(aeEventLoop *eventLoop) {
    aeApiState *state = eventLoop->apidata;

    close(state->epfd);
    zfree(state->events);
    zfree(state);
}

/*
    添加事件的时候要处理的流程如下:
    1. 确定 OP(如果已经在监听这个 fd,那么是进行 MOD,否则是 ADD)
    2. 确定 mask(如果已经监听了这个 fd, 那么需要将其之前的 mask 与本次调用的 mask 进行求或)
    3. 调用 epoll_ctl
    redis epoll 使用 level triggered,nginx 则使用 edge triggered
    总体来说 level triggered 的编程方式更符合习惯阻塞 IO 编程的开发人员
*/
static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {
    aeApiState *state = eventLoop->apidata;
    struct epoll_event ee;

    int op = eventLoop->events[fd].mask == AE_NONE ?
            EPOLL_CTL_ADD : EPOLL_CTL_MOD;

    // 注册事件到 epoll
    ee.events = 0;
    mask |= eventLoop->events[fd].mask; /* Merge old events */
    if (mask & AE_READABLE) ee.events |= EPOLLIN;
    if (mask & AE_WRITABLE) ee.events |= EPOLLOUT;
    ee.data.u64 = 0; /* avoid valgrind warning */
    ee.data.fd = fd;

    if (epoll_ctl(state->epfd,op,fd,&ee) == -1) return -1;

    return 0;
}

/*
    删除事件的时候要处理的流程如下:
    1. 确定 OP((oldmask & ~delmask) == 0 的话,是DEL,否则是 MOD)
    2. 如果是 MOD 确定 mask((oldmask & ~delmask))
    3. 调用 epoll_ctl
*/
static void aeApiDelEvent(aeEventLoop *eventLoop, int fd, int delmask) {
    aeApiState *state = eventLoop->apidata;
    struct epoll_event ee;

    int mask = eventLoop->events[fd].mask & (~delmask);

    ee.events = 0;
    if (mask & AE_READABLE) ee.events |= EPOLLIN;
    if (mask & AE_WRITABLE) ee.events |= EPOLLOUT;
    ee.data.u64 = 0; /* avoid valgrind warning */
    ee.data.fd = fd;
    if (mask != AE_NONE) {
        epoll_ctl(state->epfd,EPOLL_CTL_MOD,fd,&ee);
    } else {
        epoll_ctl(state->epfd,EPOLL_CTL_DEL,fd,&ee);
    }
}

static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
    aeApiState *state = eventLoop->apidata;
    int retval, numevents = 0;

    // polling,等待时间由 eventloop 计算传入
    // 多嘴提一句,在一些新版本的 linux 内核,提供了 timerfd 相关的函数族
    // 可以更加优雅方便的实现 epoll_wait 阻塞一段时间的需求
    retval = epoll_wait(state->epfd,state->events,eventLoop->setsize,
            tvp ? (tvp->tv_sec*1000 + tvp->tv_usec/1000) : -1);

    // 有至少一个事件就绪?
    if (retval > 0) {
        int j;

        // 修改 fired 数组,记录所有本次循环处于可读/写的 fd
        numevents = retval;
        for (j = 0; j < numevents; j++) {
            int mask = 0;
            struct epoll_event *e = state->events+j;

            if (e->events & EPOLLIN) mask |= AE_READABLE;
            if (e->events & EPOLLOUT) mask |= AE_WRITABLE;
            if (e->events & EPOLLERR) mask |= AE_WRITABLE;
            if (e->events & EPOLLHUP) mask |= AE_WRITABLE;

            eventLoop->fired[j].fd = e->data.fd;
            eventLoop->fired[j].mask = mask;
        }
    }
    
    // 返回已就绪事件个数
    return numevents;
}

总结

  1. 事件驱动编程是高性能服务器的一种常见实现方式。redis 作为一个单线程程序,也选择 IO 复用来实现高并发
  2. 单进程+单线程程序,还是无法发挥多核服务器的全部性能,在这方面 redis 做的并不合格