mirror of
https://github.com/isar/libmdbx.git
synced 2024-10-30 11:29:19 +08:00
49cccf4791
Change-Id: I9787d54df1efdef19ccc29c24168015e225ff097
378 lines
28 KiB
Markdown
378 lines
28 KiB
Markdown
libmdbx
|
||
======================================
|
||
Extended LMDB, aka "Расширенная LMDB".
|
||
|
||
*The Future will Positive. Всё будет хорошо.*
|
||
[![Build Status](https://travis-ci.org/ReOpen/libmdbx.svg?branch=devel)](https://travis-ci.org/ReOpen/libmdbx)
|
||
[![Build status](https://ci.appveyor.com/api/projects/status/v21jlh5kfmk85r7t/branch/devel?svg=true)](https://ci.appveyor.com/project/leo-yuriev/libmdbx/branch/devel)
|
||
|
||
English version by Google [is here](https://translate.googleusercontent.com/translate_c?act=url&ie=UTF8&sl=ru&tl=en&u=https://github.com/ReOpen/libmdbx/tree/devel).
|
||
|
||
|
||
## Кратко
|
||
|
||
_libmdbx_ - это встраиваемый key-value движок хранения со специфическим
|
||
набором возможностей, которые при правильном применении позволяют
|
||
создавать уникальные решения с чемпионской производительностью, идеально
|
||
сочетаясь с технологией [MRAM](https://en.wikipedia.org/wiki/Magnetoresistive_random-access_memory).
|
||
|
||
_libmdbx_ обновляет совместно используемый набор данных, никак не мешая
|
||
при этом параллельным операциям чтения, не применяя атомарных операций к
|
||
самим данным, и обеспечивая согласованность при аварийной остановке в
|
||
любой момент. Поэтому _libmdbx_ позволяя строить системы с линейным
|
||
масштабированием производительности чтения/поиска по ядрам CPU и
|
||
амортизационной стоимостью любых операций Olog(N).
|
||
|
||
### История
|
||
|
||
_libmdbx_ является потомком "Lightning Memory-Mapped Database",
|
||
известной под аббревиатурой
|
||
[LMDB](https://en.wikipedia.org/wiki/Lightning_Memory-Mapped_Database).
|
||
Изначально доработка производилась в составе проекта
|
||
[ReOpenLDAP](https://github.com/ReOpen/ReOpenLDAP). Примерно за год
|
||
работы внесенные изменения приобрели самостоятельную ценность. Осенью
|
||
2015 доработанный движок был выделен в отдельный проект, который был
|
||
[представлен на конференции Highload++
|
||
2015](http://www.highload.ru/2015/abstracts/1831.html).
|
||
|
||
|
||
Характеристики и ключевые особенности
|
||
=====================================
|
||
|
||
_libmdbx_ наследует все ключевые возможности и особенности от
|
||
своего прародителя [LMDB](https://en.wikipedia.org/wiki/Lightning_Memory-Mapped_Database),
|
||
с устранением описанных далее проблем и архитектурных недочетов.
|
||
|
||
### Общее для оригинальной _LMDB_ и _libmdbx_
|
||
|
||
1. Данные хранятся в упорядоченном отображении (ordered map), ключи всегда
|
||
отсортированы, поддерживается выборка диапазонов (range lookups).
|
||
|
||
2. Данные отображается в память каждого работающего с БД процесса.
|
||
Ключам и данным обеспечивается прямой доступ без необходимости их
|
||
копирования, так как они защищены транзакцией чтения и не изменяются.
|
||
|
||
3. Транзакции согласно
|
||
[ACID](https://ru.wikipedia.org/wiki/ACID), посредством
|
||
[MVCC](https://ru.wikipedia.org/wiki/MVCC) и
|
||
[COW](https://ru.wikipedia.org/wiki/%D0%9A%D0%BE%D0%BF%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5_%D0%BF%D1%80%D0%B8_%D0%B7%D0%B0%D0%BF%D0%B8%D1%81%D0%B8).
|
||
Изменения строго последовательны и не блокируются чтением,
|
||
конфликты между транзакциями не возможны.
|
||
При этом гарантируется чтение только зафиксированных данных, см [relaxing serializability](https://en.wikipedia.org/wiki/Serializability).
|
||
|
||
4. Чтение и поиск [без блокировок](https://ru.wikipedia.org/wiki/%D0%9D%D0%B5%D0%B1%D0%BB%D0%BE%D0%BA%D0%B8%D1%80%D1%83%D1%8E%D1%89%D0%B0%D1%8F_%D1%81%D0%B8%D0%BD%D1%85%D1%80%D0%BE%D0%BD%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D1%8F),
|
||
без [атомарных операций](https://ru.wikipedia.org/wiki/%D0%90%D1%82%D0%BE%D0%BC%D0%B0%D1%80%D0%BD%D0%B0%D1%8F_%D0%BE%D0%BF%D0%B5%D1%80%D0%B0%D1%86%D0%B8%D1%8F).
|
||
Читатели не блокируются операциями записи и не конкурируют
|
||
между собой, чтение масштабируется линейно по ядрам CPU.
|
||
|
||
5. Эффективное хранение дубликатов (ключей с несколькими
|
||
значениями), без дублирования ключей, с сортировкой значений, в
|
||
том числе целочисленных (для вторичных индексов).
|
||
|
||
6. Эффективная поддержка ключей фиксированной длины, в том числе целочисленных.
|
||
|
||
7. Амортизационная стоимость любой операции Olog(N),
|
||
[WAF](https://en.wikipedia.org/wiki/Write_amplification) и RAF также Olog(N).
|
||
|
||
8. Нет [WAL](https://en.wikipedia.org/wiki/Write-ahead_logging) и журнала
|
||
транзакций, после сбоев не требуется восстановление. Не требуется компактификация
|
||
или какое-либо периодическое обслуживание. Поддерживается резервное копирование
|
||
"по горячему", на работающей БД без приостановки изменения данных.
|
||
|
||
9. Отсутствует какое-либо внутреннее управление памятью или кэшированием. Всё
|
||
необходимое штатно выполняет ядро ОС.
|
||
|
||
|
||
### Недостатки и Компромиссы
|
||
|
||
1. Единовременно может выполняться не более одной транзакция изменения данных
|
||
(один писатель). Зато все изменения всегда последовательны, не может быть
|
||
конфликтов или ошибок при откате транзакций.
|
||
|
||
2. Отсутствие [WAL](https://en.wikipedia.org/wiki/Write-ahead_logging)
|
||
обуславливает относительно большой
|
||
[WAF](https://en.wikipedia.org/wiki/Write_amplification). Поэтому фиксация
|
||
изменений на диске может быть дорогой и является главным ограничителем для
|
||
производительности по записи. В качестве компромисса предлагается несколько
|
||
режимов ленивой и/или периодической фиксации. В том числе режим `MAPASYNC`,
|
||
при котором изменения происходят только в памяти и асинхронно фиксируются на
|
||
диске ядром ОС.
|
||
|
||
3. [COW](https://ru.wikipedia.org/wiki/%D0%9A%D0%BE%D0%BF%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5_%D0%BF%D1%80%D0%B8_%D0%B7%D0%B0%D0%BF%D0%B8%D1%81%D0%B8)
|
||
для реализации [MVCC](https://ru.wikipedia.org/wiki/MVCC) выполняется на
|
||
уровне страниц в [B+ дереве](https://ru.wikipedia.org/wiki/B-%D0%B4%D0%B5%D1%80%D0%B5%D0%B2%D0%BE).
|
||
Поэтому изменение данных амортизационно требует копирования Olog(N) страниц,
|
||
что расходует [пропускную способность оперативной
|
||
памяти](https://en.wikipedia.org/wiki/Memory_bandwidth) и является основным
|
||
ограничителем производительности в режиме `MAPASYNC`.
|
||
|
||
4. В _LMDB_ существует проблема долгих чтений (приостановленных читателей),
|
||
которая приводит к деградации производительности и переполнению БД.
|
||
В _libmdbx_ предложены средства для предотвращения, выхода из проблемной
|
||
ситуации и устранения её последствий. Подробности ниже.
|
||
|
||
5. В _LMDB_ есть вероятность разрушения БД в режиме `WRITEMAP+MAPASYNC`.
|
||
В _libmdbx_ для `WRITEMAP+MAPASYNC` гарантируется как сохранность базы,
|
||
так и согласованность данных. При этом также, в качестве альтернативы,
|
||
предложен режим `UTTERLY_NOSYNC`. Подробности ниже.
|
||
|
||
|
||
#### Проблема долгих чтений
|
||
|
||
Понимание проблемы требует некоторых пояснений, которые
|
||
изложены ниже, но могут быть сложны для быстрого восприятия.
|
||
Поэтому, тезисно:
|
||
|
||
* Изменение данных на фоне долгой операции чтения может
|
||
приводить к исчерпанию места в БД.
|
||
|
||
* После чего любая попытка обновить данные будет приводить к
|
||
ошибке `MAP_FULL` до завершения долгой операции чтения.
|
||
|
||
* Характерными примерами долгих чтений являются горячее
|
||
резервное копирования и отладка клиентского приложения при
|
||
активной транзакции чтения.
|
||
|
||
* В оригинальной _LMDB_ после этого будет наблюдаться
|
||
устойчивая деградация производительности всех механизмов
|
||
обратной записи на диск (в I/O контроллере, в гипервизоре,
|
||
в ядре ОС).
|
||
|
||
* В _libmdbx_ предусмотрен механизм аварийного прерывания таких
|
||
операций, а также режим `LIFO RECLAIM` устраняющий последующую
|
||
деградацию производительности.
|
||
|
||
Операции чтения выполняются в контексте снимка данных (версии
|
||
БД), который был актуальным на момент старта транзакции чтения.
|
||
Такой читаемый снимок поддерживается неизменным до завершения
|
||
операции. В свою очередь, это не позволяет повторно
|
||
использовать страницы БД в последующих версиях (снимках БД).
|
||
|
||
Другими словами, если обновление данных выполняется на фоне
|
||
долгой операции чтения, то вместо повторного использования
|
||
"старых" ненужных страниц будут выделяться новые, так как
|
||
"старые" страницы составляют снимок БД, который еще
|
||
используется долгой операцией чтения.
|
||
|
||
В результате, при интенсивном изменении данных и достаточно
|
||
длительной операции чтения, в БД могут быть исчерпаны свободные
|
||
страницы, что не позволит создавать новые снимки/версии БД.
|
||
Такая ситуация будет сохраняться до завершения операции чтения,
|
||
которая использует старый снимок данных и препятствует
|
||
повторному использованию страниц БД.
|
||
|
||
Однако, на этом проблемы не заканчиваются. После описанной
|
||
ситуации, все дополнительные страницы, которые были выделены
|
||
пока переработка старых была невозможна, будут участвовать в
|
||
цикле выделения/освобождения до конца жизни экземпляра БД. В
|
||
оригинальной _LMDB_ этот цикл использования страниц работает по
|
||
принципу [FIFO](https://ru.wikipedia.org/wiki/FIFO). Поэтому
|
||
увеличение количества циркулирующий страниц, с точки зрения
|
||
механизмов кэширования и/или обратной записи, выглядит как
|
||
увеличение рабочего набор данных. Проще говоря, однократное
|
||
попадание в ситуацию "уснувшего читателя" приводит к
|
||
устойчивому эффекту вымывания I/O кэша при всех последующих
|
||
изменениях данных.
|
||
|
||
Для устранения описанных проблемы в _libmdbx_ сделаны
|
||
существенные доработки, подробности ниже. Иллюстрации к
|
||
проблеме "долгих чтений" можно найти в [слайдах
|
||
презентации](http://www.slideshare.net/leoyuriev/lmdb).
|
||
Там же приведен пример количественной оценки прироста
|
||
производительности за счет эффективной работы
|
||
[BBWC](https://en.wikipedia.org/wiki/BBWC) при включении `LIFO
|
||
RECLAIM` в _libmdbx_.
|
||
|
||
|
||
#### Вероятность разрушения БД в режиме `WRITEMAP+MAPASYNC`
|
||
|
||
При работе в режиме `WRITEMAP+MAPSYNC` запись измененных
|
||
страниц выполняется ядром ОС, что имеет ряд преимуществ. Так
|
||
например, при крахе приложения, ядро ОС сохранит все изменения.
|
||
|
||
Однако, при аварийном отключении питания или сбое в ядре ОС, на
|
||
диске будет сохранена только часть измененных страниц БД. При
|
||
этом с большой вероятностью может оказаться так, что будут
|
||
сохранены мета-страницы со ссылками на страницы с новыми
|
||
версиями данных, но не сами новые данные. В этом случае БД
|
||
будет безвозвратна разрушена, даже если до аварии производилась
|
||
полная синхронизация данных (посредством `mdbx_env_sync()`).
|
||
|
||
В _libmdbx_ эта проблема устранена, подробности ниже.
|
||
|
||
|
||
Доработки _libmdbx_
|
||
===================
|
||
|
||
1. Режим `LIFO RECLAIM`.
|
||
|
||
Для повторного использования выбираются не самые старые, а
|
||
самые новые страницы из доступных. За счет этого цикл
|
||
использования страниц всегда имеет минимальную длину и не
|
||
зависит от общего числа выделенных страниц.
|
||
|
||
В результате механизмы кэширования и обратной записи работают с
|
||
максимально возможной эффективностью. В случае использования
|
||
контроллера дисков или системы хранения с
|
||
[BBWC](https://en.wikipedia.org/wiki/BBWC) возможно
|
||
многократное увеличение производительности по записи
|
||
(обновлению данных).
|
||
|
||
2. Обработчик `OOM-KICK`.
|
||
|
||
Посредством `mdbx_env_set_oomfunc()` может быть установлен
|
||
внешний обработчик (callback), который будет вызван при
|
||
исчерпания свободных страниц из-за долгой операцией чтения.
|
||
Обработчику будет передан PID и pthread_id. В свою очередь
|
||
обработчик может предпринять одно из действий:
|
||
|
||
* отправить сигнал kill (#9), если долгое чтение выполняется
|
||
сторонним процессом;
|
||
|
||
* отменить или перезапустить проблемную операцию чтения, если
|
||
операция выполняется одним из потоков текущего процесса;
|
||
|
||
* подождать некоторое время, в расчете что проблемная операция
|
||
чтения будет штатно завершена;
|
||
|
||
* перервать текущую операцию изменения данных с возвратом кода
|
||
ошибки.
|
||
|
||
3. Гарантия сохранности БД в режиме `WRITEMAP+MAPSYNC`.
|
||
|
||
При работе в режиме `WRITEMAP+MAPSYNC` запись измененных
|
||
страниц выполняется ядром ОС, что имеет ряд преимуществ. Так
|
||
например, при крахе приложения, ядро ОС сохранит все изменения.
|
||
|
||
Однако, при аварийном отключении питания или сбое в ядре ОС, на
|
||
диске будет сохранена только часть измененных страниц БД. При
|
||
этом с большой вероятностью может оказаться так, что будут
|
||
сохранены мета-страницы со ссылками на страницы с новыми
|
||
версиями данных, но не сами новые данные. В этом случае БД
|
||
будет безвозвратна разрушена, даже если до аварии производилась
|
||
полная синхронизация данных (посредством `mdbx_env_sync()`).
|
||
|
||
В _libmdbx_ эта проблема устранена путем полной переработки
|
||
пути записи данных:
|
||
|
||
* В режиме `WRITEMAP+MAPSYNC` _libmdbx_ не обновляет
|
||
мета-страницы непосредственно, а поддерживает их теневые копии
|
||
с переносом изменений после фиксации данных.
|
||
|
||
* При завершении транзакций, в зависимости от состояния
|
||
синхронности данных между диском и оперативной память,
|
||
_libmdbx_ помечает точки фиксации либо как сильные (strong),
|
||
либо как слабые (weak). Так например, в режиме
|
||
`WRITEMAP+MAPSYNC` завершаемые транзакции помечаются как
|
||
слабые, а при явной синхронизации данных как сильные.
|
||
|
||
* При открытии БД выполняется автоматический откат к последней
|
||
сильной фиксации. Этим обеспечивается гарантия сохранности БД.
|
||
|
||
К сожалению, такая гарантия надежности не дается бесплатно. Для
|
||
сохранности данных, страницы формирующие крайний снимок с
|
||
сильной фиксацией, не должны повторно использоваться
|
||
(перезаписываться) до формирования следующей сильной точки
|
||
фиксации. Таким образом, крайняя точка фиксации создает
|
||
описанный выше эффект "долгого чтения". Разница же здесь в том,
|
||
что при исчерпании свободных страниц ситуация будет
|
||
автоматически исправлена, посредством записи изменений на диск
|
||
и формированием новой сильной точки фиксации.
|
||
|
||
В последующих версиях _libmdbx_ будут предусмотрены средства
|
||
для асинхронной записи данных на диск с автоматическим
|
||
формированием сильных точек фиксации.
|
||
|
||
4. Возможность автоматического формирования контрольных точек
|
||
(сброса данных на диск) при накоплении заданного объёма изменений,
|
||
устанавливаемого функцией `mdbx_env_set_syncbytes()`.
|
||
|
||
5. Возможность получить отставание текущей транзакции чтения от
|
||
последней версии данных в БД посредством `mdbx_txn_straggler()`.
|
||
|
||
6. Утилита mdbx_chk для проверки БД и функция `mdbx_env_pgwalk()` для
|
||
обхода всех страниц БД.
|
||
|
||
7. Управление отладкой и получение отладочных сообщений посредством
|
||
`mdbx_setup_debug()`.
|
||
|
||
8. Возможность связать с каждой завершаемой транзакцией до 3
|
||
дополнительных маркеров посредством `mdbx_canary_put()`, и прочитать их
|
||
в транзакции чтения посредством `mdbx_canary_get()`.
|
||
|
||
9. Возможность узнать есть ли за текущей позицией курсора строка данных
|
||
посредством `mdbx_cursor_eof()`.
|
||
|
||
10. Возможность явно запросить обновление существующей записи, без
|
||
создания новой посредством флажка `MDB_CURRENT` для `mdbx_put()`.
|
||
|
||
11. Возможность обновить или удалить запись с получением предыдущего
|
||
значения данных посредством `mdbx_replace()`.
|
||
|
||
12. Поддержка ключей и значений нулевой длины. Включая сортированные
|
||
дубликаты, в том числе вне зависимости от порядка их добавления или
|
||
обновления.
|
||
|
||
13. Исправленный вариант `mdbx_cursor_count()`, возвращающий корректное
|
||
количество дубликатов для всех типов таблиц и любого положения курсора.
|
||
|
||
14. Возможность открыть БД в эксклюзивном режиме посредством
|
||
`mdbx_env_open_ex()`, например в целях её проверки.
|
||
|
||
15. Возможность закрыть БД в "грязном" состоянии (без сброса данных и
|
||
формирования сильной точки фиксации) посредством `mdbx_env_close_ex()`.
|
||
|
||
16. Возможность получить посредством `mdbx_env_info()` дополнительную
|
||
информацию, включая номер самой старой версии БД (снимка данных),
|
||
который используется одним из читателей.
|
||
|
||
17. Функция `mdbx_del()` не игнорирует дополнительный (уточняющий)
|
||
аргумент `data` для таблиц без дубликатов (без флажка `MDB_DUPSORT`), а
|
||
при его ненулевом значении всегда использует его для сверки с удаляемой
|
||
записью.
|
||
|
||
18. Возможность открыть dbi-таблицу, одновременно с установкой
|
||
компараторов для ключей и данных, посредством `mdbx_dbi_open_ex()`.
|
||
|
||
19. Возможность посредством `mdbx_is_dirty()` определить находятся ли
|
||
некоторый ключ или данные в "грязной" странице БД. Таким образом избегаю
|
||
лишнего копирования данных перед выполнением модифицирующих операций
|
||
(значения в размещенные "грязных" страницах могут быть перезаписаны при
|
||
изменениях, иначе они будут неизменны).
|
||
|
||
20. Корректное обновление текущей записи, в том числе сортированного
|
||
дубликата, при использовании режима `MDB_CURRENT` в `mdbx_cursor_put()`.
|
||
|
||
21. Все курсоры, как в транзакциях только для чтения, так и в пишущих,
|
||
могут быть переиспользованы посредством `mdbx_cursor_renew()` и ДОЛЖНЫ
|
||
ОСВОБОЖДАТЬСЯ ЯВНО.
|
||
>
|
||
> ## _ВАЖНО_, Обратите внимание!
|
||
>
|
||
> Это единственное изменение в API, которое значимо меняет
|
||
> семантику управления курсорами и может приводить к утечкам
|
||
> памяти. Следует отметить, что это изменение вынужденно.
|
||
> Так устраняется неоднозначность с массой тяжких последствий:
|
||
>
|
||
> - обращение к уже освобожденной памяти;
|
||
> - попытки повторного освобождения памяти;
|
||
> - memory corruption and segfaults.
|
||
|
||
22. Дополнительный код ошибки `MDBX_EMULTIVAL`, который возвращается из
|
||
`mdbx_put()` и `mdbx_replace()` при попытке выполнять неоднозначное
|
||
обновление или удаления одного из нескольких значений с одним ключом,
|
||
т.е. когда невозможно однозначно идентифицировать одно целевое значение
|
||
из нескольких.
|
||
|
||
23. Возможность посредством `mdbx_get_ex()` получить значение по
|
||
заданному ключу, одновременно с количеством дубликатов.
|
||
|
||
24. Наличие функций mdbx_cursor_on_first() и mdbx_cursor_on_last(),
|
||
которые позволяют быстро выяснить стоит ли курсор на первой/последней
|
||
позиции.
|
||
|
||
25. При завершении читающих транзакций, открытые в них DBI-хендлы не
|
||
закрываются и не теряются при завершении таких транзакций посредством
|
||
mdbx_txn_abort() или mdbx_txn_reset(). Что позволяет избавится от ряда
|
||
сложно обнаруживаемых ошибок.
|