一个进程处理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客户端库,后面有空再继续写。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容

  • 最近两个月的业余时间在写一个私人项目,目的是在Linux下写一个高性能Web服务器,名字叫Zaver。主体框架和基...
    朱佳顺阅读 9,811评论 5 43
  • 大纲 一.Socket简介 二.BSD Socket编程准备 1.地址 2.端口 3.网络字节序 4.半相关与全相...
    VD2012阅读 2,105评论 0 5
  • 1. Nginx的模块与工作原理 Nginx由内核和模块组成,其中,内核的设计非常微小和简洁,完成的工作也非常简单...
    rosekissyou阅读 10,126评论 5 124
  • 聊聊阻塞与非阻塞、同步与异步、I/O 模型 来源:huangguisu 链接:http://blog.csdn.n...
    meng_philip123阅读 1,621评论 1 13
  • 《父亲》 一一周年祭 在盛夏微凉的早晨 你孤独的离我们远去 重现你青年时离别 家乡的无奈 那时你只为奋斗 可你人生...
    湘艾人阅读 298评论 0 1