书接上文。
使用 aiohttp 作为 Web 服务器
上篇文章中提到,aiohttp 不仅仅是一个 http 客户端,同时也是一个 Web 服务器。在这一节,我们使用 aiohttp 实现一个简单的 Web 程序,同时与 flask 比较一下性能上的差别。
准备工作
首先安装我们需要的第三方库:
1 2
| pip install aiohttp pip install flask
|
然后准备好要使用的 Web 容器,这里我们使用对 aiohttp 和 flask 都很友好的 gunicorn。为了让 flask 得到异步支持, 需要同时安装 gevent:
1
| pip install gunicorn[gevent]
|
安装 wrk,它是一个简单的性能测试工具:
Hello, world
我们的起手式当然是 Hello, world。这里,我们分别使用 flask 和 aiohttp 实现一个返回 Hello, world 的 Web 服务。
flask
1 2 3 4 5 6 7 8 9
| from flask import Flask
app = Flask("flask_app")
@app.route("/") def hello(): return "Hello, world!"
|
非常简单!
接下来让我们看一下性能测试的结果。首先用 gunicorn 启动应用,将 socket 绑定到 localhost:5000,打开访问日志,使用 4 个 worker,并使用 gevent 作为 worker 的类型:
1
| gunicorn -b localhost:5000 --access-logfile - -w 4 -k gevent flask_app:app
|
然后就可以进行性能测试了,这里我们使用 8 个线程,每个线程负责 200 个请求,共测试 10 秒,并开启详细日志:
1
| wrk -t8 -c200 -d10s --latency http://localhost:5000
|
我们会得到这样的结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| Running 10s test @ http://localhost:5000 8 threads and 200 connections Thread Stats Avg Stdev Max +/- Stdev Latency 9.80ms 94.91ms 1.67s 99.06% Req/Sec 1.07k 555.92 2.14k 63.76% Latency Distribution 50% 1.55ms 75% 1.82ms 90% 2.27ms 99% 20.89ms 23325 requests in 10.01s, 3.96MB read Socket errors: connect 0, read 0, write 0, timeout 8 Requests/sec: 2330.74 Transfer/sec: 405.20KB
|
这里只关注几个重要的信息:
- Latency,可以理解为响应时间,wrk 提供了平均值,标准差,最大值,以及正负一个标准差的占比;
- Req/Sec,每个线程每秒钟的完成的请求数,同样有以上数据类型;
- Latency Distribution,响应时间的分布情况,50%、75%、90%、99%的请求在多长时间内结束;
- Socket errors,在测试中有多少错误发生;
- Requests/sec,每秒钟完成多少请求;
- Transfer/sec,每秒钟产生的数据量;
知道了上述信息的含义,就可以对应用程序的性能有一个大概的了解了。
aiohttp
同样我们用 aiohttp 实现一个 Hello world 应用:
1 2 3 4 5 6 7 8 9 10 11 12 13
| from aiohttp import web
routes = web.RouteTableDef()
@routes.get("/") async def hello(request): return web.Response(text="Hello, world!")
app = web.Application() app.add_routes(routes)
|
然后用 gunicorn 启动它:
1
| gunicorn -b localhost:5000 --access-logfile - -w 4 -k aiohttp.GunicornWebWorker aio_app:app
|
大部分参数都相同,唯一的区别是使用了 aiohttp 的 wroker 类型。
也用同样的参数启动 wrk,得到以下结果:
1 2 3 4 5 6 7 8 9 10 11 12 13
| Running 10s test @ http://localhost:5000 8 threads and 200 connections Thread Stats Avg Stdev Max +/- Stdev Latency 32.58ms 15.57ms 120.43ms 68.40% Req/Sec 773.91 167.40 1.16k 67.25% Latency Distribution 50% 36.83ms 75% 42.40ms 90% 46.73ms 99% 66.48ms 61709 requests in 10.03s, 9.65MB read Requests/sec: 6150.18 Transfer/sec: 0.96MB
|
对比
我们把性能测试的结果放在一块对比一下,左边是 flask,右边是 aiohttp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| Running 10s test @ http://localhost:5000 Running 10s test @ http://localhost:5000 8 threads and 200 connections 8 threads and 200 connections Thread Stats Avg Stdev Max +/- Stdev Thread Stats Avg Stdev Max +/- Stdev Latency 9.80ms 94.91ms 1.67s 99.06% Latency 32.58ms 15.57ms 120.43ms 68.40% Req/Sec 1.07k 555.92 2.14k 63.76% Req/Sec 773.91 167.40 1.16k 67.25% Latency Distribution Latency Distribution 50% 1.55ms 50% 36.83ms 75% 1.82ms 75% 42.40ms 90% 2.27ms 90% 46.73ms 99% 20.89ms 99% 66.48ms 23325 requests in 10.01s, 3.96MB read 61709 requests in 10.03s, 9.65MB read Socket errors: connect 0, read 0, write 0, timeout 8 Requests/sec: 2330.74 Requests/sec: 6150.18 Transfer/sec: 405.20KB Transfer/sec: 0.96MB
|
可以看出 flask 在单个请求的耗时上明显胜于 aiohttp,但是标准差巨大,在压力场景下最大耗时长达 1.67s,甚至出现了 8 个超时的连接,而 aiohttp 的请求耗时比较稳定;最重要的区别在于,aiohttp 每秒完成了多达 6150.18 个请求,是 flask 的近 3 倍!flask 中 1% 超过 20.89ms 的请求严重影响了整体的性能。
点击计数
通过上面的 Hello, world 程序我们可以发现,使用 aiohttp 可以显著提升 Web 程序的性能。当然 Web 程序并不止于此,它还需要数据库、缓存、消息队列等等组件协同工作。asyncio 的周边虽然在迅速发展,不过仍不完善。好消息是 RabbitMQ 的 Python 驱动在下一个版本也加入了 asyncio 支持,基本组件大部分都支持了 asyncio。在这一节,我们增加 redis 支持,制作一个简单的点击计数器。
准备工作
在本节,我们需要安装 redis,并启动它:
1 2
| brew install redis brew services start redis
|
准备好支持 asyncio 的 redis 驱动:
定义 App
1 2 3
| from aiohttp import web
app = web.Application()
|
初始化 redis
在 aiohttp 中使用 redis 需要在应用启动前初始化连接池,并在应用退出后关闭连接池:
1 2 3 4 5 6 7 8
| import aioredis
async def setup_redis(app): redis_url = "redis://@localhost/0" app["redis"] = await aioredis.create_redis_pool(redis_url) yield app["redis"].close() await app["redis"].wait_closed()
|
并将初始化函数注册到 app 的清理上下文:
1
| app.cleanup_ctx.append(setup_redis)
|
编写路由
1 2 3 4 5 6 7 8
| routes = web.RouteTableDef()
@routes.get("/hit") async def hello(request): redis = request.app["redis"] return web.json_response({"hit": await redis.incr("hit")})
app.add_routes(routes)
|
这样,一个点击计数器就完成了。我们试着请求一下:
1 2 3 4 5 6 7 8 9 10
| $ http localhost:5000/hit HTTP/1.1 200 OK Content-Length: 10 Content-Type: application/json; charset=utf-8 Date: Sat, 09 Jun 2018 08:37:33 GMT Server: Python/3.6 aiohttp/3.3.1
{ "hit": 1 }
|
之后每次请求 hit 的值都会 +1。
对比
同样,我也编写了一个 flask 版本的点击计数器,在这里只展示一下对比结果:
aiohttp 版本:
1 2 3 4 5 6 7 8 9 10 11 12 13
| Running 10s test @ http://localhost:5000/hit 8 threads and 200 connections Thread Stats Avg Stdev Max +/- Stdev Latency 35.83ms 7.18ms 90.80ms 70.37% Req/Sec 699.60 86.70 0.92k 68.62% Latency Distribution 50% 35.01ms 75% 40.39ms 90% 44.61ms 99% 58.03ms 55816 requests in 10.04s, 9.10MB read Requests/sec: 5559.97 Transfer/sec: 0.91MB
|
flask 版本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| Running 10s test @ http://localhost:5000/hit 8 threads and 200 connections Thread Stats Avg Stdev Max +/- Stdev Latency 123.99ms 171.06ms 1.89s 95.32% Req/Sec 262.03 163.41 670.00 69.57% Latency Distribution 50% 106.59ms 75% 156.67ms 90% 179.60ms 99% 1.05s 20484 requests in 10.07s, 3.33MB read Socket errors: connect 0, read 19, write 0, timeout 0 Requests/sec: 2033.33 Transfer/sec: 338.51KB
|
可以看出增加了 redis 的内存 io 操作后 aiohttp 的领先优势巨大,而 Web 应用大多是重 io 的,可以预见到 asyncio 在未来会占据更重要的地位。
结语
写完这篇,这个系列的文章就到此为止了。整体上 asyncio 仍在继续发展,有越来越多的基础组件已经有了可用的 asyncio 版本,一些大厂已经有转向 asyncio 的趋势了。asyncio 是未来,是一定要了解的~