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)方式,兼容性最好~