Архитектура libmdbx: механизмы и внутренняя организация
Публичная редакция для амальгамированного пакета: имена файлов движка (
src/*.c,src/*.h) относятся к дереву разработки; в амальгамированной сборке та же логика находится вmdbx.c/mdbx.h(mdbx.c++/mdbx.h++). Фрагменты, интересные только разработчикам дерева, помечены маркерами dist-cutoff и вырезаются при публикации. Смежные материалы:deep-dive.ru.md— подробное «как это работает»,improvements.ru.md— каталог доработок над LMDB.
1. MVCC и снапшоты
- Файл БД — B+tree страниц в общем mmap. Каждая write-транзакция строит новую версию изменяемых страниц по принципу copy-on-write; старые версии остаются доступными читателям.
- Читатель регистрирует слот в таблице читателей (RLT) и фиксирует номер снапшота (
txn_id); старейший активный снапшот (детент) определяет водораздел переиспользования страниц. - Мета-данные фиксируются атомарно через три мета-страницы (Troika): двухфазное обновление с минимумом барьеров памяти; конечный автомат тройки покрывает все переходы между версиями.
- Чтение — wait-free: путь чтения не берёт блокировок; 64-битные поля читаются через safe64.
2. Путь durability/flush
- Изменённые страницы накапливаются в грязном списке (DPL); при превышении лимита (
dp_limit, по умолчанию ≈ 1/42 суммы всей и доступной ОЗУ) часть страниц выгружается заранее (спилл). - На коммите конвейер идёт по стадиям (измеримо через
mdbx_txn_commit_ex()→MDBX_commit_latency): подготовка → GC-update → аудит → запись (write) → синхронизация (sync) → завершение. - Режимы синхронизации (
MDBX_SYNC_DURABLE,MDBX_NOMETASYNC,MDBX_SAFE_NOSYNC,MDBX_UTTERLY_NOSYNC,MDBX_WRITEMAP) определяют, что именно сбрасывается на диск (данные / мета / ничего) и каким способом (прямая запись /fdatasync/msync). - Авто-синхронизация по порогам (
mdbx_env_set_syncbytes/mdbx_env_set_syncperiod,MDBX_opt_presync_threshold) с дешёвым опросом (mdbx_env_sync_poll).
3. GC/freelist
- Освобождённые страницы записываются в GC-дерево (FREE_DBI) записями, ключ которых — номер освободившей транзакции; значение — список номеров страниц (PNL).
- Переиспользование допустимо только для записей с ключом ≤ детента; страницы новее старейшего снапшота «заморожены».
- Политики выбора: FIFO (по умолчанию) или LIFO (
MDBX_LIFORECLAIM). - BigFoot дробит гигантские списки освобождений на цепочки записей последовательных txnid.
- Стоимость поиска регулируется лимитами
MDBX_opt_rp_augment_limitиMDBX_opt_gc_time_limit; профилирование —MDBX_ENABLE_PROFGC(поля вMDBX_commit_latency.gc_prof). - Refund / loose-страницы: освобождённые хвостовые страницы возвращаются в неразмеченное пространство без записи в GC; loose-кэш ускоряет переиспользование внутри транзакции.
4. Рост файла и геометрия
- Геометрия задаётся
mdbx_env_set_geometry():size_lower,size_now,size_upper,growth_step,shrink_threshold,pagesize. - Когда GC исчерпан/заморожен, писатель выделяет страницы из неразмеченного хвоста, расширяя
файл шагами
growth_stepна лету (без перезапуска процессов). - Сжатие — только когда хвост свободен и никем не виден; применяется
madvise(MADV_DONTNEED/ REMOVE)+ усечение файла; гистерезис обязателен (шаг сжатия > шага роста). - При достижении
size_upperи исчерпании GC/детента —MDBX_MAP_FULL; перед этим срабатывают разрешающие меры: новый steady-point, HSR-колбэк, выселение припаркованных читателей.
5. mmap и память
- Файл отображён в память целиком; чтение без копирования, запись через CoW-копии страниц
(или напрямую в
MDBX_WRITEMAP). - Для области mmap применяется
madvise(MADV_NOHUGEPAGE)— отказ от THP; размер страницы БД управляется геометрией (степень двойки 256…65536), не связан со страницами ОС. - Большие значения — overflow-цепочки страниц; фрагментация контролируется спиллом и лимитами GC.
- Слабые модели памяти (ARM/AArch64/PPC/MIPS/RISC-V) — аккуратные атомики (acquire/release, CAS, safe64); проверка атомарности на этапе сборки.
6. Single-writer
- В любой момент времени допускается одна write-транзакция; блокировка реализована через
OS-зависимые примитивы (POSIX
fcntl/OFD, SysV семафоры, WindowsLockFileEx). - Нарушения дисциплины владения/пересечения детектируются и возвращаются явными кодами:
MDBX_THREAD_MISMATCH,MDBX_TXN_OVERLAPPING,MDBX_BAD_RSLOT,MDBX_BUSY. MDBX_NOSTICKYTHREADSразрешает передачу транзакции между потоками (для пулов/корутин);mdbx_txn_park()/unpark()— освобождение слота долгоживущим читателем.
7. Слои и модульная организация
Движок разделён на подсистемы с односторонними зависимостями «сверху вниз»:
flowchart TD
P[Публичные заголовки: mdbx.h, mdbx.h++]
A[Тонкие реализации публичного API]
TX[Транзакции: ядра транзакций, MVCC-читатели, spill/refund, coherency]
BT[B+tree: узлы, курсоры, dirty-page list, страницы, обход, сортировка, компараторы]
ST[Хранилище: мета-страницы, DBI, таблицы, mmap-слой, гистограммы, PNL]
GC[GC: выбор и запись страниц переиспользования, интервалы txnid]
OS[OSAL: блокировки, атомики, unaligned-доступ, платформенные импорты]
UT[Утилиты: аудит, логирование, chk, defrag]
P --> A
P --> CXX[C++ API: mdbx.h++ и реализация]
A --> TX
A --> BT
A --> ST
TX --> BT
TX --> GC
TX --> ST
BT --> ST
TX --> OS
BT --> OS
ST --> OS
GC --> OS
TX --> UT
Включения выстраиваются снизу вверх: платформенные предопределения → базовые типы и
утилиты → внутренние структуры (транзакция, dirty-page list, состояния DBI, интервалы
txnid) → модули. Реестр межмодульных функций — единый заголовок прототипов. В
амальгамированной сборке все модули компилируются как один трансляционный блок
(MDBX_INTERNAL превращается в static), что даёт компилятору полную видимость для
инлайнинга и устранения мёртвого кода.
Точная карта файлов по подсистемам (dev): публичный API — src/api-*.c; транзакции —
src/txn*.c, src/mvcc*.c, src/rthc.c, src/spill.c, src/refund.c, src/coherency.c;
B+tree — src/node.c, src/cursor*.c, src/dpl.c, src/page-*.c, src/tree*.c,
src/walk.c, src/sortzone*.c; хранилище — src/meta*.c, src/dbi.c, src/table.c,
src/dxb*.c, src/histogram.c, src/pnl.c, src/global.c; GC — src/gc-get.c,
src/gc-put.c, src/rkl.c; OSAL — src/osal*.c, src/lck-*.c, src/atomics-*.h,
src/windows-import.c; утилиты — src/utils.c, src/audit.c,
src/logging_and_debug.c, src/chk.c, src/defrag.c. Карта модулей:
structure.md; правила сборки: build.md (dev).
8. Ключевые структуры данных
| Структура | Роль |
|---|---|
MDBX_env |
Окружение: mmap БД, mmap файла блокировок, опции, последовательности DBI, слоты читателей |
MDBX_txn |
Транзакция: номера (front/txnid), геометрия, дескрипторы таблиц (dbs[]), состояния DBI, головы курсоров, рабочий набор (troika мета, repnl, GC-очереди, dirtylist, retired/loose/spilled) |
meta_t / troika_t |
Мета-страница и конечный автомат тройки мета-версий |
tree_t |
Дескриптор таблицы: корень, высота, счётчики страниц/записей, mod_txnid |
geo_t |
Геометрия БД: нижняя/текущая/верхняя границы, шаги роста/сжатия, pagesize |
page_t |
Заголовок страницы: txnid, флаги, номер, указатели свободного места (lower/upper) |
node_t |
Узел B+tree: размеры ключа/данных, флаги (N_BIG, N_DUP, ...), инлайн-данные |
MDBX_cursor |
Курсор: стек (страница, индекс), состояние (poor/hollow/pointed/filled), компаратор; dupsort — вложенный подв-курсор |
dpl_t/dp_t |
Dirty-page list: отложенно-сортируемое отображение pgno → страница с резервным зазором |
pnl_t |
Список номеров страниц (отсортированный массив со счётчиком-префиксом) |
rkl_t |
Отсортированное множество txnid = непрерывный интервал + список (бухгалтерия GC) |
txl_t |
Простой список txnid |
clc_t/kvx_t |
Компараторы и поисковые колбэки по ключу/значению, общие на окружение |
reader_slot_t, lck_t |
Слот таблицы читателей и общее состояние файла блокировок (мьютексы, таблица читателей, статистика) |
9. Жизненный цикл транзакций
9.1 Чтение
mdbx_txn_begin(RDONLY)
├─ регистрация слота в таблице читателей (файл блокировок)
└─ выбор недавней/steady мета-версии → снапшот (txnid, геометрия, деревья)
чтение через курсоры → поиск по дереву → page_get (mmap, без блокировок)
mdbx_txn_abort/reset → освобождение или парковка слота
Read-транзакции можно клонировать и парковать (mdbx_txn_park); парковка
позволяет долгоживущему читателю освободить слот, а «отстающие» читатели вытесняются
(протокол HSR), когда их снапшоты блокируют переиспользование страниц.
9.2 Запись
mdbx_txn_begin(RW) → захват глобального мьютекса записи + подготовка базальной транзакции
CRUD:
поиск по дереву → page_get (страница из mmap) → page_touch (CoW-копия в dirtylist)
вставка/удаление узлов → расщепление/ребалансировка страниц при необходимости
освобождённые страницы → retired-список (+ loose-кэш для быстрого переиспользования)
commit (стадии: подготовка → GC-update → аудит → запись → синхронизация):
1. GC-update: освободившиеся страницы записываются в GC-дерево FREE_DBI
(FIFO по умолчанию; LIFO с MDBX_LIFORECLAIM — политика выбирает записи по txnid)
2. refund: возврат хвостовых страниц в неразмеченное пространство
3. спилл: при переполнении dirtylist — выгрузка страниц (bulk-запись + fsync)
4. аудит (MDBX_CHECKING≥2): сверка суммарной бухгалтерии страниц
5. двухфазная фиксация мета-страниц + синхронизация
6. освобождение мьютекса, обновление читателей, чистка мёртвых слотов
abort → отбросить dirtylist, восстановить мета, снять блокировку
Вложенные транзакции: родительский dirtylist расширяется, дочерние клонируют грязные страницы; окружение владеет переиспользуемой базальной write-транзакцией.
9.3 Механика GC
- Свободные страницы хранятся GC-записями, ключ — освобождающий txnid, в дереве FREE_DBI; значения — PNL-списки номеров страниц.
- Аллокация: переиспользование FIFO по умолчанию;
MDBX_LIFORECLAIMвключает LIFO. Обе политики поддерживают плотные последовательности и уважают препятствия (медленные читатели): страница, достижимая из активного старого снапшота, не может быть переиспользована — блокировка разрешается протоколом HSR. - Интервалы переиспользованных/возвратных записей учитываются отдельной структурой (интервал + список); «bigfoot»-обработка крупных диапазонов.
- Гистограммы распределения пространства используются геометрией для решений о росте/сжатии файла.
10. Курсоры и поиск
- Курсор — стек пар (страница, индекс) до листа; позиционирование через поиск по дереву (быстрые пути first/last, branch-колбэки компараторов).
- Машина состояний:
poor(не позиционирован) /hollow(данных нет) /pointed/filled; тонкости конца данных различают «мягкий» EOF (логически в конце, последняя строка ещё читаема) и «жёсткий» EOF (за концом, чтение запрещено) — это контрактmdbx_cursor_eof(). - Изменяющие операции идут через CoW + вставку/удаление узлов с расщеплением (fallback 1→3), ребалансировкой, распространением ключей вверх и углублением дерева.
- Массовые удаления срезают ветви целиком; multi-value (dupsort) обрабатывается вложенными под-курсорами и быстрыми путями для фиксированного размера данных.
11. Backup, проверка целостности, дефрагментация
- Backup (
mdbx_env_copy*): упорядоченный обход всех страниц → bulk-запись → консистентный снапшот без блокировки читателей. - Проверка целостности (
mdbx_chk): валидация структуры, согласованности GC, порядка ключей; отчёты по областям. - Дефрагментация (
mdbx_defrag): карта живых страниц (родитель/флаги), перенос живых страниц к хвосту файла циклами с последующим ремапом; сосуществует с GC.
12. Amalgamation и варианты сборки
make dist формирует плоскую поставку (mdbx.c, mdbx.h, mdbx.h++, mdbx.c++,
mdbx-internals.h, утилиты): инлайнинг заголовков и вырезание dev-фрагментов по
маркерам dist-cutoff. Дерево разработки по умолчанию собирается единым «alloyed»
трансляционным блоком; отладочные сборки могут использовать помодульные объекты.
Подробности — в руководстве по сборке (раздел «Установка и сборка»).
13. Инварианты рефакторинга (dev)
MDBX_INTERNAL-функции остаются модульно-внутренними, если не экспортированы прототип-заголовком.- Путь чтения не берёт блокировок и не аллоцирует в горячих циклах (wait-free читатели).
- Состояния страниц (
frozen/spilled/shadowed/modifiable/tmp) обязаны выводиться изmp->txnidvstxn->txnid/front_txnid— предикаты повсеместно опираются на это. - Порядок двухфазной фиксации мета (
txnid_b=0→txnid_a=txnid→ bootid →txnid_b=txnid) менять нельзя. - GC не имеет права вернуть страницу, достижимую из любого активного снапшота (препятствия переиспользованию, HSR, бухгалтерия интервалов).
- Контракт TLS-деструкторов: при завершении потока все его регистрации читателей снимаются во всех env.
- Раскладка
txn->wrи__restrict-массивы состояний DBI — производительно-критичны; изменения требуют бенчмарков иpgop_stat. - Дисковый формат (
MDBX_MAGIC,MDBX_DATA_VERSION 3, мета/tree/geo) заморожен — рефакторинг не меняет байты на диске. - Семантика курсорного EOF (
z_eof_soft/z_eof_hard) — несущий контрактmdbx_cursor_eof(); закреплена тестами. - Уровни
MDBX_CHECKING/MDBX_DEBUGгейтируют panic/assert/log-пути; стоимость выключенных проверок должна быть нулевой.
Подробное изложение механизмов — deep-dive.ru.md;
каталог доработок над LMDB — improvements.ru.md;
карта модулей и правила сборки — structure.md, build.md (dev).