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 · 6607 次点击
    这是一个创建于 1290 天前的主题,其中的信息可能已经有所发展或是发生改变。

    新手写了一个多线程的爬虫,所有线程都执行完了,但是一直占着 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   ·   我们的愿景   ·   实用小工具   ·   2739 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 29ms · UTC 14:44 · PVG 22:44 · LAX 07:44 · JFK 10:44
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.