ioloop的blocking_signal

2016-08-04 Li Shuai 更多博文 » 博客 » GitHub »

Python Tornado

原文链接 https://cyrusin.github.io/2016/08/04/ioloop-blocking-signal-20160804/
注:以下为加速网络访问所做的原文缓存,经过重新格式化,可能存在格式方面的问题,或偶有遗漏信息,请以原文为准。


这里的blocking signal里的blocking并不是传统意义上的针对IO的blocking, 尽管这可能是引起ioloop阻塞的一个原因之一。在这里,blocking指的是ioloop在epoll返回之后开始依次处理各监听文件句柄上的IO事件时,直到下一次进入epoll调用的这段时间的ioloop的状态。我们知道Tornado是单线程的,在处理完某次epoll调用返回的读写就绪事件之前,Tornado无法启动下次epoll监听,所以这段时间理论上是越短越好,这样,ioloop可以充分及时的获取就绪文件句柄,不会影响整体IO性能。然而在实际的使用过程中,难免会出现某次处理时间过长,从而导致ioloop的blocking时间过长的现象。此时,假如有大量连接到达或者有多个IO事件等待处理,ioloop是不能及时获取的,进而会影响性能。

Tornado在ioloop中给我们提供了针对blocking_signal的处理方法,通过singal实现软中断,当ioloop的blocking时间达到阈值时,允许我们注册信号处理程序,我们可以借此实现一些额外的功能,比如当blocking超过一定时间时,log出当前调用栈,这样后续可以分析出到底哪块儿调用耗费了太多时间。

使用这个功能时,可以在ioloop启动之前针对ioloop的实例调用set_blocking_signal_threshold方法,这个方法的实现是:

def set_blocking_signal_threshold(self, seconds, action):
    if not hasattr(signal, "setitimer"):
        gen_log.error("set_blocking_signal_threshold requires a signal module "
                      "with the setitimer method")
        return
    self._blocking_signal_threshold = seconds
    if seconds is not None:
        signal.signal(signal.SIGALRM,
                      action if action is not None else signal.SIG_DFL)

从代码可以看出,主要是注册了针对signal.SIGALRM信号的处理程序,这个程序有我们自己来定义。以log出当前调用栈为例:

def log_stack(self, signal, frame):
    """Signal handler to log the stack trace of the current thread.

    For use with `set_blocking_signal_threshold`.
    """
    gen_log.warning('IOLoop blocked for %f seconds in\n%s',
                    self._blocking_signal_threshold,
                    ''.join(traceback.format_stack(frame)))

即标准的信号处理程序的定义方法,log出当前调用栈的话,就直接记日志就行。

Tornado是如何在ioloop中触发signal的,看ioloop的start方法的实现:

def start(self):
   ...
   try:
        while True:
        ...
        if self._blocking_signal_threshold is not None:
        # clear alarm so it doesn't fire while poll is waiting for events.
        signal.setitimer(signal.ITIMER_REAL, 0, 0) # 1
        try:
            event_pairs = self._impl.poll(poll_timeout)
        except Exception as e: 
            ...
        if self._blocking_signal_threshold is not None:
            signal.setitimer(signal.ITIMER_REAL, self._blocking_signal_threshold, 0) # 2
        #处理event_pairs
    except:
        ....

ioloop的start方法的大致框架如上所示,可以看出其实现技巧和注意点是在调用epoll之前,务必将timer清除,防止signal的软中断错误的打断epoll对IO事件的监听,然后在epoll返回之后,假如之前有调用过set_blocking_signal_threshold方法的话,_blocking_signal_threshold不为None,则会设置signal.setitimer这个定时器。而这个signal.ITIMER_REAL对应的定时器则会在self._blocking_signal_threshold时间(假如IO事件的处理到这个阈值还没结束)之后发出signal.SIGALRM信号。我们注册的signal.SIGALRM的信号处理程序就会被调用。