Производительность 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 на голом диске:

  • Перед тестированием отключите кэш записи диска: 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
  • Задержка случайного чтения: 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=randread -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
  • Пиковые IOPS случайной записи: fio -ioengine=libaio -direct=1 -invalidate=1 -name=test -bs=4k -iodepth=128 -rw=randwrite -runtime=60 -filename=/dev/sdX

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

Как тестировать 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 Warning: Сначала плохая новость!

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

При этом оптимизацией транзакций там, похоже, вообще никто нормально не занимался (все используют хорошие серверные SSD, которые болт клали на fsync) и поэтому при работе OSD sync-ов не то что столько же, сколько запросов записи — их даже БОЛЬШЕ (смешно, да). В таких условиях «deferred» операции превращаются не в deferred, а непонятно во что, поэтому понятия «асинхронности» там фактически никакого нет.

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

Note.svg Старый FileStore на плохих дисках быстрее BlueStore где-то в 1.5 раза; хорошие он лучше утилизирует в один поток, то есть, у него меньше задержки. Но это не значит, что его надо использовать — по остальным параметрам FileStore устарел. Например, снапшоты там работают со скоростью снапшотов LVM (очень медленно), нет частичной перезаписи объектов в EC-пулах, нет контрольных сумм (вследствие чего вообще нельзя использовать size=2). Также CPU filestore жрёт сильнее и пиковые параллельные IOPS-ы у него хуже bluestore.

В общем, чтобы понять, сколько у вас будет IOPS-ов на запись в Ceph, диски под него нужно тестировать с опцией fio fdatasync=1. Либо можно fsync=1, но в файле поверх ФС будут различия, либо можно sync=1 iodepth=1 — почти то же самое, но чуть с другого бока. Разница между опциями (см. man fio):

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

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

Нас спасёт такое чудо инженерной мысли, как 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 гб).

Контроллеры

  • 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: отключение экономии энергии процессором:
    • 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
  • Также жор 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 — энергонезависимый кэш случайной записи прямо на пластинах. Включается он только при отключении обычного энергозависимого кэша. Действует, естественно, временно — когда он кончится, производительность случайной записи опять упадёт. Однако плюс в том, что в 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