Как стать автором
Обновить

Key-value для хранения метаданных в СХД. Тестируем выделенные базы данных

Время на прочтение5 мин
Количество просмотров6K
Всего голосов 5: ↑5 и ↓0+5
Комментарии8

Комментарии 8

интересная статья, спасибо!
у меня очень хорошие результаты получались с LMDB. Читающие потоки не блокируются пишущими, а пишущие блокируют только друг друга. Если обновления вести аккуратно, желательно в одном потоке, то получается и космическая скорость чтения и приемлемлемая скорость обновления. Имеет смысл если операций чтения на порядки больше записи.
К сожалению — это только локальное (встроенное) K-V хранилище. Если нужна распределенность и удаленность — вроде есть memcachedb ( github.com/LMDB/memcachedb ) c вариантом использования LMDB в качестве хранилища, но мне не довелось его потестировать. Собственно вопрос — может, вы сможете сравнить что у вас получится в случае memcachedb?

Что-то я не заметил вторую часть статьи вовремя. Только сейчас наткнулся посредством поиска.


Кроме этого, еще в 2017 году я обещал выдать критику — думаю лучше поздно чем никогда.




Некоторые результаты для меня выглядят странно и вызывают серьезные сомнения.
А отдельные цифры наводят на мысль о полной компрометации результатов.


Как разработчик libmdbx я хорошо представляю в каких сценариях и по каким причинам RocksDB будет "уделывать" MDBX/LMDB.
Собственно, это те сценарии где хорошо работает WAL и LSM:


  • много мелких апдеййтов, которые хорошо жмутся в WAL-страницы.
  • много коротко-живущих данных (перезаписываются, удаляются или умирают по TTL), тогда движку не приходиться искать вглубь LSM-дерева.
  • данные хорошо сживаются, тогда за счет сжатия в RocksDB больше данных поместиться в кэш страниц ядра ОС.

Однако, это не совпадает с наблюдаемым, плюс есть масса нестыковок и странностей:


  1. Субъективно: Видно неуверенное (мягко говоря) владение настройками движков и понимание того, как именно и на что они влияют. Проще говоря, исполнители просто запускали ioarena "из коробки" не вникая в трансляцию опций на уровень движок хранения. Следовательно, результаты во многом зависят от удачности дефолтовых установок для сценария тестирования.
  2. Из фразы "с RocksDB что-то случается после 800 млн ключей. К сожалению, не были выяснены причины происходящего." следует, что авторы не поняли как работает LSM, не догадались смотреть нагрузки на диски и не научились мониторить процессы внутри RocksDB. Отсюда рождается подозрение, что "программное регулирования" объема ОЗУ не включало страничный кэш файловой системы и поэтому RockDB работал волшебно быстро пока LSM-дерево помещалось в файловый кэш ОС. Это также косвенно подтверждается очень ровным графиком latency на первом миллиарде записей и фразой "А вот RocksDB в итоге спустился ниже 100k IOPS" при заполнении БД до 17 млрд записей, т.е. когда БД действительно перестала помещаться в ОЗУ.
  3. В тексте есть фраза "В этом случае MDBX с отрывом уходит вперёд в тесте на чтение", но нет графиков или цифр, которые это демонстрируют. С ними (наверное) были-бы чуть понятнее причины остальных странностей и нестыковок.
  4. При большом размере БД (сильно больше ОЗУ) MDBX ожидаемо тормозит, так как случайное чтение/обновление данных с большой вероятностью приводит к подкачке с диска. Но и RocksDB при этом также будет сильно проседать, за исключениям ситуаций:
    • в данных сильно большой статистический сдвиг: читаются/пишется подмножество данных, либо данные очень хорошо сживаются (повторяются).
    • записанные на диск данные кешируются ядром ОС или гипервизором.
      В результатах подобного проседания не видно, хотя есть мега-странности с latency (см далее).
  5. Если смотреть на max-latency для MDBX, то значения практически не меняется с самого начала до конца теста, т.е. время выполнения запроса к БД примерно не меняется, даже когда БД перестает помещаться в память и случайное чтение или запись ведет к подкачке с диска. Очевидно, что такого не может быть, т.е. налицо какая-то ошибка.
  6. Если посмотреть на сводные диаграммы latency ближе к концу первой части статьи, то результаты вызывают полное непонимание:
    • Для RockDB максимальное время выполнения запроса порядка 0,5 секунд при чтении и около 3 секунд при смешанной нагрузке.
      Предположим, что получив неудачный запрос RockDB может глубоко и неудачно (из-за фильтра Блума) искать в LSM-дереве, но 3 секунды на NVMe-диске — это невероятно много! Особенно с с учетом того, что слияние LSM делается фоновым тредом и механизм многократно "допиливали", в том числе для стабилизации latency.
    • Для MDBX максимум порядка 100 секунд (10**5 миллисекунд) при чтении на NVE-диске — это явная чушь и/или ошибка.
      MDBX не делает ничего лишнего, просто поиск по B+Tree. Поэтому, в худшем случае, MDBX прочитает кол-во страниц на 2 больше высоты B+Tree дерева. При не-длинных ключах (как в рассматриваемом случае), на одну branch-страницу поместиться 50-100-200 ключей (размер страницы 4К или 8К, примерно делим на размер ключа). Т.е. будет максимум 10 чтений с NVMe (модель указана), который имеет производительность более 500K IOPS при случайном чтении. Поэтому мы должны увидеть max-latency близкую к 10 / 500K = 0,2 миллисекунды. Откуда взялось 100 секунд?
  7. Даже в начале прогонов, когда размер БД еще небольшой и полностью помещается в память, производительность MDBX почему-то меньше чем на моём ноуте с i7-4600U @ 2.1 GHz. Для сравнения см. мои скрипты и результаты тестирования производительности.

