Когда фиксировать данные в ZODB

Я пытаюсь передать данные, сгенерированные следующим фрагментом кода:

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 элементами каждый), его трудно сохранить в памяти. Я искал решение, в котором хранится пара ключей: значение (в виде списка), как только они будут сгенерированы. Здесь было рекомендовано Написание и чтение словаря в определенном формате (Python), чтобы использовать ZODB в сочетании с Btree.

Потерпите меня, если это слишком наивно, мой вопрос: когда нужно позвонить transaction.commit(), чтобы зафиксировать данные? Если я назову его в конце внутреннего цикла, результирующий файл будет чрезвычайно большим (не знаю, почему). Вот фрагмент:

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()? Опять же, я не уверен, почему, но это приводит к меньшему размеру файла на диске.

Я хочу свести к минимуму данные, хранящиеся в памяти. Любое руководство будет оценено!

Ответ 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 по мере совершения транзакций.

Удаление старых данных называется упаковкой, которая похожа на команду VACUUM в PostgreSQL и других реляционных базах данных. Просто вызовите метод .pack() в переменной db, чтобы удалить все старые версии, или используйте параметры t и days этого метода, чтобы установить ограничения на сколько истории сохранить, первая временная метка time.time() (секунды с эпохи), перед которой вы можете упаковать, а days - это количество дней в прошлом для сохранения с текущего времени или t, если указано, Упаковка должна значительно уменьшить ваш файл данных, поскольку частичные списки в более старых транзакциях будут удалены. Обратите внимание, что упаковка является дорогостоящей операцией и, следовательно, может занять некоторое время, в зависимости от размера вашего набора данных.

Использование транзакции для управления памятью

Вы пытаетесь создать очень большой набор данных, используя упорство, чтобы обойти ограничения с памятью, и используете транзакции, чтобы попытаться сбросить все на диск. Обычно, однако, используя сигналы фиксации транзакции, вы завершили построение своего набора данных, что вы можете использовать как одно атомное целое.

То, что вам нужно использовать здесь, - это точка сохранения. Savepoints - это по существу субтранзакции, точка во время всей транзакции, где вы можете запросить временное сохранение данных на диске. При совершении транзакции они станут постоянными. Чтобы создать точку сохранения, вызовите метод .savepoint в транзакции:

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, чтобы разбить ваши данные на более постоянные объекты. A 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() для обработки.

Ответ 2

Что представляет собой транзакция, зависит от того, что должно быть "атомарным" в вашем приложении. Если транзакция завершится неудачно, она будет отклонена в предыдущее состояние (сразу после последнего фиксации). Из вашего кода приложения видно, что вы хотите вычислить значение для каждого Gnodes. Итак, ваша фиксация может идти в конце цикла 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. Если это дорогостоящая операция, вы вычисляете ее 1000 раз для каждого Gnodes, хотя это не влияет на его расчет. Итак, я вытащил бы его из цикла.

# 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()