这是内存泄漏吗(使用sqlalchemy/sqlite的Python程序)
我有一段代码,它处理的数据量很大(200万条)。在运行完之前,它就把我电脑的4G内存全用光了。
for sample in session.query(CodeSample).yield_per(100):
for proj in projects:
if sample.filename.startswith(proj.abs_source):
sample.filename = "some other path"
session.add(sample)
然后我用一小部分数据重新运行了一遍,并用heapy工具分析了内存情况。get_rp()给了我一些提示。
0: _ --- [-] 47821 (0x9163aec | 0x9165fec | 0x916d6cc | 0x9251414 | 0x925704...
1: a [-] 8244 tuple: 0x903ec8c*37, 0x903fcfc*13, 0x9052ecc*46...
2: aa ---- [S] 3446 types.CodeType: parseresult.py:73:src_path...
3: ab [S] 364 type: __builtin__.Struct, _random.Random, sqlite3.Cache...
4: ac ---- [-] 90 sqlalchemy.sql.visitors.VisitableType: 0x9162f2c...
5: aca [S] 11 dict of module: ..sql..., codemodel, sqlalchemy
6: acb ---- [-] 48 sqlalchemy.sql.visitors.VisitableType: 0x9162f2c...
7: acba [S] 9 dict of module: ..sql..., codemodel, sqlalchemy
8: acbb ---- [-] 45 sqlalchemy.sql.visitors.VisitableType: 0x9165fec...
9: acbba [S] 8 dict of module: ..sql..., codemodel, sqlalchemy
我对sqlalchemy还不太熟悉。这算是内存泄漏吗?谢谢。
2 个回答
这个会话会记录你获取的所有 CodeSample
对象。所以当你遍历了200万个对象后,这个会话会保留对它们的引用。会话需要这些引用,以便在 flush
时能把正确的更改写入数据库。因此,我认为你看到的情况是正常的。
如果你想一次只在内存中保留 N 个对象,可以参考下面的代码(灵感来自于这个回答,不过我没有测试过)。
offset = 0
N = 10000
got_rows = True
while got_rows:
got_rows = False
for sample in session.query(CodeSample).limit(N).offset(offset):
got_rows = True
for proj in projects:
if sample.filename.startswith(proj.abs_source):
sample.filename = "some other path"
offset += N
session.flush() # writes changes to DB
session.expunge_all() # removes objects from session
不过上面的做法有点笨拙,也许一些SQLAlchemy的大牛能提供更好的解决方案。
顺便说一下,你其实不需要使用 session.add(),因为会话会自动跟踪对象的变化。你为什么要使用 yield_per
呢?(编辑:我猜这是为了从数据库中分块获取行数据,对吗?反正会话会跟踪所有对象。)
编辑:
嗯,看起来我有些误解了。从文档中了解到:
weak_identity_map: 当设置为默认值 True 时,会使用弱引用映射; 没有外部引用的实例会立即被垃圾回收。 对于那些有待处理更改的被解除引用的实例, 属性管理系统会创建一个临时的强引用, 这个引用会持续到更改被写入数据库为止, 然后它又会被解除引用。相反, 当设置为 False 时,身份映射会使用常规的 Python 字典来存储实例。 会话会保持所有实例,直到通过 expunge()、clear() 或 purge() 被移除。
还有
prune(): 移除身份映射中未被引用的实例。
注意,这个方法只有在“weak_identity_map”设置为 False 时才有意义。默认的弱身份映射会自动清理。
移除会话身份映射中未被用户代码引用、未修改、新创建或计划删除的任何对象。返回被清理的对象数量。
大多数数据库API,比如psycopg2和mysql-python,在把结果返回给客户端之前,会把所有的结果都加载到内存里。SQLAlchemy的yield_per()选项并不能解决这个问题,只有一个例外情况,所以一般来说这个选项不是特别有用(编辑:有用的意思是它可以在实际行完全获取之前开始流式传输结果)。
这种行为的例外情况有:
- 使用不缓存行的数据库API。比如cx_oracle就是这样,因为它的工作方式决定了这一点。我不太确定pg8000的表现如何,还有一个新的MySQL数据库API叫OurSQL,听说它的创建者说不缓存行。pg8000和OurSQL在SQLAlchemy 0.6中是被支持的。
- 对于psycopg2,可以使用“服务器端游标”。SQLAlchemy支持一个create_engine()的选项“server_side_cursors=True”,这个选项会在所有选择行的操作中使用服务器端游标。不过,由于服务器端游标通常比较耗费资源,因此对于小查询来说会降低性能,所以在SQLAlchemy 0.6中,支持psycopg2的服务器端游标可以按每个语句或每个查询来使用,方法是通过.execution_options(stream_results=True)来设置,其中execution_options可以在Query、select()、text()和Connection上使用。当使用yield_per()时,Query对象会调用这个选项,所以在0.6版本中,yield_per()结合psycopg2实际上是有用的。