【问题标题】:when to commit data in ZODB何时在 ZODB 中提交数据
【发布时间】:2012-06-28 23:42:16
【问题描述】:

我正在尝试处理以下代码生成的数据:

for Gnodes in G.nodes()       # Gnodes iterates over 10000 values 
    Gvalue = someoperation(Gnodes)
    for Hnodes in H.nodes()   # Hnodes iterates over 10000 values 
        Hvalue =someoperation(Hnodes)
        score = SomeOperation on (Gvalue,Hvalue)
        dic_score.setdefault(Gnodes,[]).append([Hnodes, score, -1 ])

由于字典很大(10000 个键 X 10000 个列表,每个包含 3 个元素),因此很难将其保存在内存中。我一直在寻找一种解决方案,它在生成键:值(以列表的形式)对后立即存储它们。这里建议Writing and reading a dictionary in specific format (Python) 将 ZODB 与 Btree 结合使用。

如果这太天真了,请容忍我,我的问题是,什么时候应该调用transaction.commit() 来提交数据?如果我在内循环结束时调用它,则生成的文件非常大(不知道为什么)。这是一个sn-p:

storage = FileStorage('Data.fs')
db = DB(store)
connection = db.open()
root = connection.root()
btree_container = IOBTree
root[0] = btree_container 
for nodes in G.nodes()
    btree_container[nodes] = PersistentList () ## I was loosing data prior to doing this 

for Gnodes in G.nodes()       # Gnodes iterates over 10000 values 
    Gvalue = someoperation(Gnodes)
    for Hnodes in H.nodes()   # Hnodes iterates over 10000 values 
        Hvalue =someoperation(Hnodes)
        score = SomeOperation on (Gvalue,Hvalue)
        btree_container.setdefault(Gnodes,[]).append([Hnodes, score, -1 ])
        transaction.commit()

如果我在两个循环之外调用它会怎样?类似的东西:

    ......
       ......
          score = SomeOperation on (Gvalue,Hvalue)
          btree_container.setdefault(Gnodes,[]).append([Hnodes, score, -1 ])
    transaction.commit()

在我调用 transaction.commit() 之前,所有数据是否都会保存在内存中?同样,我不知道为什么,但这会导致磁盘上的文件变小。

我想最小化内存中保存的数据。任何指导将不胜感激!

【问题讨论】:

  • 请注意,您可以松开 setdefault 调用,也可以将默认的 [] 替换为 PersistentList 并将循环放在 G.nodes() 上以设置您拥有的初始 PersistentLists在那里。
  • @MartijnPieters:感谢您的提示!您能否评论一下我最初的查询,即数据何时保存在内存中?例如,如果我在两个循环之外调用 commit,所有数据是否仍会保留在内存中,从而基本上违背了我的目的?
  • 耐心点!一个好的答案需要时间。 :-)

标签: python zodb


【解决方案1】:

您的目标是使您的流程在内存限制内易于管理。为了能够将 ZODB 作为一种工具来执行此操作,您需要了解 ZODB 事务的工作原理以及如何使用它们。

为什么您的 ZODB 会变得如此之大

首先您需要了解事务提交在这里做了什么,这也解释了为什么您的 Data.fs 变得如此之大。

ZODB 在每个事务中写入数据,其中任何已更改的持久对象都会写入磁盘。这里的重要细节是已更改的持久对象; ZODB 以持久对象为单位工作。

并非每个 python 值都是持久对象。如果我定义一个直接的 python 类,它不会是持久的,也不是任何内置的 python 类型,例如 int 或 list。另一方面,您定义的任何继承自persistence.Persistent 的类都是 持久对象。 BTrees 类集以及您在代码中使用的 PeristentList继承自 Persistent

现在,在事务提交时,任何已更改的持久对象都会作为该事务的一部分写入磁盘。因此,任何已附加到的PersistentList 对象都将完整地写入磁盘BTrees 处理这个更有效率;它们存储存储桶,它们本身是持久的,而存储桶又保存实际存储的对象。因此,对于您创建的每几个新节点,都会将一个 Bucket 写入事务,而不是整个 BTree 结构。请注意,由于树中保存的项目本身就是持久对象,因此只有对它们的引用存储在 Bucket 记录中。

现在,ZODB 通过将事务数据附加到Data.fs 文件来写入事务数据,并且它不会自动删除旧数据。它可以通过从存储中查找给定对象的最新版本来构造数据库的当前状态。这就是为什么您的 Data.fs 增长如此之快的原因,随着事务的提交,您正在编写越来越大的 PersistentList 实例的新版本。

删除旧数据称为打包,类似于PostgreSQL和其他关系数据库中的VACUUM命令。只需在db 变量上调用the .pack() method 即可删除所有 旧版本,或使用该方法的tdays 参数来设置要保留多少历史记录的限制,第一个是一个time.time() 时间戳(自纪元以来的秒数),您可以在该时间戳之前打包,days 是从当前时间开始保留的过去天数,如果指定,则为 t。随着旧事务中的部分列表被删除,打包应该会大大减少您的数据文件。请注意,打包是一项昂贵的操作,因此可能需要一段时间,具体取决于数据集的大小。

使用事务管理内存

您正在尝试构建一个非常大的数据集,通过使用持久性来解决内存限制,并使用事务来尝试将内容刷新到磁盘。然而,通常情况下,使用事务提交信号您已经完成了数据集的构建,您可以将其用作一个原子整体。

你需要在这里使用的是一个保存点。保存点本质上是子事务,在整个事务期间,您可以要求将数据临时存储在磁盘上。当您提交事务时,它们将成为永久性的。要创建保存点,请在事务上调用.savepoint method

