"non-blocking IO + IO multiplexing(IO多路复用)"即reactor模型。Redis就采用这种模型。




redis 非本地连接拒绝 redis 非阻塞io_select阻塞


Redis中单线程Reactor模式

select/epoll/poll等API分为两个阶段:①等待数据就绪阶段。这阶段不需由应用程序来监控,转而由内核替代应用程序监视文件描述符,具体内核监控的机制不同又产生了像epoll、select等API接口。②从内核拷贝数据到用户空间,这里是同步的。

  • 使用IO多路复用的优势

与普通的进程线程相比较,不需要创建维护多个进程线程,使得系统开销相对较小,节省系统资源。

  • 非阻塞IO

非阻塞IO只指如监听网络事件socket API的非阻塞。为什么要这样?

Linux中“man 2 select”BUGS可以看到:


redis 非本地连接拒绝 redis 非阻塞io_如何查找历史线程阻塞原因_02

“man 2 select”BUGS


上面介绍了虽然API返回可读,但是仍然可能阻塞,例如,当数据已经到达,但是在检查时出现错误的校验和并被丢弃时,就会发生这种情况。可能在其他情况下,文件描述符被错误地报告为ready。建议使用非阻塞。

在实际使用一些情形:

  1. 应用网络数据比较大超过发送缓冲区时,如果阻塞会造成整个进程/线程阻塞,这种情况是不能容忍的。
  2. 惊群效应。子父进程同时监控一文件描述符时会发生。
  3. epoll边缘触发模式。epoll不会返回可读数据长度,所以read API会阻塞。

Redis下伪代码如下


bool isRun = true;
while(isRun)
{
    int  timeoutMs = aeSearchNearestTimer();                  // 寻找里目前时间最近的时间事件
    int ret = aeApiPoll(maxfd, rfd, wfd, timeoutMs);
    if(ret < 0){
        // 错误处理
    }else if(ret > 0){
        // 有时间来,需处理IO,如注册的读、写回调等操作,非阻塞
    }
    // 处理系统要做其他事物,非阻塞。如:doBeforeSleep()等
}


  • aeApiPoll()

Redis对这个接口进行了封装以适应不同平台。


// ae.c
#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


上面这种写法对C/C++的人员再熟悉不过,采用宏条件语句判断,在预处理阶段把对应平台使用的ae_xxx.c文件拷贝到ae.c文件中。把各个平台提供的如:epoll、poll、select、kqueue等统一封装成aeApiPoll()。

  • aeSearchNearestTimer()

寻找目前时间最近的时间事件唤醒本线程,做一些事情,如:100ms的用于删除设定了过期的key、AOF从内存buf空间系统调用write写入OS缓冲区(磁盘fsync有阻塞属性,在另外进程执行)。

aeSearchNearestTimer()在内部是通过查找timeEventHead无序链表,时间复杂度O(n),这里是由于链表较短,只有一个serverCron 时间事件,性能无影响。


redis 非本地连接拒绝 redis 非阻塞io_如何查找历史线程阻塞原因_03

在redis.c initServer()中注册的serverCron

如果时间事件较多,需要采用有序数据结构如:红黑树(nginx)查找时间复杂度O(logn)。