lmdb examples

This commit is contained in:
Alexander Popov 2024-06-25 22:29:17 +03:00
parent b9059af19f
commit 240151ecee
Signed by: iiiypuk
GPG Key ID: E47FE0AB36CD5ED6
3 changed files with 305 additions and 1 deletions

View File

@ -0,0 +1,165 @@
---
title: "🗄️ LMDB: Введение"
date: 2024-06-25T21:59:48+03:00
draft: true
tags: [db, tutorial]
---
Этот пост является моим вольным (_yandex-translate_) переводом страницы из документации LMDB:
http://www.lmdb.tech/doc/starting.html.
# Введение в LMDB
LMDB компактная, быстрая, мощная и надежная.
LMDB представляет из себя упрощенный вариант API Berkeley DB (BDB).
После прочтения этой страницы основная документация по LMDB API должна стать понятной.
Спасибо Берту Хьюберту за создание первоначальной версии этого обзора.
Все начинается с среды, созданной с помощью `mdb_env_create()`.
После создания эта среда также должна быть открыта с помощью `mdb_env_open()`.
`mdb_env_open()` получает имя, которое интерпретируется как путь к каталогу.
Обратите внимание, что этот каталог должен уже существовать, он не будет создан за вас.
В этом каталоге будут созданы файл блокировки и файл хранения данных.
Если вы не хотите использовать каталог, вы можете указать параметр `MDB_NOSUBDIR`,
и в этом случае указанный вами путь будет использоваться непосредственно в качестве файла данных,
а в качестве файла блокировки будет использоваться другой файл с добавленным суффиксом «`-lock`».
Как только среда открыта, в ней можно создать транзакцию с помощью функции `mdb_txn_begin()`.
Транзакции могут быть доступны для чтения или только для записи,
а транзакции чтения-записи могут быть вложенными.
Транзакция должна использоваться только одним потоком одновременно.
Транзакции требуются всегда, даже для доступа только для чтения.
Транзакция обеспечивает согласованное представление данных.
Как только транзакция создана, база данных может быть открыта в ней с помощью функции `mdb_dbi_open()`.
Если в среде будет использоваться только одна база данных,
в качестве имени базы данных может быть передано значение `NULL`.
Для именованных баз данных необходимо использовать флаг `MDB_CREATE` для создания базы данных, если она еще не существует.
Кроме того, функция `mdb_env_set_max dps()` должна вызываться после функции `mdb_env_create()`
и перед функцией `mdb_env_open()`, чтобы задать максимальное количество именованных баз данных, которые вы хотите поддерживать.
**Примечание:** одна транзакция может открыть несколько баз данных. Как правило, базы данных следует открывать только один раз,
с помощью первой транзакции в процессе. После завершения первой транзакции дескрипторы базы данных могут свободно использоваться
всеми последующими транзакциями.
В рамках транзакции `mdb_get()` и `mdb_put()` могут хранить отдельные пары ключ/значение,
если это все, что вам нужно сделать (но смотрите раздел `Cursors` ниже, если вы хотите сделать больше).
## Курсоры (Cursors)
Чтобы выполнять более мощные действия, мы должны использовать курсор.
Внутри транзакции курсор может быть создан с помощью функции `mdb_cursor_open()`.
С помощью этого курсора мы можем сохранять/извлекать/удалять (несколько) значений,
используя `mdb_cursor_get()`, `mdb_cursor_put()` и `mdb_cursor_del()`.
Функция `mdb_cursor_get()` позиционирует себя в зависимости от запрашиваемой операции с курсором,
а для некоторых операций - от предоставленного ключа.
Например, чтобы вывести список всех пар ключ/значение в базе данных,
используйте операцию `MDB_FIRST` для первого вызова функции `mdb_cursor_get()` и `MDB_NEXT` для последующих вызовов,
пока не будет достигнут конец.
Чтобы получить все ключи, начиная с указанного значения ключа, используйте `MDB_SET`.
Дополнительные операции с курсором см. в документации
[IMDB API](http://www.lmdb.tech/doc/group__mdb.html).
При использовании функции `mdb_cursor_put()` либо функция установит курсор для вас на основе **ключа**,
либо вы можете использовать операцию `MDB_CURRENT` для использования текущей позиции курсора.
Обратите внимание, что в этом случае ключ должен соответствовать ключу текущей позиции.
### Подведение итогов
Итак, у нас есть курсор в транзакции, который открывает базу данных в среде,
которая открывается из файловой системы после того, как она была создана отдельно.
Или же мы создаем среду, открываем ее из файловой системы, создаем в ней транзакцию,
открываем базу данных в рамках этой транзакции и создаем курсор во всем вышеперечисленном.
Понятно?
## Потоки и процессы
LMDB использует POSIX-блокировки для файлов, и эти блокировки могут вызвать проблемы,
если один процесс открывает файл несколько раз.
По этой причине не выполняйте `mdb_env_open()` многократное открытие файла одним процессом.
Вместо этого предоставьте общий доступ к среде LMDB, в которой был открыт файл, для всех потоков.
В противном случае, если один и тот же процесс открывает одну и ту же среду несколько раз,
однократное ее закрытие приведет к снятию всех сохраненных на ней блокировок,
а другие экземпляры будут уязвимы для повреждения другими процессами.
Также обратите внимание, что транзакция по умолчанию привязана к одному потоку, используя локальное хранилище потоков.
Если вы хотите передавать транзакции только для чтения между потоками, вы можете использовать параметр `MDB_NOTLS` в среде.
## Транзакции, откаты и т.д.
Чтобы действительно что-то сделать, транзакция должна быть зафиксирована с помощью функции `mdb_txn_commit()`.
В качестве альтернативы, все операции транзакции могут быть отменены с помощью функции `mdb_txn_abort()`.
В транзакции только для чтения никакие курсоры автоматически освобождаться не будут.
В транзакции чтения-записи все курсоры будут освобождены и не должны использоваться повторно.
Очевидно, что в транзакциях только для чтения нет ничего, что можно было бы сохранить.
Транзакция все равно должна быть в конечном итоге прервана, чтобы закрыть все открытые в ней дескрипторы базы данных,
или зафиксирована, чтобы сохранить дескрипторы базы данных для повторного использования в новых транзакциях.
Кроме того, пока транзакция открыта, поддерживается согласованное представление базы данных, для чего требуется хранение.
Транзакция только для чтения, для которой больше не требуется это согласованное представление,
должна быть завершена (зафиксирована или прервана), когда представление больше не требуется (но смотрите ниже для оптимизации).
Одновременно может быть несколько активных транзакций только для чтения, но только одна из них может выполнять запись.
Как только будет открыта единственная транзакция чтения-записи, все дальнейшие попытки запустить ее будут блокироваться до тех пор,
пока первая не будет зафиксирована или прервана. Однако это никак не влияет на транзакции, доступные только для чтения,
и они могут по-прежнему открываться в любое время.
## Дублирование ключей
Функции `mdb_get()` и `mdb_put()`, соответственно, не поддерживают несколько пар ключ/значение с одинаковыми ключами
и поддерживают их только в некоторой степени. Если у ключа несколько значений, функция `mdb_get()` вернет только первое значение.
Если требуется несколько значений для одного ключа, передайте флаг `MDB_SUPPORT` в функцию `mdb_dbi_open()`.
В базе данных `MDB_SUPPORT` по умолчанию функция `mdb_put()` не заменяет значение ключа,
если ключ уже существует. Вместо этого она добавит новое значение к ключу.
Кроме того, функция `mdb_del()` также обратит внимание на поле `value`, позволяя удалять определенные значения ключа.
Наконец, становятся доступны дополнительные операции с курсором для перемещения и извлечения повторяющихся значений.
## Некоторая оптимизация
Если вы часто запускаете и прерываете транзакции только для чтения,
в качестве оптимизации можно только сбросить и возобновить транзакцию.
Функция `mdb_txn_reset()` освобождает все старые копии данных, сохраненные для транзакции только для чтения.
Чтобы повторно использовать эту транзакцию сброса, вызовите для нее функцию `mdb_txn_renew()`.
Все курсоры в этой транзакции также должны быть обновлены с помощью функции `mdb_cursor_renew()`.
Обратите внимание, что функция `mdb_txn_reset()` аналогична функции `mdb_txn_abort()`
и закроет все базы данных, открытые вами в рамках транзакции.
Чтобы окончательно освободить транзакцию, независимо от того, сбрасывается она или нет, используйте функцию `mdb_txn_abort()`.
## Завершение
Для транзакций, доступных только для чтения, все созданные в нем курсоры должны быть закрыты с помощью функции `mdb_cursor_close()`.
Очень редко возникает необходимость закрывать дескриптор базы данных, и, как правило, их следует просто оставлять открытыми.
## В полном API
В полной документации по API LMDB приведены дополнительные сведения, например, как:
* Увеличить размер базы данных (ограничения по умолчанию намеренно невелики)
* Удалить и очистить базу
* Выявление и сообщения ошибок
* Оптимизация скорости загрузки
* (временно) Уменьшить надежность, чтобы увеличить скорость
* Сбор статистики о базе данных
* Определение пользовательских порядков сортировки

View File

@ -0,0 +1,137 @@
---
title: "🗄️ LMDB: Short Guide"
date: 2024-06-25T22:22:40+03:00
draft: true
tags: [db, tutorial]
---
Этот пост является моим вольным (_yandex-translate_) переводом статьи из блога **KOLABNOW**:
https://blogs.kolabnow.com/2018/06/07/a-short-guide-to-lmdb.
# A short guide to LMDB
LMDB is a great embeddable key-value store that we use extensively for Kube.
Using it is not completely straightforward though, so heres a short guide for future reference.
LMDB has a couple of unique properties:
* Its embeddable. You open a file and read/write it in-process.
* The database is a memory-mapped file, so zero-copy reading of data is possible.
* It has good write performance and great read performance.
* It supports single-writer/multi-reader multi-process concurrency.
* It supports multiple named databases.
* The database is append only, so the file will never shrink.
* Sorted keys are supported, and you can search for partial keys (only prefix, not random substrings).
* Works on Linux, Mac OS and Windows.
# High level concepts
## Environment
To do anything with LMDB you first have to open an environment. The environment provides the necessary data structures to access a single database-file, so there is always a 1:1 mapping of database-files you access and environments you have opened. There should only ever be one environment open per database-file per process. Usually there is no good reason to close an environment once its open, so you can just leave it upon until the process ends.
```c
MDB_env *env;
if (const int rc = mdb_env_create(&env)) {
//Error
}
//Limit large enough to accommodate all our named dbs. This only starts to matter if the number gets large, otherwise it's just a bunch of extra entries in the main table.
mdb_env_set_maxdbs(env, 50);
//This is the maximum size of the db (but will not be used directly), so we make it large enough that we hopefully never run into the limit.
mdb_env_set_mapsize(env, (size_t)1048576 * (size_t)100000); // 1MB * 100000
if (const int rc = mdb_env_open(env, "/path/to/database", MDB_NOTLS | MDB_RDONLY, 0664)) {
//Error
}
```
* MDB_NOTLS is used to disable any LMDB internal thread related locking. As long as you manage locking yourself this allows you to have multiple read-only transactions per thread.
* MDB_RDONLY opens the database in read-only mode.
## Transaction
Any interaction with the database content, reading or writing, has to go through a transaction. A transaction always applies to the whole environment (representing the database file), and never to individual named databases. Transactions provide full ACID semantics.
```c
MDB_txn *parentTransaction = nullptr;
MDB_txn *transaction;
if (const int rc = mdb_txn_begin(env, parentTransaction, readOnly ? MDB_RDONLY : 0, &transaction)) {
//Error
}
if (readOnly) {
mdb_txn_abort(transaction);
} else {
mdb_txn_commit(transaction);
}
```
* Transactions can be nested using the parentTransaction argument.
* MDB_RDONLY is useful because you can have multiple read-only transactions open, but only ever one write transaction.
## Named Database
Named databases are similar to tables in relational databases in that they act as sub-databases of the overall database. Each named database is opened using a name and then identified by a DBI (DataBase Identifier). If named databases are not used, the database with the name “” is implicitly used.
Named databases should only be opened once per process, and the DBI should then be reused. Special care must be taken while opening databases because there must not be two concurrent transactions opening the same database (more on that in Caveats).
```c
MDB_dbi dbi;
if (const int rc = mdb_dbi_open(transaction, "databasename", MDB_DUPSORT | MDB_CREATE, &dbi)) {
//Error
}
```
* Various flags can be passed to mdb_dbi_open to alter various properties, such as how the database deals with duplicates or whether to use string or integer keys. Databases must always be opened with the same flags once created.
## Cursors
Cursors allow you to lookup a key in a database with sorted keys, and then iterate over preceding or following keys.
```c
MDB_dbi dbi;
MDB_cursor *cursor;
if (int rc = mdb_cursor_open(d->transaction, d->dbi, &cursor)) {
//Error
}
//Initialize the key with the key we're looking for
MDB_val key = {(size_t)someString.size(), (void *)someString.data()};
MDB_val data;
//Position the cursor, key and data are available in key
if (int rc = mdb_cursor_get(cursor, &key, &data, MDB_SET_RANGE)) {
//No value found
mdb_cursor_close(cursor);
return 0;
}
//Position the curser at the next position
mdb_cursor_get(cursor, &key, &data, MDB_NEXT)
mdb_cursor_close(cursor);
```
## Caveats
Using LMDB in complex scenarios has plenty of pitfalls and its well worth to invest some time into learning what you can and cannot do.
Heres a list of things worth highlighting:
* Retrieved values AND keys will point directly into mapped memory. That memory will only remain valid until the transaction either aborts or commits, or the next operation is executed within the transaction. This essentially means that whenever you hold on to a value you must copy the memory.
* Opening of named databases with mdb_dbi_open must fulfill the following:
1. There must not ever be two concurrent transactions using mdb_dbi_open.
2. A dbi created using mdb_dbi_open will only valid in transactions after that transaction used in mdb_dbi_open has been comitted (you also need to commit read-only transactions in this case).
* An LMDB database is a memory mapped file, and as such large chunks of it will be loaded into memory and show up as your programs memory usage. It is important to understand that this memory can be efficiently reclaimed by the OS if required. [Blogpost](https://blogs.kolabnow.com/2018/02/13/using-and-abusing-memory-with-lmdb-in-kube).
* The database file will never shrink. Memory that is no longer used due to removed values, is internally kept track of and reused, but the file itself will never shrink. To shrink the database it would need to be copied.
* LMDB uses 4KB pages internally (that can be changed at compile time), so in the worst-case scenario, if all values are slightly over 2KB, you will end up with twice the database file-size to what the actual payload is (plus some overhead for the keys and the B+Tree).
* No space is reused while a read-only transaction is active, so long running transactions will result in an ever-growing database. Use short-lived read-only transactions.
* Do not use LMDB datbases on remote filesystems.
* LMDB does no internal bookkeeping of named databases, and you will have to ensure yourself that you open named databases with the same flags every time. This can be challenging when creating named databases dynamically (Im maintaining the flags used for a particular named database in a separate named database).
* In order to run LMDB under Valgrind, the maximum mapsize [must be smaller than half your available ram](https://github.com/BVLC/caffe/issues/2404).
* On windows you will require [a couple of patches](https://github.com/cmollekopf/lmdb) from master that have not yet made it into a release to avoid the database file immediately being the size of the maximum mapsize. Its perhaps better to just try master than my random cherry-picks 😉
And as a last tip; read the docs in lmdb.h closely. Ultimately everything is described in there, you just have to find the relevant sections.

View File

@ -3,10 +3,12 @@
IN=./public/
DIR=Sites/iiiypuk.me/www/
hugo && rsync -avz --delete \
./hugo && rsync -avz --delete \
--exclude 'ytcg' \
--exclude 'chola' \
--exclude 'y' \
--exclude 'a.html' \
--exclude 'DeusEx.png' \
${IN} kvm5:~/${DIR}
exit 0