Tornado多线程异步服务实现
Python的Tornado框架一向是以高性能而著称的,但是自己在测试一个项目的时候,发现由于框架的单线程机制,一不小心就写出了阻塞服务(block)的代码,性能急剧下降。
Tornado的异步包括两个方面,异步服务端和异步客户端,其高性能源于基于Epoll(unix为kqueue)的异步网络IO,具体的异步模型又可以分为回调(callback)和协程(coroutine)。这里探究实现异步的三种方式,yield和IO循环以及线程池。
1、yield
# -*- coding: utf-8 -*- __author__ = 'iyiyang' import tornado.ioloop import tornado.web import tornado.httpserver import tornado.gen import os import functools import datetime class MainHandler(tornado.web.RequestHandler): @tornado.web.asynchronous @tornado.gen.coroutine def get(self, *args, **kwargs): response = yield tornado.gen.Task(self.ping, "127.0.0.1") print response self.finish() @tornado.gen.coroutine def ping(self, url): print 'start ping' os.system("ping " + url) return 'after' if __name__ == "__main__": application = tornado.web.Application([ (r"/", MainHandler)], debug = True, ) application.listen(80) tornado.ioloop.IOLoop.instance().start()
使用yield挂起,虽然看样子我们使用了asynchronous以及coroutine,但是整个服务仍然是单线程阻塞模式,我们查看服务端输出即可看到。测试发现多线程请求20次耗时57s左右。很显然,在处理此类耗时问题时,使用yield挂载性能堪忧。
2、IO循环
前面提到了Tornado框架的高性能基于Epoll(unix为kqueue)的异步网络IO(姑且认为是这个原因吧),实际测试通过IO循环实现异步速度比yield快多了。
# -*- coding: utf-8 -*- __author__ = 'iyiyang' import tornado.ioloop import tornado.web import tornado.httpserver import tornado.gen import os import functools import datetime class MainHandler(tornado.web.RequestHandler): @tornado.web.asynchronous @tornado.gen.coroutine def get(self, *args, **kwargs): tornado.ioloop.IOLoop.instance().add_timeout(1, callback=functools.partial(self.ping, '127.0.0.1')) self.finish() @tornado.gen.coroutine def ping(self, url): print 'start ping' os.system("ping " + url) return 'after' # raise tornado.gen.Return('after') if __name__ == "__main__": application = tornado.web.Application([ (r"/", MainHandler)], debug = True, ) application.listen(80) tornado.ioloop.IOLoop.instance().start()
使用这次方式处理,多线程请求20次耗时28s,比起yield快了一倍。
3、线程池
上述IO循环适合于无需返回的耗时逻辑处理,在我们的耗时处理需要返回值时,可以用线程池来使得计算逻辑超出于主线程之外运行,需要用到futures这个三方模块,python2 可以直接pip安装。
# -*- coding: utf-8 -*- __author__ = 'iyiyang' import tornado.ioloop import tornado.web import tornado.httpserver import tornado.gen import os import functools import datetime from concurrent.futures import ThreadPoolExecutor class Executor(ThreadPoolExecutor): _instance = None def __new__(cls, *args, **kwargs): if not getattr(cls, '_instance', None): cls._instance = ThreadPoolExecutor(max_workers=10) return cls._instance class MainHandler(tornado.web.RequestHandler): @tornado.web.asynchronous @tornado.gen.coroutine def get(self, *args, **kwargs): _future = Executor().submit(self.ping, '127.0.0.1') response = yield tornado.gen.with_timeout(datetime.timedelta(1), _future,quiet_exceptions=tornado.gen.TimeoutError) print response.result() self.finish() @tornado.gen.coroutine def ping(self, url): print 'start ping' os.system("ping " + url) return 'after' # raise tornado.gen.Return('after') if __name__ == "__main__": application = tornado.web.Application([ (r"/", MainHandler)], debug = True, ) application.listen(80) tornado.ioloop.IOLoop.instance().start()
使用线程池的方式,多线程请求20次耗时测试仅需8s,速度有明显提升。
在我们实际使用时,可能由于程序逻辑复杂而导致qps过低,此时我们可以尝试优化逻辑以及多线程异步服务的方式来处理,至于底层优化实现原理,还得多探究。
附qps多线程测试脚本:
#!/usr/bin/env python # -*- coding:utf-8 –*- __author__ = 'iyiyang' import time import threading from Queue import Queue import requests import time import traceback import random class Qps_test(): def __init__(self): self.lock = threading.Lock() self.ipdict = dict() self.sp = Queue() def test(self): while 1: ip = self.sp.get() self.lock.acquire() print 'start to test [%s]' % ip self.lock.release() try: res = requests.get("http://127.0.0.1/?ip=" + ip) self.lock.acquire() print res.text self.lock.release() except Exception, e: self.sp.task_done() continue self.sp.task_done() def run(self, threads, iplist): print "QPS test..." starttime = time.time() for i in xrange(threads): st = threading.Thread(target=self.test) st.setDaemon(True) st.start() for ip in iplist: self.sp.put(ip) self.sp.join() print "[*] Test done,it has Elapsed time:%s " % (time.time() - starttime) if __name__ == "__main__": ip_list = [] ip = '123.23.24.' for i in range(1,20): ip_list.append(ip+str(i)) _ = Qps_test() _.run(10,ip_list)
浙公网安备 33010602011771号