V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
• 请不要在回答技术问题时复制粘贴 AI 生成的内容
css3
V2EX  ›  程序员

python3 多进程求助 OSError: [Errno 24] Too many open files

  •  
  •   css3 · 2021-03-30 10:47:24 +08:00 · 3804 次点击
    这是一个创建于 1366 天前的主题,其中的信息可能已经有所发展或是发生改变。

    业务:有大量(本次测试时 1 万多张)图片需要转成 base64 编码后,送入 http 接口请求处理,我采用以下代码: base64 用生成器处理, request 用多进程。 但下面代码跑到一半的时候,直接抛了 OSError: [Errno 24] Too many open files, 百度了一下,看上去是进程超过所能开启的最大文件数了, ulimit -n # mac 8192 请教下各位,我怎么应该 fix 这个问题,最终需求就是想快速高效的完成这个操作,可能我写的代码一开始就有问题,还希望大佬们指点一下。

    import json
    import time
    import requests
    import base64
    import os
    from multiprocessing import Process
    
    
    
    
    def img_to_base64(img_path):
        r = {}
        for root, dirs, files in os.walk(img_path):
            for pic in files:
                if pic.endswith(('.png', '.jpg', '.jpeg', '.tiff', '.bmp', '.gif')):
                    img = os.path.join(root, pic)
                    with open(img, 'rb') as f:
                        bs64 = base64.b64encode(f.read()).decode('utf-8')
                        r[img] = bs64
                        yield r
    
    
    
    def req(host, img_path):
        bs64_generator = img_to_base64(img_path)
        procs = []
    
        for items in bs64_generator:
            body, pic = None, None
            for pic, base64 in items.items():
                body = {
                    "requests": [
                        {
                            "resource": {
                                "base64": base64
                            }
                        }
                    ]
                }
    
            p = Process(target=r, args=(host, body, pic))
            procs.append(p)
            p.start()
        for proc in procs:
            proc.join()
    
    
    
    
    def r(host, body, img):
        url = f'http://{host}/demo/'
        r = requests.post(url, data=json.dumps(body))
        print(img, r.json().get('results'))
        ret = r.json().get('results')[0]['status']
        if ret != 'OK':
            print(img, ret)
    
    
    
    
    req('10.10.23.17:3345', './mypic/')
    
    32 条回复    2021-03-31 18:43:37 +08:00
    BrettD
        1
    BrettD  
       2021-03-30 10:50:11 +08:00 via iPhone
    ulimit 不行吗
    liprais
        2
    liprais  
       2021-03-30 10:53:46 +08:00 via iPhone   ❤️ 2
    yield 那里缩进不对
    CRVV
        3
    CRVV  
       2021-03-30 10:55:17 +08:00
    1. 发 HTTP 请求不需要用多进程
    2. 如果在乎性能,请用 requests.session
    3. 如果单线程顺序发请求不够快,可以用 ThreadPoolExecutor 或者 aiohttp
    sss495088732
        4
    sss495088732  
       2021-03-30 10:56:12 +08:00
    做个缓冲区....要限制一下并行的进程,句柄数...你这能跑到一半儿,那电脑也是非常不戳
    UN2758
        5
    UN2758  
       2021-03-30 11:00:27 +08:00
    你定义的 img_to_base64 是单纯的生成器,想要协程支持的生成器,得给它加上 @asyncio.coroutine
    UN2758
        6
    UN2758  
       2021-03-30 11:02:48 +08:00
    @UN2758 #5 看错
    xiaolinjia
        7
    xiaolinjia  
       2021-03-30 11:07:01 +08:00
    开了 1w 多进程牛的,整个 multiprocessing.Pool 进程池限制下并行数吧。
    css3
        8
    css3  
    OP
       2021-03-30 11:24:33 +08:00
    @BrettD mac 8192 跑满了

    @liprais 这个缩进应该没问题吧,每处理 1 张,yield 出来

    @CRVV 为啥不需要用多进程呢

    @sss495088732 不知道咋搞缓冲区


    @UN2758 老哥想表达啥,我没有看太懂啊

    @xiaolinjia 好的,多谢,我找下文档
    ch2
        9
    ch2  
       2021-03-30 12:14:41 +08:00
    1 万个任务正常做法是用进程池开 n 个进程(n<=你的 cpu 核心数*2)
    分批陆续完成,而不是 1 万个进程一拥而上
    而且你的这个代码瓶颈在网速上,根本不需要多进程,用多线程就能处理
    但是最优的做法是用 grequests 一次批量发 1000 个请求,配合上错误重试,分 10-20 次就搞定了
    ch2
        10
    ch2  
       2021-03-30 12:18:40 +08:00
    随便找到网图,grequests 是编程最简单的大批量发 http 请求的方法,几乎没有唯二的其他选择,你会用 requests 就会用 grequests
    hsfzxjy
        11
    hsfzxjy  
       2021-03-30 12:19:05 +08:00 via Android
    @css3 你要等文件关了再 yield
    shuax
        12
    shuax  
       2021-03-30 12:48:53 +08:00
    开 100 个进程差不多了,再多也不快。
    zonyitoo
        13
    zonyitoo  
       2021-03-30 13:24:32 +08:00
    每个图标单独开一个进程去跑,这居然能跑得动电脑的配置也着实厉害了
    renmu123
        14
    renmu123  
       2021-03-30 13:26:30 +08:00 via Android
    hhttp 直接开个进程池就可以了,或者用 grequest 。基本都是开箱即用
    css3
        15
    css3  
    OP
       2021-03-30 13:43:48 +08:00 via iPhone
    @ch2 感谢老哥,学习了,我试试这个库。
    @hsfzxjy,我的理解是 with 自动会关闭呢,另外我也尝试把 yield 给放到 with 外边,也还是这个问题,应该主要问题出在多进程哪里,如同 13 扣老哥说的
    @shuax 嗯,多谢老哥
    @zonyitoo 学业不精,惭愧
    @renmu123 多谢,我试试
    aladdindingding
        16
    aladdindingding  
       2021-03-30 13:45:09 +08:00
    ulimit 改成 65535
    css3
        17
    css3  
    OP
       2021-03-30 14:11:53 +08:00
    @aladdindingding 本质是我进程和图片数一致了,这里有问题,改这个值,解决不了本质上的问题呢
    LeeReamond
        18
    LeeReamond  
       2021-03-30 14:33:27 +08:00 via Android
    你这个需求 py 的最佳实践应该是多线程 ffi 加异步 io,大概会比你现在的方案快很多很多很多很多
    laqow
        19
    laqow  
       2021-03-30 14:45:25 +08:00 via Android
    看起来是 2 楼的问题,yield 写 with 里面了,每次迭代文件都没关

    不改程序的话用 linux,想开多少文件开多少
    laqow
        20
    laqow  
       2021-03-30 14:52:41 +08:00 via Android
    感觉这样写每个图都是主进程开的,还没进 worker 就已经错误了
    css3
        21
    css3  
    OP
       2021-03-30 15:01:09 +08:00
    @laqow 我把 yield 放到 with 外,也是跑一半 OSError: [Errno 24] Too many open files, 放 linux 服务器上跑,跑的时间久点儿,最终也会 OSError: [Errno 24] Too many open files
    LeeReamond
        22
    LeeReamond  
       2021-03-30 15:26:10 +08:00 via Android   ❤️ 1
    @css3 跟 yield 没有关系,yield 只是起到保存状态中断执行的作用,你在循环里每次迭代,生成器也循环,with 管理器是正常结束的。另外仔细看了一下你的代码,你的多进程似乎仅负责网络通信,这是非常不合理的使用方法,建议了解 python 中的异步网络通信
    LeeReamond
        23
    LeeReamond  
       2021-03-30 15:34:10 +08:00 via Android
    @LeeReamond 你每新建进程,系统要开辟专门的文件指标指向输入输出流,而进程内部又为网络访问开辟了专门的文件。且 tcp 访问后有 timewait 状态,占用文件不会立即被释放,导致你的资源吃满。现代服务器单机每秒可以处理几十万个请求,即使用 python 也一样,绝不是你这仅仅一万个不现实请求能搞崩的。一个简单的多访问问题被你搞成这样。
    mjawp
        24
    mjawp  
       2021-03-30 15:41:26 +08:00
    这种业务需求应该是多线程比较好吧?

    1.多进程切换开销比多线程切换开销大
    2.合理的进程数应该是等于你的 cpu 核心数
    3.速度瓶颈主要还是等待请求和 IO,所以在等待 IO 与网络请求的时候可以切换很多很多不同的线程进行其他操作了
    imn1
        25
    imn1  
       2021-03-30 16:29:29 +08:00
    @liprais #2 说的是关键点
    不要在 with open 里面 yield
    yield 相当于生成器,数据是集中处理的,不是逐个处理,这就造成打开太多
    简单说就是全部 yield 的数据 都 获取后集中才处理,这时每个 open 都没有 close
    imn1
        26
    imn1  
       2021-03-30 17:11:04 +08:00
    前置重点:看下面第四点

    我做过类似的,不过不是 base64,而是 CRC32,应该比 base64 耗时,8K 张图片
    建议:
    1. base64 移出 os.walk,同时也建议 os.scandir 替换 walk,递归只 yield 返回路径就好了
    2. 多进程可以尝试换成 Pool+Pool.imap(),注意要用 close(),参考手册,Pool.close 要在 Pool.join 前面,同时限制线程数量
    3. 小问题,扩展名列表只有小写,你确保一万多文件扩展名都没有大写字母么?不小心会漏掉文件的
    4. 最后是严重的逻辑错误,img_to_base64 里面的 r 是个字典,你最后 return 一次就够了,怎么是不停 yield 这个字典呢?我觉得这是最大问题

    我以前考虑是遍历的同时处理文件,还是遍历了路径再处理文件,后来我看到遍历树是递归+yield,就不纠结了
    递归里面处理文件,处理文件 yield 结果,这两个都不是好想法,肯定有说不清的问题(因为 python 是调用系统 API 打开文件的),所以直接就用递归 yield 路径,然后再考虑其他方式优化文件处理
    imn1
        27
    imn1  
       2021-03-30 17:19:30 +08:00
    补充:你这样重复 yield 这个字典,里面还有 open,这样不是打开一万次(每文件一次),而是打开一万次的阶乘!!
    julyclyde
        28
    julyclyde  
       2021-03-30 19:29:07 +08:00
    字典还能 yield ??
    slidoooor
        29
    slidoooor  
       2021-03-30 23:26:47 +08:00
    学习了,感谢!
    itwhat
        30
    itwhat  
       2021-03-31 10:00:48 +08:00
    IO 密集型应考虑多线程
    css3
        31
    css3  
    OP
       2021-03-31 10:25:44 +08:00
    @itwhat 请教下,图片转 base64 仅仅是 IO 密集型吗?
    maybedk
        32
    maybedk  
       2021-03-31 18:43:37 +08:00
    r 那个字典没必要直接 yield (img,bs64),与 with 对齐;
    开进程池 pool = multiprocessing.Pool(4);
    基本能跑起来,速度多快不知道
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   5351 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 28ms · UTC 01:21 · PVG 09:21 · LAX 17:21 · JFK 20:21
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.