请教 Python 多线程内存不释放怎么排查 - 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
r150r
V2EX    Python

请教 Python 多线程内存不释放怎么排查

  •  
  •   r150r 2020-10-06 23:51:58 +08:00 7337 次点击
    这是一个创建于 1838 天前的主题,其中的信息可能已经有所发展或是发生改变。

    新手写了一个多线程的爬虫,所有线程都执行完了,但是一直占着 1.5GB 的内存(任务数越多不释放的内存越多) 不知道怎么排查哪里出问题,pympler 看不太懂问题到底出在哪里,请教该如何正确的排查问题

    执行多线程函数的代码:

     def mainfunc(tasknum, thread): tr = tracker.SummaryTracker() tr.print_diff() list = [] for i in range(tasknum): list.append(str(i)) pool = threadpool.ThreadPool(thread) requests = threadpool.makeRequests(childfunc, list) for req in requests: pool.putRequest(req) pool.wait() tr.print_diff() 

    tr.print_diff()打印的内容

    初始化:

     types | # objects | total size ========================== | =========== | ============ list | 3741 | 350.84 KB str | 3739 | 260.01 KB int | 673 | 18.40 KB dict | 2 | 352 B tuple | 4 | 256 B code | 1 | 144 B function (store_info) | 1 | 136 B cell | 2 | 96 B functools._lru_list_elem | 1 | 80 B method | -1 | -64 B 

    所有线程结束后:

     types | # objects | total size ===================================== | =========== | ============ dict | 202860 | 43.69 MB list | 100169 | 8.47 MB str | 102446 | 5.62 MB threadpool.WorkRequest | 100000 | 5.34 MB int | 100836 | 3.08 MB _io.BufferedReader | 294 | 2.35 MB tuple | 1480 | 93.30 KB type | 76 | 85.98 KB code | 572 | 80.57 KB bytes | 1219 | 51.49 KB set | 32 | 43.50 KB socket.socket | 294 | 27.56 KB pymysql.connections.Connection | 294 | 16.08 KB socket.SocketIO | 294 | 16.08 KB DBUtils.SteadyDB.SteadyDBConnection | 294 | 16.08 KB 
    第 1 条附言    2020-10-07 13:41:10 +08:00

    附上可以复现问题的最小化代码,执行完输出done后,htop显示python3一直占用着那一部分内存,除非kill掉否则不释放(发不了链接base64编码了一下)

    #!/usr/bin/pyyhon # -*- coding: UTF-8 -*- import threadpool, time, requests, base64 s = requests.Session() def childfunc(id): url = base64.b64decode('aHR0cHM6Ly91cGxvYWQud2lraW1lZGlhLm9yZy93aWtpcGVkaWEvY29tbW9ucy9mL2ZmL1BpemlnYW5pXzEzNjdfQ2hhcnRfMTBNQi5qcGc=') res = s.get(url, timeout=(5, 60)) def mainfunc(tasknum, thread): list = [] for i in range(tasknum): list.append(str(i)) pool = threadpool.ThreadPool(thread) requests = threadpool.makeRequests(childfunc, list) for req in requests: pool.putRequest(req) pool.wait() print('done') while True: time.sleep(1) if __name__ == '__main__': mainfunc(10000, 50) 
    第 2 条附言    2020-10-07 15:03:52 +08:00

    如果把代码里的session.requests替换成str = ' ' * (500 * 1024 * 1024),使用的内存会马上就归还给系统

    #!/usr/bin/pyyhon # -*- coding: UTF-8 -*- import threadpool, time def childfunc(id): #这htop显示占用500m str = ' ' * (500 * 1024 * 1024) time.sleep(10) def mainfunc(tasknum, thread): list = [] for i in range(tasknum): list.append(str(i)) pool = threadpool.ThreadPool(thread) requests = threadpool.makeRequests(childfunc, list) for req in requests: pool.putRequest(req) pool.wait() print('done') #这htop显示已释放500m while True: time.sleep(1) if __name__ == '__main__': mainfunc(1, 1) 
    25 条回复    2020-10-12 00:08:39 +08:00
    scriptB0y
        1
    scriptB0y  
       2020-10-06 23:59:43 +08:00
    线程池里面的任务,检查一下所有的函数最后都有 return,没有的加一下,再试试。
    r150r
        2
    r150r  
    OP
       2020-10-07 00:15:05 +08:00
    @scriptB0y 所有的函数都有 return 了
    wevsty
        3
    wevsty  
       2020-10-07 00:51:33 +08:00
    检查有没有循环引用数据结构的问题。
    Hstar
        4
    Hstar  
       2020-10-07 00:56:58 +08:00   3
    把你代码复制跑了一遍,是你代码里 requests 这个变量的问题,这个变量缓存了你所有任务的引用,只要你的 mainfunc 函数不结束这些引用就不会消失。
    你可以在 mainfunc 外面套一层函数再打印 tr.print_diff()看看,会发现内存占用消失了。
    也可以把
    for req in requests:
    pool.putRequest(req)
    改成
    while requests:
    pool.putRequest(requests.pop())
    r150r
        5
    r150r  
    OP
       2020-10-07 01:42:38 +08:00
    @Hstar 谢谢解答!改成 requests.pop()后打印 tr.print_diff(),list 和 dict 明显少了。不过 htop 显示 1.5G 内存还是没释放,除非这个 mainfunc 结束,看来是 childfunc 的问题。
    byaiu
        6
    byaiu  
       2020-10-07 05:15:41 +08:00 via Android
    内存分配是有状态的
    superrichman
        7
    superrichman  
       2020-10-07 06:41:09 +08:00 via iPhone
    爬虫,你是不是用了 beautiful soup ?这个用完了要手动 decompose 一下,不然内存会爆炸
    zhuangzhuang1988
        8
    zhuangzhuang1988  
       2020-10-07 09:07:33 +08:00
    不好查
    国内的 python 核心开发着也扯到了 https://pythonhunter.org/episodes/9
    noobsheldon
        9
    noobsheldon  
       2020-10-07 09:11:06 +08:00
    把这个 mainfunc 放入一个子进程执行, 子进程结束,让系统自己回收内存呢?
    mumbler
        10
    mumbler  
       2020-10-07 09:24:17 +08:00 via Android
    “del 变量名” 可以手动释放内存
    cloudyplain
        11
    cloudyplain  
       2020-10-07 10:35:57 +08:00 via iPhone
    1.threadpool 改为全局? 2.换 tcmalloc
    cheng6563
        12
    cheng6563  
       2020-10-07 12:14:15 +08:00
    不会 python
    现代的 gc 一般就算回收了内存也不会把内存还给操作系统
    可以考虑新启一个进程,操作完结束进程
    chenqh
        13
    chenqh  
       2020-10-07 13:06:59 +08:00 via Android
    req 是什么东西?
    r150r
        14
    r150r  
    OP
       2020-10-07 13:41:31 +08:00
    @superrichman 没有使用 beautiful soup
    wangritian
        15
    wangritian  
       2020-10-07 13:46:30 +08:00
    变量释放后,可能仅仅被 py 标记为垃圾,并没有归还操作系统,下次你再申请变量优先从垃圾堆里找
    开子进程用完销毁是最可行的方案,另外找找有没有像 go 的 debug.FreeOSMemory()这种强制归还操作系统的函数
    r150r
        16
    r150r  
    OP
       2020-10-07 13:51:02 +08:00
    @noobsheldon 目前是把 mainfunc 放入子进程,分段每 100000 个任务执行 1 次
    可目标站的 tid 越高,需要解析的资源就越多,每 100000 个任务需要的内存也就越来越高。
    现在 100000 个任务要 26GB 内存了,只能手动调整任务数
    r150r
        17
    r150r  
    OP
       2020-10-07 13:53:19 +08:00
    已更新可以复现问题的最小化代码
    chenqh
        18
    chenqh  
       2020-10-07 13:54:02 +08:00
    不用 threadpool 试试?
    r150r
        19
    r150r  
    OP
       2020-10-07 14:02:07 +08:00
    @wangritian 只保持 50 个线程 get 一个相同链接,内存使用量却跟随任务数量无止尽增长,请问这是没有被标记为垃圾,所以无法回收吗?
    r150r
        20
    r150r  
    OP
       2020-10-07 14:09:42 +08:00
    @chenqh 之前试过 multiprocessing.dummy 也不释放
    r150r
        21
    r150r  
    OP
       2020-10-07 15:06:12 +08:00
    python 标记变量为垃圾而不释放有什么条件吗?
    如果把代码里的 session.get 替换成 str = ' ' * (500 * 1024 * 1024),使用的内存会马上就归还给系统,是 requests 的问题吗
    mywaiting
        22
    mywaiting  
       2020-10-07 17:06:07 +08:00
    之前遇到过类似的问题,把 requests 的 timeout 调小一点吧 timeout=(5, 10) 试试
    changePro
        23
    changePro  
       2020-10-08 22:18:26 +08:00
    这个问题我今晚研究了下,Py 自己管理内存

    ```
    str = ' ' * (500 * 1024 * 1024)
    ```

    这段代码有可能是在栈上面的,用完了 frame 就没了,内存自然释放

    但是

    ```
    res = s.get(url, timeout=(5, 60))
    ```

    有可能是在堆上面的,GC 回收的话,应该有内在策略,找时间可以分析分析内存布局
    HappyTrail
        24
    HappyTrail  
       2020-10-10 14:01:23 +08:00
    https 改成 http 试试看 - -
    nisonGe
        25
    nisonGe  
       2020-10-12 00:08:39 +08:00
    个人猜测是因为有大量的异常导致,异常递归。task 越多,异常越多,内存占用也越多。
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     1082 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 26ms UTC 18:21 PVG 02:21 LAX 11:21 JFK 14:21
    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