Nginx源代码分析之网络超时管理(九)

NGX对于不是立即成功的socket I/O,即返回WSAEINPROGRESS,EINPROGRESS,EWOULDBLOCK,EAGAIN,WSAEWOULDBLOCK这几个值的(其中WSAEINPROGRESS,EINPROGRESS属于非阻塞socket的返回值,另外3个则是阻塞模式下I/O模型的返回值)则会调用ngx_add_timer向一个全局红黑树里面添加一个定时器。


当初我在自己写一个IOCP框架的时候,也曾思考过这问题,怎样对IOCP上面的单I/O进行超时管理。是对每个I/O设置一个定时器吗,这样如果系统存在大量定时器,对服务器的压力会不会很大,下面看看NGX怎么解决这个问题。


 NGX利用红黑树来组织那些等待处理并且需要关注其是否超时的事件对象。将红黑树节点作为ngx_event_t的值成员,而非指针成员,则通过红黑树找到最先到期的节点后,通过offsetof宏可以直接得到ngx_event_t本身.因此所有定时器都放在红黑树中.

nginx处理超时有2种方案。

          (1)常规的定时检测机制:设置定时器,每过一定的时间就对红黑树管理的所有事件对象进行一次超时检测

          (2)经历距离当前最快发生超时的事件对象的时间就进行一次超时检测。


在配置文件中,通过使用timer_resulotion 来设置使用常规的定时检测机制,如果没有设置timer_resulotion指令,那么Nginx使用2)中方案。


超时的初始化在ngx_event_process_init函数中进行,调用的是ngx_event_timer_init,代码如下

ngx_int_t
ngx_event_timer_init(ngx_log_t *log)
{
    ngx_rbtree_init(&ngx_event_timer_rbtree, &ngx_event_timer_sentinel,
                    ngx_rbtree_insert_timer_value);


    return NGX_OK;
}


如果设置了timer_resulotion,会创建一个固定的定时器,并进行初始化,调用sigaction设置信号处理函数ngx_timer_signal_handler,代码如下:


    if (ngx_timer_resolution && !(ngx_event_flags & NGX_USE_TIMER_EVENT)) {
        struct sigaction  sa;
        struct itimerval  itv;


        ngx_memzero(&sa, sizeof(struct sigaction));
        sa.sa_handler = ngx_timer_signal_handler;
        sigemptyset(&sa.sa_mask);


        if (sigaction(SIGALRM, &sa, NULL) == -1) {
            ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
                          "sigaction(SIGALRM) failed");
            return NGX_ERROR;
        }


        itv.it_interval.tv_sec = ngx_timer_resolution / 1000;
        itv.it_interval.tv_usec = (ngx_timer_resolution % 1000) * 1000;
        itv.it_value.tv_sec = ngx_timer_resolution / 1000;
        itv.it_value.tv_usec = (ngx_timer_resolution % 1000 ) * 1000;


        if (setitimer(ITIMER_REAL, &itv, NULL) == -1) {
            ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
                          "setitimer() failed");
        }
    }


初始化就这样了,超时的处理过程主要在ngx_process_events_and_timers函数里面,此函数是一个重要处理节点,在前面的系列中多次提到。这个函数首先会决定超时处理的方案,代码如下:

    if (ngx_timer_resolution) {
        timer = NGX_TIMER_INFINITE;
        flags = 0;


    } else {
        timer = ngx_event_find_timer();
        flags = NGX_UPDATE_TIME;


#if (NGX_THREADS)


        if (timer == NGX_TIMER_INFINITE || timer > 500) {
            timer = 500;
        }


#endif
    }

