Комментарии 8
К сожалению — это только локальное (встроенное) K-V хранилище. Если нужна распределенность и удаленность — вроде есть memcachedb ( github.com/LMDB/memcachedb ) c вариантом использования LMDB в качестве хранилища, но мне не довелось его потестировать. Собственно вопрос — может, вы сможете сравнить что у вас получится в случае memcachedb?
Что-то я не заметил вторую часть статьи вовремя. Только сейчас наткнулся посредством поиска.
Кроме этого, еще в 2017 году я обещал выдать критику — думаю лучше поздно чем никогда.
Некоторые результаты для меня выглядят странно и вызывают серьезные сомнения.
А отдельные цифры наводят на мысль о полной компрометации результатов.
Как разработчик libmdbx я хорошо представляю в каких сценариях и по каким причинам RocksDB будет "уделывать" MDBX/LMDB.
Собственно, это те сценарии где хорошо работает WAL и LSM:
- много мелких апдеййтов, которые хорошо жмутся в WAL-страницы.
- много коротко-живущих данных (перезаписываются, удаляются или умирают по TTL), тогда движку не приходиться искать вглубь LSM-дерева.
- данные хорошо сживаются, тогда за счет сжатия в RocksDB больше данных поместиться в кэш страниц ядра ОС.
Однако, это не совпадает с наблюдаемым, плюс есть масса нестыковок и странностей:
- Субъективно: Видно неуверенное (мягко говоря) владение настройками движков и понимание того, как именно и на что они влияют. Проще говоря, исполнители просто запускали ioarena "из коробки" не вникая в трансляцию опций на уровень движок хранения. Следовательно, результаты во многом зависят от удачности дефолтовых установок для сценария тестирования.
- Из фразы "с RocksDB что-то случается после 800 млн ключей. К сожалению, не были выяснены причины происходящего." следует, что авторы не поняли как работает LSM, не догадались смотреть нагрузки на диски и не научились мониторить процессы внутри RocksDB. Отсюда рождается подозрение, что "программное регулирования" объема ОЗУ не включало страничный кэш файловой системы и поэтому RockDB работал волшебно быстро пока LSM-дерево помещалось в файловый кэш ОС. Это также косвенно подтверждается очень ровным графиком latency на первом миллиарде записей и фразой "А вот RocksDB в итоге спустился ниже 100k IOPS" при заполнении БД до 17 млрд записей, т.е. когда БД действительно перестала помещаться в ОЗУ.
- В тексте есть фраза "В этом случае MDBX с отрывом уходит вперёд в тесте на чтение", но нет графиков или цифр, которые это демонстрируют. С ними (наверное) были-бы чуть понятнее причины остальных странностей и нестыковок.
- При большом размере БД (сильно больше ОЗУ) MDBX ожидаемо тормозит, так как случайное чтение/обновление данных с большой вероятностью приводит к подкачке с диска. Но и RocksDB при этом также будет сильно проседать, за исключениям ситуаций:
- в данных сильно большой статистический сдвиг: читаются/пишется подмножество данных, либо данные очень хорошо сживаются (повторяются).
- записанные на диск данные кешируются ядром ОС или гипервизором.
В результатах подобного проседания не видно, хотя есть мега-странности с latency (см далее).
- Если смотреть на max-latency для MDBX, то значения практически не меняется с самого начала до конца теста, т.е. время выполнения запроса к БД примерно не меняется, даже когда БД перестает помещаться в память и случайное чтение или запись ведет к подкачке с диска. Очевидно, что такого не может быть, т.е. налицо какая-то ошибка.
- Если посмотреть на сводные диаграммы 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 секунд?
- Для RockDB максимальное время выполнения запроса порядка 0,5 секунд при чтении и около 3 секунд при смешанной нагрузке.
- Даже в начале прогонов, когда размер БД еще небольшой и полностью помещается в память, производительность MDBX почему-то меньше чем на моём ноуте с i7-4600U @ 2.1 GHz. Для сравнения см. мои скрипты и результаты тестирования производительности.
Поэтому, в сухом остатке — показанным цифрам, графика и выводам нельзя верить.
Во-первых, если каждую запись коммитеть в отдельной транзакции, то конечно будет жутко медленно. Заливать данные нужно с разумным батчингом, где-то по 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 будет еще достаточно долго жужжать диском после окончания загрузки (и все это время достаточно сильно тормозить со случайным чтением/обновлением).
с 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? Если устойчивость к крашам не нужна (в данном конкретном случае — при начальной заливке).
Я уже все вам написал и более-менее объяснил. Просто следуйте советам или вникайте глубже.
Key-value для хранения метаданных в СХД. Тестируем выделенные базы данных