Skip to content

Ограничения и подводные камни

Смежные: Обзор · Первые шаги · Режимы долговечности · Учебник: Том 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() для лимитов в зависимости от флагов таблицы). Два распространённых способа:

  1. В обычной таблице — ключ K дополняется суффиксом с номером чанка: K#0, K#1, K#2, … Поиск по ключу как по префиксу (K) находит все чанки значения в порядке следования.
  2. В таблице с мультизначениями (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-мьютексы); известных проблем на поддерживаемых платформах нет.