Pull to refresh

Comments 14

Безотносительно к самой статье, хочу раскритиковать как Berkeley DB, так и подход работы с БД как с stl-контейнером.




Berkeley DB (далее BDB) — очень старый продукт в области, которая активно развивалась в последнее время, перенесла несколько революций и родила массу альтернатив с учетом набитых шишек. Лучшее враг хорошего и сейчас при (пожалуй) любом наборе критериев можно выбрать альтернативу превосходящую BDB. К этому добавляется ряд проблем/недочетов BDB: плохая производительность и/или deadlock-и при конкурентной обработки транзакций, полу-ручное восстановление БД после падений, смена лицензии на AGPL (неприемлема для некоторых проектов).


BDB является встраиваемым движком хранения, который предлагает больше чем key-value. Но в результате получился комбайн, у которого что-то не так с каждой из features. Поэтому в последние годы (несмотря на усилия и деньги Oracle) разработчики предпочитают мигрировать с BDB, когда им нужна хотя-бы одна features (производительность, надежность, масштабируемость, репликация, шифрование и т.д.) на актуальном для индустрии уровне. Сомнительным, но всё-же аргументом, стало то, что при всей "чудесности" BDB (исходя из проспектов Oracle) удаленный из MySQL 5.1 бэкенд хранение так и не был возвращен (работы и багов много, а толку нет).


Поэтому рекомендую читателям десять раз подумать и посмотреть на альтернативы, прежде чем запрягать этого "старого мерина" в новые проекты.




Использование БД как stl-контейнера может порождать массу проблем, так как это разные абстракции. Основное отличие в том, что stl-контейнеры и их интерфейс предназначены для использования внутри одного процесса при монопольном доступе из одного потока выполнения.
В том время как у БД другое время жизни, (как правило) есть транзакции и доступ из нескольких процессов.


Тут можно резонно возразить, что достаточно вовремя открывать/закрывать БД и правильно расставить условные begin_transacton() и end_transaction(bool abort). Во многих случаях, особенно простых так и будет. Однако, дьявол в деталях:


  • У операций с БД другая стоимость, поэтому удобно применив stl-алгоритмы к stl-контейнеру вы можете получить неприемлемую производительность или огромное потребление ресурсов.
  • Вам могут потребоваться вложенные транзакции, которых нет у большинства встраиваемых storage engines и высокопроизводительных БД.
  • В некоторых БД вы можете обнаружить, что "запись" живет отдельно от "чтения" и после обновления виртуального stl-контейнера, чтение из него не видит изменений до коммита транзакции и/или сразу после.
  • В некоторых БД повторное чтение может дать другой результат, что позволит сделать трудно-обнаруживаемые ошибки при использовании stl-подхода.
  • В БД эксплуатирующих MVCC (коих большинство) долгое удержание транзакции на фоне изменений данных (скорее всего) приведет к накоплению мусора и/или деградации производительности. При этом вы можете обнаружить, что рестарт транзакции (переход от старого MVCC-снимка к новому) может потребовать "перечитать всё" и непонятно когда это делать.

Поэтому не советую работать с БД как с stl-контейнером, кроме совсем тривиальных и прозрачных случаев.

Какой key/value аналог BDB на С/С++ вы можете посоветовать для open source проекта?
Необходимо, чтобы поддерживались:


  • транзакционность
  • вторичные индексы
  • кроссплатформенность
  • шифрование данных (опционально)
Вопрос поставлен некорректно.
* объем данных (количество записей/размер записи)
* соотношение CRUD (создание/чтение/обновление/удаление)
* достаточная скорость («максимальная» — неверный ответ)
«Серебряной пули» в DB не существует.
Где-то SQLite обгонит MySQL, где-то проиграет.
С key-value — и BDB хорош, и Tokyo Cabinet хорош, и Round Robin хорош, и…
Каждый хорош в своей нише.

Вы же не пытаетесь заменить молоток, гаечные ключи, отвертку и разводной ключ на одни пассатижи?
Нет, ну можно конечно… Теоретически. Но идея плохая.

Я бы не смешивал key-value и вторичные индексы. Понятно что через первое можно сделать второе, но именно поэтому это разные вещи.


Следующий важный момент это — вопрос встраиваемости, включая неизбежный компромисс между легковесностью движка (простой встраивания) и предлагаемыми возможностями. Например: хорошая производительность во write-сценариях потребует WAL и запуск rollback/redo при открытии БД, LSM потребует фоновых потоков для выполнения слияния, а репликация потребует event loop + поддержку сети + массу настроек и средств диагностики.


Если к обозначенным критериям добавить легкость встраивания, то начать можно с SQLite и libfpta, затем Firebird Embedded и MySQL Embedded. Уверен что есть что-то еще подходящее, но нужно смотреть по остальным критериями, начиная с лицензии (пошла мода на двойное лицензирования с бесплатностью для open-source проектов, т.е. все меняется и нужно смотреть по-факту). Другими словами, при встраивании full-featured движка хранения можно прийти к пропорции "один конь / один рябчик".




