Torando適配Uvloop與Asyncio下的性能簡測

Python已經(jīng)relase3.6版本了,嘗試使用PY3來構(gòu)建服務(wù),由于比較熟悉Tornado,故測試一下tornado在Python3下的常見用法。

業(yè)務(wù)代碼通常需要訪問三方服務(wù)和數(shù)據(jù)庫,因此針對(duì)異步的http和數(shù)據(jù)庫io進(jìn)行測試。

事件循環(huán)

Python3.5+ 的標(biāo)準(zhǔn)庫asyncio提供了事件循環(huán)用來實(shí)現(xiàn)協(xié)程,并引入了async/await關(guān)鍵字語法以定義協(xié)程。Tornado通過yield生成器實(shí)現(xiàn)協(xié)程,它自身實(shí)現(xiàn)了一個(gè)事件循環(huán)。由于一些三方庫都是基于asyncio進(jìn)行,為了更好的使用python3新特效帶來的異步IO,實(shí)際測試了Tornado在不同的事件循環(huán)中的性能,以及搭配三方庫(motor,asyncpg,aiomysql)的方式。

tornado app基本結(jié)構(gòu)

一個(gè)基本的tornado app代碼如下:

import tornado.httpserver as httpserver
import tornado.ioloop as ioloop
import tornado.options as options
import tornado.web as web

options.parse_command_line()
class IndexHandler(web.RequestHandler):
    def get(self):
        self.finish("It works")


class App(web.Application):
    def __init__(self):
        settings = {
            'debug': True
        }
        super(App, self).__init__(
            handlers=[
                (r'/', IndexHandler)
            ],
            **settings)


if __name__ == '__main__':
    app = App()
    server = httpserver.HTTPServer(app, xheaders=True)
    server.listen(5010)
    ioloop.IOLoop.instance().start()

使用tornado默認(rèn)的事件循環(huán)驅(qū)動(dòng)app,IOLoop會(huì)創(chuàng)建一個(gè)事件循環(huán),用于響應(yīng)epoll事件,并調(diào)用響應(yīng)的handler處理請求。

異步http client

Tornado提供了一個(gè)異步的HTTPClient,用于handler中訪問三方的api,即使當(dāng)前的三方api訪問被阻塞了,也不會(huì)阻塞tornado響應(yīng)其他的handler。

class GenHttpHandler(web.RequestHandler):
    @gen.coroutine
    def get(self):
        url = 'http://127.0.0.1:5000/'
        client = httpclient.AsyncHTTPClient()
        resp = yield client.fetch(url)
        print(resp.body)
        self.finish(resp.body)

gen是tornado提供的協(xié)程模塊。python3中還可以使用 async/await的語法

class AsyncHttpHandler(web.RequestHandler):
    async def get(self):
        url = 'http://127.0.0.1:5000/'
        client = httpclient.AsyncHTTPClient()
        resp = await client.fetch(url)
        print(resp.body)
        self.finish(resp.body)

asyncio 事件循環(huán)

Aysnc定義協(xié)程方式基本符合tornado的協(xié)程,但是畢竟不是全兼容了。例如asyncio.sleep 將不會(huì)work。

class SleepHandler(web.RequestHandler):
    async def get(self):
        print("hello tornado")
        await asyncio.sleep(5)
        self.write('It works!')

想要上面的asyncio.sleep 能夠正常,需要替換I使用asyncio的事件循環(huán)替換ioloop。

if __name__ == '__main__':
    tornado_asyncio.AsyncIOMainLoop().install()
    app = App()
    server = httpserver.HTTPServer(app, xheaders=True)
    server.listen(5020)
    asyncio.get_event_loop().run_forever()

使用 tornado_asyncio.AsyncIOMainLoop() 可以替換默認(rèn)的ioloop。

uvloop 事件循環(huán)

除了標(biāo)準(zhǔn)庫asyncio的事件循環(huán),社區(qū)使用Cython實(shí)現(xiàn)了另外一個(gè)事件循環(huán)uvloop。用來取代標(biāo)準(zhǔn)庫。號(hào)稱是性能最好的python異步IO庫。使用uvloop的方式如下:

