1
nullcc 2017-08-15 18:07:08 +08:00
换 tornado
|
2
aisk 2017-08-15 18:10:58 +08:00
直接上 gevent,代码都不用改,启动 gunicorn 的时候增加一个参数 -k gevent,自动帮你打 monkey patch 好 monkey patch 了。
|
4
woostundy OP @aisk monkey patch 对 requests 现在还有效吗?我自己单开测试的时候还是阻塞的啊。
加了 httplib=True 会报已经不支持。 |
6
terrawu 2017-08-15 18:18:13 +08:00
gevent 和 requests 相性不和,楼主你用 httplib2 + gevent 吧,这坑我踩过的。
|
7
cloudyplain 2017-08-15 18:43:29 +08:00
gevent+requests 没问题。
|
8
troywinter 2017-08-15 19:41:32 +08:00
看 gunicorn 关于 worker 类别的文档,对于这种第三方请求服务有很具体的说明。
|
9
aisk 2017-08-15 22:58:08 +08:00
有效,你怎么测试的?如果你只看单个请求,看上去 requests 去请求远程接口的时候是阻塞的,不过如果这个时候还有其他请求过来,gunicorn 还能继续提供服务的。
|
10
gouchaoer 2017-08-15 23:06:15 +08:00 via Android
说换 tornado 的,你们去看看 tornado 的那个异步 httpclient 好用不,而且人家都用 flask 写了
我说结论吧,无解。。。换 go 语言吧 如果你使用 php,那 swoole/zanphp 可以做到同步方式来写异步 /协程 |
11
gouchaoer 2017-08-15 23:06:56 +08:00 via Android
你 flask 是同步的,用异步 httpclient 没用啊。。。
|
12
lerry 2017-08-15 23:13:00 +08:00
|
14
EchoUtopia 2017-08-15 23:52:51 +08:00 via iPhone
gevent 可以把你的 python 同步阻塞代码自动变成异步的,patch_all 加个 httplib=True 参数,你看下在 gunicorn 下对应怎么处理
|
15
EchoUtopia 2017-08-15 23:54:38 +08:00 via iPhone
好吧,没看到你说不起作用了
|
16
SlipStupig 2017-08-16 00:09:21 +08:00
grequest
|
17
laoli2017 2017-08-16 09:46:58 +08:00
推荐 sanic,这个完全是基于 asyncio 来实现的,当然现在还不太成熟,但是性能上完全没问题(我做过压测,比 tomcat 强多了)。而且也不需要其他应用服务器,比如独角兽什么的。
|
18
mengskysama 2017-08-16 10:07:19 +08:00 via iPhone
gevent requests 一点毛病没有,参考 grequests。
|
19
yonoho 2017-08-16 10:19:18 +08:00
我猜楼主的测试是在一个请求里调用多次 requests,并期待并行执行的效果。但因为代码本身写成了同步顺序执行(多行或者循环),所以用了 monkey patch 也没用。这种情况下不改代码是不可能并行的,最简单的方法是依然使用 gevent worker,然后使用 #12 提供的 grequests 包重写第三方 http 请求部分,把这些 http 请求放在一起调用 grequests.map ( gevent.spawn ),就能实现并行加速。
|
20
alvinbone88 2017-08-16 11:01:19 +08:00
建议换 aiohttp
|
21
gouchaoer 2017-08-16 11:37:29 +08:00
@aisk gevent 就一个把同步 api 来 hack 成异步的嘛,但是 controller 里面凡是涉及 IO 的东西都能弄成异步的?(包括数据库、redis 等等),这不可能的吧。。。。你能给个 github 的 repo 例子让我瞻仰一下么?
|
22
aisk 2017-08-16 11:47:28 +08:00
@gouchaoer 网络 IO 是可以的,文件 IO 不行,因为 gevent 直接 pactch 的自带的 socket 等模块。不过有些直接用 C 来实现的模块,内部没有使用 Python 的 socket 模块,就不会被 patch 到了,比如最常见的那个 mysql 库。
|
23
aisk 2017-08-16 11:48:54 +08:00
看 gevent 的文档: http://www.gevent.org/intro.html#monkey-patching
|
25
neoblackcap 2017-08-16 13:16:18 +08:00
楼主你需要同步返回给客户端吗?假如是的话,flask 是解决不了这样的,哪怕是 Gunicorn + gevent 也是一样。毕竟你用了大量的同步库,比如数据库连接什么的,那 gevent 肯定也是堵塞的。你可以做的只有用另外的语言或者框架来替代这个 API,比如上面封装一层 Tornado 或者 Golang 写的模块。
如果是异步的接口,那么你可以将所有请求扔到 celery,由 celery 处理,celery 来起一个 gevent 类型的 worker,gevent 的 worker 处理所有的网络请求,完美兼容你的 requests 逻辑。前提是你的接口是异步的,celery 的 worker 能独立地返回请求给客户端。 |
27
neoblackcap 2017-08-16 13:22:13 +08:00
@woostundy 还有就是你发现 gevent 的 monkeypatch 用了不起效,很有可能是你已经载入了 Python 的底层网络库,那么 gevent 的 monkeypatch 就不起效了,monkeypatch 必须在网络库载入之前使用,否则无效。
requests 对 gevent 的兼容性挺好的,毕竟 requests 是一个纯 Python 的 http 库,gevent 能完美支持的,一般不支持的是因为底层用了 C 库,monkeypatch 没法对这些库进行打补丁导致,具体例子就是 mysql-python。 |
28
woostundy OP 多谢各位。先解释下上面的测试,我单独用 gevent 的 monkeypatch,requests 的确没效果,gevent 显示 httplib=True 已不再支持。但用 gunicorn 开 gevent 是有效的。
|
29
Dominator 2017-08-16 13:48:40 +08:00
恕我直言。。你可以用 grequests 先试试,如果能解决问题就解决了。那种一上来就换大框架换到 tornado 的,不敢恭维。
|
30
upwell 2017-08-16 13:53:26 +08:00
有个做法是,把外部依赖的接口改写成用异步框架实现的,不走 flask,其它的请求仍然走 flask。
|
31
terrawu 2017-08-16 14:02:49 +08:00
一群小学生,没有踩坑就不要瞎猜,受不了了。看我 #6 回复。
|
32
aisk 2017-08-16 14:28:42 +08:00
受不了了,直接贴代码:
https://gist.github.com/aisk/b1d8c07b96a8fecb319c7902773f9c0a 安装 gevent, flask, requests,然后用 gunicorn fuck:app 来启动项目。 访问 http://127.0.0.1:8000/suck 这个地址,会用 requests 去访问一个外部的会卡十秒的接口,这个时候再访问 http://127.0.0.1:8000/fuck,这个地址会因为当前进程被 requests 卡住,所以不能响应,要等 /suck 响应了之后才能响应。 然后启动命令改成 gunicorn a:app -k gevent,这个时候 gunicorn 会自动帮你打 monkey patch,因此你的代码一行也不用改。 然后访问 /suck,再访问 /fuck,你看现在能不能正确响应? 说 gevent 和 reuqests 不能配合的,都是老黄历了,不要出来误人子弟了,说打 monkey patch 的时候要加 httplib=True 也是老黄历了。直接推荐别人换 tornado 或者 asncio,甚至推荐上 celery 的,本来加一个参数就解决的问题,是要让别人重写项目? |
33
terrawu 2017-08-16 14:43:11 +08:00 1
楼上的大兄弟,你的代码的确不会 block,但是你测过 “并发度” 么?
requests 的 streaming 和 pool size 的机制会导致并发瓶颈,monkey patch 的并不能帮助这个问题。 而 httplib2 没有 streaming 的接口,反而能提高真的的并发。 |
34
clino 2017-08-16 14:46:01 +08:00
我建议你的应用和 openresty 配合起来用,要用 request 的地方都改用 openresty 来实现
|
38
yangxin0 2017-08-16 14:54:30 +08:00 via iPhone
首先你们业务是猛增吗? 如果是我觉得可以快速加服务器解决当前的困境。 然后可以查一查是请求处理哪个环节出了问题。问题没定位出来不要贸然的换语言、换框架,打猴子补丁。
|
39
woostundy OP @aisk 我之前说的测试方法的确有问题,用 Gunicorn 开了 gevent 实际是有效的,但是当并发量稍微一大还是会出现阻塞的情况。另外我自己单独写个 py 脚本打上 monkey patch 没效的,而且在官方文档里提到用 grequests,综上让我误以为是 gevent 没生效。
所以你说的也是对的,Gunicorn+gevent 可以让 requests 变异步,但并发高了出问题应该是 terrawu 所说的原因。 |
40
gouchaoer 2017-08-16 14:57:09 +08:00
@aisk gevent 在 hack 掉 requests 底层 i 之后遇到 IO 会使用协程主动让渡 cpu 到别的协程没错,不只 httpclient 有 IO,mysql 以及 redis 都有 IO 的,也就是说你 mysql/redis 的 client 不使用协程的话如果 qps 大了,那么 mysql/redis 阻塞了之后那么别的协程就无法调度。。。。我能问问你使用 gevnet 之后 mysql 之类的涉及 IO 的库都自动切换到协程版本? python 这么神通广大?
|
42
aisk 2017-08-16 14:59:24 +08:00 1
@woostundy 单独打 monkey patch 再用 gunicorn 启动是无效的,因为 gunicorn 要自己先启动,这个时候 socket 和 thread 模块都已经加载进来了。然后执行你的代码时,才会进行 monkey patch,这个时候已经来不及了。解决了问题就好。
|
43
aisk 2017-08-16 15:02:18 +08:00
@gouchaoer 我上面说了,直接用 C 来调用 socket 的都不能 patch。不过 redis 那个库网络部分就是 Python 实现的,Mysql 的话现在的方案都是用 pymysql 这个库,和 C 的那个接口一致,所以都没有问题。
|
45
terrawu 2017-08-16 15:05:09 +08:00
实在要用 c socket 的库也有一些奇怪的方案, 比如这个 https://github.com/douban/greenify
|
46
terrawu 2017-08-16 15:09:02 +08:00
最后多说一句,讨论问题谦虚一些为好,否则有人忍不住(比如我)就会跳出来开嘲讽了。
|
47
aisk 2017-08-16 15:15:33 +08:00
@woostundy 我怀疑是不是你没有开 keep alive 导致的? requests 并不会默认帮你开启 keep alive,需要先创建一个 Session 对象,后续都用这个 Session 对象发送请求,才会 keep alive
|
48
1iuh 2017-08-16 15:17:41 +08:00
|
49
ljcarsenal 2017-08-16 15:17:56 +08:00
node go
|
50
TJT 2017-08-16 15:19:26 +08:00
增加 worker,多线程,分布式。
|
51
gouchaoer 2017-08-16 16:22:51 +08:00
@aisk requests 只是 httpclient 客户端,不同请求还能公用同一个 requests 的 tcp 连接?这是一个 tcp 连接池的问题,有些内部 tcp 调用是 http 方式的,为了优化使用了 tcp 连接池(也就是 keep-alive )。。。另外 LZ 的需求是典型的微服务后台都会遇到的问题,go、java 甚至 php 都有比较成熟的方案了,你一上来就提出一个似乎可行的方案,但是这个问题很难相信就是 hook 一下底层 io 库就能健壮的把现有的同步代码改成协程版本的
|
54
sunwei0325 2017-08-16 17:20:36 +08:00
没有人推荐用 grequests 么?
|
55
ChangHaoWei 2017-08-16 17:21:39 +08:00
|
56
ToughGuy 2017-08-16 17:23:03 +08:00
哈哈哈哈, 看了楼主 /t/325328 里面问 tornado+flask+requests 阻塞解决方案, 一楼喊换成 gunicorn+flask, 然后就有了现在这个帖子。
哈哈,看老帖子的 1,3 楼和这个的 1,2 楼笑死我了。 楼主: 黑人问号... 期待楼主问题得到妥善解决。 |
57
ChangHaoWei 2017-08-16 17:24:25 +08:00
|
58
ChangHaoWei 2017-08-16 17:31:07 +08:00
@ToughGuy 我要笑出腹肌了😄
|
59
gouchaoer 2017-08-16 17:41:09 +08:00
@aisk 问题是你让人在 flask 的 requests 里加上 keep-alive 这很蠢啊,说明你后台的很多基础概念都没搞清楚,狂妄没什么比起谦虚的类型,我个人反而喜欢那种自信 /自大 /狂妄的人,可是你很蠢啊
|
60
woostundy OP 多谢各位帮忙,我觉得有必要重新描述一下问题:
现在有服务 A 和服务 B,服务 A 有一部分数据库读写操作,每次都会通过 requests 访问服务 B (每次都只会访问一次,B 服务性能完全足够) 服务 A 的结构是 Flask + Gunicorn + Gevent,当 A 服务器 QPS 到 50 时,数据库访问没有压力,但会出现 requests 到服务 B 的速度变慢甚至超时。 不是 A 一次请求内多次请求 B 服务,而是每次 A 都会请求一次 B,所以设置 session 共享 tcp 连接( keep-alive )应该是无效的。 |
61
terrawu 2017-08-16 18:01:56 +08:00
@woostundy 你的这种情况如果没有突发的高并发数目的话,还不如增大 requests.Session 的 pool size 试下
https://stackoverflow.com/questions/18466079/can-i-change-the-connection-pool-size-for-pythons-requests-module |
62
ChangHaoWei 2017-08-16 18:04:20 +08:00
|
63
icedx 2017-08-16 18:05:41 +08:00 via Android
Flask+Uwsgi 吧
Flask+Gevent 数据库部分有小 Bug 前几天经常拉倒整个进程 换了之后没出过毛病 |
64
terrawu 2017-08-16 18:09:13 +08:00
@ChangHaoWei 没有,我手写的,我情况和楼主不一样,我的类似 gateway, 需要同时请求 N 个后端 http 服务,突发比较高。用的全局 dict 存多个 httplib2.HTTP 实例,跑的很流畅。
|
65
terrawu 2017-08-16 18:10:23 +08:00
不过已经被我用 go 改写了,效果好太多。
|
66
neoblackcap 2017-08-16 19:30:56 +08:00
@woostundy 你的 QPS 没有增长,那么你是 gunicorn 的 worker 跑满了吗,要不然不应该线性增长吗?要不直接加大 gunicorn 的 worker 看看,反正你数据库又不是瓶颈
|
67
yonoho 2017-08-16 19:32:20 +08:00
@1iuh 请问这个问题应该怎么复现?我用最简化的测试方法(/a 访问 1000 次 /b, 每个 /b 访问 1 次 /c, /c ),1000 次 10s+,没感觉有阻塞
|
68
yonoho 2017-08-16 19:33:35 +08:00
patch #67: /c sleep 1,访问一次 /a 10s+
|
69
1iuh 2017-08-16 21:13:10 +08:00
@yonoho #68 我是这样测试的。
用 Gunicorn + Gevent + Flask 启动 8 个 worker。 测试 1: flask 什么都不做,直接 return "hello world"。 用 ab 测试 100 个并发 100000 次请求。结果 QPS 700 左右。 测试 2: flask 直接 requests.get 百度,然后 return result.text。测试方式一样,结果 QPS 只有 40 左右。 |
71
terrawu 2017-08-16 22:23:47 +08:00 1
@chenqh 好了非常多,但也不能说是 python 不行。
因为我的程序需要聚合一下后端的数据,然后做一个 digest(一些哈希操作), 这个步骤会比较吃 CPU。 后来 IO 方面的瓶颈都优化完毕后,程序的瓶颈就体现在这里了 1. python 代码效率低,太耗 CPU 2. GIL 不能用多核 3. 内存耗费也比较大。 所以优化到这一步时候,用 go 改写就很适合了,所谓性能的话,提升了几十倍吧。 |
73
lolizeppelin 2017-08-17 09:01:17 +08:00 via Android
你现在是 a 被访问一次就去 a 就去访问 b 一次?
和数据库用链接池一样 requests 的 session 也池化 一开始就建立一定数量的链接然后维护心跳 不然老是 socket connect 也很耗资源的 简单实现可以炒 py redis 的写法试试 |
74
shiina 2017-08-17 09:35:29 +08:00
从楼上兄弟链接去旧贴看到了 Livid 老哥
[doge] |
75
lolizeppelin 2017-08-17 10:23:42 +08:00
详细看了下源码
pool_key = (scheme, host, port) 也就是说默认 requests 的默认的池在你目的地只有一个的时候是无效的 所以一个是直接用 session 做池,还有一个办法是重写 HTTPAdapter 好像 init_poolmanager 的 pool_kwargs 传入 maxsize 就能直接池化单个目的地了!! 我操都封装好了嘛........... |
76
woostundy OP @lolizeppelin 我试过了,并没有效果。这需要多个请求用同一个 session,而 flask 不同请求之间没法用同一个 session。
我又尝试在外层弄一个全局的 session,HTTPAdapter 里把 maxsize 调到 100,但是效率更低了,超过一半的请求都超时。 |
77
lolizeppelin 2017-08-17 10:34:22 +08:00
理论上不会那么差啊 池化以后是长链接了 单纯的 http 请求后端没问题的话前端 qps 50 应该不成问题的
等等, 你是单进程的还是多进程的? |
78
woostundy OP @lolizeppelin 之前是 8 个子进程( 4 核 CPU ),考虑到 CPU 没压力,瓶颈在 IO 这,又开到了 16 子进程,然后能跑到 50 qps 了,再高就会出现 HttpsConnectionError 了。
另外,你确定不是同一个请求里的 session 能共用同一个长连接? |
79
terrawu 2017-08-17 11:09:59 +08:00 1
> 弄一个全局的 session,HTTPAdapter 里把 maxsize 调到 100
这个太低,调到 1000 试下,还不行的话,你的需求就用 httplib2 实现吧。 |
80
lolizeppelin 2017-08-17 11:16:07 +08:00
HTTPConnectionPool 维护的是 HTTPConnection,再下面就是 socket 了
所以 init_poolmanager 的时候增加 maxsize 参数就增加了 HTTPConnection 的数量, conn.urlopen 的时候就是 HTTPConnectionPool.urlopen HTTPConnectionPool 会从自己的队列里取出一个 HTTPConnection 去访问 url 所以只要你 request 的 session 是同一个(用 session.request ) 那么你的请求都是从同一个 HTTPConnectionPool 里出来的,所有 con 都没调用过 close (除非你主动 session.close ) 都是长链接的,可以被复用 要不这样你抛开你的框架 直接写个单文 fork 8 进程用 requests 去请求你 b 服务器的一个接口 看看 qps 这样不就知道是不是 requests 的问题了 测玩可以改成协程的试试性能有没有提高 |
81
lolizeppelin 2017-08-17 11:18:28 +08:00
|
82
Zzzzzzzzz 2017-08-17 11:30:03 +08:00
调大 pool 跑不满的话, 问题是不是卡在 DNS 查询上, 启动程序前设置环境变量 GEVENT_RESOLVER = ares, 最好再弄个本地的 DNS 缓存服务
|
83
lolizeppelin 2017-08-17 11:33:34 +08:00
是哦 你的目的地是域名的最好是
host 用 IP 然后 set 域名头的方式去访问 这样不用折腾 dns |
84
lolizeppelin 2017-08-17 11:34:56 +08:00
0v0 不对 已经是长连接池了 只有一个目的地没这问题 233
|
87
terrawu 2017-08-17 11:41:08 +08:00
@smallHao 我的程序后来瓶颈不在 IO 上了,是在 CPU 上。python 代码执行效率较低以及 GIL, 不换 go 无法继续优化了。
|
90
woostundy OP @terrawu 有效果!现在 16 个子进程,用全局 session,maxsize 1000,能跑到 150 qps 了。非常感谢!
|
91
woostundy OP @terrawu 我之前认为我的进程开的这么多,每一个开 100 的 maxsize 已经足够了,结果就是 session 提出来和不提出来没啥差别。
现在把 maxsize 提高上去,CPU 马上能跑满了。神奇。 |
93
terrawu 2017-08-17 12:47:37 +08:00
@woostundy 好说,本来我是打算在 #6 一击脱离的,不过后来再点开看,发现很多盆友没踩过坑就来强答,然后还吵了起来了,真是受不了。
|
94
terrawu 2017-08-17 13:04:42 +08:00
我的程序在实现初期就打算用 go 的,不过项目组内对于 go 选型还是有些质疑的,最后一路优化下来,目前大家对 go 的无脑质疑已经不见了,会认真评估 python 和 go 了。
|
96
JasperYanky 2017-08-17 14:38:49 +08:00
小白在这儿坐等结论了~ 希望最后总结下~
|
97
chenqh 2017-08-17 22:09:19 +08:00
@terrawu 估计你项目的并发比较大。。我上个项目并发最多一分钟 3000 也就是一秒钟 50,所以感觉暂时用 python 就好了
|
98
yonoho 2017-08-18 19:21:01 +08:00 3
@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。 |
99
JasperYanky 2017-08-19 10:23:52 +08:00
@yonoho 太谢谢了 👍👍👍👍
|
100
1iuh 2017-08-22 10:17:34 +08:00
@yonoho #98 这个测试非常好,但是有一个问题, 这个测试方式其实是 CPU 密集型的,因为没有带延迟的 IO。最终测试的其实就是代码执行的效率。所以用 GO/C 模块有绝对的优势。 可以测试一下 IO 密集型的(比如请求带延迟的 URL )相信这样更有说服力。
|