How are blobs removed in RelStorage pack?
This question is related to How to pack blobstorage with Plone and RelStorage
Using zodb database with RelStorage and sqlite as its backend I am trying to remove unused blobs. Currently db.pack does not remove the blobs from disc. The minimum working example below demonstrates this behavior:
import logging
import numpy as np
import os
import persistent
from persistent.list import PersistentList
import shutil
import time
from ZODB import config, blob
connectionString = """
%import relstorage
<zodb main>
<relstorage>
blob-dir ./blob
keep-history false
cache-local-mb 0
<sqlite3>
data-dir .
</sqlite3>
</relstorage>
</zodb>
"""
class Data(persistent.Persistent):
def __init__(self, data):
super().__init__()
self.children = PersistentList()
self.data = blob.Blob()
with self.data.open("w") as f:
np.save(f, data)
def main():
logging.basicConfig(level=logging.INFO)
# Initial cleanup
for f in os.listdir("."):
if f.endswith("sqlite3"):
os.remove(f)
if os.path.exists("blob"):
shutil.rmtree("blob", True)
# Initializing database
db = config.databaseFromString(connectionString)
with db.transaction() as conn:
root = Data(np.arange(10))
conn.root.Root = root
child = Data(np.arange(10))
root.children.append(child)
# Removing child reference from root
with db.transaction() as conn:
conn.root.Root.children.pop()
db.close()
print("blob directory:", [[os.path.join(rootDir, f) for f in files] for rootDir, _, files in os.walk("blob") if files])
db = config.databaseFromString(connectionString)
db.pack(time.time() + 1)
db.close()
print("blob directory:", [[os.path.join(rootDir, f) for f in files] for rootDir, _, files in os.walk("blob") if files])
if __name__ == "__main__":
main()
The example above does the following:
- Remove any previous database in the current directory along with the blob directory.
- Create a database/storage from scratch adding two objects (root and child), while child is referenced by root and perform a transaction.
- Remove the linkage from root to child and perform a transaction.
- Close the database/storage
- Open the database/storage and perform
db.pack
for one second in the future.
The output of the minimum working example is the following:
INFO:ZODB.blob:(23376) Blob directory '<some path>/blob/' does not exist. Created new directory.
INFO:ZODB.blob:(23376) Blob temporary directory './blob/tmp' does not exist. Created new directory.
blob directory: [['blob/.layout'], ['blob/3/.lock', 'blob/3/0.03da352c4c5d8877.blob'], ['blob/6/.lock', 'blob/6/0.03da352c4c5d8877.blob']]
INFO:relstorage.storage.pack:pack: beginning pre-pack
INFO:relstorage.storage.pack:Analyzing transactions committed Thu Aug 27 11:48:17 2020 or before (TID 277592791412927078)
INFO:relstorage.adapters.packundo:pre_pack: filling the pack_object table
INFO:relstorage.adapters.packundo:pre_pack: Filled the pack_object table
INFO:relstorage.adapters.packundo:pre_pack: analyzing references from 7 object(s) (memory delta: 256.00 KB)
INFO:relstorage.adapters.packundo:pre_pack: objects analyzed: 7/7
INFO:relstorage.adapters.packundo:pre_pack: downloading pack_object and object_ref.
INFO:relstorage.adapters.packundo:pre_pack: traversing the object graph to find reachable objects.
INFO:relstorage.adapters.packundo:pre_pack: marking objects reachable: 4
INFO:relstorage.adapters.packundo:pre_pack: finished successfully
INFO:relstorage.storage.pack:pack: pre-pack complete
INFO:relstorage.adapters.packundo:pack: will remove 3 object(s)
INFO:relstorage.adapters.packundo:pack: cleaning up
INFO:relstorage.adapters.packundo:pack: finished successfully
blob directory: [['blob/.layout'], ['blob/3/.lock', 'blob/3/0.03da352c4c5d8877.blob'], ['blob/6/.lock', 'blob/6/0.03da352c4c5d8877.blob']]
As you can see db.pack
does remove 3 objects "will remove 3 object(s)" but the blobs in the file system are unchanged.
In the unit tests of RelStorage it appears that they do test if the blobs are removed from the file system ( see here), but in the script above it does not work.
What am I doing wrong? Any hint/link/help is appreciated.
1 ответ
По умолчанию каталог хранилища больших двоичных объектов используется в качестве кэша, в котором хранятся данные большого двоичного объекта, которые также хранятся в базе данных; идея заключается в том, что загрузка данных большого двоичного объекта из кеша локального диска происходит быстрее, чем с удаленного сервера базы данных. Упаковка в хранилище без истории с кэширующим хранилищем больших двоичных объектов не удаляет недоступные файлы больших двоичных объектов, вместо этого полагаясь на ограничитель размера файла, чтобы удалить устаревшие данные кэша, когда необходимо освободить место. Однако вы не установили ограничение на размер, поэтому размер кэша неограничен, и эти недоступные файлы больших двоичных объектов будут жить вечно.
При упаковке здесь нельзя удалить файлы больших двоичных объектов, поскольку кэш является локальным для каждого клиента ZODB; он как бы находится вне юрисдикции хранилища ZODB. Это может быть не так очевидно при использовании SQLite в качестве уровня базы данных, но представьте, что вместо этого вы используете Postgres, на отдельном сервере, с несколькими клиентами на разных компьютерах, и вы можете увидеть, что очистка кеша при упаковке невозможна.
Обратите внимание, что другим вариантом хранилища BLOB-объектов является общее хранилище BLOB-объектов, которое, вероятно, ближе к тому, что вы ожидали: все данные BLOB-объектов хранятся на диске, а не в базе данных. При использовании с удаленным сервером базы данных и несколькими клиентами вам нужно будет разместить это что-то вроде общего ресурса NTFS. В этом случае упаковка работает непосредственно с большими двоичными объектами, а недоступные файлы больших двоичных объектов удаляются сразу после упаковки.
У вас есть два варианта:
Установите ограничение на размер кэша больших двоичных объектов, установив
blob-cache-size
. При упаковке файлы BLOB-объектов по-прежнему не удаляются, но они будут удалены, когда станет мало места.Переключиться на общий кеш больших двоичных объектов (установить
shared-blob-dir
к истине). Для RelStorage с поддержкой sqlite это, вероятно, имеет больше смысла, чем кэширующее хранилище BLOB-объектов, несмотря на ужасные предупреждения в документации!
Итак, самым простым изменением было бы переключение режимов хранения BLOB-объектов:
connectionString = """
%import relstorage
<zodb main>
<relstorage>
blob-dir ./blob
shared-blob-dir true
keep-history false
cache-local-mb 0
<sqlite3>
data-dir .
</sqlite3>
</relstorage>
</zodb>
"""
Затем вывод изменится на:
INFO:ZODB.blob:(26177) Blob directory '<some path>/blob/' does not exist. Created new directory.
INFO:ZODB.blob:(26177) Blob temporary directory './blob/tmp' does not exist. Created new directory.
blob directory: [['blob/.layout'], ['blob/0x00/0x00/0x00/0x00/0x00/0x00/0x00/0x03/0x03da4f169582cd22.blob', 'blob/0x00/0x00/0x00/0x00/0x00/0x00/0x00/0x03/.lock'], ['blob/0x00/0x00/0x00/0x00/0x00/0x00/0x00/0x06/0x03da4f169582cd22.blob', 'blob/0x00/0x00/0x00/0x00/0x00/0x00/0x00/0x06/.lock']]
INFO:relstorage.storage.pack:pack: beginning pre-pack
INFO:relstorage.storage.pack:Analyzing transactions committed Tue Sep 1 01:22:35 2020 or before (TID 277621285453417864)
INFO:relstorage.adapters.packundo:pre_pack: filling the pack_object table
INFO:relstorage.adapters.packundo:pre_pack: Filled the pack_object table
INFO:relstorage.adapters.packundo:pre_pack: analyzing references from 7 object(s) (memory delta: 0 KB)
INFO:relstorage.adapters.packundo:pre_pack: objects analyzed: 7/7
INFO:relstorage.adapters.packundo:pre_pack: downloading pack_object and object_ref.
INFO:relstorage.adapters.packundo:pre_pack: traversing the object graph to find reachable objects.
INFO:relstorage.adapters.packundo:pre_pack: marking objects reachable: 4
INFO:relstorage.adapters.packundo:pre_pack: finished successfully
INFO:relstorage.storage.pack:pack: pre-pack complete
INFO:relstorage.adapters.packundo:pack: will remove 3 object(s)
INFO:relstorage.adapters.packundo:pack: cleaning up
INFO:relstorage.adapters.packundo:pack: finished successfully
blob directory: [['blob/.layout'], ['blob/0x00/0x00/0x00/0x00/0x00/0x00/0x00/0x03/0x03da4f169582cd22.blob', 'blob/0x00/0x00/0x00/0x00/0x00/0x00/0x00/0x03/.lock']]
И да, макет каталога blob меняется, так что он может работать со всеми возможными OID, когда-либо. Однако OID 6 был удален.
Найденные вами модульные тесты запускаются только при тестировании с общим кешем больших двоичных объектов:
# If the blob directory is a cache, don't test packing,
# since packing can not remove blobs from all caches.
test_packing = shared_blob_dir