if __name__ == '__main__':
    asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
    tornado_asyncio.AsyncIOMainLoop().install()
    app = App()
    server = httpserver.HTTPServer(app, xheaders=True)
    server.listen(5030)
    asyncio.get_event_loop().run_forever()

由于 uvloop依賴 cython,因此需要按照 cython,兩者都可以使用pip直接按照。

三種事件循環(huán)的性能

三種事件循環(huán)中,ioloop對(duì)asyncio.sleep 兼容性不好。主要考察后面兩者事件循環(huán)的性能。測試接口類型為三種:

1.單純的返回一個(gè)子串
2.異步httpclient性能
3.數(shù)據(jù)庫讀寫性能

單純返回子串

IOLoop

使用 100并發(fā)連接,10000請求量壓測

ab -k -c100 -n10000 http://127.0.0.1:5010/

Server Software:        TornadoServer/4.5.1
Server Hostname:        127.0.0.1
Server Port:            5010

Document Path:          /
Document Length:        8 bytes

Concurrency Level:      100
Time taken for tests:   5.615 seconds
Complete requests:      10000
Failed requests:        0
Keep-Alive requests:    10000
Total transferred:      2260000 bytes
HTML transferred:       80000 bytes
Requests per second:    1780.84 [#/sec] (mean)
Time per request:       56.153 [ms] (mean)
Time per request:       0.562 [ms] (mean, across all concurrent requests)
Transfer rate:          393.04 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:     2   56   5.9     56     154
Waiting:        2   56   5.9     56     154
Total:          5   56   5.8     56     158

Qps 為 1780.84

使用 wrk 壓測的結(jié)果,并發(fā)500線程連接,持續(xù)測試一分鐘:

?  ~ wrk -t12 -c500 -d60 http://127.0.0.1:5010/
Running 1m test @ http://127.0.0.1:5010/
  12 threads and 500 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   284.66ms   57.85ms 422.16ms   85.62%
    Req/Sec   139.33     94.69   696.00     64.84%
  99270 requests in 1.00m, 19.12MB read
  Socket errors: connect 0, read 582, write 0, timeout 0
Requests/sec:   1651.92
Transfer/sec:    325.87KB
Asyncio
Concurrency Level:      100
Time taken for tests:   5.616 seconds
Complete requests:      10000
Failed requests:        0
Keep-Alive requests:    10000
Total transferred:      2260000 bytes
HTML transferred:       80000 bytes
Requests per second:    1780.69 [#/sec] (mean)
Time per request:       56.158 [ms] (mean)
Time per request:       0.562 [ms] (mean, across all concurrent requests)
Transfer rate:          393.00 [Kbytes/sec] received

qps 1780.69

Wrk 壓測結(jié)果

?  ~ wrk -t12 -c500 -d60 http://127.0.0.1:5020/
Running 1m test @ http://127.0.0.1:5020/
  12 threads and 500 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   265.34ms   32.16ms 453.76ms   83.32%
    Req/Sec   157.85    104.58   696.00     63.36%
  108364 requests in 1.00m, 20.88MB read
  Socket errors: connect 0, read 458, write 2, timeout 0
Requests/sec:   1803.34
Transfer/sec:    355.74KB
uvloop

uvloop的測試結(jié)果

Concurrency Level:      100
Time taken for tests:   5.612 seconds
Complete requests:      10000
Failed requests:        0
Keep-Alive requests:    10000
Total transferred:      2260000 bytes
HTML transferred:       80000 bytes
Requests per second:    1781.98 [#/sec] (mean)
Time per request:       56.117 [ms] (mean)
Time per request:       0.561 [ms] (mean, across all concurrent requests)
Transfer rate:          393.29 [Kbytes/sec] received

Wrk 壓測結(jié)果

?  ~ wrk -t12 -c500 -d60 http://127.0.0.1:5030/
Running 1m test @ http://127.0.0.1:5030/
  12 threads and 500 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   272.23ms   47.65ms 457.63ms   87.26%
    Req/Sec   148.17    103.62   570.00     63.33%
  104625 requests in 1.00m, 20.16MB read
  Socket errors: connect 0, read 567, write 0, timeout 0
Requests/sec:   1740.76
Transfer/sec:    343.39KB

異步httpclient性能

異步的httpclient性能指在handler中訪問別的api,如三方請求。測試的性能大致如下:

- loop asyncio uvloop
ab 571.12 462.64 534.99
wrk 448.11 444.63 411.19

結(jié)論

通過一些壓測,在三種的橫向?qū)Ρ戎?,其性能大致在一個(gè)數(shù)量級(jí)上,并沒有拉開很大的距離,在性能上使用哪一個(gè)差不多??紤]到三方庫兼容標(biāo)準(zhǔn)的異步IO,并且uvloop驅(qū)動(dòng)的另外一些框架 sanic和 japronto都比較不錯(cuò),并且還可以使用cython加速,因此下面針對(duì)數(shù)據(jù)庫驅(qū)動(dòng),使用事件循環(huán)為 uvloop。

數(shù)據(jù)庫測試

Python中最常用的是 mysqldb,可是mysqldb不支持python3。python3中mysql驅(qū)動(dòng)以pymysql為基礎(chǔ)的aiomysql。而postgresql和mongodb都提供了基于asyncio事件循環(huán)的驅(qū)動(dòng)。

asyncpg

對(duì)于 postgresql,比較好的驅(qū)動(dòng)是 asyncpg,維護(hù)的活躍度和性能都比 aiopg更好。使用asyncpg的方式如下:


class DatabaseHandler(web.RequestHandler):
    async def get(self):
        conn = await asyncpg.connect('postgresql://postgres@localhost/test')

        # rows = await conn.fetchrow('select pg_sleep(5)')
        rows = await conn.fetchrow('select * from public.user')
        print(rows[0])
        await conn.close()

        self.finish("ok")


class PoolHandler(web.RequestHandler):
    async def get(self):
        pool = self.application.pool
        async with pool.acquire() as connection:
            # Open a transaction.
            async with connection.transaction():
                # Run the query passing the request argument.
                rows = await connection.fetch("SELECT * FROM public.user ")
                # rows = await connection.fetch("SELECT pg_sleep(1) ")
                print(rows)

        self.finish("ok")


class App(web.Application):
    def __init__(self, pool):
        settings = {
            'debug': True
        }
        self._pool = pool
        super(App, self).__init__(
            handlers=[
                (r'/', IndexHandler),
                (r'/db', DatabaseHandler),
                (r'/pool', PoolHandler),
            ],
            **settings)

    @property
    def pool(self):
        return self._pool


async def init_db_pool():
    return await asyncpg.create_pool(database='test',
                                     user='postgres')


def init_app(pool):
    app = App(pool)
    return app


if __name__ == '__main__':
    asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
    tornado_asyncio.AsyncIOMainLoop().install()

    loop = asyncio.get_event_loop()
    pool = loop.run_until_complete(init_db_pool())
    app = init_app(pool=pool)
    server = httpserver.HTTPServer(app, xheaders=True)
    server.listen(5040)
    loop.run_forever()

一種方式使用了短鏈接,即每一個(gè)請求,handler會(huì)創(chuàng)建一個(gè)數(shù)據(jù)庫連接,完成查詢再關(guān)閉,另外一種方式則是使用數(shù)據(jù)庫連接池。當(dāng)超過連接池的訪問,handler會(huì)阻塞,但是不會(huì)阻塞整個(gè)服務(wù)。

aiomysql

class PoolHandler(web.RequestHandler):
    async def get(self):
        pool = self.application.pool
        async with pool.acquire() as conn:
            async with conn.cursor() as cur:
                await cur.execute("SELECT * FROM users_account LIMIT 1")
                ret = await cur.fetchone()
                print(ret)

        self.finish("ok")

class App(web.Application):
    def __init__(self, pool):
        settings = {
            'debug': True
        }
        self._pool = pool
        super(App, self).__init__(
            handlers=[
                (r'/pool', PoolHandler),
            ],
            **settings)

    @property
    def pool(self):
        return self._pool


async def init_db_pool(loop):

    return await aiomysql.create_pool(host='127.0.0.1', port=3306,
                                      user='root', password='root',
                                      db='hydra', loop=loop)

def init_app(pool):
    app = App(pool)
    return app




if __name__ == '__main__':
    asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
    tornado_asyncio.AsyncIOMainLoop().install()

    loop = asyncio.get_event_loop()
    pool = loop.run_until_complete(init_db_pool(loop=loop))
    app = init_app(pool=pool)
    server = httpserver.HTTPServer(app, xheaders=True)
    server.listen(5070)
    loop.run_forever()

motor

Mongodb的驅(qū)動(dòng)為motor,它也實(shí)現(xiàn)了對(duì)asyncio的支持,其使用方式如下:


class MongodbHandler(web.RequestHandler):
    async def get(self):
        ret = await self.application.motor_client.hello.find_one()
        # ret = await self.application.motor_client.hello.insert({'hello': 'world'})
        print(ret)
        self.finish("It works !")

class App(web.Application):
    def __init__(self):
        settings = {
            'debug': True
        }
        super(App, self).__init__(
            handlers=[
                (r'/', IndexHandler),
                (r'/mongodb', MongodbHandler),

            ],
            **settings)

    @property
    def motor_client(self):
        client = motor_asyncio.AsyncIOMotorClient('mongodb://localhost:27017')
        return client['test']


if __name__ == '__main__':
    asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
    tornado_asyncio.AsyncIOMainLoop().install()
    app = App()
    server = httpserver.HTTPServer(app, xheaders=True)
    server.listen(5060)
    asyncio.get_event_loop().run_forever()

讀取數(shù)據(jù)的性能

ab -c100 -n10000

Wrk -t12 -c100 -d60s

asyncpg-db asyncpg-pool aiomysql motor
ab 305.49 898.84 669.75 236.82
wrk 281.60 819.23 655.58 252.51

壓測中,使用 wrk 500的連接,壓測 db的時(shí)候,會(huì)出現(xiàn)連接異常(Too Many Connection)。mongodb也會(huì)出現(xiàn)Can't assign requested address的異常。

因?yàn)閿?shù)據(jù)庫讀寫都是non-block,因此db和mongodb模式都會(huì)因請求的增長而增長,當(dāng)瞬時(shí)達(dá)到最大連接數(shù)將會(huì)raise異常。而pool的方式會(huì)等待連接釋放,再發(fā)起數(shù)據(jù)庫查詢。而且性能最好。aiomysql的連接池方式與pq類似。

