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



    В этой статье мы продолжаем рассказывать о том, как можно хранить метаданные в СХД при помощи баз данных key-value.

    На этот раз в центре нашего внимания выделенные БД: Aerospike и RocksDB. Описание значимости метаданных в СХД, а также результаты тестирования встроенных БД можно посмотреть тут.

    Параметры тестирования key-value БД


    Кратко напомним основные параметры, по которым мы проводили тестирование (подробности в предыдущей статье).

    Основной workload — Mix50/50. Дополнительно оценивали: RR, Mix70/30 и Mix30/70.

    Тестирование проводили в 3 этапа:

    1. Заполнение БД — заполняем в 1 поток БД до необходимого количества ключей.
      1.1 Сбрасываем кэши! Иначе тесты будут нечестными: БД обычно пишут данные поверх файловой системы, поэтому срабатывает кэш операционной системы. Важно сбрасывать его перед каждым тестом.
    2. Тесты на 32 потока — прогоняем workload'ы
      2.1 Random Read
      • Сбрасываем кэши!
      2.2 Mix70/30
      • Сбрасываем кэши!
      2.3 Mix50/50
      • Сбрасываем кэши!
      2.4 Mix30/70
      • Сбрасываем кэши!
    3. Тесты на 256 потоков.
      3.1 То же самое, что и на 32 потока.

    Измеряемые показатели


    • Пропускная способности/throughput (IOPS/RPS — кто какие обозначения больше любит).
    • Задержки/latency (msec):
      • Min.
      • Max.
      • Среднее квадратическое значение — более показательное значение, чем среднее арифметическое, т.к. учитывает квадратичное отклонение.
      • Перцентиль 99.99.

    Тестовое окружение


    Конфигурация:
    CPU: 2x Intel Xeon E5-2620 v4 2.10GHz
    RAM: 16GB
    Disk: [2x] NVMe HGST SN100 1.5TB
    OS: CentOS Linux 7.2 kernel 3.11
    FS: EXT4

    Объем доступной RAM регулировался не физически, а программно — часть заполнялась искусственно скриптом на Python, а остаток был свободен для БД и кэшей.

    Выделенные БД. Aerospike


    Чем Aerospike отличается от движков, которые мы до этого тестировали?

    • Индекс в RAM
    • Использование RAW дисков (= нет ФС)
    • Не дерево, а хэш.

    Так сложилось, что в индексе Aerospike на каждый ключ хранится 64Б (а сам ключ у нас всего 8Б). При этом индекс всегда полностью должен быть в RAM.

    Это означает, что при нашем количестве ключей индекс не поместится в памяти, которую мы выделяем. Надо уменьшить количество ключей. И наши данные это позволяют!


    Рис. 1. Схема упаковки 1


    Рис. 2. Схема упаковки 2

    Итак, с помощью такой упаковки мы уменьшили количество ключей в 4 раза. Таким же образом можем уменьшить их количество в k раз. Тогда размер значения будет 16*k Б.

    Тестирование. 17 млрд ключей


    Чтобы индекс Aerospike на 17 млрд ключей (17 млрд сопоставлений lba->metadata) поместился в RAM, надо упаковать это всё в 64 раза.

    В итоге получим 265 625 000 ключей (каждому из ключей будет соответствовать значение размером 1024Б, содержащее 64 экземпляра метаданных).

    Тестировать будем с помощью YCSB. Он не выдаёт среднюю квадратическую задержку, поэтому её не будет на графиках.

    Заполнение


    Aerospike показал неплохой результат на заполнение. Ведёт себя очень стабильно.

    Но заполнение проводилось в 16 потоков, а не в один, как было с движками. В один поток Aerospike выдавал около 20k IOPS. Скорее всего, дело в бенчмарке (т.е. в один поток бенчмарк просто не «выжимает» БД). Либо же Aerospike любит много потоков, а в один не готов выдавать большую пропускную способность.


    Рис. 3. Производительность заполнения базы

    Максимальная задержка также держалась на примерно одинаковом уровне на протяжении всего заполнения.


    Рис. 4. Latency заполнения базы

    Тесты


    Здесь важно отметить, что на этих графиках нельзя напрямую сравнивать Aerospike и RocksDB, т.к тесты проводились в разных условиях и разными бенчмарками – Aerospike использовался «с упаковкой», а RocksDB — без.

    Также стоит учесть, что 1 IO у Aerospike = извлечение 64 значений (экземпляров метаданных).
    Результаты RocksDB здесь приведены в качестве опорных.


    Рис. 5. Сравнение Aerospike и RocksDB.100% Read


    Рис. 6. Сравнение Aerospike и RocksDB. Mix 70%/30%


    Рис. 7. Сравнение Aerospike и RocksDB Mix 50%/50%


    Рис. 8. Сравнение Aerospike и RocksDB Mix 30%/70%

    В итоге запись на малом количестве потоков получилась медленнее, чем у RocksDB (при этом следует помнить, что в случае Aerospike за один раз записывается сразу 64 значения).

    Но на большом количестве потоков Aerospike всё же выдаёт более высокие значения.


    Рис. 9. Aerospike. Latency Read 100%

    Тут мы, наконец-то, смогли получить приемлемый уровень задержки. Но только в тесте на чтение.


    Рис. 10. Aerospike.Latency Mix 50%/50%

    Теперь можно обновить список выводов:

    • Запись + мало потоков => WiredTiger
    • Запись + много потоков => RocksDB
    • Чтение + DATA > RAM => RocksDB
    • Чтение + DATA < RAM => MDBX
    • Много потоков + DATA > RAM + Упаковка => Aerospike

    Итоговый вывод по выбору БД для метаданных: в таком виде ни один из претендентов не достигает нужных нам показателей. Ближе всех Aerospike.

    Ниже мы расскажем о том, что с этим можно делать, и что нам дает хранение данных в прямой адресации.

    Прямая адресация. 137 млрд ключей


    Рассмотрим системы хранения объемом 512 ТВ. Метаданные такой системы хранения как раз помещаются на одну NVME и соответствуют 137 млрд ключей для key-value базы данных.

    Рассмотрим простейшую реализацию прямой адресации. Для этого на одном узле создадим SPDK NVMf таргет, на другом узле возьмем локальную NVMe и этот NVMf таргет и объединим их в логический RAID1.

    Такой подход позволит записывать метаданные и защищать их репликой на случай отказа.
    При тестировании производительности в несколько потоков каждый поток будет писать в свою область и эти области не будут пересекаться.

    Тестирование проводилось с помощью бенчмарка FIO в 8 потоков с глубиной очереди 32. В таблице ниже представлены результаты тестирования.

    Таблица 1. Тестирование прямой адресации
    rand read 4k (IOPS) rand write 4k (IOPS) rand r/w 50/50 4K(IOPS) rand r/w 70/30 4K(IOPS) rand r/w 30/70 4K(IOPS)
    spdk 760 — 770 K 350 — 360 K 400 — 410 K 520 — 540 K 410 — 450 K
    lat (ms) avg/max 0.3 / 5 0.4 / 23 0.3 / 19 0,5 / 21 1,2 / 28

    Теперь протестируем Aerospike в аналогичной конфигурации, где 137 млрд ключей как раз также помещаются на одну NVMe и реплицируются на другую NVMe.

    Тестируем с помощью «родного» бенчмарка Aerospike. Берем два бенчмарка – каждый на своем узле – и 256 потоков, чтобы выжать максимальную производительность.

    Получаем следующие результаты:

    Таблица 2. Тестирование Aerospike c репликацией
    R/W IOPS >1ms >2ms >4ms 99.99 lat <=
    100/0 635000 7% 1% 0% 50
    70/30 425000 8% 3% 1% 50
    50/50 342000 8% 4% 1% 50
    30/70 271000 8% 4% 1% 40
    0/100 200000 0% 0% 0% 36

    Ниже приведены результаты тестирования без репликации, один бенчмарк в 256 потоков.

    Таблица 3. Тестирование Aerospike без репликации
    R/W IOPS <=1ms >1ms >2ms 99.99 lat <=
    100/0 413000 99% 1% 0% 5
    70/30 376000 95% 5% 2% 7
    50/50 360000 92% 8% 3% 8
    30/70 326000 93% 7% 3% 8
    0/100 260000 94% 6% 2% 5

    Заметим, что Aerospike c репликацией работает не хуже, а даже лучше. При этом возрастает latency по сравнению с тестированием Aerospike без репликации.

    Также приведем результаты тестирования RocksDB без репликации (у RocksDB нет родной встроенной репликации) своим бенчмарком в 256 потоков, 137 млрд ключей с упаковкой.

    Таблица 4. Тестирование RocksDB без репликации
    R/W IOPS <=1ms >1ms >2ms 99.99 lat <=
    100/0 444000 93% 7% 2% 300
    70/30 188000 86% 14% 4% 2000
    50/50 107000 75% 25% 3% 1800
    30/70 73000 85% 15% 0% 1200
    0/100 97000 74% 26% 17% 2500

    Выводы


    • RocksDB «c упаковкой» не проходит тесты по latency
    • Aerospike c репликацией почти удовлетворяет критериям
    • Простейшая реализация прямой адресации соответствует всем критериям по производительности и задержкам.

    Постскриптум. Ограничения


    Отметим важные параметры, которые остались за рамками данного исследования:

    • Файловая система (ФС) и её настройки
    • Настройки виртуальной памяти
    • Настройки БД.

    1. ФС и её настройки


    • Есть мнение, что для RocksDB, например, хорошо подходит F2FS или XFS
    • Также выбор ФС зависит от того, какие накопители используются в системе
    • Размер страницы (блока) ФС

    2. Настройки виртуальной памяти


    • Можно почитать тут.

    3. Настройки БД


    • MDBX. От разработчика этого движка была получена рекомендация попробовать другой размер страниц в движке: 2 килобайта или размер кластера в NVME. В целом этот параметр важен и у других БД (даже в Aerospike).
    • WiredTiger. Кроме параметра cache_size, который мы меняли, есть ещё масса параметров. Читать тут.
    • RocksDB.
      • Можно попробовать Direct IO: т.е. общение с диском, минуя page cache. Но тогда, скорее всего, надо будет увеличить block cache, встроенный в RocksDB. Об этом тут.
      • Есть информация о том, как настроить RocksDB для NVME. Тут, кстати, используется XFS.
    • Aerospike. Также имеет много настроек, но их тюнинг за несколько часов не принёс ощутимых результатов.
    RAIDIX
    0,00
    Компания
    Поделиться публикацией

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

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

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


          Кроме этого, еще в 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. Для сравнения см. мои скрипты и результаты тестирования производительности.

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

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

              Во-первых, если каждую запись коммитеть в отдельной транзакции, то конечно будет жутко медленно. Заливать данные нужно с разумным батчингом, где-то по 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 будет еще достаточно долго жужжать диском после окончания загрузки (и все это время достаточно сильно тормозить со случайным чтением/обновлением).

                0
                Ну конечнно не по одной записи. Там есть ограничения, кажется до 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? Если устойчивость к крашам не нужна (в данном конкретном случае — при начальной заливке).
                  0

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

          0
          Ну конечнно не по одной записи. Там есть ограничения, кажется до 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? Если устойчивость к крашам не нужна (в данном конкретном случае — при начальной заливке).

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

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

          Самое читаемое