2021 年,用 Python 部署异步网络服务的最佳实践是什么?

2021-01-24 19:41:19 +08:00
 LeeReamond

先说几句题外话,前两天看见一个帖子,提到异步框架的,里面很多人推荐 fastapi 。我个人说来很惭愧,学习 python 的 web 框架,入门是 flask,异步是 aiohttp,一直与 django 和 fastapi 这类主流的、用的人比较多的框架无缘。

所以这次也是想学习一下 fastapi,看看相对于一直使用的原生 aiohttp 有什么区别。

根据我个人理解,异步从 python3.4 版本提出以来,现在已经不是像 3.6 版本时候那样大家都不会用,现在用异步的人应该越来越多了。目前主流不管是公司内部服务,还是生产级服务,如果上 python 的话,如果要用异步的话,应该是很多人使用 django 的 asgi,一些人使用 fastapi,几乎没有人使用 aiohttp 这样。tornado 我不太了解,因为我最初接触异步是 3.5 时代,彼时 tornado 的异步是用猴子补丁实现的,所以一直也没做接触,不知道现在是怎么样了。

使用异步框架当然第一步还是看性能,我去 fastapi 官网看了一下教学,教学写的很友好,直接就推荐了 fastapi+uvicorn 的部署方案。

官网上写了 fastapi 是最快的框架之一,我们都知道 python 异步刚出的时候有很多昙花一现的框架,比如 Vibora,japronto 这些,性能做的都非常夸张,单例可以达到十万 qps,实际上是用 py 胶水封装了一下 c 框架而已,性能高也很正常,可惜这些开发社区做了 demo 出来以后都不怎么活跃了,bug 不修,没法投入生产级。

倒是 aiohttp 这个一上来看起来就很弱的,表现也不怎么亮眼的,一直更新到现在,投入生产级也完全没问题了,说句题外话,我个人使用起来主要优势就是用的熟,想实现什么效果几乎以前都做过,很快都能找到解决方案,所以学习 fastapi 对我来说倒是要考虑学习成本问题。

=====================================================================

说回正题,关于压力测试,我在虚拟机上用 wrk 进行压测,测试结果 fastapi 其实表现并不好,想问一下各位 fastapi 用的比较熟练的大佬,是我部署错误,还是它的性能表现就是这样的。

另外想问一下切换到生产级服务的话,fastapi 这条路线目前坑度怎么样,比如 web 部署里的一些常用插件,cors,basic auth,jwt 等等,还有中间件开发,支持 ws 协议等等,目前这些坑都踩的差不多了吗?这个框架从名字来看就可以看出是为 api 设计的,如果用来一体化部署 spa 之类的,有额外的坑吗?

谢谢大家

=====================================================================

附一些压测数据

#笔记本随手测一下,虚拟机给了 8 核心,所以用 16 线程 500 并发进行测试

# fastapi + uvicorn 部署,单进程
Running 20s test @ http://127.0.0.1:8000
  16 threads and 500 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    20.88ms    3.51ms  50.90ms   95.22%
    Req/Sec     0.96k    66.29     1.21k    83.54%
  95737 requests in 20.01s, 13.70MB read
Requests/sec:   4784.35
Transfer/sec:    700.83KB

# aiohttp + 自带服务部署,单进程
Running 20s test @ http://127.0.0.1:8000
  16 threads and 500 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    12.90ms    2.94ms  58.01ms   94.81%
    Req/Sec     1.57k   156.69     1.82k    80.90%
  156446 requests in 20.05s, 24.32MB read
Requests/sec:   7803.19
Transfer/sec:      1.21MB

# aiohttp + gunicorn(uvloop 模式) ,单进程
Running 20s test @ http://127.0.0.1:8000
  16 threads and 500 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     6.12ms    1.08ms  23.27ms   90.35%
    Req/Sec     3.28k   216.93     5.05k    72.67%
  327908 requests in 20.10s, 50.97MB read
Requests/sec:  16315.63
Transfer/sec:      2.54MB

