【问题标题】:psycopg2 leaking memory after large query大型查询后 psycopg2 泄漏内存
【发布时间】:2013-06-16 10:51:24
【问题描述】:

我正在使用 psycopg2(我升级到版本 2.5)在 python 脚本中针对我的 postgres 数据库运行大型查询。查询完成后,我关闭游标和连接,甚至运行gc,但该过程仍然消耗大量内存(确切地说是7.3gb)。我错过了清理步骤吗?

import psycopg2
conn = psycopg2.connect("dbname='dbname' user='user' host='host'")
cursor = conn.cursor()
cursor.execute("""large query""")
rows = cursor.fetchall()
del rows
cursor.close()
conn.close()
import gc
gc.collect()

【问题讨论】:

    标签: python postgresql psycopg2


    【解决方案1】:

    请参阅@joeblog 的下一个答案以获得更好的解决方案。


    首先,您不应该首先需要所有的 RAM。您应该在这里做的是获取结果集的 chunks。不要做fetchall()。相反,使用更有效的cursor.fetchmany 方法。见the psycopg2 documentation。

    现在,解释为什么它没有被释放,以及为什么在该术语的正式正确使用中这不是内存泄漏。

    大多数进程在释放内存时不会将内存释放回操作系统,它们只是使其可在程序的其他地方重复使用。

    只有当程序可以压缩分散在内存中的剩余对象时,内存才能释放给操作系统。这仅在使用间接句柄引用时才有可能,因为否则移动对象会使指向该对象的现有指针无效。间接引用的效率相当低,尤其是在现代 CPU 上,追逐指针会对性能造成可怕的影响。

    除非程序格外小心,否则通常会发生的情况是,分配给brk() 的每个大块内存都会有一些仍在使用的小块。

    操作系统无法判断程序是否认为该内存仍在使用中,因此不能直接将其收回。由于程序不倾向于访问内存,因此操作系统通常会随着时间的推移将其换出,从而释放物理内存以供其他用途。这是您应该拥有交换空间的原因之一。

    可以编写将内存交还给操作系统的程序,但我不确定您是否可以使用 Python 来做到这一点。

    另见:

    所以:这实际上不是内存泄漏。如果您执行其他使用大量内存的操作,则该进程应该不会增长太多,它会重新使用上次大分配中先前释放的内存。

    【讨论】:

    • 谢谢!确认通过在同一进程中运行上述代码两次来重用内存。第二次运行时内存没有增加。
    • 虽然这里所说的一切都是正确的,但查询结果通常会完全在客户端传输(不是fetch*(),而是execute())。因此,虽然使用 fetchmany() 而不是 fetchall() 可能会在 Python 对象创建方面节省一些内存,但使用 @joeblog 建议的服务器端游标是正确的解决方案。
    【解决方案2】:

    我遇到了类似的问题,经过几个小时的血汗和泪水,我发现答案只需要添加一个参数。

    代替

    cursor = conn.cursor()
    

    写

    cursor = conn.cursor(name="my_cursor_name")
    

    或者更简单的

    cursor = conn.cursor("my_cursor_name")
    

    详情见http://initd.org/psycopg/docs/usage.html#server-side-cursors

    我发现说明有点混乱,因为我需要重写我的 SQL 以包含 “DECLARE my_cursor_name ....”然后是“FETCH count 2000 FROM my_cursor_name”,但事实证明,如果您在创建游标时简单地覆盖“name=None”默认参数,psycopg 会在后台为您完成这一切。

    上述使用 fetchone 或 fetchmany 的建议并不能解决问题,因为如果您未设置 name 参数,psycopg 将默认尝试将整个查询加载到 ram 中。您可能需要做的唯一另一件事(除了声明名称参数)是将 cursor.itersize 属性从默认的 2000 更改为 1000,如果您的内存仍然太少。

    【讨论】:

    • 我在sqlalchemy 中找不到任何可以帮助我避免 OOM 问题的内容,但这个解决方案对我有用。谢谢!
    • @RyanTuck 看来您可以在sqlalchemy 中通过将server_sider_cursors=True 传递给create_engine 来完成此操作:docs.sqlalchemy.org/en/latest/dialects/…
    • @joeblog 您能否在上面的答案中添加更多亮点?你是在声明变量并给它一个值(即在创建游标之前my_cursor_name = 1000?我想在调用cursor.execute('query')之前从我的python脚本中采用你的解决方案设置FECTH_COUNT
    • conn.cursor 中的(唯一)参数称为名称,我刚刚将“my_cursor_name”作为 Python 字符串分配给它。我想它可以是任何东西,只要它可以被翻译成 PostrgreSQL 标识符。您不会将 1000 分配给游标名称,而是在默认 2000 太高的极少数情况下分配给 cursor.itersize。
    • @FoxMulder900 作为记录,看起来server_side_cursors 已被stream_results 取代
    【解决方案3】:

    Joeblog 有正确答案。处理获取的方式很重要,但比必须定义游标的方式要明显得多。这是一个简单的示例来说明这一点,并为您提供一些可以复制粘贴的内容。

    import datetime as dt
    import psycopg2
    import sys
    import time
    
    conPG = psycopg2.connect("dbname='myDearDB'")
    curPG = conPG.cursor('testCursor')
    curPG.itersize = 100000 # Rows fetched at one time from the server
    
    curPG.execute("SELECT * FROM myBigTable LIMIT 10000000")
    # Warning: curPG.rowcount == -1 ALWAYS !!
    cptLigne = 0
    for rec in curPG:
       cptLigne += 1
       if cptLigne % 10000 == 0:
          print('.', end='')
          sys.stdout.flush() # To see the progression
    conPG.commit() # Also close the cursor
    conPG.close()
    

    正如您将看到的,点按组快速出现,而不是暂停以获取行缓冲区(迭代大小),因此您无需使用 fetchmany 来提高性能。当我使用/usr/bin/time -v 运行它时,我在不到 3 分钟的时间内就得到了结果,1000 万行只使用了 200MB 的 RAM(而不是使用客户端游标的 60GB)。服务器不需要更多内存,因为它使用临时表。

    【讨论】:

    • A commit() 用于选择查询?
    • @AmitNaidu 是的,关闭事务是一个好习惯,以防您在关闭 PG 连接之前有很长时间的停顿,以免收到可怕的环绕警报。
    猜你喜欢
    • 1970-01-01
    • 2010-10-09
    • 1970-01-01
    • 2017-10-21
    • 2011-12-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-11-29
    相关资源
    最近更新 更多