for Gnodes in G.nodes():      # Gnodes iterates over 10000 values 
    Gvalue = someoperation(Gnodes)
    for Hnodes in H.nodes():  # Hnodes iterates over 10000 values 
        Hvalue =someoperation(Hnodes)
        score = SomeOperation on (Gvalue,Hvalue)
        btree_container.setdefault(Gnodes, PersistentList()).append(
            [Hnodes, score, -1 ])
    transaction.savepoint(True)
transaction.commit()

在上面的示例中,我将 optimistic 标志设置为 True,这意味着:我不打算回滚到这个保存点;某些存储不支持回滚,并且发出不需要回滚的信号会使您的代码在这种情况下工作。

另请注意,transaction.commit() 发生在整个数据集已被处理时,这是提交应该实现的。

保存点所做的一件事是调用 ZODB 缓存的垃圾收集,这意味着当前未使用的所有数据都将从内存中删除。

请注意那里的“当前未使用”部分;如果您的任何代码在变量中保留较大的值,则无法从内存中清除数据。据我可以从您向我们展示的代码中确定,这看起来不错。但是我不知道您的操作是如何工作的,也不知道您是如何生成节点的;例如,当迭代器会在内存中构建完整的列表时,请注意避免在内存中构建完整的列表,或者构建引用所有列表列表的大型字典。

您可以对创建保存点的位置进行一些试验;您可以在每次处理一个HNodes 时创建一个,或者仅在使用GNodes 循环完成时创建一个,就像我在上面所做的那样。您正在为每个 GNodes 构建一个列表,因此它会保存在内存中,同时遍历所有 H.nodes(),并且只有在您完成完整构建后才可能将其刷新到磁盘。

但是,如果您发现需要更频繁地清除内存,则应考虑使用BTrees.OOBTree.TreeSet 类或BTrees.IOBTree.BTree 类而不是PersistentList 将数据分解为更持久的对象。 TreeSet 是有序的,但不是(容易)可索引的,而 BTree 可以通过使用简单的递增索引键用作列表:

for i, Hnodes in enumerate(H.nodes()):
    ...
    btree_container.setdefault(Gnodes, IOBTree())[i] = [Hnodes, score, -1]
    if i % 100 == 0:
        transaction.savepoint(True)

上面的代码使用 BTree 而不是 PersistentList 并且每处理 100 个HNodes 创建一个保存点。因为 BTree 使用本身就是持久对象的存储桶,所以可以更轻松地将整个结构刷新到保存点,而不必为了处理所有 H.nodes() 而留在内存中。

【讨论】:

  • 好答案!一个挑剔:“A TreeSet 是无序的”语句很奇怪。 TreeSets 按排序顺序显示它们的键(由存储对象的 __cmp__() 方法定义); TreeSet 类的 .keys() 方法采用 min_key 和 max_key 参数,可用于迭代给定集中键的排序子集。我有代码可以利用它进行分页。 :)
  • @RichardBarrell:是的,我觉得这超出了本文的范围。 :-)
  • Tom 说“嗨”,Jess 说“哇,他忘了我☹”。 ;)
  • @RichardBarrell:提到的 TreeSet 已经更新,至少澄清了它关于排序的性质。
  • @MartijnPieters:我怎么能感谢你如此清晰明了的解释!这真的很有帮助,比我在网上找到的大多数教程更好:-) 如果我可以问最后一件事作为跟进。是否仍然需要使用 packing 方法,即使我使用 savepoint 方法,或者由增量构建自动处理?
【解决方案2】:

什么构成事务取决于您的应用程序中需要“原子”的内容。如果事务失败,它将回滚到之前的状态(就在最后一次提交之后)。从您的应用程序代码中可以看出,您希望为每个 Gnode 计算一个值。因此,您的提交可以像这样在 Gnodes 循环结束时进入:

for Gnodes in G.nodes():       # Gnodes iterates over 10000 values 
    Gvalue = someoperation(Gnodes)
    for Hnodes in H.nodes():   # Hnodes iterates over 10000 values 
        Hvalue =someoperation(Hnodes)
        score = SomeOperation on (Gvalue,Hvalue)
        btree_container.setdefault(Gnodes,[]).append([Hnodes, score, -1 ])
    # once we calculate the value for a Gnodes, commit
    transaction.commit()

从您的代码看来,“Hvalue”组合不依赖于 Gvalue 或 Gnodes。 如果这是一项昂贵的操作,那么即使每个 Gnode 都需要计算 1000 次 它不影响其计算。所以,我会把它移出循环。

# Hnodes iterates over 10000 values
hvals = dict((Hnodes, someoperation(Hnodes)) for Hnodes in H.nodes())
# now you have mapping of Hnodes and Hvalues

for Gnodes in G.nodes():       # Gnodes iterates over 10000 values 
    Gvalue = someoperation(Gnodes)
    for Hnodes, Hvalue in hvals.iteritems(): 
        score = SomeOperation on (Gvalue,Hvalue)
        btree_container.setdefault(Gnodes,[]).append([Hnodes, score, -1 ])
    # once we calculate the value for a given Gnodes, commit
    transaction.commit()

【讨论】:

  • 感谢您的回复!我在这里写的代码是一个玩具示例。在原始版本中,我无法在循环之外取出 Hvalue,确实取决于 Gnodes。因此,我想从概念上了解发生了什么。直到什么时候数据保存在内存中?如果我在内部循环中调用 'commit()',** 它会继续附加到我的列表还是创建冗余列表**?
  • 从您的代码中,它将为每个 gNode 创建一个值列表,这就是您想要的样子。它不会附加到新 gNode 的相同列表中。无论如何,您的问题似乎更多是关于您的应用程序的逻辑,而不是确切的提交点。每次 gNodes 计算后,您都可以安全地提交。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-07-07
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多