UPDATE: Еще стоит подумать над "встраиванием" тарантула.

Наверное стоило сразу очертить юзкейс. Я потихоньку пилю многопоточное приложение SecureDialogues. Некоторые сущности должны храниться на диске в зашифрованном виде. Между этими сущностями есть отношение один ко многим, в дочернем компоненте есть идентификатор родителя. Так же при удалении/обновлении/вставке объектов генерируется событие (вроде триггера), на которое подписаны другие компоненты.


Вторичные индексы я планирую использовать для того, чтобы:


  • при удалении объекта, извлекать из базы все его дочерние (по индексу родителя).
  • удалять их из БД
  • выкидывать сообщение об удалении каждого дочернего объекта.

В принципе скорость работы не так уж важна.


а тарантул разве встраиваемый?

Тарантул не встраиваемый, но (думаю не в вашем случае) можно встроиться в таратнул, в том числе "размазав" логику приложения между lua и C/C++.

Какой движок вам подходит должны решить вы сами. Могу предложить вам (неполный) список не-очевидных вопросов, на которые стоит ответить при оценке вариантов:


  • Нужен ли вам WAL (думаю что нет, но всё-же)?
    pro: Наличие WAL позволяет движку обеспечить меньший Write Amplification Factor, в результате меньшее количество IOPS на транзакцию, в результате меньшую latency и более высокий RPS.
    cons: WAL требует обслуживания, в том числе будет отдельный "файл журнала" (или директория с файлами) и некая суета с этими журналами (rollback/redo после аварии, ротация, checkpoints).
  • Насколько большие объекты вам нужно хранить? Как это делает БД и как она себя поведёт в случае (например) "10000 писем по 10 мегабайт"?
  • Насколько большие ключи вам требуются? Каковы ограничения БД и как она себя при этом ведет?
  • Как часто (а самом деле) вам нужно сбрасывать данные на диск с получением гарантии, что последние изменения не будут потеряны при системной аварии (выключении питания)?
  • Насколько "мохнато" выглядит устройство БД при взгляде снаружи: какие внутренние треды там работают, насколько много своего управления памятью, как выглядит БД в файловой системе?
  • Что БД предлагает для сопутствующих подзадач: проверка целостности, резервное копирование, dump/restore, управление размером БД?
  • Насколько большое API (размер h-файла и кол-во функций), как много исходного кода, сможете ли вы в нём ориентироваться, понять что происходит при баг-репорте от пользователя?

Исходя из вашей задачи (хранение email-ов) думаю вам также стоит глянуть на libmdbx, либо подумать о её комбинации с упомянутой libfpta (второе работает поверх первого).


MDBX может без проблем хранить большие объекты (десятки-сотни мегабайт), а ключей в ~1000 байт вам должно хватить. Кроме этого, MDBX используется в схожем проекте, а dartraiden (наверное) выдаст feedback по опыту использования.


+Добавлю, что это будет в тренде наблюдаемой миграции с Berkeley DB на LMDB (OpenLDAP, Samba, OrangeFS, Postfix, Exim, Cyrus и т.д.), так как MDBX является развитием LMDB ;)

Спасибо за развернутый ответ. Буду сравнивать варианты

Там key-value, т.е. нет вторичных индексов и "колонок" (не путать с Column Families).

В key-value никто не ограничивает формат ключей. Пример вторичного индекса по firstName.


| Key             | Value                     |
|-----------------|---------------------------|
| {id]            | {id, firstName, lastName} |
| {firstName, id} | {}                        |

Сам запрос будет иметь вид {firstName, 0}, а дальше делаем range.

Все верно и есть несколько способов реализовать "вторичные индексы" поверх key-value.


В вашем примере, когда внутри key-value только одно key-scpace, это выглядит обычно так:


|  Key                  | Value                     |
|-----------------------|---------------------------|
| {0, 0, id]            | {id, firstName, lastName} |
| {0, 1, firstName, id} | {}                        |
| {0, 2, lastName, id}  | {}                        |

Т.е. добавляются префиксы. Условно здесь первый дополнительный байтик — это номер таблицы, а второй — номер индекса (первичный, вторичный и т.д.). Минус в том, что увеличивается длина всех ключей (это отдельная тема).


В MDBX доступны множественные key-spaces (aka sub-DB) и есть дополнительные "фишечки": ключи фиксированного размера (не хранится длина) и multi-value с хранением значений во вложенных B+Tree. Поэтому в libfpta индексы располагаются в отдельных пространствах key-value.


space#0
|  Key        | Value                     |
|-------------|---------------------------|
| {id}        | {id, firstName, lastName} |

space#1
|  Key        | Value                     |
|-------------|---------------------------|
| {firstName} | {id}                      |

space#2
|  Key        | Value                     |
|-------------|---------------------------|
| {lastName}  | {id}                      |
Sign up to leave a comment.

Articles