Производительность Ceph

Материал из YourcmcWiki
Перейти к: навигация, поиск

Бенчмаркинг

Основные направления тестирования:

  • Линейное чтение/запись (большими блоками)
  • Пиковая производительность высоко-параллельного случайного чтения/записи мелкими блоками
  • Задержка однопоточного случайного чтения мелкими блоками (4-8 Кб)
  • Задержка однопоточной транзакционной записи мелкими блоками (4-8 Кб) — обычно последовательной, как в журнал СУБД, но в один поток это обычно слабо отличается от случайной

Задержки обычно важнее простой пиковой производительности случайного чтения/записи, так как далеко не каждое приложение может загрузить диск при большом параллелизме / глубокой очереди (32-128 запросов).

Ceph — это SDS, его задержки всегда выше, чем у устройств при прямом доступе, и от этого никуда не денешься. В интернете есть доклад Nick Fisk «Low-Latency Ceph», в его исполнении Low latency это 0.7ms, то есть на лучший результат рассчитывать особенно не приходится. 0.7ms — это всего лишь примерно ~1500 iops в 1 поток (хорошая новость — другие SDS и просто SAN-ы тоже тормозят :)).

Тестирование дисков

Сначала прогоните fio на голом диске:

Warning icon.svg ВНИМАНИЕ! Для тех, кто в танке — fio-тест записи на диск ДЕСТРУКТИВНЫЙ. Не вздумайте запускать его на дисках/разделах, на которых есть нужные данные… например, журналы OSD (был прецедент).
  • Перед тестированием отключите кэш записи диска: hdparm -W 0 /dev/sdX (SATA-диски через SATA или HBA), sdparm --set WCE=0 /dev/sdX (SAS-диски). Не совсем ясно, почему, но эта операция на серверных SSD может увеличить IOPS-ы на 2 порядка. Также см.ниже #Картина маслом «Тормозящий кэш».
  • Линейное чтение: fio -ioengine=libaio -direct=1 -invalidate=1 -name=test -bs=4M -iodepth=32 -rw=read -runtime=60 -filename=/dev/sdX
  • Линейная запись: fio -ioengine=libaio -direct=1 -invalidate=1 -name=test -bs=4M -iodepth=32 -rw=write -runtime=60 -filename=/dev/sdX
  • Пиковые IOPS случайного чтения: fio -ioengine=libaio -direct=1 -invalidate=1 -name=test -bs=4k -iodepth=128 -rw=randread -runtime=60 -filename=/dev/sdX
  • Задержка случайного чтения: fio -ioengine=libaio -sync=1 -direct=1 -invalidate=1 -name=test -bs=4k -iodepth=1 -rw=randread -runtime=60 -filename=/dev/sdX
  • Пиковые IOPS случайной записи: fio -ioengine=libaio -direct=1 -invalidate=1 -name=test -bs=4k -iodepth=128 -rw=randwrite -runtime=60 -filename=/dev/sdX
  • Задержка записи в журнал: fio -ioengine=libaio -sync=1 -direct=1 -invalidate=1 -name=test -bs=4k -iodepth=1 -rw=write -runtime=60 -filename=/dev/sdX
  • Задержка случайной записи: fio -ioengine=libaio -sync=1 -direct=1 -invalidate=1 -name=test -bs=4k -iodepth=1 -rw=randwrite -runtime=60 -filename=/dev/sdX

«А почему так мало…» — см.ниже.

Тестирование кластера Ceph

Как тестировать Ceph после сборки:

  • rados bench лучше не использовать — он создаёт для тестирования очень мало объектов (в 1 поток всего 2, в 128 — несколько сотен). Из-за этого, например, на заполненном HDD результаты будут сильно оптимистичнее, так как снимается необходимость постоянного поиска метаданных в RocksDB.
  • fio в RBD: fio -ioengine=rbd -direct=1 -invalidate=1 -name=test -bs=4k -iodepth=128 -rw=randwrite -pool=rpool_hdd -runtime=60 -rbdname=testimg
    • лучше запускать с другого узла — результат будет в 1.5 раза лучше, видимо, из-за отсутствия переключения контекстов между ceph OSD и fio. можно запускать два теста параллельно, bluestore любит параллелизм — суммарный результат вполне может оказаться ещё в 2 раза лучше.
    • для мазохистов — параметр iodepth=128 поменять на iodepth=1. цифра будет раз в 20 хуже и она будет отражать то, сколько примерно TPS сможет выполнить ваша OLTP СУБД изнутри Ceph.
  • Встроенной утилитой rbd bench --io-size 4096 --io-threads 64 --io-total 10G --io-pattern rand --io-type write rpool_hdd/testimg
  • Можно тестировать и fio изнутри виртуалки, rbd драйвер нормально создаёт параллельную нагрузку — проверено.
  • Производительность может отличаться на заполненном и незаполненном RBD-образе. Но отличия небольшие, думать, что там будет разница в несколько раз — не нужно.
  • При тестировании случайной записи в ceph в один поток (fsync/fdatasync/sync/iodepth=1/rados bench -t 1) вы фактически всё время тестируете ОДИН диск. То есть, всё время тестируются разные диски, но в каждый отдельный момент времени запрос идёт только к одной placement group (тройке-четвёрке-пятёрке дисков).
  • Соответственно, вы не увидите 100 % утилизации дисков на хостах при тестировании в один поток, однопоточная нагрузка не может полностью загрузить кластер.