一般来说这些框架都会自带一个 web 服务,可以用来做测试什么的,一般因为稳定性,性能等等原因,都不会用在生产环境部署。但是根据这个单线程测试,fastapi 实际上单进程只有 aiohttp 的 60%,如果用 gunicorn 部署的话(值得吐槽的是 gunicorn 似乎本身也是 python 中不算快的部署方式。。),fastapi+uvicorn 的组合只有 aiohttp+gunicorn 25%左右的性能

然后是多进程 prefork 测试,采用 8 线程部署服务。

# fastapi 8 线程
Running 20s test @ http://127.0.0.1:8000
  16 threads and 500 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     6.53ms    1.94ms  33.58ms   79.44%
    Req/Sec     3.09k   476.62     4.02k    60.60%
  307858 requests in 20.07s, 28.48MB read
Requests/sec:  15341.09
Transfer/sec:      1.42MB

# fastapi 增大 echo 报文长度
Running 20s test @ http://127.0.0.1:8000
  16 threads and 500 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     9.56ms    6.59ms  51.53ms   61.89%
    Req/Sec     2.18k     1.18k    6.39k    87.10%
  217184 requests in 20.05s, 24.85MB read
Requests/sec:  10834.00
Transfer/sec:      1.24MB

# aiohttp 8 线程
Running 20s test @ http://127.0.0.1:8000
  16 threads and 500 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     2.77ms    1.34ms  16.60ms   65.92%
    Req/Sec     7.30k     2.28k   19.59k    73.53%
  726936 requests in 20.10s, 123.40MB read
Requests/sec:  36170.63
Transfer/sec:      6.14MB

可以看到同样地,fastapi 性能只有 aiohttp 的三成左右。另外值得吐槽的是使用长报文测试下,fastapi 的 echo 性能衰退又有点厉害啊,直接掉三成。

2983 次点击
所在节点    问与答
26 条回复
TypeError
2021-01-24 21:09:48 +08:00
https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=fortune&l=zijzen-1r

看这个 benchmark,FastAPI 比 aiohttp 高不少,
我感觉 Python 异步框架没必要追求极致性能,我倾向顺手和生态成熟的,
目前线上生产环境 Tornado 、aiohttp 都有,更喜欢 Tornado,
接下来可能迁到 Go 了,性能方面还是静态语言优势大
LeeReamond
2021-01-24 22:36:27 +08:00
@TypeError 不知道它这个怎么测试的,aiohttp 倒是和我测的差不多,即使 prefork 也有一个上限,大概四万 qps 左右,并不能 8 线程就 8 倍可用性。fastapi 我已经用 uvicorn 部署了,理论上已经是最佳配置了,不知道它这个怎么搞出来五万 qps 的。

go 和 py 这种属于无谓争论,用 py 上生产肯定还是看重开发效率,py 内部换个框架都要问一问有没有坑的问题,换 go 就更苦了,go 目前这个生态。。。
Carry0317
2021-01-24 23:02:36 +08:00
请教 我想提供一个 gpu 的服务 用哪种方式性能最高
so1n
2021-01-25 00:07:57 +08:00
可能有些配置没写对吧.
fastapi 的底子就是 starlette, 但为了做类型转化和参数校验,性能会比 starlette 略差一点. 然后 aio 的库相比其他同类的库都不太好(可能是出现太早的原因)
LeeReamond
2021-01-25 01:46:12 +08:00
@Carry0317 这篇帖子跟 GPU 没什么关系吧。你的意思是想提供一个高可用的 gpu 服务接口?
LeeReamond
2021-01-25 01:52:46 +08:00
@so1n 就是很简单的按照 quick start 定义了一个异步函数,绑定到'/',返回一个 echo,没有其他任何东西。部署方面,8 个 fork,关闭 log 。uvicorn main:app --worker 8 --log-level error,不知道有哪里配置还能提高性能的。

