一个进程处理10K连接:分析Tornado源码

本文将通过最简单的Hello world代码分析Tornado本身的结构,在此基础上再简单介绍下Tornado适用的一些场景。

在众多的Python web框架中,Tornado是比较有特色的一个,不仅仅是因为Tornado自带webserver,而且这个webserver是非阻塞的并且使用了epoll。对比来说其他的webserver,比如使用了pre fork模式的gunicorn,是通过预先创建好的线程来处理并发请求,但是假如请求阻塞了那这个线程也就阻塞了,假如所有预分配的线程都阻塞了那新的请求就不能读取了。当然我们可以增加进程或者线程,但进程与线程都是很昂贵的,而且迟早会遇到C10K问题。而Tornado利用了操作系统的epoll或者kqueue技术来非阻塞的处理高并发的请求解决了这个问题。

但如果Tornado只是webserver是非阻塞的还不足以应对高并发场景,webserver只能增加接收请求时的性能,处理请求本身也存在阻塞的问题,为此Tornado的web框架自带了一套异步I/O库并且与框架本身深度集成。Tornado并非创造了什么新的技术,而是综合了三种技术于一身:基于epoll等的异步 HTTP Server(类似的像Nginx)、异步I/O库(类似的像Gevent)、web框架(类似的像Flask)。

Tornado HTTP Server的创建过程

先看下Tornado官网给出的Hello world代码:

import tornado.ioloop
import tornado.web

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.write("Hello, world")

def make_app():
    return tornado.web.Application([(r"/", MainHandler),])

if __name__ == "__main__":
    app = make_app()
    app.listen(8888)
    tornado.ioloop.IOLoop.current().start()

先看main,首先执行了 app = make_app(),很显然返回了

tornado.web.Application([(r"/", MainHandler), ])

上面的代码创建了Application类的实例app,用来配置应用的各种信息,对于一个web应用来说,最重要的信息就是路由信息了,当然还有很多其他的配置,在Hello world代码中没有展示。路由信息事实上是一个list,这个list的每个元素都是一个tuple,每个tuple就代表了一条路由,tuple的第一个元素是路由规则用正则式表达,第二个元素就是一个处理请求的类比如MainHandler。这样Application就记下了r"/"用MainHandler来处理。
然后是

app.listen(8888)

这段代码直接来看是监听8888这个端口,详细的来看listen方法如下:

def listen(self, port, address="", **kwargs):
        from tornado.httpserver import HTTPServer
                server = HTTPServer(self, **kwargs)
                server.listen(port, address)

这里可以发现tornado创建了HTTPServer类的实例server,这个HTTPServer做了一些初始化的工作,创建了后面要用到的一些对象,在执行完server.listen(port, address)后指定的端口与地址就绑定到了这个server上并创建sockets对象,但更重要的是同时IOLoop的实例IOLoop.current()被创建并赋值给server.io_loop。IOLoop创建的就是负责主循环的实例了,上面已经说了Tornado的webserver是非阻塞的,实际上是使用了I/O多路复用的技术(在初始化时epoll会被优先选择其次是选kqueue最后是select)。
add_sockets的代码如下:

 def add_sockets(self, sockets):
        if self.io_loop is None:
            self.io_loop = IOLoop.current()

        for sock in sockets:
            self._sockets[sock.fileno()] = sock
            add_accept_handler(sock, self._handle_connection,
                               io_loop=self.io_loop)

这里的sockets就是刚刚调用listen方法时创建的对象,这个对象是可迭代的,但这个时候事实上只有一个socket。可以看到sock的fd(就是sock.fileno())与sock本身以key-value对的形式保存到了self._sockets中,self._sockets在后面会以这种形式维护并发的socket。接下来add_accept_handler方法实际上是调用了 io_loop.add_handler(sock, accept_handler, IOLoop.READ)这个方法,fd-accept_handler这样的key-value对就维护到了io_loop中,换句话说,这个时候io_loop(主循环)就维护了一个字典,这个字典的key是fd,value是处理这个fd描述的socket被创建时要执行的回调函数(这个回调函数具体在后面介绍),但是到目前为止这个信息还只是维护在了app里,操作系统并不知道,要让操作系统知道这些信息就需要通过刚才已经初始化好的epoll方法,epoll方法作为app与操作系统的代理可以把fd以及对应的socket事件注册到操作系统中去。这些准备工作完成后io_loop的start方法就会被调用,开始进入事件循环,不断的接收I/O event并调用回调函数进行处理。

Request的处理过程

先看下上面提到的回调函数的实际代码(节选主要的):

     try:
            stream = IOStream(connection, io_loop=self.io_loop,
                                  max_buffer_size=self.max_buffer_size,
                                  read_chunk_size=self.read_chunk_size)
            future = self.handle_stream(stream, address)
        except Exception:
            app_log.error("Error in connection callback", exc_info=True)

可以看到当有connection进来的时候,首先创建了IOStream的实例stream ,然后把这个stream交给handle_stream函数处理,而这个函数实际上是把stream(通过了一些解析)交给app来处理,app就是一开始创建的Application对象,在app初始化的时候主要的参数就是路由信息,当调用app的__call__方法时,就会从路由中找出对应的处理方法,在Hello world例子中就是MainHandler,MainHandler继承自RequestHandler类,RequestHandler是Tornado中大家非常熟悉的类了,我们通过实现head、get、post、delete、patch、put、options等中的至少一个方法来实现业务逻辑。在Hello world例子中实现的是get方法,处理完后调用了write方法将数据写入缓冲区。最终由finish方法(自动调用)完成本次响应。

小节

上面已经把整个Hello world的代码分析一遍,覆盖了Tornado web方面比较重要的Application、HTTPServer、RequestHandler以及network方面的IOLoop这几个库。
因为Tornado是异步非阻塞的,所以比较适用的场景是一些有阻塞的长连接,比如websocket以及需要比较长处理时间且并发高的请求。相比较prefork模式的web server,Tornado能处理的并发更高,但如果请求的处理过程中包含了很多同步逻辑就不适合用Tornado来处理的,为了解决这个问题Tornado在tornado.httpclient还实现了异步的http客户端库,后面有空再继续写。

推荐阅读更多精彩内容