Как тестировать производительность отдельных OSD:

  • Создать pool без репликации ceph osd pool create r1pool 128 replicated; ceph osd pool r1pool set size 1; ceph osd pool r1pool set min_size 1 и с числом PG, достаточным, чтобы при случайном выборе туда попали все OSD
  • Воспользоваться бенчилкой Марка https://github.com/socketpair/ceph-bench - команда вида: python main.py --keyring /etc/ceph/ceph.client.admin.keyring r1pool osd
  • Полученный результат, в частности, может помочь выявить отдельную тупящую OSD

Производительность случайной записи

Warning icon.svg Плохая новость!

Важная особенность Ceph — вся запись, даже та, для которой никто этого явно не просит, ведётся транзакционно. То есть, никакая операция записи не завершается, пока она не записана в журналы всех OSD и не сделан fsync() диска. Так сделано, чтобы предотвращать RAID WRITE HOLE-подобные ситуации рассинхронизации данных между репликами при отключении питания, потере сети и т.п…

Это приводит к тому, что типичная настольная SSD под журналом в Ceph выдаёт неприлично низкие IOPS-ы — обычно от 500 до 2000. И это при том, что при обычном тестировании почти любая SSD выдаёт > 20000 iops. Даже самый паршивый китайский noname выдаёт не менее 10000 iops. NVMe легко выжимает 150000 и больше. Но стоит начать использовать fsync… и та же NVMe выдаёт 600 iops (на 2.5 порядка меньше).

В общем, чтобы понять, сколько у вас теоретически может быть IOPS-ов на запись в Ceph, диски под него нужно тестировать с опциями fio sync=1 iodepth=1. Это даст "журнальные" иопсы (производительность последовательного коммита операций по одной).

Другие почти идентичные варианты: fdatasync=1 (в файле поверх ФС) либо fsync=1 (на голом девайсе). Разница между опциями:

  • fsync=1 синхронизирует данные и метаданные тестируемого файла отдельным запросом после каждой операции записи. Так работает BlueStore.
  • fdatasync=1 синхронизирует только данные (но не метаданные) тестируемого файла после каждой операции записи. Соответственно, от fsync=1 это отличается, только если тестируется файл в ФС, а не блочное устройство.
    Note.svg fdatasync=1 надо использовать, когда на диске уже есть ФС, а прогнать тест хочется. Результаты будут достаточно корректными.
  • sync=1 использует O_SYNC и синхронный ввод/вывод, то есть, каждая операция начинается только после завершения предыдущей. Так работает FileStore.
    Но ещё нужна опция iodepth=1, иначе в очередь диска до синхронизации «пролезает» несколько операций и IOPS-ы растут, тест перестаёт быть тестом журнала.

Конденсаторы!

Нас спасёт такое чудо инженерной мысли, как SSD с конденсаторами (точнее, обычно суперконденсаторами — ионисторами). Которые на M.2 SSD, кстати, прекрасно видны невооружённым глазом:

Micron 5100 sata m2.jpg

Конденсаторы работают фактически как встроенный в SSD ИБП и позволяют SSD успеть сбросить кэш во флеш-память при потере питания. Таким образом кэш становится «энергонезависимым» — и таким образом SSD может просто игнорировать запросы fsync, так как точно знает, что данные из кэша в любом случае доедут до постоянной памяти.

При этом IOPS-ы транзакционной записи становятся равны IOPS-ам нетранзакционной.

Конденсаторы в официальных описаниях SSD-шек обычно называются «enhanced/advanced power loss protection». Этой характеристикой обладают, как правило, только «серверные» SSD, да и то не все. Например, в Intel DC S3100 конденсаторов нет, а в Intel DC S4600 есть.

