Ограничения и подводные камни
Смежные: Обзор · Первые шаги · Режимы долговечности · Учебник: Том V «Экспертные темы»
Ограничения количественных параметров — в Обзоре; здесь — поведенческие ограничения, операционные риски и способы их обхода.
Многоверсионность и «долгоживущие» читатели
Понимание проблемы долгоживущих читателей требует небольшого контекста, но
сложно для быстрого восприятия. Поэтому разумно упростить так: проблема
сборки мусора существует во всех БД в том или ином виде — например, VACUUM
в PostgreSQL. Но в libmdbx она заметнее из-за высокой скорости транзакций и
намеренного упрощения внутренностей в пользу производительности: libmdbx
использует многоверсионное управление конкурентным доступом
на базе copy-on-write,
что позволяет нескольким читателям работать параллельно с write-транзакцией без
блокировок. Каждая write-транзакция нуждается в свободных страницах для
изменённых данных; libmdbx эффективно переиспользует такие страницы из более
ранних неиспользуемых снапшотов, но это невозможно, пока read-транзакция
держит старый снапшот.
Механика: каждая write-транзакция
строит новую версию изменяемых страниц (MVCC на базе copy-on-write); читатели
работают со снапшотами и не блокируются. Страницы, освободившиеся в новых
write-транзакциях, не могут быть переиспользованы, пока на них смотрит хоть
один активный старый снапшот. Поэтому активная модификация данных при
параллельном долгом чтении увеличивает рабочий набор и может быстро
исчерпать свободное место (MDBX_MAP_FULL).
Типичные источники долгих читателей: горячий backup на медленное хранилище, отладка клиентского приложения с активной read-транзакцией, приостановленные (SUSPEND/ptrace) процессы с открытым чтением.
Инструменты смягчения — уже доступны:
- Парковка транзакций —
mdbx_txn_park()/mdbx_txn_unpark(): припаркованный читатель помечается псевдо-TID, перестаёт влиять на водораздел переиспользования и может быть вытеснен ( ousted ) с автоматическим перезапуском при обращении. - Handle-Slow-Readers —
mdbx_env_set_hsr(): колбэк вызывается, когда БД заполнилась из-за отставших читателей; может подождать, асинхронно прервать транзакцию читателя (возврат 1) или зафиксировать гибель процесса (возврат 2+). MDBX_LIFORECLAIM— LIFO-переиспользование укорачивает цикл оборота страниц и смягчает последствия на write-back носителях.- Выселение припаркованных читателей автоматически перед исчерпанием места (вместе с установкой нового steady-снапшота).
- Читайте второстепенные данные на второстепенной копии БД.
Roadmap: радикальное преодоление архитектурного ограничения (переиспользование страниц независимо от старых MVCC-снапшотов — nonsequential GC recycling) — в следующей версии движка (MithrilDB), где проблема долгоживущих читателей решена полностью; частичные улучшения планируются и в ближайших релизах libmdbx.
Избегайте: остановки/блокировки потоков с активным чтением (включая отладку), abort'ов процессов с чтением (libmdbx чистит слоты читателей автоматически, но проверка не может выполняться слишком часто без ущерба производительности).
Хранение длинных значений
Данные лежат в B+tree как пары «ключ → значение». Значение, которое не
помещается в листовую страницу вместе с ключом и заголовком узла
(практически — длиннее примерно половины страницы), не разрезается на
части: оно хранится одним неразрывным куском на последовательных страницах
(это и есть large/overflow-страницы), а в листе остаётся только указатель на
кусок {pgno, npages}. Чтение такого куска — прямое, без копирования и без
накладных расходов (максимальная скорость чтения). Подробно о механике —
internals.
Именно из требования «неразрывности» вырастает главное ограничение:
- запись длинного значения требует найти непрерывный диапазон
(«span») свободных страниц нужной длины, а не «любую свободную страницу».
Чем длиннее значение, тем длиннее необходимый span и тем труднее он
находится: при фрагментации B+tree движку приходится сканировать и сливать
многие записи GC (тот самый «глубокий поиск»), обрабатывать весь GC или
дописывать страницы в конец файла, а когда последовательность не дают ни
GC, ни верхняя граница геометрии — возникает MDBX_MAP_FULL;
- перезапись длинного значения — это CoW всего span: выделить новый
непрерывный диапазон, скопировать данные, освободить старый. Частые
перезаписи «перемешивают» свободное пространство, фрагментируют файл и
порождают много записей GC;
- очень длинные значения раздувают GC: одна запись GC вмещает ≈1000
номеров страниц (~4 МБ при странице 4 КБ); значение на сотни тысяч страниц
порождает цепочки bigfoot из десятков/сотен записей, раздувая GC-дерево;
- write amplification растёт пропорционально размеру значения: каждый
коммит переписывает все страницы span, а не один лист.
Что уже доступно: поиск свободных последовательностей в GC существенно ускорен (SIMD: NEON/SSE2/AVX2/AVX512); спилл и refund уменьшают фрагментацию; геометрия с автоматическим ростом помогает при нехватке последовательностей.
Базовый совет
Храните значения так, чтобы каждое вместе с ключом помещалось в одну
страницу — то есть не длиннее mdbx_limits_valsize4page_max(). Тогда
значения не порождают «span-проблем» вовсе. Дополнительно:
- увеличивайте страницу (до 64 КБ), если значения «в среднем» велики;
- следите за БД через mdbx_env_stat() (число overflow-страниц) и
гистограммы mdbx_chk (large_pages, val_len) для выявления churn;
- при фрагментации свободного пространства используйте дефрагментацию
(mdbx_env_defrag() / mdbx_defrag);
- настраивайте rp_augment_limit/gc_time_limit под профиль записи
(неконтролируемый рост файла против «застревающих» коммитов);
- «тяжёлые» данные (изображения, бинарные объекты) выносите из БД, храня в
значениях только ссылки (например, контентно-адресуемые ключи).
Фрагментирование длинных значений на чанки
Если длинное значение всё же нужно хранить в БД и оно не помещается в одну
страницу, его можно явно разбить на чанки, не превышающие
mdbx_limits_valsize4page_max() (см. также mdbx_limits_valsize_max() для
лимитов в зависимости от флагов таблицы). Два распространённых способа:
- В обычной таблице — ключ
Kдополняется суффиксом с номером чанка:K#0,K#1,K#2, … Поиск по ключу как по префиксу (K) находит все чанки значения в порядке следования. - В таблице с мультизначениями (DUPSORT) — чанки не длиннее
mdbx_limits_valsize_max(MDBX_DUPSORT); данные чанка дополняются префиксом с номером мульти-значения (если под одним ключом хранится несколько длинных значений) и номером чанка. Поиск по ключу приводит к набору всех мульти-значений и их чанков, а поиск по префиксу с номером мульти-значения — к набору его чанков.
Оба способа — это ручное фрагментирование, которое потребуется только до появления планируемого в roadmap потокового API для длинных значений («Streaming BLOBs»), снимающего необходимость такого разбиения.
Roadmap
Кроме переработки хранения длинных значений в MithrilDB (где проблема снята архитектурно), в roadmap заложен потоковый режим чтения и записи длинных значений — «Streaming BLOBs»: API, который позволит читать и писать длинные значения потоково, без ручного фрагментирования на чанки. См. также Том V, гл. 33 и improvements.
Огромные транзакции
Аналогичная ситуация с транзакциями, освобождающими много страниц: список освобождённых страниц сам по себе — длинное значение, требующее последовательности свободных страниц.
Что уже доступно: функция «Big Foot» (включена по умолчанию) — крупные
PNL-списки дробятся на цепочки записей последовательных txnid, исключая поиск,
выделение и хранение длинных последовательностей; см. MDBX_ENABLE_BIGFOOT.
Дополнительно — LIFO-политика и refund уменьшают объём бухгалтерии GC.
Roadmap: сохраняется в MithrilDB — где списки освобождённых страниц вписаны в архитектуру без поиска свободных последовательностей.
Резервирование адресного пространства
Конфигурация БД часто резервирует заметный объём виртуального адресного
пространства и (потенциально) файла под рост — это не потребляет реальную
память и диск. Но на 64-битных системах с малым объёмом RAM задание неадекватно
большого size_upper через mdbx_env_set_geometry() может исчерпать системные
ресурсы (ENOMEM). Не задавайте верхнюю границу «про запас».
Сетевые файловые системы
- Не размещайте БД на удалённых файловых системах — даже между процессами одного хоста: это ломает файловые блокировки на некоторых платформах и синхронизацию memory mapping.
- Исключения: эксклюзивный режим работы по сети и кооперативное read-only чтение с read-only сетевых шар.
Дочерние процессы и fork()
- Не используйте открытое окружение в потомке после
fork(). Лучший способ — не иметь открытых экземпляров libmdbx на моментfork(); build-опцияMDBX_ENV_CHECKPID(по умолчанию ON на не-Windows) позволяет выявить такие ошибки раньше, но не даёт гарантий. - С v0.13.1 доступна
mdbx_env_resurrect_after_fork()— уже доступное переиспользование уже открытого окружения в потомке, строго без наследования транзакций родителя. - Не вызывайте
fork()одновременно из нескольких потоков.
Read-only режим
«Чистого» read-only режима нет: читателям нужна запись в LCK-файл, чтобы быть
видимыми писателю. libmdbx всегда пытается открыть/создать LCK в read-write;
при ошибках (EROFS, EACCES, EPERM) и запрошенном MDBX_RDONLY
переключается в режим без LCK.
Один поток — одна транзакция
Поток может использовать только одну транзакцию одновременно (плюс вложенные
write-транзакции вне WRITEMAP). Каждая транзакция принадлежит одному потоку;
write-транзакция завершается в породившем её потоке. Нарушения возвращают
MDBX_TXN_OVERLAPPING, MDBX_BAD_RSLOT, MDBX_BUSY. Опция
MDBX_NOSTICKYTHREADS разрешает передачу транзакций между потоками — только
для тех, кто точно понимает, что делает (дедлоки, чтение чужих данных).
Не открывайте БД дважды
Повторное открытие одной БД в одном процессе недопустимо: при отсутствии
OFD-блокировок используются POSIX-блокировки, которые ломаются при
многократном открытии файла одним процессом (закрытие одного экземпляра
снимает все блокировки). libmdbx отслеживает и предотвращает это
(MDBX_BUSY); совместимость с LMDB — mdbx_setup_debug(MDBX_DBG_LEGACY_MULTIOPEN, ...)
с восстановлением блокировок (возможны паузы).
Диагностика LCK-файла
LCK-файл очищается при старте первого процесса с БД: при любых повреждениях LCK достаточно закрыть БД во всех процессах (или остановить их) и перезапустить.
- Повреждение LCK возможно при некорректном приложении, пишущем через указатели в shared-память; переносимого способа защиты нет (LCK обновляется конкуренто и lock-free — блокировки дали бы большие накладные расходы).
- Застрявшие читатели (после аварийного завершения программы) приводят к быстрому росту БД; libmdbx проверяет их при открытии окружения и перед ростом.
- Застрявшие писатели чистятся автоматически (платформенно-зависимо, через robust-мьютексы); известных проблем на поддерживаемых платформах нет.