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 · 15035 次点击
    这是一个创建于 2644 天前的主题,其中的信息可能已经有所发展或是发生改变。
    该怎么优化呢?异步 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   ·   实用小工具   ·   1623 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 52ms · UTC 16:50 · PVG 00:50 · LAX 08:50 · JFK 11:50
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.