Note.svg Это и является главным отличием серверных SSD от настольных. Обычному пользователю транзакции нужны редко — а вот на серверах живут СУБД, которым транзакции как раз нужны позарез.

То есть, под Ceph следует закупать только SSD с конденсаторами. Даже если рассматривать NVMe — NVMe без конденсаторов хуже, чем SATA с оными.

И ещё один вариант — Intel Optane. Это тоже SSD, но они основаны не на Flash памяти (не NAND и не NOR), а вообще на другой технологии, называющейся 3D XPoint. Хз, как она работает, но заявляются 550000 iops при полном отсутствии необходимости в стирании блоков, кэше и конденсаторах. Но а) в применении к Ceph — нужно проверять — не факт, что Ceph вообще сможет выжать из них их iops-ы б) вариант дорогой, раза в 3 дороже типичной SSD (1500$ за 960 гб, 500$ за 240 гб).

BlueStore vs FileStore

Хоть BlueStore и считается «новым» бэкендом хранилища, есть очень популярное мнение, что он тормозит и поэтому не нужен.

После долгого ковыряния в проблеме разъясняю: нет, BlueStore не тормозит «в целом». В частности:

  • BlueStore в 2 раза быстрее FileStore в линейной записи (можно считать, что всегда), так как в нём крупные блоки пишутся 1 раз — сразу на устройство, а не 2 (в журнал и потом на устройство).
  • BlueStore примерно равен FileStore по производительности случайной записи и latency в All-Flash кластерах. Пиковая производительность при этом обычно немного выше, однопоточная — немного ниже. Жор CPU тоже немного меньше. Всё это варьируется, но по крайней мере блюстор здесь не хуже.

ОДНАКО! BlueStore без дополнительных ухищрений действительно ХУЖЕ FileStore в популярной конфигурации HDD + журнал на SSD:

  • Условно, собрав небольшой кластер на BlueStore, в 1 поток на HDD+SSD вы получите ~100 иопс на запись, а в FileStore ~500.
  • Почему? Потому, что BlueStore очень часто делает сброс данных из очереди отложенной записи на HDD (каждые 32 операции, deferred_batch_ops) и при сбросе тормозит все последующие операции.
  • В итоге случайная запись ждёт окончания записи не только на SSD, а также и на HDD.
  • Ждёт оно так не каждую операцию, поэтому iops-ы лучше, чем просто на HDD. Но всё-таки ждёт, поэтому iops-ы хуже, чем в FileStore.

Как это полечить?

  • Либо вместо журнала сделать из SSD bcache поверх HDD.
  • Либо использовать HDD с Media Cache или аналогом (перманентным кэшем случайной записи на пластинах). Например, в дисках HGST это включается при отключении волатильного кэша командой `hdparm -W 0 /dev/sdXX`.

Также BlueStore делает огромное количество fsync-ов (что очень смешно — даже больше, чем запросов записи), из-за чего не терпит десктопных SSD под журналом. Но FileStore работает похоже, и кардинальной разницы производительности это не вносит.

Ну и ещё FileStore отстаёт от BlueStore по функциональности:

  • Снапшоты там работают со скоростью LVM, то есть, при записи 4 кб после снятия снапшота копируется весь 4 Мб объект. То есть, после снятия снапшота RBD ВМ тормозят.
  • Плюс отсутствуют некоторые другие фичи BlueStore: нет частичной перезаписи в EC-пулах (соответственно, EC нельзя использовать под CephFS и RBD), нет сжатия (хотя оно и не особо нужно), нет контрольных сумм (а вот они полезны, в частности, из-за их отсутствия нельзя использовать size=2).

Контроллеры

  • SATA — это нормально, SAS не обязателен от слова «совсем». SATA за счёт того, что «не умничает», достаточно быстрая и точно лучше, чем старые RAID контроллеры.
  • Разница в IOPS между RAID и HBA/SATA может быть колоссальна. В производительность не самого нового RAID контроллера легко упереться. Плохо даже не то, что на 1 диск вы получите 48000 iops вместо 60000, хуже то, что при подключении 8 дисков вы получите 6000 iops на каждый диск вместо 60000, так как 48000 поделятся на всех. Также в RAID режиме увеличивается задержка в 1 поток.
  • Так что свой RAID контроллер либо переключите в режим passthrough (если он умеет), либо перепрошейте, чтобы умел, либо выкиньте в помойку и купите HBA («RAID без RAID-функционала», например, LSI 9300-8i). Это актуально для всех видов программных хранилок — Ceph, ZFS и т. п.
  • Если не выкинули RAID — отключайте все кэши контроллера, чтобы уменьшить влияние прослойки и не страдать при разряде батарейки / перемещении диска в другой сервер. Наверное, в теории можно выжить и с включенным кэшем, но это стрельба себе в ногу.
  • У HBA тоже есть предел IOPS. К примеру, у LSI 9211-8i это ~280000 iops на весь контроллер.
  • При подключении через SATA или HBA контроллер не забывайте для SATA дисков сделать hdparm -W 0 /dev/sdX, для SAS — sdparm --set WCE=0 /dev/sdX.
  • Для SAS и NVMe включайте blk-mq (ну или юзайте свежие ядра, в районе 4.18 оно включается по умолчанию). Но для SATA blk-mq обычно бесполезен или почти бесполезен.
  • Фактическая глубина очереди, используемая Ceph OSD при случайной записи, редко больше 10 (посмотреть можно при работе утилитой iostat -xmt 1).

