阿里云-云小站(无限量代金券发放中)
【腾讯云】云服务器、云数据库、COS、CDN、短信等热卖云产品特惠抢购

Tornado实现多进程/多线程的HTTP服务

173次阅读
没有评论

共计 3174 个字符,预计需要花费 8 分钟才能阅读完成。

用 Tornado Web 服务的基本流程

1. 实现处理请求的 Handler,该类继承自 tornado.web.RequestHandler,实现用于处理请求的对应方法如:get、post 等。返回内容用 self.write 方法输出。

2. 实例化一个 Application。构造函数的参数是一个 Handlers 列表,通过正则表达式,将请求与 Handler 对应起来。通过 dict 将 Handler 需要的其他对象以参数的方式传递给 Handler 的 initialize 方法。

3. 初始化一个 tornado.httpserver.HTTPServer 对象,构造函数的参数是上一步的 Application 对象。

4. 为 HTTPServer 对象绑定一个端口。

5. 开始 IOLoop。

需要用到的特性

由于 tornado 的亮点是异步请求,所以这里首先想到的是将所有请求都改造为异步的。但是这里遇到一个问题,就是异步函数内一定不能有阻塞调用出现,否则整个 IOLoop 都会被卡住。这就要求彻底地去改造服务,将所有 IO 或是用时较长的请求都改造为异步函数。这个工程量是非常大的,需要去修改已有的代码。因此,我们考虑用线程池的方式去实现。当一个线程阻塞在某个请求或 IO 时,其他线程或 IOLoop 会继续执行。

另外一个瓶颈就是 GIL 限制了 CPU 的并发数量,因此考虑用子进程的方式增加进程数,提高服务能力上限。

综合上面的分析,大致用以下方案:

1. 通过子进程的方式复制多个进程,使子进程中的只读页指向同一个物理页。

2. 线程池。回避异步改造的工作量,增加 IO 的并发量。

测试代码

首先测试线程池,测试用例为:

对 sleep 页面同时发出两个请求:

1. 在线程池中运行的函数(这里是 self.block_task)能够同时执行。表现为在控制台交替打印出数字。

2. 两个 get 请求几乎同时返回,在浏览器上显示返回的内容。

线程池的测试代码如下:

import os
import sys
import time
 
import tornado.httpserver
import tornado.ioloop
import tornado.options
import tornado.web
import tornado.gen
from tornado.concurrent import run_on_executor
from concurrent.futures import ThreadPoolExecutor
from tornado.options import define, options
 
class HasBlockTaskHandler(tornado.web.RequestHandler):
    executor = ThreadPoolExecutor(20)  #起线程池,由当前 RequestHandler 持有
   
    @tornado.gen.coroutine
    def get(self):
        strTime = time.strftime(“%Y-%m-%d %H:%M:%S”)
        print “in get before block_task %s” % strTime
        result = yield self.block_task(strTime)
        print “in get after block_task”
        self.write(“%s” % (result))
 
    @run_on_executor
    def block_task(self, strTime):
        print “in block_task %s” % strTime
        for i in range(1, 16):
            time.sleep(1)
            print “step %d : %s” % (i, strTime)
        return “Finish %s” % strTime
 
if __name__ == “__main__”:
    tornado.options.parse_command_line()
    app = tornado.web.Application(handlers=[(r”/sleep”, HasBlockTaskHandler)], autoreload=False, debug=False)
    http_server = tornado.httpserver.HTTPServer(app)
    http_server.bind(8888)
    tornado.ioloop.IOLoop.instance().start()

整个代码里有几个位置值得关注:

1.executor = ThreadPoolExecutor(20)。这是给 Handler 类初始化了一个线程池。其中 concurrent.futures 不属于 tornado,是 Python 的一个独立模块,在 python3 中是内置模块,python2.7 需要自己安装。

2. 修饰符 @run_on_executor。这个修饰符将同步函数改造为在 executor(这里是线程池)上运行的异步函数,内部实现是将被修饰的函数 submit 到 executor,返回一个 Future 对象。

3. 修饰符 @tornado.gen.coroutine。被这个修饰符修饰的函数,是一个以同步函数方式编写的异步函数。原本通过 callback 方式编写的异步代码,有了这个修饰符,可以通过 yield 一个 Future 的方式来写。被修饰的函数在 yield 了一个 Future 对象后将会被挂起,Future 对象的结果返回后继续执行。

运行代码后,在两个不同浏览器上访问 sleep 页面,得到了想要的效果。这里有一个小插曲,就是如果在同一浏览器的两个 tab 上进行测试,是无法看到想要的效果。第二个 get 请求会被 block,直到第一个 get 请求返回,服务端才开始处理第二个 get 请求。这让我一度觉得多线程没有生效,用了半天时间查了很多资料,才看到是浏览器把相同的第二个请求 block 了,具体链接参考这里。

由于 tornado 很方便地支持多进程模型,多进程的使用要简单很多,在以上例子中,只需要对启动部分稍作改动即可。具体代码如下所示:

if __name__ == “__main__”:
    tornado.options.parse_command_line()
    app = tornado.web.Application(handlers=[(r”/sleep”, HasBlockTaskHandler)], autoreload=False, debug=False)
    http_server = tornado.httpserver.HTTPServer(app)
    http_server.bind(8888)
    print tornado.ioloop.IOLoop.initialized()
    http_server.start(5)
    tornado.ioloop.IOLoop.instance().start()

需要注意的地方有两点:

app = tornado.web.Application(handlers=[(r”/sleep”, HasBlockTaskHandler)], autoreload=False, debug=False),在生成 Application 对象时,要将 autoreload 和 debug 两个参数至为 False。也就是需要保证在 fork 子进程之前 IOLoop 是未被初始化的。这个可以通过 tornado.ioloop.IOLoop.initialized() 函数来跟。
http_server.start(5) 在启动 IOLoop 之前通过 start 函数设置进程数量,如果设置为 0 表示每个 CPU 都启动一个进程。

最后的效果是可以看到 n + 1 个进程在运行,且公用同一个端口。

正文完
星哥说事-微信公众号
post-qrcode
 0
星锅
版权声明:本站原创文章,由 星锅 于2022-01-21发表,共计3174字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
【腾讯云】推广者专属福利,新客户无门槛领取总价值高达2860元代金券,每种代金券限量500张,先到先得。
阿里云-最新活动爆款每日限量供应
评论(没有评论)
验证码
【腾讯云】云服务器、云数据库、COS、CDN、短信等云产品特惠热卖中