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)

 

posted on 2018-07-17 00:03  镱鍚  阅读(979)  评论(0)    收藏  举报

导航