6

我花了一天时间尝试调试 Python 脚本中的内存问题。我使用 SQL Alchemy 作为我的 ORM。这里有几个令人困惑的问题,我希望如果我把它们都列出来,有人能指出我正确的方向。

为了达到我想要的性能,我读入了一个表中的所有记录(~400k),然后遍历电子表格,匹配我之前读入的记录,然后创建新记录(~800k)到另一张桌子。代码大致如下:

dimensionMap = {}
for d in connection.session.query(Dimension):
   dimensionMap[d.businessKey] = d.primarySyntheticKey

# len(dimensionMap) == ~400k, sys.getsizeof(dimensionMap) == ~4MB

allfacts = []
sheet = open_spreadsheet(path)
for row in sheet.allrows():
    dimensionId = dimensionMap[row[0]]
    metric = row[1]

    fact = Fact(dimensionId, metric)
    connection.session.add(fact)
    allfacts.append(fact)

    if row.number % 20000 == 0:
        connection.session.flush()

# len(allfacts) == ~800k, sys.getsizeof(allfacts) == ~50MB

connection.session.commit()

sys.stdout.write('All Done')

400k 和 800k 对我来说似乎不是特别大的数字,但我仍然遇到了内存问题,一台 4GB 内存的机器。这对我来说真的很奇怪,因为我在我的两个最大的集合上运行了 sys.getsizeof,而且它们都在任何会导致问题的大小之下。

在试图弄清楚这一点时,我注意到脚本运行得非常非常缓慢。所以我在上面运行了一个配置文件,希望结果能把我引向记忆问题的方向,并提出了两个令人困惑的问题。

探查器输出

首先,87% 的程序时间都花在了提交上,特别是在这行代码上:

self.transaction._new[state] = True

这可以在 中找到session.py:1367self.transaction._new是 的一个实例weakref.WeakKeyDictionary()。为什么要weakref:261:__setitem__占用这么多时间?

其次,即使程序完成('All Done' 已打印到标准输出),脚本仍在继续,看似永远,使用了 2.2GB 的内存。

我已经对weakrefs 进行了一些搜索,但没有看到有人提到我面临的性能问题。最终,鉴于它深埋在 SQL Alchemy 中,我对此无能为力,但我仍然会很感激任何想法。

主要学习

正如@zzzeek 所提到的,维护持久对象需要大量开销。这是一个显示增长的小图表。

使用的总内存与持久实例的数量

趋势线表明每个持久实例占用大约 2KB 的内存开销,即使实例本身只有 30 个字节。这实际上给我带来了我学到的另一件事,那就是sys.getsizeof带着一大粒盐。

此函数仅返回对象的大小,并且不考虑任何其他需要存在的对象才能使第一个对象有意义(__dict__例如 )。你真的需要使用像 Heapy 这样的东西来很好地理解实例的实际内存占用。

我学到的最后一件事是,当 Python 即将耗尽内存并且疯狂地颠簸时,会发生不应该作为问题的一部分的奇怪事情。在我的情况下,大规模减速、指向弱引用创建的配置文件以及程序完成后的挂断都是内存问题的影响。一旦我停止创建和保留持久实例,而是只保留我需要的对象属性,所有其他问题都消失了。

4

1 回答 1

9

800K ORM 对象非常大。这些是 Python 对象,每个对象都有一个__dict__以及一个_sa_instance_state属性,该属性本身就是一个对象,然后在其中有弱引用和其他东西,然后Session对你的对象有多个弱引用 - 一个 ORM 对象是身份跟踪的,该功能提供了高度自动化的持久性,但代价是更多的内存和函数调用开销。

至于为什么你的分析都集中在一个弱引用行上,这看起来很奇怪,我很想在那里看到实际的分析结果(参见我如何分析一个 SQLAlchemy 驱动的应用程序?背景)。

您的代码示例可以修改为不使用任何 ORM 身份映射对象,如下所示。有关批量插入的更多详细信息,请参阅为什么使用 sqlite 的 SQLAlchemy 插入比直接使用 sqlite3 慢 25 倍?.

# 1. only load individual columns - loading simple tuples instead 
# of full ORM objects with identity tracking.  these tuples can be
# used directly in a dict comprehension
dimensionMap = dict(
    connection.session.query(Dimension.businessKey, Dimension.primarySyntheticKey)
)

# 2. For bulk inserts, use Table.insert() call with
# multiparams in chunks
buf = []
for row in sheet.allrows():
    dimensionId = dimensionMap[row[0]]
    metric = row[1]

    buf.append({"dimensionId": dimensionId, "metric": metric})

    if len(buf == 20000):
        connection.session.execute(Fact.__table__.insert(), params=buf)
        buf[:] = []

connection.session.execute(Fact.__table__.insert(), params=buf)
sys.stdout.write('All Done')
于 2013-10-02T00:08:25.463 回答