在同步帶 mysql 驅(qū)動(dòng)中,經(jīng)常維護(hù)一個(gè)mysql長連接。而異步的驅(qū)動(dòng)則不能這樣,因?yàn)橐粋€(gè)連接阻塞了,另外的協(xié)程還是無法讀取這個(gè)連接。最好的方式還是使用連接池管理連接。

結(jié)論

Tornado的作者也指出過,他的測試過程中,使用asyncio和tornado自帶的epoll事件循環(huán)性能差不多。并且tornado5.0會(huì)考慮完全吸納asyncio。在此之前,使用tornado無論是使用自帶的事件循環(huán)還是asyncio活著uvloop,在性能方面上都差不不大。需要兼容數(shù)據(jù)庫或http庫的時(shí)候,使用uvloop的驅(qū)動(dòng)方式,兼容性最好~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 環(huán)境管理管理Python版本和環(huán)境的工具。p–非常簡單的交互式python版本管理工具。pyenv–簡單的Pyth...
    MrHamster閱讀 3,956評(píng)論 1 61
  • GitHub 上有一個(gè) Awesome - XXX 系列的資源整理,資源非常豐富,涉及面非常廣。awesome-p...
    若與閱讀 19,321評(píng)論 4 417
  • title標(biāo)題: A Web Crawler With asyncio Coroutinesauthor作者: A...
    彰樂樂樂樂閱讀 2,209評(píng)論 0 8
  • 就是個(gè)垃圾,總以為自己牛逼的很,今天不管你是真心想幫我還是為了在我面前顯示你牛逼,就那么隨意的我要好好做的事...
    煙花雨下的諾言閱讀 201評(píng)論 0 0
  • 痛在自己身上 愛在自己身上 都說旁觀者清當(dāng)局者迷 其實(shí) 局中人比任何人都明白自己的咎由自取 也比任何人都明白曾有多...
    花妖姬閱讀 246評(píng)論 0 0

友情鏈接更多精彩內(nèi)容