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
Te11UA
V2EX  ›  Python

多线程与协程爬虫有很大区别吗?多线程转协程能优化多少?

  •  1
     
  •   Te11UA · 2020-08-06 15:38:56 +08:00 · 7216 次点击
    这是一个创建于 1330 天前的主题,其中的信息可能已经有所发展或是发生改变。
    目前一个爬虫项目运行在 4c8g 下的机器,requests 运行在 4 进程* 400 线程下,单页面 200k 左右。
    最近发现 CPU 和内存都有一点瓶颈,如果转成协程的话是否能优化爬取速率呢?
    (不讨论反爬的情况,单纯从爬虫效率上看)
    54 条回复    2020-08-08 19:03:25 +08:00
    xiaolinjia
        1
    xiaolinjia  
       2020-08-06 15:47:32 +08:00
    理论上协程少了线程之间的切换,速度和内存应该会更优点。
    araraloren
        2
    araraloren  
       2020-08-06 15:53:22 +08:00
    协程的东西都运行在一个线程,python 的话应该有不少优势,毕竟 python 有 GIL
    Vegetable
        3
    Vegetable  
       2020-08-06 16:12:12 +08:00
    协程理论上是更好的方案,但是效率上是否能提高存疑。
    你这已经有 1600 线程了,单页面 200k 就是 200k*1600 同时下载,超过 300mb 的流量,带宽不会成为瓶颈吗
    locoz
        4
    locoz  
       2020-08-06 16:17:53 +08:00
    对于爬虫而言,从多线程换成协程在性能方面提升很小,甚至如果你操作流程中有同步的东西的话还会降低性能。但用协程来做之后,内存占用方面会有明显下降,也就是说你可能能开更高的并发,相对而言也会有一定的采集速率提升。
    youxiachai
        5
    youxiachai  
       2020-08-06 16:20:45 +08:00
    @locoz ls 有个大佬做了一个算术题, 相比更高的并发...带宽才是瓶颈吧..
    wysnylc
        6
    wysnylc  
       2020-08-06 16:30:19 +08:00
    协程是跑在一个线程里,多线程转成协程除了内存占用下降性能也会下降
    协程就不是这么用的别无脑协程
    locoz
        7
    locoz  
       2020-08-06 16:38:45 +08:00
    @youxiachai #5 带宽是可以随便加的,而且也不会有真正意义上的同时下载(毕竟服务器返回的时间并不相同),所以其实带宽并不是什么大问题...
    locoz
        8
    locoz  
       2020-08-06 16:40:52 +08:00
    @wysnylc #6 你忘了 GIL
    Te11UA
        9
    Te11UA  
    OP
       2020-08-06 17:13:50 +08:00
    @Vegetable 带宽实际上没有那么多,因为代理质量一般,而且有一定的解析操作。用的是按量付费,看监控就十几 MB 而已。
    tigerstudent
        10
    tigerstudent  
       2020-08-06 17:14:59 +08:00 via Android
    gevent 使用很简单,可以直接试试
    Te11UA
        11
    Te11UA  
    OP
       2020-08-06 17:15:15 +08:00
    @locoz 了解,我去尝试先,目前内存的确是有点瓶颈
    newmlp
        12
    newmlp  
       2020-08-06 17:30:55 +08:00   ❤️ 5
    别骗自己了,就线程和协程那点性能差异你用一行垃圾代码就搞没了,还是花时间优化下自己的业务代码吧
    hakono
        13
    hakono  
       2020-08-06 17:50:33 +08:00 via Android
    400 个线程,你 cpu 不瓶颈就怪了。。。。。
    现代的程序一般是一个线程对应 cpu 的一个核心性能才比较好,开 400 个线程和以前单核 cou 跑多线程一样性能没什么提升反倒降低性能

    这个情况用协程应该能改善楼主的 cpu 和内存使用情况,而且协程可以实现比多线程更高的并发(协程可以一次轻松发出几千几万个请求,你用线程难道开几千个几万个线程么)
    当然你网络带宽死的话,协程也只是比线程更省资源罢了
    wysnylc
        14
    wysnylc  
       2020-08-06 17:53:50 +08:00
    @newmlp #12 "协程可以一次轻松发出几千几万个请求,你用线程难道开几千个几万个线程么"
    看的我尬的不行
    xcstream
        15
    xcstream  
       2020-08-06 17:54:04 +08:00
    没多大用 又不是服务器
    itskingname
        16
    itskingname  
       2020-08-06 18:22:22 +08:00 via iPhone
    之前爬斗鱼直播弹幕,第一版爬虫用的多线程,5 台服务器最多能同时监控 250 个房间的弹幕。后来使用 asyncio 重构成异步爬虫,只用 3 台机器,可以同时监控 25000 个房间。
    zengxs
        17
    zengxs  
       2020-08-06 18:31:10 +08:00
    曾经和小伙伴讨论过这个问题

    异步编程要考虑不能引入同步的代码,而 Python 的生态目前来讲,大部分库仍然是同步的,因此强行上异步,会造成开发效率的下降,而性能却不见得能比用多线程快很多

    我们最后得出的结论是,除非有一个场景,特别适合使用 asyncio,否则一般还是倾向于使用多线程
    zengxs
        18
    zengxs  
       2020-08-06 18:37:37 +08:00
    看了一下楼主的描述,4 核却开了 400 线程,cpu 做线程调度的开销都很大了吧

    不考虑反爬单纯考虑性能的话,我觉得可以考虑换成协程
    supermoonie
        19
    supermoonie  
       2020-08-06 18:42:54 +08:00 via iPhone
    还不如异步 io
    saberlong
        20
    saberlong  
       2020-08-06 19:01:50 +08:00 via Android
    @wysnylc 现在一般普通主流计算机,直接开几万协程确实没问题。以前开发写测试时,不小心开了几十万协程,还能跑。但是线程却不行,资源占用和上下文切换决定了它不可能开启同样多的数量时还能保证可用。
    你的怀疑点应该是一次性发出这么多请求。这得看理解了,如果指发出第一个数据包开始,确实可能。
    假设极端情况,被访问的服务器响应很慢,导致发出数据包请求后就在 io 等待。那么协程确实能做到同时有几万个请求在等待响应。只是线程要做到同样量级,需要机器资源就更多了。
    不过实际上并没有优势,现实中不会这么极端,瓶颈通常也不在这。
    auxox
        21
    auxox  
       2020-08-06 19:10:19 +08:00
    系统优化的第一步是先找到瓶颈点
    saberlong
        22
    saberlong  
       2020-08-06 19:30:24 +08:00 via Android
    @wysnylc 另外你可以找下百万 Go TCP, 百万 websocket 同时连接的文章。有每个连接使用 go 程,和 reactor epoll 的性能对比。
    newmlp
        23
    newmlp  
       2020-08-06 19:41:06 +08:00
    @wysnylc 你的认知就觉得除了协程,就没其他办法一次发出几千几万个请求了?
    Vibra
        24
    Vibra  
       2020-08-06 19:54:35 +08:00 via iPhone
    省了切换的时间,但是尽管有 Gil 的存在,因为线程 io 的自动切换,其实差不了太多。
    wysnylc
        25
    wysnylc  
       2020-08-06 21:22:23 +08:00
    @newmlp #23 你是不是回错人了
    wysnylc
        26
    wysnylc  
       2020-08-06 21:24:37 +08:00
    @saberlong #22 你是不是忘了 netty 是用什么写的?别无脑吹 go 了百万链接瓶颈根本不在线程协程而是内存带宽磁盘,线程创建也没那么耗费资源
    给你个链接学学吧:
    为什么 Java 坚持多线程不选择协程? - 大宽宽的回答 - 知乎
    https://www.zhihu.com/question/332042250/answer/734115120
    ClericPy
        27
    ClericPy  
       2020-08-06 21:25:13 +08:00   ❤️ 1
    Talk is cheap, 试试就知道了

    直接说结论:

    1. 本次测试不记带宽发送本地端口的请求, 本地 server 使用 golang 默认的 net/http 简单实现的

    2. 对比 httpx 协程和 requests 多线程, 二者都使用连接池复用连接. 前者比后者快了大概 20%(虽然后者被我负优化了...), 启用 uvloop 以后也没拉开太大差距.

    3. 对比 aiohttp 和 requests, 不使用 uvloop 的时候前者是后者 3 倍, 开了 uvloop 大概 3068 qps, 是后者的 3.15 倍, 而 golang 内置的 net/http 测试也才 3300. 虽然很大因素是 aiohttp 内核很多地方使用 Cython 实现的...

    4. 对比 windows vs linux, 前者是游戏本所以多核(但是只能用一个...), 后者在阿里云服务器上虽然单 CPU 但是有 uvloop 加成, 速度提高一倍

    部分代码: https://github.com/ClericPy/torequests#benchmarks


    一句话总结, 以上测试纯属娱乐不作数, 真想用快的, 就选 aiohttp 就好了

    之前貌似还看到, 裸 uvloop 单核情况下和 golang 差不多
    silencefly
        28
    silencefly  
       2020-08-06 21:31:11 +08:00 via iPhone
    多进程加协程
    OysterQAQ
        29
    OysterQAQ  
       2020-08-06 21:45:56 +08:00
    协程属于用户空间的多线程实现,有上下文切换 但是会比内核空间的上下文切换代价小很多,但是如果从瓶颈来看的话 需要看看要爬的网站是否有连接数以及速率的限制,楼上说 cpu 只有 4 核多线程没意义,但是其实爬虫这种 io 密集型场景,线程在 http 请求阻塞的时候是不占用 cpu 的,超过核心数的线程在这时候能更好的占满 cpu,不至于让 cpu 空转
    wuhanchu
        30
    wuhanchu  
       2020-08-06 22:06:08 +08:00 via iPhone
    不应该一起用吗
    chihiro2014
        31
    chihiro2014  
       2020-08-06 22:10:35 +08:00
    协程不就是单线程线程池么
    wangritian
        32
    wangritian  
       2020-08-07 01:56:01 +08:00
    如果更换协程后仍然限制 1600 总并发,猜测 cpu 稍微减轻,内存较大下降;不限制并发,cpu 和带宽会有一个顶满
    binux
        33
    binux  
       2020-08-07 02:09:09 +08:00
    @wysnylc 我发现你根本就没看懂这里讨论的问题是什么。
    你用 netty 搞 IO 异步 + worker 线程和 python 的协程没有任何区别。
    wnpllrzodiac
        34
    wnpllrzodiac  
       2020-08-07 07:28:01 +08:00 via Android
    协程不能多核,自己开多实例。但是代码逻辑简单清晰易维护
    xiaolinjia
        35
    xiaolinjia  
       2020-08-07 08:50:25 +08:00
    @wnpllrzodiac 之前看过一个库,说是可以多进程+协程。叫 aiomultiprocess 。
    cwjokaka
        36
    cwjokaka  
       2020-08-07 09:25:40 +08:00
    转成协程对应使用的库也要改成异步的,不小心用了阻塞的方法就完蛋
    warcraft1236
        37
    warcraft1236  
       2020-08-07 09:27:03 +08:00
    看上边说 GIL 的,你们真的理解 GIL 是啥吗?他这个又不是 CPU 负载高,GIL 没啥影响
    CriseLYJ
        38
    CriseLYJ  
       2020-08-07 09:45:57 +08:00
    多进程+异步实现,充分利用多个 cpu,异步减少 cpu 资源消耗。
    CODEWEA
        39
    CODEWEA  
       2020-08-07 10:25:55 +08:00
    你资源就那么大,再玩也没有招
    knva
        40
    knva  
       2020-08-07 11:28:55 +08:00
    没啥区别,爬虫爬快了不怕对面 block ?
    chazyu1996
        41
    chazyu1996  
       2020-08-07 11:36:14 +08:00
    没有 sleep 的爬虫没有灵魂,那么快有啥用
    est
        42
    est  
       2020-08-07 11:46:17 +08:00
    @chazyu1996 真相了。
    tairan2006
        43
    tairan2006  
       2020-08-07 13:25:39 +08:00
    你如果是 4 核的话,400 个线程有点过分啊…虽然 IO 密集型,也不能这么夸张吧
    binux
        44
    binux  
       2020-08-07 13:29:43 +08:00 via Android
    Cloutain
        45
    Cloutain  
       2020-08-07 15:37:28 +08:00
    在多线程下再实现协程,这样避免过多的用户态内核态切换。
    hakono
        46
    hakono  
       2020-08-07 16:01:21 +08:00
    @wysnylc #14 如果你有什么高见还请你说清楚。不过我反倒觉得你在这楼的回复让人看得尬死了。
    skinny
        47
    skinny  
       2020-08-07 16:26:52 +08:00
    瓶颈难道不是在反爬、被爬网络带宽和服务质量、爬虫网络带宽吗?
    老说协程提高效率节省资源什么的,一些时候确实是这样,但是在一般爬虫项目里线程协程真的没什么差别,我同意 12 楼的看法。
    wysnylc
        48
    wysnylc  
       2020-08-07 16:36:28 +08:00
    @hakono #46 "现在一般普通主流计算机,直接开几万协程确实没问题。以前开发写测试时,不小心开了几十万协程,还能跑。但是线程却不行,资源占用和上下文切换决定了它不可能开启同样多的数量时还能保证可用。"
    协程也是跑在线程之上,如果真的同时运行几十万线程而不挂只有一种可能,活跃的线程其实没几个,我后面贴的为什么 Java 坚持多线程不选择协程?里面有这样一句话:上面的讨论简化了 RSS 和 VM 的区别。实际上一个线程启动后只会在虚拟地址上占位置那么多的内存。除非实际用上,是不会真的消耗物理内存的。
    所以所谓的"几万"根本就是扯淡,多协程本质上和多线程没有区别,多线程该有的问题多协程一样有,而单个协程是跑在单线程上单线程可能跑几万请求?用脚指头想也知道答案

    "另外你可以找下百万 Go TCP, 百万 websocket 同时连接的文章。有每个连接使用 go 程,和 reactor epoll 的性能对比。
    "
    老 Go 吹了,上手就是百万 233333 教程一大把就是没啥公司用
    sss495088732
        49
    sss495088732  
       2020-08-07 16:43:53 +08:00
    python 同一个业务内全使用 aio 家族
    bytesmith
        50
    bytesmith  
       2020-08-07 16:56:29 +08:00
    别想啦,你这估计差不多到极限了,你费劲巴拉搞的协程,还不如直接加硬件来的实在。
    bytesmith
        51
    bytesmith  
       2020-08-07 17:01:00 +08:00
    或者用 工具看看 cpu 的瓶颈在哪里, 是都用在切换上下文了还是说在跑 计算密集的任务,然后在决定怎么优化,别无脑优化
    sunriz
        52
    sunriz  
       2020-08-08 00:57:57 +08:00
    爬虫应该是 Io 密集的吧
    black11black
        53
    black11black  
       2020-08-08 09:55:22 +08:00
    楼上一大堆人不知道说的啥。IO 复用不光是解决内核态开销问题,还有一个大问题是解决线程切片时间的问题啊。

    你以为的多线程+GIL:一个线程执行完了释放 GIL,切换到下一个线程,申请 GIL,开始业务逻辑
    实际的多线程+GIL:一个线程执行完了释放 GIL,等待很长时间,系统 call 你了才能切换
    ClericPy
        54
    ClericPy  
       2020-08-08 19:03:25 +08:00   ❤️ 1
    看到有几个回复挺反常识的, 提醒几个 Python 并发编程的常识问题吧

    1. 线程开的越多, 执行起来就越快吗?

    并不会.

    一方面, 线程开太多, CPU 切换的成本会变高, 也就相对降低了 CPU 利用率, CPU 很多时间浪费在调度上而不是计算上. 有关怎么切换的, 可以随处找找 GIL 的文章, 不过还是不建议自己修改对应参数 setswitchinterval (旧版本的 setcheckinterval )
    另一方面, 对爬虫来说, 如果连接速度靠谱的话, 有可能一个线程就跑满了带宽, 那开多线程除了让所有任务一起抢资源, 并不会降低总时长, 也就是常见场景: 为什么我开 5 线程比开 100 线程还快(或者差不多). 与普通程序不同, 爬虫程序传输数据一方面看你的带宽, 另一方面还特别看重目标服务器的负载能力.

    2. 有一个比较合理的并发数量吗?

    参考:
    Python3 里面 ThreadPoolExecutor 的 max_workers 默认值是 (os.cpu_count() or 1) * 5
    可以根据带宽使用率适当调整这个数值.
    (另: 多进程 ProcessPoolExecutor 默认 max_workers 就是 os.cpu_count() or 1)


    3. 不计带宽和 CPU 能力的情况下, 是不是线程开的越多, 速度越快?

    也不完全是.

    拿 Requests 库来举例, 它的 Session 默认连接池大小取决于 HTTPAdapter 对象的 pool_connections, 这个默认值 DEFAULT_POOLSIZE = 10
    简单的说, 如果不修改 HTTPAdapter 连接池的大小, 那可能瓶颈基本限定在这里了. 至于有些人选择不用 Session 复用连接, 我举个例子算了: 之前抓某东的某数据, 复用连接的情况下速度比每次新建连接大概快了十几倍.

    4. 是不是用协程就比线程快, 节省 CPU?

    不一定.

    协程提高的是 CPU 效率, 遇到高并发的抓取, 你会发现协程 CPU 一直 100%, 因为它真的很忙, 而多线程反而可能在 80~100% 波动. 至于速度, 简单的提个例子, falcon 那是相当擅长 Benchmark.

    协程中最使我受益的并不是性能, 而是它属于随时可以 Cancel 的, 一个已经执行的线程, 想在外部杀死它简直太费劲了.
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   961 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 30ms · UTC 21:46 · PVG 05:46 · LAX 14:46 · JFK 17:46
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.