Процессоры

  • На SSD Ceph ОЧЕНЬ СИЛЬНО упирается в процессор. Можно сказать, что процессор — основной bottleneck.
  • Как сказано в презентации Ника Фиска — Ceph is a Software-Defined Storage and every piece of Ceph «Software» will run faster with every GHz of CPU frequency.
  • Кроме частоты, на серверных процессорах часто наличествует NUMA (Non-Uniform Memory Access). То есть, часть памяти и оборудования доступна процессору напрямую, а часть — только через другой процессор.
  • Для максимизации производительности конфигураций с NUMA лучше избегать, а процессорам с бОльшим числом ядер и меньшей частотой лучше предпочитать бОльшую частоту и меньшее число ядер…
  • …но в пределах разумного, так как даже один OSD на серверном SSD под нагрузкой может спокойно выжрать на 100 % ядер 6.
  • Под частотой подразумевается номинальная частота, а не Turbo Boost, так как оный актуален только для однопоточных нагрузок.
  • Рекомендации по привязке OSD к отдельным CPU (taskset), можно сказать, неактуальны, так как Ceph OSD сильно многопоточные — при записи постоянно активно как минимум 4 потока, и ограничение их несколькими ядрами сильно урезает производительность.
  • Есть два параметра, которые регулируют число рабочих потоков OSD — osd_op_num_shards и osd_op_num_threads_per_shard…
  • …Но менять их бесполезно, поднять производительность таким образом не получается абсолютно, дефолтные значения (1x5 на HDD и 2x8 на SSD) оптимальны.
  • Есть одна мера, которая помогает поднять производительность сразу раза в 2-3: отключение экономии энергии процессором:
    • cpupower idle-set -D 1 — отключает C-States (либо опции ядра processor.max_cstate=1 intel_idle.max_cstate=0)
    • for i in {0..63}; do cpufreq-set -c $i -g performance; done (вместо 63 подставьте своё число ядер минус 1) — отключает снижение частоты через множитель
  • После этих двух команд процессор начинает греться как ПЕЧ, но iops-ы увеличиваются сразу раза в 2 (а то и 3)
  • Также жор CPU — одна из причин НЕ делать из Ceph «гиперконвергентное облако» (в котором совмещены узлы хранения и запуска виртуальных машин)

Оценка производительности кластера

  • Оценка производительности кластера просто по спецификациям входящих в него SSD не совсем верна (точнее, скорее совсем не верна), причём в обе стороны:
    • Bluestore, видимо, за счёт параллелизма, выдаёт чуть больше iops-ов даже на SSD без конденсаторов, чем просто те же ssd могут выдать с fsync — я лично смог добиться от тестового пула на 3-х Intel SSDSC2KW256G8 8000 iops (случайная запись), хотя сами ssd с fsync выдают примерно 5500
    • И обратно, даже если SSD мегабыстрая, цеф — это огромный оверхед, он жрёт проц и никаких 220000 iops из одной SSD не выжмет. См. ниже #Пример теста от Micron — там в суперкрутом сетапе у них вышло всего 8750 iops в пересчёте на 1 NVMe (но это у них без SPDK/DPDK).
  • Можно считать, что лимит iops-ов в пересчёте на одну SSD/NVMe находится на уровне ~10000-15000.
  • Большой разницы между хорошей SATA SSD и даже NVMe в цефе — нет.
  • Если SATA SSD плохая — разница есть. :) плохим можно считать всё, что даёт меньше 20000 iops в один поток с sync.
  • Лайфхак для ускорения однопоточной нагрузки: mdadm RAID 0 из RBD-образов внутри самой виртуалки — проверено, не работает.
  • Лайфхак для очень быстрых дисков: несколько OSD на одном диске — работает, но ценой сильного увеличения жора CPU.
  • Гипотетический монстр производительности в вакууме: мощные процы, Intel NVMe, сеть 25+ Гбит/с или Infiniband, SPDK/DPDK (SPDK работает, но не даёт прироста, DPDK не работает вообще).

