Python 2.7 Gunicorn + Flask,有大量的第三方服务 http 请求, requests 库阻塞导致出现性能瓶颈

2017-08-15 17:48:12 +08:00
 woostundy
该怎么优化呢?异步 http 客户端的库哪个比较靠谱?
14788 次点击
所在节点    Python
101 条回复
lolizeppelin
2017-08-17 11:18:28 +08:00
@terrawu

连接池不够报的错是 EmptyPoolError 不是 HttpsConnectionError 不是他的目前的问题
Zzzzzzzzz
2017-08-17 11:30:03 +08:00
调大 pool 跑不满的话, 问题是不是卡在 DNS 查询上, 启动程序前设置环境变量 GEVENT_RESOLVER = ares, 最好再弄个本地的 DNS 缓存服务
lolizeppelin
2017-08-17 11:33:34 +08:00
是哦 你的目的地是域名的最好是
host 用 IP 然后 set 域名头的方式去访问

这样不用折腾 dns
lolizeppelin
2017-08-17 11:34:56 +08:00
0v0 不对 已经是长连接池了 只有一个目的地没这问题 233
enomine
2017-08-17 11:36:53 +08:00
@terrawu gevent + requests 没任何问题
smallHao
2017-08-17 11:38:03 +08:00
@terrawu 兄弟 有试过 japronto 吗 这个底层 uvloop 应该跟 go 并发是在一个级别的
terrawu
2017-08-17 11:41:08 +08:00
@smallHao 我的程序后来瓶颈不在 IO 上了,是在 CPU 上。python 代码执行效率较低以及 GIL, 不换 go 无法继续优化了。
terrawu
2017-08-17 11:42:00 +08:00
@enomine 回帖之前看下我的场景描述,你没遇到问题,不代表我没问题。
enomine
2017-08-17 11:42:50 +08:00
@gouchaoer gevent + gunicorn + requests + pymysql 没有问题
woostundy
2017-08-17 11:45:11 +08:00
@terrawu 有效果!现在 16 个子进程,用全局 session,maxsize 1000,能跑到 150 qps 了。非常感谢!
woostundy
2017-08-17 11:49:00 +08:00
@terrawu 我之前认为我的进程开的这么多,每一个开 100 的 maxsize 已经足够了,结果就是 session 提出来和不提出来没啥差别。
现在把 maxsize 提高上去,CPU 马上能跑满了。神奇。
enomine
2017-08-17 11:49:01 +08:00
@terrawu 是我没有具体看场景,抱歉
terrawu
2017-08-17 12:47:37 +08:00
@woostundy 好说,本来我是打算在 #6 一击脱离的,不过后来再点开看,发现很多盆友没踩过坑就来强答,然后还吵了起来了,真是受不了。
terrawu
2017-08-17 13:04:42 +08:00
我的程序在实现初期就打算用 go 的,不过项目组内对于 go 选型还是有些质疑的,最后一路优化下来,目前大家对 go 的无脑质疑已经不见了,会认真评估 python 和 go 了。
woostundy
2017-08-17 13:14:04 +08:00
@terrawu 的确是的。非常感谢。
JasperYanky
2017-08-17 14:38:49 +08:00
小白在这儿坐等结论了~ 希望最后总结下~
chenqh
2017-08-17 22:09:19 +08:00
@terrawu 估计你项目的并发比较大。。我上个项目并发最多一分钟 3000 也就是一秒钟 50,所以感觉暂时用 python 就好了
yonoho
2017-08-18 19:21:01 +08:00
@JasperYanky 总结一下:本文内的问题是,在基于 gevent 的 http server 上大量使用 requests 时速度很慢,甚至会超时,看起来像阻塞了一样。最后楼主通过调大 pool manager 的 maxsize 解决了问题。

然后我通过类似 #69 的测试方法复现了这个问题,并横向测试了其他方案的一些表现。测试用例方面为了排除外部变量,与 #69 的第二步不同,我没有选择 baidu 的页面,而是用第一步中自己的 /hello 页面来进行测试。即完整的测试方案为:

0. 写一个简单的 http server,提供两个接口。第一个 /hello 简单返回 "hello";第二个 /world 会通过 http 访问 /hello 然后把拿到的东西返回出去(不使用公共 session,裸 requests.get )
1. ab -c 100 -n 5000 http://127.0.0.1:5000/hello
2. ab -c 100 -n 5000 http://127.0.0.1:5000/world

先来看一下这个用例,会发现第二步比第一步多的就只是一次 /hello 的访问,因此理论上第二步的 QPS 应该为第一步的一半(在未达到处理极限的前提下)。然后测试数据如下(全部测试跑在我的小本本上,默认 2 个 worker,CPython3.6,gunicorn,gevent,测试有偏差,15% 以内大概,看个比例就行)

go 8784 3544

--------------------------

gevent+requests 1079 261

gevent+httplib2 988 336

gevent+gcurl 1079 562

--------------------------

sanic+aiohttp_client 6631 1513

以 go 版本为对照组,第二步 QPS 能达到第一步的 40%,基本满足预期(而且绝对值上也是最高的)。然后第二组就是有问题的 gevent + requests 了,第二步只有第一步的 24%。看来确实有问题,这里考虑一下,io 已经被 patch 成异步的了不会阻塞,那多半是 requests 自己慢,再想一下它那些高级的接口和冗长的面向对象代码,可能是慢的原因,于是把第二步中的 http client 换成了更底层的 httplib2,发现 QPS 提高到了 336 ( 34%),效果显著但还不够好。于是想进一步替换成更高效的 pycurl,同时为了对接 gevent,找了个 gcurl 包,这次 QPS 达到了 562 ( 52%),效果拔群。到此基本可以确定,是 requests 代码本身的执行效率低导致的问题,与 gevent 应该没什么关系。当 requests 不能满足你的需求时,可以换一个更快的 http client。

最后说一下绝对 QPS 的问题,gevent 下的 /hello 接口都只有大约 1000,比 go 低了一个数量级,造成这个问题的原因与 requests 类似,都不是 io 的问题,而是 python 代码本身执行效率低。即使不用 monkey patch,改成原生的 tornado,/hello 的 QPS 也只有 1200 左右。上面测试数据的最后一组我用了 sanic 框架,这个框架基于原生 asyncio 并把 ioloop 和 httpparser 都替换成了 C 版,才使 /hello 接口的 QPS 接近 go,但因为没有用 C 版的 http client,/world 的 QPS 比仍然偏低( 22%)。综上,当你的 python 代码执行效率遇到瓶颈的时候,要么简化代码,要么上 C 模块,要么也可以考虑换成 go。
JasperYanky
2017-08-19 10:23:52 +08:00
@yonoho 太谢谢了 👍👍👍👍
1iuh
2017-08-22 10:17:34 +08:00
@yonoho #98 这个测试非常好,但是有一个问题, 这个测试方式其实是 CPU 密集型的,因为没有带延迟的 IO。最终测试的其实就是代码执行的效率。所以用 GO/C 模块有绝对的优势。 可以测试一下 IO 密集型的(比如请求带延迟的 URL )相信这样更有说服力。

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

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

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

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

© 2021 V2EX