如果是ngx_timer_resolution,timer的值是无限长,如果结合epoll模型来看,就是epoll_wait的等待时间是无限长。如果很长时间没有任何I/O就绪,那么epoll会一直挂在这里,并且导致整个进程也挂起吗,显然不是,前面初始化的时候已经调用setitimer设置了一个定时器,因此当没有继续I/O而定时器周期耗尽的时候,SIGALARM信号就触发了,此信号首先会导致epoll_wait返回,并返回错误代码,另外会调用ngx_timer_signal_handler函数,设置ngx_event_timer_alarm为1,如果errno的值是,EINTR,那么epoll_wait是被因SIGALARM导致的退出,那么此时返回NGX_OK,否则返回NGX_ERROR,代码如下

    if (flags & NGX_UPDATE_TIME) {
        ngx_time_update(0, 0);
    }


    if (err) {
        if (err == NGX_EINTR) {


            if (ngx_event_timer_alarm) {
                ngx_event_timer_alarm = 0;
                return NGX_OK;
            }


            level = NGX_LOG_INFO;


        } else {
            level = NGX_LOG_ALERT;
        }


        ngx_log_error(level, cycle->log, err, "epoll_wait() failed");
        return NGX_ERROR;
    }


      采用经历距离当前最快发生超时的事件对象的时间就进行一次超时检测方案.  timer = ngx_event_find_timer();  flags = NGX_UPDATE_TIME;epoll_wait等待时间为timer,当timer耗尽后,epoll_wait会退出,此时events=0;当timer!=-1时,退出返回NGX_OK。
      如果events >0,说明有I/O处于就绪状态,此时进程会去处理这些I/O。由于NGX的epoll使用的是EPOLL模式,因此处理这些I/O的handle消耗的时间是很短的,因此在ngx_process_events_and_timers函数里面,会通过ngx_process_events消耗的时间来判断是否存在I/O超时,代码如下:
    delta = ngx_current_msec;
    (void) ngx_process_events(cycle, timer, flags);
    delta = ngx_current_msec - delta;
ngx_current_msec是NGX自定义的一个计时器,档ngx_process_events执行前后有时间差,即delta>0的时候,说明存在I/O超时,此时调用ngx_event_expire_timers函数来处理超时事件,

void
ngx_event_expire_timers(void)
{
    ngx_event_t        *ev;
    ngx_rbtree_node_t  *node, *root, *sentinel;


    sentinel = ngx_event_timer_rbtree.sentinel;


    for ( ;; ) {
        root = ngx_event_timer_rbtree.root;


        if (root == sentinel) {
            return;
        }


        node = ngx_rbtree_min(root, sentinel);


        /* node->key > ngx_current_time */


        if ((ngx_msec_int_t) (node->key - ngx_current_msec) > 0) {
            return;
        }


        ev = (ngx_event_t *) ((char *) node - offsetof(ngx_event_t, timer));


        ngx_log_debug2(NGX_LOG_DEBUG_EVENT, ev->log, 0,
                       "event timer del: %d: %M",
                       ngx_event_ident(ev->data), ev->timer.key);


        ngx_rbtree_delete(&ngx_event_timer_rbtree, &ev->timer);


#if (NGX_DEBUG)
        ev->timer.left = NULL;
        ev->timer.right = NULL;
        ev->timer.parent = NULL;
#endif


        ev->timer_set = 0;


        ev->timedout = 1;


        ev->handler(ev);
    }
}
       由于nginx利用红黑树来组织管理超时事件,因此检测是否有事件超时并不需要遍历所有的事件对象,而直接找到最近的即将超时的事件对象,判断其是否超时,如果超时则进行相应的超时处理,处理完了再判断第二近的即将超时的事件对象,如此反复,直到遇到某个事件还未超时或所有事件都超时并已处理就结束检测。对于超时的ngx_event_t,最重要的是设置他的timedout为1,然后调用对应的handle。
如果是我们前面分析的upstream模块的ngx_http_upstream_connect函数,connect成功,ngx_http_upstream_send_request也已发送完成,此时等待SERVER返回数据,如果超时,此时调用的应该是ngx_http_upstream_process_header函数,由于超时,会执行下面代码

    if (c->read->timedout) {
        ngx_http_upstream_next(r, u, NGX_HTTP_UPSTREAM_FT_TIMEOUT);
        return;
    }
   再看看ngx_http_upstream_next函数, 函数的处理过程比较复杂,我们只分析比较简单的情况,当 r->connection->error 非0的时候,说明当前的TCP连接出现了异常,立即调用ngx_http_upstream_finalize_request,释放socket的资源,以及可能存在的ssl资源,并调用ngx_close_connection彻底关闭这个套接口,然后返回。
    另外,会统计u->peer.tries的次数,如果r->connection->error等于0,在一定的情况下,调用ngx_http_upstream_connect重新尝试连接。
    需要提一下的是,NGX并没有针对异步单I/O的撤销操作,这在各个OS上面似乎也不是推荐的方法,但是在WIN平台上有专门针对单I/O的撤销操作CancelIO,但这是不是官方推荐的方法,不得而知,目前来说,即使在IOCP模型里面,权威的资料也推荐通过关闭socket来让其上的异步I/O自动撤销。

你可能感兴趣的:(Nginx源代码分析之网络超时管理(九))