aio 库性能差我觉得应该没有这个说法。python 的异步从一开始就没有什么黑魔法,当初 dableaz 在 pycon 花半小时就实现一个功能完整的 eventloop,可以完全替代原生进行 basic tcp socket programming 的。所以 eventloop 相同的情况下,实现方面完全可追溯,封装程度其实区别不大,影响也不大,理论上无所谓 aio 库慢与否,实践当中我也从没听说过有人说 aiolibs 里面的东西比同类慢。
ManjusakaL
2021-01-25 02:00:39 +08:00
直接 Gevent 不香么...
asyncio 那么多💩,活着不好么😂😂🐶🐶
LeeReamond
2021-01-25 02:04:39 +08:00
@ManjusakaL 我觉得你对屎可能有些误解。即使在 python3.5 时代,原生异步也并不屎,这种用户态完全可控、可预测的状态显然是更优的设计。gevent 无法做到以上任何一项,用户用脚投票也说明了这点。
ManjusakaL
2021-01-25 02:38:52 +08:00
@LeeReamond 很抱歉,可能用💩来形容不太合适,不过依旧只能用糟糕来形容.
顺便指出几个误区

1. Gevent 也是完全可控,可预测
2. 原生 asyncio bug 到现在为止 bug 太多,随手举几个例子,BPO-30698 和 BPO-29406 这两个横跨 asyncio 到现在的会导致一些 https 链接泄漏的 bug 到现在依旧没有修
3. 生态一如即往的糟糕,随手举个例子,aiomysql,目前 asyncio 生态中的 mysql lib,三个月没更新可以说是个 dead project 了. 当然你要说我用 thread Future 封个 mysqlclient 当我没说

我自己应该是最早一批在国内推动 asyncio 上生产的人( 17-18 年)在给予厚望后,我写下了这篇文章 https://manjusaka.itscoder.com/posts/2018/10/05/why-i-dont-use-async/