Поэтому, в сухом остатке — показанным цифрам, графика и выводам нельзя верить.

Вот одна из недавних проблем с LMDB. Мне нужно наполнить таблицу. В процессе заливки данных, ее никто не читает, кроме самого процесса, который заливает. Если наполнение закончится аварийно, что крайне маловероятно, то не проблема начать сначала. Вопрос лишь сделать наполнение _быстрым_. А оно в LMDB на объемах порядка 1-2млрд записей с файлом порядка 150-250GB работает адски медленно и это с выключенным SYNC. Неделю может ползти на SSD Samsung. Подозреваю, что это из-за O_DIRECT флага на открытия файла, т.е. ОС ничего не кеширует. Это, может, и хорошо в продакшене, где целостность нужнее, и где небольшие объемы апдейтов. Но для начального массированного наполнения это не гуд.

Во-первых, если каждую запись коммитеть в отдельной транзакции, то конечно будет жутко медленно. Заливать данные нужно с разумным батчингом, где-то по 10К-1000К записей за одну транзакцию. При этом лучше открыть БД в режиме MDBX_WRITEMAP|MDBX_UTTERLY_NOSYNC.
UPDATE: Наполение БД в 256 Гб с размером страницы в 4К должно поместиться в одну транзакцию, см MDBX_DPL_TXNFULL.


Во-вторых, если данных существенно больше чем размер ОЗУ, то вливая не-сортированные данные вы неизбежно и безвыходно заставите движок выполнять сортировку вставками со сложностью O(N**2). Этим породите множественные случайные изменения в B+Tree, с многократным перекладыванием всей БД с диска в память и обратно.


