python

Torando适配Uvloop与Asyncio下的性能简测 - 简书

文章暂存

systemime
2021-04-07
14 min

摘要.

0.5242017.10.19 17:03:13 字数 1,381 阅读 7,934

Python 已经 relase3.6 版本了,尝试使用 PY3 来构建服务,由于比较熟悉 Tornado,故测试一下 tornado 在 Python3 下的常见用法。

业务代码通常需要访问三方服务和数据库,因此针对异步的 http 和数据库 io 进行测试。

# 事件循环

Python3.5+ 的标准库asyncio提供了事件循环用来实现协程,并引入了async/await关键字语法以定义协程。Tornado 通过 yield 生成器实现协程,它自身实现了一个事件循环。由于一些三方库都是基于 asyncio 进行,为了更好的使用 python3 新特效带来的异步 IO,实际测试了 Tornado 在不同的事件循环中的性能,以及搭配三方库(motor,asyncpg,aiomysql)的方式。

# tornado app 基本结构

一个基本的 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 默认的事件循环驱动 app,IOLoop 会创建一个事件循环,用于响应 epoll 事件,并调用响应的 handler 处理请求。

# 异步 http client

Tornado 提供了一个异步的 HTTPClient,用于 handler 中访问三方的 api,即使当前的三方 api 访问被阻塞了,也不会阻塞 tornado 响应其他的 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 提供的协程模块。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 事件循环

Aysnc 定义协程方式基本符合 tornado 的协程,但是毕竟不是全兼容了。例如 asyncio.sleep 将不会 work。

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

想要上面的 asyncio.sleep 能够正常,需要替换 I 使用 asyncio 的事件循环替换 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() 可以替换默认的 ioloop。

# uvloop 事件循环

除了标准库 asyncio 的事件循环,社区使用 Cython 实现了另外一个事件循环 uvloop。用来取代标准库。号称是性能最好的 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 直接按照。

# 三种事件循环的性能

三种事件循环中,ioloop 对 asyncio.sleep 兼容性不好。主要考察后面两者事件循环的性能。测试接口类型为三种:

1. 单纯的返回一个子串
2. 异步 httpclient 性能
3. 数据库读写性能

# 单纯返回子串

# IOLoop

使用 100 并发连接,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 压测的结果,并发 500 线程连接,持续测试一分钟:

➜  ~ 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 压测结果

➜  ~ 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 的测试结果

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 压测结果

➜  ~ 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

# 结论

通过一些压测,在三种的横向对比中,其性能大致在一个数量级上,并没有拉开很大的距离,在性能上使用哪一个差不多。考虑到三方库兼容标准的异步 IO,并且 uvloop 驱动的另外一些框架 sanic 和 japronto 都比较不错,并且还可以使用 cython 加速,因此下面针对数据库驱动,使用事件循环为 uvloop。

# 数据库测试

Python 中最常用的是 mysqldb,可是 mysqldb 不支持 python3。python3 中 mysql 驱动以 pymysql 为基础的 aiomysql。而 postgresql 和 mongodb 都提供了基于 asyncio 事件循环的驱动。

# asyncpg

对于 postgresql,比较好的驱动是 asyncpg,维护的活跃度和性能都比 aiopg 更好。使用 asyncpg 的方式如下:

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

        
        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:
            
            async with connection.transaction():
                
                rows = await connection.fetch("SELECT * FROM public.user ")
                
                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() 

一种方式使用了短链接,即每一个请求,handler 会创建一个数据库连接,完成查询再关闭,另外一种方式则是使用数据库连接池。当超过连接池的访问,handler 会阻塞,但是不会阻塞整个服务。

# 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 的驱动为 motor,它也实现了对 asyncio 的支持,其使用方式如下:

 class MongodbHandler(web.RequestHandler):
    async def get(self):
        ret = await self.application.motor_client.hello.find_one()
        
        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() 

# 读取数据的性能

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 的时候,会出现连接异常(Too Many Connection)。mongodb 也会出现Can't assign requested address的异常。

因为数据库读写都是 non-block,因此 db 和 mongodb 模式都会因请求的增长而增长,当瞬时达到最大连接数将会 raise 异常。而 pool 的方式会等待连接释放,再发起数据库查询。而且性能最好。aiomysql 的连接池方式与 pq 类似。

在同步带 mysql 驱动中,经常维护一个 mysql 长连接。而异步的驱动则不能这样,因为一个连接阻塞了,另外的协程还是无法读取这个连接。最好的方式还是使用连接池管理连接。

# 结论

Tornado 的作者也指出过,他的测试过程中,使用 asyncio 和 tornado 自带的 epoll 事件循环性能差不多。并且 tornado5.0 会考虑完全吸纳 asyncio。在此之前,使用 tornado 无论是使用自带的事件循环还是 asyncio 活着 uvloop,在性能方面上都差不不大。需要兼容数据库或 http 库的时候,使用 uvloop 的驱动方式,兼容性最好~

更多精彩内容下载简书 APP

"小礼物走一走,来简书关注我"

共 1 人赞赏

总资产 558 (约 35.95 元) 共写了 19.5W 字获得 2,695 个赞共 2,369 个粉丝

# 被以下专题收入,发现更多相似内容

# 推荐阅读更多精彩内容

  • 环境管理管理 Python 版本和环境的工具。p–非常简单的交互式 python 版本管理工具。pyenv–简单的 Pyth...

  • GitHub 上有一个 Awesome - XXX 系列的资源整理, 资源非常丰富,涉及面非常广。awesome-p...

    若与阅读 17,102 评论 4 赞 419

  • title 标题: A Web Crawler With asyncio Coroutinesauthor 作者: A...

  • 就是个垃圾,总以为自己牛逼的很,今天不管你是真心想帮我还是为了在我面前显示你牛逼,就那么随意的我要好好做的事...

  • 痛在自己身上 爱在自己身上 都说旁观者清当局者迷 其实 局中人比任何人都明白自己的咎由自取 也比任何人都明白曾有多... 花妖姬阅读 79 评论 0 赞 0 https://www.jianshu.com/p/6d6fa94a01ef https://www.jianshu.com/p/6d6fa94a01ef

上次编辑于: 2021/5/20 下午3:26:49