我可以很负责任的说,我这篇文章中写的大部分弊端依旧适用于 2021 年的现在
ManjusakaL
2021-01-25 03:00:06 +08:00
@LeeReamond BTW 你对于 asyncio 的可预测存在误解
event loop 是无法真正意义上做到“可预测”的
此处“可预测”指 A task 执行完后能预测下一个 task 是 B 还是 C
无论是 asyncio 还是 Gevent 我们都只能做到一个基础的保证,即正常情况下,我能在一个切出点后能切入执行后续代码
但是在生产环境中,配合 Python 的 GIL,能做到这点也是奢望
随手举例时间,我们在请求 www.v2ex.com 的时候,会通过 gethostbyname ( Linux 下,参见 https://man7.org/linux/man-pages/man3/gethostbyname.3.html )来做 DNS 解析,而这个函数是不可调度函数,所以那么一旦 DNS 解析出现问题,那么可能炸整个 event loop 导致所有 task 不可调度. 而这样复杂的可能阻塞整个 event loop 调度情况还有很多,此处不一一列举. 诚然我们可以通过很多额外的手段来尽可能规避这种情况. 但是就其本身而言,无论 asyncio 还是 Gevent,其所要面对的问题都是一样的
LeeReamond
2021-01-25 03:31:16 +08:00
@ManjusakaL 认真看完了,大佬确实经验丰富。我因为学习异步的时候已经出现原生异步了,所以对猴子补丁天生有不信任,承认错误。我们在简单的生产环境(非内部管理平台)中使用原生异步体验良好,可能有些过于信任。

搜了一下你说的 BPO-30698,ssl 链接泄露应该如何理解,似乎不是一个导致明文泄露的恶性 bug,而是导致内存不能回收的问题,不知是否理解正确。在 17/18 年左右倒是听说过有人 aiohttp 框架出现 ssl 内存泄露,我从未遇到过类似问题,以为在新版中已经修好了。看了这个 issue,不理解如何复现。

你在帖子中提到的同步异步混合,以及生态不支持 c 插件等问题,我个人理解这两个目前已经不是问题,我的理解中异步代码中首先不应存在同步内容,我从未体验过同步异步同时维护的复杂度。另外生态方面主要是接入后端,python 本身的阻塞实现倒是能用附带线程池的方式梭掉,顺带 cython 还能解决掉 gil,而后端方面,mysql 和 redis,oracle 也有异步连接方式,我使用 aiolibs 的库体验良好,可能是接入服务数少,我个人而言这方面没什么不满。

另外大佬这么推崇猴子补丁,有没有 gevent 系列比较合适的入门文章,我想完整评估一下 gevent 相对于原生异步方案的性能和稳定性
wdhwg001
2021-01-25 04:15:11 +08:00
fastapi 不是要用 gunicorn 套 uvicorn 吗? techempower 是开源的,可以去看他们的部署和代码。
wdhwg001
2021-01-25 04:34:19 +08:00
另外 techempower 里的 fastapi 代码是有轻微作弊的,主要是 ujson,不过也不严重,你甚至可以用 orjson 跑的更高一点。

我的观点是常量级差距都不用太在意的,fastapi 还有完善的 openapi 支持什么的,那些要更吸引人一点。
wdhwg001
2021-01-25 04:49:43 +08:00
另外 asyncio 应该是大势所趋了,生态在逐步完善,但是距离 wsgi 时代还是有差距的,然而依旧是好兆头。
其实单说 fastapi 也是问题多多的,缺少 session 支持是一个,不完全遵守 asgi 是一个,中间件还有闭包引用不可靠的问题,自带路由是遍历而不是树优化也是一个问题。但即使如此,fastapi 的设计也依然是比 flask 好一些的。
而且其实更大的坑是 orm,gino 和 tortoise 都有各自的坑,django 的 async orm 还在难产,我这边项目用的 tortoise,设计上基本就是抄 django 了,没什么创新点。
LeeReamond
2021-01-25 05:00:33 +08:00
@wdhwg001 gunicorn 套 uvicorn 怎么实现,感觉这两个不能互相套啊

楼上说的 ssl 泄露的 issue 我看倒是确实没人理。印象中 17 年左右 stackoverflow 的 asyncio 区还是极端冷清的,不知道现在怎么样。我个人体验来讲,倒是 3.5 时代感觉原生异步的学习过程很底层,从生成器概念一步步概念学上来,最近两年倒是完全感觉在使用高级 api,完全没有底层的感觉了,基本和写同步代码没有任何区别,只是外面要套一层扳机而已。
LeeReamond
2021-01-25 05:02:18 +08:00
@wdhwg001 orm 方面我是完全不做任何希望了,我觉得以 python 社区的生产能力 orm 大概是要永远难产下去了。我个人使用体验上倒是没体会到 orm 对开发速度有多大帮助,都是直接操作数据库,所以倒是感觉不很有所谓
spcharc
2021-01-25 05:35:05 +08:00
aiohttp 库 contributor 路过,很惭愧只做了一点微小的工作(大概+366 −48 这样子)
我感觉 aiohttp,一般不是配合 aiodns 来用吗?
官方都提供了 pip install aiohttp[speedups]这种安装方式来捆绑销售 aiodns,不就是因为一旦 dns 服务不稳定,就可能阻塞整个 loop 嘛?
另外官方也推荐搭配使用 uvloop,比 asyncio 自带 loop 速度快,也没有上面提到的 https 泄露之类的问题
而且 loop (不管是 python 官方的还是第三方的)都提供了 run_in_executor 的吧,有可能长时间阻塞的函数都应该用这个来运行来避免 loop 阻塞啊
LeeReamond
2021-01-25 06:15:38 +08:00
@spcharc 我印象中确实是有见过生产级部署以后出现莫名泄露的问题的帖子,大概几年前。我自己没遇到过。另外我对楼上说的猴子布丁原理上性能高于 libuv 仿品很好奇
so1n
2021-01-25 09:24:04 +08:00
@LeeReamond 我不是说性能,比如 aiohttp 的 client 就一堆隐藏坑,aioredis 停止更新,集群到现在都没支持等等
Carry0317
2021-01-25 09:41:27 +08:00
@LeeReamond 是的 高可用的 gpu 服务接口

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/747931

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX