Python 2.7 Gunicorn + Flask,有大量的第三方服务 http 请求, requests 库阻塞导致出现性能瓶颈 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐学习书目
Learn Python the Hard Way
Python Sites
PyPI - Python Package Index
http://diveintopython.org/toc/index.html
Pocoo
值得关注的项目
PyPy
Celery
Jinja2
Read the Docs
gevent
pyenv
virtualenv
Stackless Python
Beautiful Soup
结巴中文分词
Green Unicorn
Sentry
Shovel
Pyflakes
pytest
Python 编程
pep8 Checker
Styles
PEP 8
Google Python Style Guide
Code Style from The Hitchhiker's Guide
woostundy
V2EX    Python

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

  •  3
     
  •   woostundy 2017-08-15 17:48:12 +08:00 16023 次点击
    这是一个创建于 2987 天前的主题,其中的信息可能已经有所发展或是发生改变。
    该怎么优化呢?异步 http 客户端的库哪个比较靠谱?
    第 1 条附言    2017-08-16 17:49:17 +08:00
    多谢各位帮忙,我觉得有必要重新描述一下问题:
    现在有服务 A 和服务 B,服务 A 有一部分数据库读写操作,同时每次都会通过 requests 访问服务 B (每次都只会访问一次,B 服务性能完全足够)
    服务 A 的结构是 Flask + Gunicorn + Gevent,当 A 服务器 QPS 到 50 时,数据库访问没有压力,但会出现 requests 到服务 B 的速度变慢甚至超时。

    不是 A 一次请求内多次请求 B 服务,而是每次 A 都会请求一次 B,所以设置 session 共享 tcp 连接( keep-alive )应该是无效的。
    101 条回复    2017-08-26 13:08:33 +08:00
    1  2  
    nullcc
        1
    nullcc  
       2017-08-15 18:07:08 +08:00
    换 tornado
    aisk
        2
    aisk  
       2017-08-15 18:10:58 +08:00
    直接上 gevent,代码都不用改,启动 gunicorn 的时候增加一个参数 -k gevent,自动帮你打 monkey patch 好 monkey patch 了。
    woostundy
        3
    woostundy  
    OP
       2017-08-15 18:11:21 +08:00
    @nullcc 兄弟。。这成本有点大啊
    woostundy
        4
    woostundy  
    OP
       2017-08-15 18:16:33 +08:00
    @aisk monkey patch 对 requests 现在还有效吗?我自己单开测试的时候还是阻塞的啊。
    加了 httplib=True 会报已经不支持。
    yonoho
        5
    yonoho  
       2017-08-15 18:17:28 +08:00
    @woostundy 有效啊,你咋测的
    terrawu
        6
    terrawu  
       2017-08-15 18:18:13 +08:00
    gevent 和 requests 相性不和,楼主你用 httplib2 + gevent 吧,这坑我踩过的。
    cloudyplain
        7
    cloudyplain  
       2017-08-15 18:43:29 +08:00
    gevent+requests 没问题。
    troywinter
        8
    troywinter  
       2017-08-15 19:41:32 +08:00
    看 gunicorn 关于 worker 类别的文档,对于这种第三方请求服务有很具体的说明。
    aisk
        9
    aisk  
       2017-08-15 22:58:08 +08:00
    有效,你怎么测试的?如果你只看单个请求,看上去 requests 去请求远程接口的时候是阻塞的,不过如果这个时候还有其他请求过来,gunicorn 还能继续提供服务的。
    gouchaoer
        10
    gouchaoer  
       2017-08-15 23:06:15 +08:00 via Android
    说换 tornado 的,你们去看看 tornado 的那个异步 httpclient 好用不,而且人家都用 flask 写了

    我说结论吧,无解。。。换 go 语言吧

    如果你使用 php,那 swoole/zanphp 可以做到同步方式来写异步 /协程
    gouchaoer
        11
    gouchaoer  
       2017-08-15 23:06:56 +08:00 via Android
    你 flask 是同步的,用异步 httpclient 没用啊。。。
    lerry
        12
    lerry  
       2017-08-15 23:13:00 +08:00
    多开一些 worker

    或者楼主你来看这个 https://github.com/kennethreitz/grequests
    aisk
        13
    aisk  
       2017-08-15 23:52:16 +08:00
    @gouchaoer 看了 gevent 你就懂了,flask 照样玩异步
    EchoUtopia
        14
    EchoUtopia  
       2017-08-15 23:52:51 +08:00 via iPhone
    gevent 可以把你的 python 同步阻塞代码自动变成异步的,patch_all 加个 httplib=True 参数,你看下在 gunicorn 下对应怎么处理
    EchoUtopia
        15
    EchoUtopia  
       2017-08-15 23:54:38 +08:00 via iPhone
    好吧,没看到你说不起作用了
    SlipStupig
        16
    SlipStupig  
       2017-08-16 00:09:21 +08:00
    grequest
    laoli2017
        17
    laoli2017  
       2017-08-16 09:46:58 +08:00
    推荐 sanic,这个完全是基于 asyncio 来实现的,当然现在还不太成熟,但是性能上完全没问题(我做过压测,比 tomcat 强多了)。而且也不需要其他应用服务器,比如独角兽什么的。
    mengskysama
        18
    mengskysama  
       2017-08-16 10:07:19 +08:00 via iPhone
    gevent requests 一点毛病没有,参考 grequests。
    yonoho
        19
    yonoho  
       2017-08-16 10:19:18 +08:00
    我猜楼主的测试是在一个请求里调用多次 requests,并期待并行执行的效果。但因为代码本身写成了同步顺序执行(多行或者循环),所以用了 monkey patch 也没用。这种情况下不改代码是不可能并行的,最简单的方法是依然使用 gevent worker,然后使用 #12 提供的 grequests 包重写第三方 http 请求部分,把这些 http 请求放在一起调用 grequests.map ( gevent.spawn ),就能实现并行加速。
    alvinbone88
        20
    alvinbone88  
       2017-08-16 11:01:19 +08:00
    建议换 aiohttp
    gouchaoer
        21
    gouchaoer  
       2017-08-16 11:37:29 +08:00
    @aisk gevent 就一个把同步 api 来 hack 成异步的嘛,但是 controller 里面凡是涉及 IO 的东西都能弄成异步的?(包括数据库、redis 等等),这不可能的吧。。。。你能给个 github 的 repo 例子让我瞻仰一下么?
    aisk
        22
    aisk  
       2017-08-16 11:47:28 +08:00
    @gouchaoer 网络 IO 是可以的,文件 IO 不行,因为 gevent 直接 pactch 的自带的 socket 等模块。不过有些直接用 C 来实现的模块,内部没有使用 Python 的 socket 模块,就不会被 patch 到了,比如最常见的那个 mysql 库。
    aisk
        23
    aisk  
       2017-08-16 11:48:54 +08:00
    gouchaoer
        24
    gouchaoer  
       2017-08-16 11:50:33 +08:00
    @aisk show me the code
    neoblackcap
        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 能独立地返回请求给客户端。
    aisk
        26
    aisk  
       2017-08-16 13:16:20 +08:00
    @gouchaoer suck my dick
    neoblackcap
        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。
    woostundy
        28
    woostundy  
    OP
       2017-08-16 13:43:52 +08:00
    多谢各位。先解释下上面的测试,我单独用 gevent 的 monkeypatch,requests 的确没效果,gevent 显示 httplib=True 已不再支持。但用 gunicorn 开 gevent 是有效的。
    Dominator
        29
    Dominator  
       2017-08-16 13:48:40 +08:00
    恕我直言。。你可以用 grequests 先试试,如果能解决问题就解决了。那种一上来就换大框架换到 tornado 的,不敢恭维。
    upwell
        30
    upwell  
       2017-08-16 13:53:26 +08:00
    有个做法是,把外部依赖的接口改写成用异步框架实现的,不走 flask,其它的请求仍然走 flask。
    terrawu
        31
    terrawu  
       2017-08-16 14:02:49 +08:00
    一群小学生,没有踩坑就不要瞎猜,受不了了。看我 #6 回复。
    aisk
        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 的,本来加一个参数就解决的问题,是要让别人重写项目?
    terrawu
        33
    terrawu  
       2017-08-16 14:43:11 +08:00   1
    楼上的大兄弟,你的代码的确不会 block,但是你测过 “并发度” 么?

    requests 的 streaming 和 pool size 的机制会导致并发瓶颈,monkey patch 的并不能帮助这个问题。
    而 httplib2 没有 streaming 的接口,反而能提高真的的并发。
    clino
        34
    clino  
       2017-08-16 14:46:01 +08:00
    我建议你的应用和 openresty 配合起来用,要用 request 的地方都改用 openresty 来实现
    aisk
        35
    aisk  
       2017-08-16 14:50:05 +08:00
    @terrawu 你是想解决楼主的问题,还是想从我的回复里挑个刺儿,怼我一下?
    woostundy
        36
    woostundy  
    OP
       2017-08-16 14:51:24 +08:00
    @terrawu 兄弟,你所说是最贴合我的实际情况的。
    terrawu
        37
    terrawu  
       2017-08-16 14:51:53 +08:00
    @aisk 我是在售卖我的 #6 回复的优点。
    yangxin0
        38
    yangxin0  
       2017-08-16 14:54:30 +08:00 via iPhone
    首先你们业务是猛增吗? 如果是我觉得可以快速加服务器解决当前的困境。 然后可以查一查是请求处理哪个环节出了问题。问题没定位出来不要贸然的换语言、换框架,打猴子补丁。
    woostundy
        39
    woostundy  
    OP
       2017-08-16 14:55:36 +08:00
    @aisk 我之前说的测试方法的确有问题,用 Gunicorn 开了 gevent 实际是有效的,但是当并发量稍微一大还是会出现阻塞的情况。另外我自己单独写个 py 脚本打上 monkey patch 没效的,而且在官方文档里提到用 grequests,综上让我误以为是 gevent 没生效。

    所以你说的也是对的,Gunicorn+gevent 可以让 requests 变异步,但并发高了出问题应该是 terrawu 所说的原因。
    gouchaoer
        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 这么神通广大?
    terrawu
        41
    terrawu  
       2017-08-16 14:58:33 +08:00
    @gouchaoer 大兄弟,gevent 的第一准则,不要使用 c socket 的库。
    aisk
        42
    aisk  
       2017-08-16 14:59:24 +08:00   1
    @woostundy 单独打 monkey patch 再用 gunicorn 启动是无效的,因为 gunicorn 要自己先启动,这个时候 socket 和 thread 模块都已经加载进来了。然后执行你的代码时,才会进行 monkey patch,这个时候已经来不及了。解决了问题就好。
    aisk
        43
    aisk  
       2017-08-16 15:02:18 +08:00
    @gouchaoer 我上面说了,直接用 C 来调用 socket 的都不能 patch。不过 redis 那个库网络部分就是 Python 实现的,Mysql 的话现在的方案都是用 pymysql 这个库,和 C 的那个接口一致,所以都没有问题。
    aisk
        44
    aisk  
       2017-08-16 15:02:45 +08:00
    @gouchaoer 就是这么神通广大,服不服?
    terrawu
        45
    terrawu  
       2017-08-16 15:05:09 +08:00
    实在要用 c socket 的库也有一些奇怪的方案, 比如这个 https://github.com/douban/greenify
    terrawu
        46
    terrawu  
       2017-08-16 15:09:02 +08:00
    最后多说一句,讨论问题谦虚一些为好,否则有人忍不住(比如我)就会跳出来开嘲讽了。
    aisk
        47
    aisk  
       2017-08-16 15:15:33 +08:00
    @woostundy 我怀疑是不是你没有开 keep alive 导致的? requests 并不会默认帮你开启 keep alive,需要先创建一个 Session 对象,后续都用这个 Session 对象发送请求,才会 keep alive
    1iuh
        48
    1iuh  
       2017-08-16 15:17:41 +08:00
    @aisk 你说的没错 gevent + requests 确实可以工作。 但是 @terrawu 比你更正确。
    gevent + requests 在低并发的情况下确实是异步的,但是在高并发的情况下会阻塞。
    不信你可以自己测试一下。
    ljcarsenal
        49
    ljcarsenal  
       2017-08-16 15:17:56 +08:00
    node go
    TJT
        50
    TJT  
       2017-08-16 15:19:26 +08:00
    增加 worker,多线程,分布式。
    gouchaoer
        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 库就能健壮的把现有的同步代码改成协程版本的
    aisk
        52
    aisk  
       2017-08-16 16:58:29 +08:00
    @gouchaoer 搜索 http keep-alive,有真相
    aisk
        53
    aisk  
       2017-08-16 17:02:13 +08:00
    @gouchaoer 你信不信关我屁事?
    sunwei0325
        54
    sunwei0325  
       2017-08-16 17:20:36 +08:00
    没有人推荐用 grequests 么?
    ChangHaoWei
        55
    ChangHaoWei  
       2017-08-16 17:21:39 +08:00
    没有人推荐用 celery 吗? 还有就是 socket.io 吗? django 和 flask 都有 socket.io 的插件啊
    ToughGuy
        56
    ToughGuy  
       2017-08-16 17:23:03 +08:00
    哈哈哈哈, 看了楼主 /t/325328 里面问 tornado+flask+requests 阻塞解决方案, 一楼喊换成 gunicorn+flask, 然后就有了现在这个帖子。

    哈哈,看老帖子的 1,3 楼和这个的 1,2 楼笑死我了。

    楼主: 黑人问号...

    期待楼主问题得到妥善解决。
    ChangHaoWei
        57
    ChangHaoWei  
       2017-08-16 17:24:25 +08:00
    @terrawu 大兄弟,你的观点我认同,因为我同样踩过这个坑。不过我不太懂为什么会出现这种情况。

    还有就是问一下你的解决方案有开源库采用的吗?请给个链接。

    谢谢。
    ChangHaoWei
        58
    ChangHaoWei  
       2017-08-16 17:31:07 +08:00
    @ToughGuy 我要笑出腹肌了
    gouchaoer
        59
    gouchaoer  
       2017-08-16 17:41:09 +08:00
    @aisk 问题是你让人在 flask 的 requests 里加上 keep-alive 这很蠢啊,说明你后台的很多基础概念都没搞清楚,狂妄没什么比起谦虚的类型,我个人反而喜欢那种自信 /自大 /狂妄的人,可是你很蠢啊
    woostundy
        60
    woostundy  
    OP
       2017-08-16 17:47:08 +08:00
    多谢各位帮忙,我觉得有必要重新描述一下问题:
    现在有服务 A 和服务 B,服务 A 有一部分数据库读写操作,每次都会通过 requests 访问服务 B (每次都只会访问一次,B 服务性能完全足够)
    服务 A 的结构是 Flask + Gunicorn + Gevent,当 A 服务器 QPS 到 50 时,数据库访问没有压力,但会出现 requests 到服务 B 的速度变慢甚至超时。

    不是 A 一次请求内多次请求 B 服务,而是每次 A 都会请求一次 B,所以设置 session 共享 tcp 连接( keep-alive )应该是无效的。
    terrawu
        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
    ChangHaoWei
        62
    ChangHaoWei  
       2017-08-16 18:04:20 +08:00
    @woostundy socket.io 是即时通讯框架。支持 websocket。可以主动联系 c 端。也就是请求和返回可以分开。
    icedx
        63
    icedx  
       2017-08-16 18:05:41 +08:00 via Android
    Flask+Uwsgi 吧
    Flask+Gevent 数据库部分有小 Bug
    前几天经常拉倒整个进程 换了之后没出过毛病
    terrawu
        64
    terrawu  
       2017-08-16 18:09:13 +08:00
    @ChangHaoWei 没有,我手写的,我情况和楼主不一样,我的类似 gateway, 需要同时请求 N 个后端 http 服务,突发比较高。用的全局 dict 存多个 httplib2.HTTP 实例,跑的很流畅。
    terrawu
        65
    terrawu  
       2017-08-16 18:10:23 +08:00
    不过已经被我用 go 改写了,效果好太多。
    neoblackcap
        66
    neoblackcap  
       2017-08-16 19:30:56 +08:00
    @woostundy 你的 QPS 没有增长,那么你是 gunicorn 的 worker 跑满了吗,要不然不应该线性增长吗?要不直接加大 gunicorn 的 worker 看看,反正你数据库又不是瓶颈
    yonoho
        67
    yonoho  
       2017-08-16 19:32:20 +08:00
    @1iuh 请问这个问题应该怎么复现?我用最简化的测试方法(/a 访问 1000 次 /b, 每个 /b 访问 1 次 /c, /c ),1000 次 10s+,没感觉有阻塞
    yonoho
        68
    yonoho  
       2017-08-16 19:33:35 +08:00
    patch #67: /c sleep 1,访问一次 /a 10s+
    1iuh
        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 左右。
    chenqh
        70
    chenqh  
       2017-08-16 21:33:56 +08:00 via iPhone
    @terrawu golang 性能好了多少?
    terrawu
        71
    terrawu  
       2017-08-16 22:23:47 +08:00   1
    @chenqh 好了非常多,但也不能说是 python 不行。

    因为我的程序需要聚合一下后端的数据,然后做一个 digest(一些哈希操作), 这个步骤会比较吃 CPU。
    后来 IO 方面的瓶颈都优化完毕后,程序的瓶颈就体现在这里了

    1. python 代码效率低,太耗 CPU
    2. GIL 不能用多核
    3. 内存耗费也比较大。

    所以优化到这一步时候,用 go 改写就很适合了,所谓性能的话,提升了几十倍吧。
    chenqh
        72
    chenqh  
       2017-08-17 08:34:42 +08:00
    @terrawu 好吧,反正我的 python web 程序,CPU 基于不到 10%,
    lolizeppelin
        73
    lolizeppelin  
       2017-08-17 09:01:17 +08:00 via Android
    你现在是 a 被访问一次就去 a 就去访问 b 一次?

    和数据库用链接池一样 requests 的 session 也池化
    一开始就建立一定数量的链接然后维护心跳
    不然老是 socket connect 也很耗资源的

    简单实现可以炒 py redis 的写法试试
    shiina
        74
    shiina  
       2017-08-17 09:35:29 +08:00
    从楼上兄弟链接去旧贴看到了 Livid 老哥
    [doge]
    lolizeppelin
        75
    lolizeppelin  
       2017-08-17 10:23:42 +08:00
    详细看了下源码

    pool_key = (scheme, host, port)

    也就是说默认 requests 的默认的池在你目的地只有一个的时候是无效的

    所以一个是直接用 session 做池,还有一个办法是重写 HTTPAdapter

    好像 init_poolmanager 的 pool_kwargs 传入 maxsize 就能直接池化单个目的地了!!

    我操都封装好了嘛...........
    woostundy
        76
    woostundy  
    OP
       2017-08-17 10:28:35 +08:00
    @lolizeppelin 我试过了,并没有效果。这需要多个请求用同一个 session,而 flask 不同请求之间没法用同一个 session。

    我又尝试在外层弄一个全局的 session,HTTPAdapter 里把 maxsize 调到 100,但是效率更低了,超过一半的请求都超时。
    lolizeppelin
        77
    lolizeppelin  
       2017-08-17 10:34:22 +08:00
    理论上不会那么差啊 池化以后是长链接了 单纯的 http 请求后端没问题的话前端 qps 50 应该不成问题的

    等等, 你是单进程的还是多进程的?
    woostundy
        78
    woostundy  
    OP
       2017-08-17 10:50:12 +08:00
    @lolizeppelin 之前是 8 个子进程( 4 核 CPU ),考虑到 CPU 没压力,瓶颈在 IO 这,又开到了 16 子进程,然后能跑到 50 qps 了,再高就会出现 HttpsConnectionError 了。
    另外,你确定不是同一个请求里的 session 能共用同一个长连接?
    terrawu
        79
    terrawu  
       2017-08-17 11:09:59 +08:00   1
    > 弄一个全局的 session,HTTPAdapter 里把 maxsize 调到 100

    这个太低,调到 1000 试下,还不行的话,你的需求就用 httplib2 实现吧。
    lolizeppelin
    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 的问题了

    测玩可以改成协程的试试性能有没有提高
    lolizeppelin
        81
    lolizeppelin  
       2017-08-17 11:18:28 +08:00
    @terrawu

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

    这样不用折腾 dns
    lolizeppelin
        84
    lolizeppelin  
       2017-08-17 11:34:56 +08:00
    0v0 不对 已经是长连接池了 只有一个目的地没这问题 233
    enomine
        85
    enomine  
       2017-08-17 11:36:53 +08:00
    @terrawu gevent + requests 没任何问题
    smallHao
        86
    smallHao  
       2017-08-17 11:38:03 +08:00
    @terrawu 兄弟 有试过 japronto 吗 这个底层 uvloop 应该跟 go 并发是在一个级别的
    terrawu
        87
    terrawu  
       2017-08-17 11:41:08 +08:00
    @smallHao 我的程序后来瓶颈不在 IO 上了,是在 CPU 上。python 代码执行效率较低以及 GIL, 不换 go 无法继续优化了。
    terrawu
        88
    terrawu  
       2017-08-17 11:42:00 +08:00
    @enomine 回帖之前看下我的场景描述,你没遇到问题,不代表我没问题。
    enomine
        89
    enomine  
       2017-08-17 11:42:50 +08:00
    @gouchaoer gevent + gunicorn + requests + pymysql 没有问题
    woostundy
        90
    woostundy  
    OP
       2017-08-17 11:45:11 +08:00
    @terrawu 有效果!现在 16 个子进程,用全局 session,maxsize 1000,能跑到 150 qps 了。非常感谢!
    woostundy
        91
    woostundy  
    OP
       2017-08-17 11:49:00 +08:00
    @terrawu 我之前认为我的进程开的这么多,每一个开 100 的 maxsize 已经足够了,结果就是 session 提出来和不提出来没啥差别。
    现在把 maxsize 提高上去,CPU 马上能跑满了。神奇。
    enomine
        92
    enomine  
       2017-08-17 11:49:01 +08:00
    @terrawu 是我没有具体看场景,抱歉
    terrawu
        93
    terrawu  
       2017-08-17 12:47:37 +08:00
    @woostundy 好说,本来我是打算在 #6 一击脱离的,不过后来再点开看,发现很多盆友没踩过坑就来强答,然后还吵了起来了,真是受不了。
    terrawu
        94
    terrawu  
       2017-08-17 13:04:42 +08:00
    我的程序在实现初期就打算用 go 的,不过项目组内对于 go 选型还是有些质疑的,最后一路优化下来,目前大家对 go 的无脑质疑已经不见了,会认真评估 python 和 go 了。
    woostundy
        95
    woostundy  
    OP
       2017-08-17 13:14:04 +08:00
    @terrawu 的确是的。非常感谢。
    JasperYanky
        96
    JasperYanky  
       2017-08-17 14:38:49 +08:00
    小白在这儿坐等结论了~ 希望最后总结下~
    chenqh
        97
    chenqh  
       2017-08-17 22:09:19 +08:00
    @terrawu 估计你项目的并发比较大。。我上个项目并发最多一分钟 3000 也就是一秒钟 50,所以感觉暂时用 python 就好了
    yonoho
        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。
    JasperYanky
        99
    JasperYanky  
       2017-08-19 10:23:52 +08:00
    @yonoho 太谢谢了
    1iuh
        100
    1iuh  
       2017-08-22 10:17:34 +08:00
    @yonoho #98 这个测试非常好,但是有一个问题, 这个测试方式其实是 CPU 密集型的,因为没有带延迟的 IO。最终测试的其实就是代码执行的效率。所以用 GO/C 模块有绝对的优势。 可以测试一下 IO 密集型的(比如请求带延迟的 URL )相信这样更有说服力。
    1  2  
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     2623 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 32ms UTC 12:31 PVG 20:31 LAX 05:31 JFK 08:31
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86