Картина маслом «Тормозящий кэш»

Дано: 3 компа с 3x 7200rpm SATA HDD (в одном 4x HDD, но не суть важно), с 1 SSD (десктопным) под систему и ceph-mon и с 1 SSD (старым, но серверным, 25000 iops) под журналы. Не самая быстрая 10-гигабитная сеть — флуд пингом средний RTT (задержка) 0.098ms. Развёрнут Ceph + OpenNebula с KVM. Диски под Ceph отформатированы в Bluestore утилитой ceph-volume (то есть используется LVM). Диски виртуалок лежат в обычном реплицированном ceph pool с size=3.

Создаём Debian-виртуалку (настройки диска kvm по умолчанию — bus=virtio, cache=none), ставим fio, запускаем в ней тест на задержку транзакционной случайной записи: fio -ioengine=libaio -size=10G -sync=1 -direct=1 -name=test -bs=4k -iodepth=1 -rw=randwrite -runtime=60 -filename=./testfile (или можно не случайной, тогда rw=write, но результат идентичный).

  1. Настройки по умолчанию — все кэши дисков включены (везде hdparm -W 1, в /sys/block/*/queue/write_cache везде write back) — Ж О П А, iops=59, avg lat = 16.88ms
  2. Отключаю кэш записи SSD с журналами: hdparm -W 0 /dev/sdb — остаётся Ж О П А, iops=58, avg lat = 16.99ms
  3. Всем LVM-девайсам отключаю кэш записи: for i in /sys/block/dm-*; do echo write through > $i/queue/write_cache; done` — А Ф И Г Е Т Ь, iops=584, avg lat = 1.7ms
  4. Обратно включаю кэш SSDшке с журналами: hdparm -W 1 /dev/sdb — остаётся iops=582, avg lat = 1.7ms
  5. Откручиваю все отключения кэшей LVM: for i in /sys/block/dm-*; do echo write back > $i/queue/write_cache; done — обратно жопа, 57 iops, avg lat = 17.2ms
  6. Опять отключаю кэш журнальным LVM-девайсам: for i in `ls /dev/ceph-journals/lvol*`; do j=readlink $i; echo write through > /sys/block/${j##../}/queue/write_cache; done — никакого улучшения, всё та же жопа (но с ними это точно безопасно, так как они с конденсаторами :))
  7. Отключаю кэш HDD LVM-разделам (for i in `ls /dev/ceph-*/osd-block*`; do j=readlink $i; echo write through > /sys/block/${j##../}/queue/write_cache; done) — бинго, iops=603, avg lat = 1.65ms
  8. Ага. Простите. Обнаруживаю, что просто писать куда-то write through небезопасно без hdparm -W 0 /dev/sd*, так как https://www.kernel.org/doc/Documentation/block/queue-sysfs.txt - Writing to this file can change the kernels view of the device, but it doesn’t alter the device state. ок, добавляю for i in /dev/sd?; do hdparm -W 0 $i; done (отключаю все кэши) — результат похуже, iops=405, avg lat = 2.47ms — но это всё равно лучше, чем изначальная жопа.

Виртуалку, в которой тестировал — даже не перезапускал между тестами.

Note.svg Мораль: отключайте кэш записи всем устройствам

Разгадка: в жёстких дисках HGST есть Media Cache — энергонезависимый кэш случайной записи прямо на пластинах. Включается он только при отключении обычного энергозависимого кэша. А блюстор при сбросе отложенной записи на диск блокирует последующие операции. Но сбрасывает он их всего лишь по 32 штуки, это очень мало, поэтому блокирует последующие операции он постоянно. Следовательно, когда включается медиакэш, HDD начинает рандомно писать сильно быстрее, и вот эти вот блокировки при сбросе уходят. Действует медиакэш, естественно, временно — когда он кончится, производительность случайной записи опять упадёт. Однако плюс в том, что в Ceph-е этого, скорее всего, не произойдёт, так как скорость случайной записи ограничивается, собственно, самим Ceph-ом и распределяется по всем дискам кластера :). Другие производители эту технику, кстати, уже тоже переняли.

Почему вообще Bluestore такой медленный?

Ведь старались-старались, уходили от двойной записи и журналирования журнала в filestore. И опять то же самое…

Речь о random iops. Вкратце: потому что писано всё кривым **ем.

Все мы знаем, что 1 7200rpm HDD может выдать примерно 100—120 iops. Дальше нам говорят — ну, там типа журналирование.

Ну ок, как мы рассуждаем — ну, типа, есть журнал, есть диск. Значит типа вроде как синхронно записало в журнал, потом асинхронно постепенно перенесло на диск. Значит, берём 100, умножаем на число дисков в кластере, делим на фактор репликации (3), делим на 1.5-2 (данные+журнал), мы же держим в уме, что наверняка там всё асинхронно и оптимизировано… Получаем, скажем, 100 * 9 дисков / 2 / 3 = 150 iops. Запускаем тест на собранном кластере — ОЙ. 30 iops. Как так-то?

А просто всё на самом деле довольно криво устроено.

Журнала собственного нет, вместо него RocksDB. Она как бы keyvalue база, но она же LSM, она же так и работает — типа до лимита пишет в память+журнал, потом когда упирается в лимит — делает compaction по уровням. По сути как бы БД-журнал. Вот они её как журнал и юзают.

В той же RocksDB метаданные, и их дохрена, ибо виртуальные клоны и всякое такое. Плюс чексуммы. Плюс идея, что надо побыстрее закоммитить запрос в минимальном виде и ответить «ок». Плюс два варианта записи — прямой и отложенный.

В итоге алгоритм записи примерно такой:

  1. Пришла транзакция — писнулась в журнал. Sync.
  2. Если запись мелкая, она откладывается и на время «откладывания» записывается в ту же rocksdb в отдельное место. Sync.
  3. Упёрлись в лимит сброса отложенных транзакций (всего лишь 32 шт). Прочитали порцию из rocksdb, смержили, отправили в диск. С*ка опять sync, чтобы понимать, когда уже можно будет обновить метаданные.
  4. Обновляем метаданные. Sync.
  5. Если запись крупная, то она сразу отправляется на диск. Потом Sync.))). и потом тоже обновляем метаданные. Sync.

И ещё всё это приправлено тредами, блокировками… Все мы знаем, что наиболее оптимальный способ написания любых i/o приложений — nginx-подобный — «one thread per core + zero-copy». А тут OSD при старте сразу создаёт ~50 потоков, из которых при записи постоянно активны как минимум 4.

То есть получается, что есть как бы журнал, есть данные и есть метаданные. И ещё если мелкие записи то есть очередь отложенной записи (в той же бд, но отдельная), её сначала надо писать, а потом сбрасывать и очищать. В итоге получается на 1 входящую транзакцию штук 5 реальных транзакций.

Без отложенной записи (на SSD это дефолт) поэтому всё и легче — остаётся только журнал, данные и метаданные — это хотя бы 3 транзакции, а не 5. А когда нет SSD, то казалось бы журнал хоть немного должен помогать, авотх*й, потому что seek-ов то меньше, но зато транзакций не 3, а 5.

Смысл видимо именно в отсутствии собственного журнала. Если бы он был и они работали как традиционная СУБД, то по идее, они могли бы делать примерно так:

  • приняли операцию
  • BEGIN
  • UPDATE метаданные
  • UPDATE данные
  • COMMIT = ОДНА синхронная запись в журнал
  • ответили клиенту «ок»

Ну а потом журнал просто потихоньку чистим. А у них так не получается — у них rocksdb, данные и метаданные в логически разных местах, а журналирование — фактически отдельная транзакция.

Вот простой эксперимент: «кластер» из 2 SSD, по 4 OSD на SSD, чуть подкрученные SSD-настройки (shards=1, threads=1, min_alloc_size=4096, bluestore_sync_submit_transaction=true). Делаю strace одной из 4-х osd на одной из двух SSD во время запуска теста fio в 1 поток на 60 секунд.

root@m2:~# ceph daemon osd.0 perf dump | jq '.osd.op + .osd.subop'
6193
root@m2:~# grep -P 'io_submit\(' osd0-trace.txt |wc -l
12383
root@m2:~# grep -P 'io_submit\(.*iov_len=4096' osd0-trace.txt |wc -l
12382
root@m2:~# grep -P 'pwritev\(' osd0-trace.txt |wc -l
6203
root@m2:~# grep -P 'sync.*\(' osd0-trace.txt |wc -l
24776
root@m2:~# grep -P 'pwritev.*= 4096$' osd0-trace.txt |wc -l
3163
root@m2:~# grep -P 'pwritev.*= 8192$' osd0-trace.txt |wc -l
2991
root@m2:~# grep -P 'pwritev.*= 12288$' osd0-trace.txt |wc -l
49

То есть эта тварь на 6193 запроса записи сделала 18585 записей (суммарно 21674*4к) и 24776 (!!!) синков! (sync_file_range и fdatasync)

Итого Write Amplification с SSD-настройками = 3.5. О чём и шла речь: данные, метаданные, журнал. Но fsync-ов просто море — больше, чем запросов записи :)

Тот же тест на HDD (уже завёрнут в скрипт, отдельные команды не привожу):

ops in writes writes/4kb sync
1183 3020 4926 3664

Итого WA = 4.16 (4926/1183), а sync-ов опять больше, чем запросов записи.

Теоретически 1 запись 4кб блока должна представляться как просто запись 4кб блока + обновление максимум одного сектора в журнале БД (обновление 1 блока в списке блоков объекта всяко не должно занимать больше 512 байт). Если бы так и было — WA было бы 1.125. Однако в Bluestore WA находится на уровне 3-5. Предположительные причины:

  • отсутствие собственного механизма журналирования
  • хранение блоков, занимаемых объектом, не в виде дерева extent-ов (как это обычно делается во всех файловых системах), а в виде длинных списков extent-ов, хранящихся целиком в одном-нескольких ключах в RocksDB

DPDK и SPDK

  • DPDK включается через ms_type=async+dpdk
  • SPDK включается для NVMe-шек передачей в качестве пути девайса spdk:<серийный номер pcie устройства>
  • Это в теории — на практике НИ ХРЕНА не работает, ни DPDK, ни SPDK
    • С DPDK Ceph «из коробки» даже не собирается — это в общем-то довольно легко исправить, но даже когда добиваешься сборки и запуска — OSD падают после обработки ~50 пакетов
    • С SPDK Ceph собирается и даже собран по умолчанию — но оно опять-таки не работает — вскоре после запуска OSD просто виснет в пространстве
    • Code is there, так что, вероятно, всё это можно исправить, если подебажить подольше
  • Однако, похоже, в силу неоптимальной реализации самого сетевого кода Ceph ни от DPDK, ни от RDMA ожидать ускорения не приходится — потому что один чувак недавно отрезал код AsyncMessenger-а от всего остального цефа и попробовал побенчить его отдельно: https://www.spinics.net/lists/ceph-devel/msg43555.html - и получил всего лишь ~80000 iops.

Краткий экскурс в устройство SSD и флеш-памяти

Особенность флеш-памяти (NAND/NOR) заключается в том, что пишется она мелкими блоками (обычно 512 байт), а стирается большими (2-4 мегабайта) — и при этом писать можно только в предварительно стёртую область. Чтение блока при этом быстрое, запись тоже быстрая; стирание же медленное, да ещё и число стираний каждого erase unit-а ограничено — после нескольких тысяч (типичное значение для MLC) блок физически выходит из строя. В более дешёвых и плотных (MLC, TLC, QLC — 2-4 бита на ячейку) чипах лимит стираний меньше, в более дорогих и менее плотных (SLC, один бит на ячейку) — больше. Соответственно, при «тупом» подходе — если при записи каждого блока его просто стирать и перезаписывать — случайная запись во флеш-память, во-первых, будет очень медленной, а во-вторых, она будет быстро выводить её из строя.

Но почему тогда SSD быстрые? А потому, что внутри SSD на самом деле есть очень мощный и умный контроллер (1-2 гигагерца, примерно как процессоры мобильников), и на нём выполняется нечто, называемое Flash Translation Layer — прошивка, которая переназначает каждый мелкий логический сектор в произвольное место диска. FTL всё время поддерживает некоторое количество свободных стёртых блоков и направляет каждую мелкую случайную запись в новое место диска, в заранее стёртую область. Поэтому запись быстрая. Одновременно FTL делает дефрагментацию свободного места и Wear Leveling (распределение износа), направляя запись и перемещая данные так, чтобы все блоки диска стирались примерно одинаковое количество раз. Кроме того, во всех SSD некоторый % реального места зарезервирован под Wear Leveling («overprovision»), а в хороших серверных SSD этот процент весьма большой — например, в Micron 5100 Max это +60 % ёмкости.

Именно из наличия FTL вытекает и проблема с энергонезависимостью и «power loss protection»-ом. Карты отображения секторов — это метаданные, которые при сбросе кэша тоже нужно сбрасывать в постоянную память, и именно этот сброс и вносит торможение в работу настольных SSD с fsync.

Дополнение: когда я попытался кого-то в списке рассылки полечить на тему, что «все SSD делают fsync», мне в ответ кинули статью: https://www.usenix.org/system/files/conference/fast13/fast13-final80.pdf. В общем, суть статьи в том, что в 2013 году нормой было то, что SSD вообще не сбрасывали метаданные на диск при fsync, и при отключении питания это приводило к разным весёлым вещам вплоть до (!!!) полного отказа SSD.

Есть экземпляры старых SSD без конденсаторов (OCZ Vector/Vertex), которые при этом выдают большие iops на запись с fsync. Как это возможно? Неизвестно, но есть предположение, что суть как раз в небезопасности записи. Принцип работы флеш-памяти за последние годы вроде как не менялся — в SSD как раньше был FTL, так и сейчас FTL. Как достигнуть быстрой записи, если постоянно сбрасывать на диск карты трансляции — хз… наверное, если только сделать некое подобие лог-структурированной ФС внутри — писать всё время вперемешку метаданные и данные. Но при этом, по идее, при старте всё это «добро» придётся сканировать и старт/монтирование станет долгим. А в SSD долгого монтирования вроде как нет.

Ну и, собственно, «power loss protection», видимо, бывает простой, а бывает advanced. Простой означает просто «мы корректно делаем fsync и не сдохнем при отключении питания», а advanced означает наличие конденсаторов и быструю безопасную запись с fsync. Сейчас, в 2018—2019 годах, «обычный» PLP, похоже, всё-таки стал нормой и при отключении питания большая часть SSD терять данные и умирать уже не должна.

Бонус: USB-флешки

А почему тогда USB-флешки такие медленные? Случайная запись на флешку 512-байтными (или 4 Кб) блоками обычно идёт со скоростью 2-3 iops. А флеш-память там ровно та же, что в SSD — ну, может, более дешёвые вариации, но разница же не на порядки. Ответ кроется в том, что на флешках тоже есть FTL (и даже Wear Leveling), но по сравнению с SSD-шным он маленький и тупой. У него слабый процессор и мало памяти. Из-за малого объёма RAM контроллеру флешки, в отличие от контроллера SSD, негде хранить полную таблицу сопоставления виртуальных и реальных секторов — поэтому отображаются не сектора, а крупные блоки где-то по мегабайту или больше, а при записи есть лимит на количество «открытых» блоков. Как это происходит:

  • Допустим, вы пишете в сектор X.
  • Контроллер отображает блок, которому принадлежит этот сектор, на реальный блок, и «открывает» его — выделяет пустой блок, запоминает, что он «дочерний» для открытого и записывает туда один изменённый вами сектор.
  • Таким макаром можно открыть максимум N разных блоков; число N обычно очень маленькое — от 3 до 6.
  • Дальше если вы пишете следующий сектор из уже открытого блока — он просто записывается в его дочерний блок (что быстро).
  • Если же следующий записываемый сектор принадлежит другому блоку — какой-то из открытых блоков приходится закрывать и «сливать» содержимое дочернего блока с оригинальным.

Для копирования больших файлов на флешку, отформатированную в любую из стандартных файловых систем, двух блоков достаточно: в один открытый блок пишутся данные, во второй — метаданные записываемого файла. Запись последовательная, всё быстро. А вот при случайной записи вы перестаёте попадать в уже «открытые» блоки и каждая операция записи превращается в полное стирание. Тут-то и начинаются тормоза…

Пример теста от Micron

Пример самолётного сетапа от Micron с процами по полляма (2x Xeon Gold), 100-гбит сетью и 10x топовыми NVMe (с конденсаторами, ага) в каждом узле, 4 узла, репликация 2x: https://www.micron.com/resource-details/30c00464-e089-479c-8469-5ecb02cfe06f

Всего 350000 iops на запись в пике на весь кластер, при 100 % загрузке CPU. Казалось бы, довольно много, но если поделить 350000/40 osd — получится 8750 иопс на 1 osd. С учётом репликации на диски нагрузка двойная, выходит, 17500 иопс. Ок, журналы тоже удваивают нагрузку, итого — 35000 iops на запись смог выжать ceph из одной NVMe… которая сама по спеке может 260000 иопс в одиночку. Вот такой вот overhead.

Данных по задержкам в 1 поток нет (а было бы интересно узнать).

UPD: Поправка: в чём микрон неправ — они не использовали SPDK и DPDK. Есть большая вероятность, что в их случае выигрыш мог быть в несколько раз. Нет, это бессмысленно.

Модели

  • Micron серий 5100/5200
  • HGST SN260
  • Intel P4500

https://docs.google.com/spreadsheets/d/1E9-eXjzsKboiCCX-0u0r5fAjjufLKayaut_FOPxYZjc