Поэтому, при большом кол-ве записей, в "миллион раз" выгоднее предварительно отсортировать данные сортировкой слиянием (сложность O(N*Log(N)) в порядке возрастания ключей. Утилита sort реализует алгоритм сортировки сама. А затем влить отсортированные данные в MDBX (с ключом MDBX_APPED будет еще быстрее). Такая заливка займет время сравнимое с копированием данных на используемый диск.


Такой трюк оправдан для всех БД, но движки на основе LSM более толерантны к массивной загрузке без сортировки — фактически БД будет выполнять сортировку слиянием внутри себя, т.е. какой-нибудь RocksDB будет еще достаточно долго жужжать диском после окончания загрузки (и все это время достаточно сильно тормозить со случайным чтением/обновлением).

Ну конечнно не по одной записи. Там есть ограничения, кажется до 1024 за раз. Мы пишем по 500-600 записей на транзакцию. В strace видим pwrite вызовы с 4К кусочками. И их очень много.

с WRITE_MAP пробовали. Получаем sparse file, который, хотя и занимает реально меньше места, чем показывает ls -la, всё же дико всех пугает, и всё же, он в итоге физически получается в разы больше, чем если создавать без WRITE_MAP. Похоже там какие-то утечки или хуже дела с фрагментацией.
И даже без WRITE_MAP приходится потом перезаливать файл еще раз через внешний текстовый дамп. Файл становится где-то на 30-40% меньше.

SYNC — мы не видим никаких SYNC в strace.

RAM — 128GB, сравнимо с файлом. Но RAM поже используется никак. Совсем никак. Всё-же O_DIRECT.

Отсортировать полностью не получается — миллиард записей совсем не хочется вычитывать в память. А те куски, которые вычитываем за раз — конечно сортируем в памяти перед вставкой. Но это слабо помогает.
Похоже это тот случай, когда выгоднее сначала слить в LSM, а уже потом думать о чем-то другом.
А можно ли как-то отключить O_DIRECT? Он точно там нужен даже без SYNC? Если устойчивость к крашам не нужна (в данном конкретном случае — при начальной заливке).

Любопытно, что у вас получилось?

Ну конечнно не по одной записи. Там есть ограничения, кажется до 1024 за раз. Мы пишем по 500-600 записей на транзакцию.

Никакого ограничения в 1024 операции в транзакции нет. Есть ограничения на "размер транзакции" по кол-ву грязных страниц, в MDBX это 4194302. Пока производимые изменения приводят к изменению/добавлению меньшего кол-ва страниц всё будет работать. При достижении лимита будет ошибка MDBX_TXN_FULL (Кстати, я ошибся написав что заполнение всей вашей БД поместится в одну транзакцию — потребуется порядка 30-50).


Так или иначе вставка по 500-600 записей — это очень мало. Тут нужно ворочить миллионами.


В strace видим pwrite вызовы с 4К кусочками. И их очень много.

Потому-что БД состоит из страниц, размер которых по-умолчанию равен 4096.


с WRITE_MAP пробовали. Получаем sparse file, который, хотя и занимает реально меньше места, чем показывает ls -la, всё же дико всех пугает, и всё же, он в итоге физически получается в разы больше, чем если создавать без WRITE_MAP.

LMDB не создает sparce-файлов и если это делает ядро ОС, то могут быть еще какие-то странности. Размер создаваемого файла задается через API (по умолчанию кажется 2 Mb), а затем он может увеличиваться пока есть место на диске. Но LMDB не умеет уменьшать файл и не выполняется компактификацию (только при копировании БД). Поэтому я рекомендую всё-таки перейти на MDBX и использовать mdbx_env_set_geometry().


Похоже там какие-то утечки или хуже дела с фрагментацией.

В LMDB были подобные ошибки. Текущую ситуацию не знаю, но крайний коммит связан с подобными ошибками, а проблема перебалансировки похоже так и осталась… В MDBX всё подобное поправлено и перепроверено, в том числе есть mdbx_chk и развитый тест, включая стохастический.


И даже без WRITE_MAP приходится потом перезаливать файл еще раз через внешний текстовый дамп. Файл становится где-то на 30-40% меньше.

Видимо вы не понимаете как работает B+Tree. При случайных вставках (т.е. несортированных данных) заполнение страниц может находиться в пределах от 25% до 100%. При вставке сортированных в порядке ключей с APPEND-флажком получаться ровно заполненные страницы.


Создавая тектовый дамп вы получаете отсортированные данные, а заливая базу из дампа получаете ровно заполненные страницы. При этом вы сначала заставляете LMDB выполнить сортировку вставками (с эффективностью O(N*2)), вместо того чтобы использовать сортировку слиянием (с эффективностью O(NLog(N)), как было предложено. Вы понимаете что на вашем объеме данных это примерно в миллиард раз медленнее?


SYNC — мы не видим никаких SYNC в strace.

На не-дремучих ядрах linux должен быть fdatasync, а не fsync (и ни в коем случае не sync).


RAM — 128GB, сравнимо с файлом. Но RAM поже используется никак. Совсем никак. Всё-же O_DIRECT.

O_DIRECT используется для записи мета-страниц (одна запись на транзакцию), а также включается при копировании БД без компактификации.
Поэтому сделайте как я написал (используйте MDBX вместо LMDB, используйте WRITEMAP + UTTERLY_NOSYNC при вливании данных).


В отличии от LMDB, в MDBX это работает корректно: движок не даст одновременно открыть БД в несовместимых режимах, движок поддерживает три мета-старицы вместо двух и поддерживает разные режимы фиксации (steady, weak, legacy).


Отсортировать полностью не получается — миллиард записей совсем не хочется вычитывать в память. А те куски, которые вычитываем за раз — конечно сортируем в памяти перед вставкой. Но это слабо помогает.

Частичная сортировка даст очень мало эффекта, точнее даст для первого куска, но не для следующих.


Загружать все данные в память не нужно. Вам нужен алгоритм сортировки слиянием, который прекрасно умеет утилита sort. При этом затраты на такую сортировку много-много-кратно ниже затрат на сортировку вставками внутри B+Tree и кратно ниже стандартных эвристик LSM.
Для скорости используйте --buffer-size=XYZ.


Похоже это тот случай, когда выгоднее сначала слить в LSM, а уже потом думать о чем-то другом.

Я пока не видел LSM работающий быстрее sort. Теоретически RocksDB может отработать быстрее на сильно сжимаемых данных, но не видел и думаю какой-нибудь lz4 sort его обгонит.


А можно ли как-то отключить O_DIRECT? Он точно там нужен даже без SYNC? Если устойчивость к крашам не нужна (в данном конкретном случае — при начальной заливке).

Я уже все вам написал и более-менее объяснил. Просто следуйте советам или вникайте глубже.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий