У корпоративных NVMe-накопителей есть неприятная особенность: средняя задержка прекрасная, а хвостовая способна подло подвести в самый ответственный момент. Причина в фоновой работе контроллера, сборке мусора, переносе страниц, освобождении быстрых SLC-областей, которая всегда где-то идёт и время от времени встаёт на пути гостевых запросов. Для банка, биржи или телефонии разница между средними ста микросекундами и хвостовыми десятью миллисекундами это разница между работающей и неработающей системой. До стандартизации каждый вендор решал это по-своему, фирменными режимами низкой задержки в прошивках, и у администратора не было ни единого интерфейса, ни обязанностей накопителя, записанных в спецификации. Спецификация NVMe 1.4 принесла механизм именно под эту задачу: Predictable Latency Mode, где хост договаривается с накопителем об окнах времени и порогах задержки, а контроллер уводит всю тяжёлую фоновую работу за пределы окон и честно отчитывается, если порог всё же пробит.
Разберём, как устроены окна DTWIN и пороги, чем управляет хост через nvme-cli, что происходит внутри контроллера и где у механизма границы применимости.
Механизм PLM в спецификации и его место среди возможностей
Predictable Latency Mode появился в NVMe 1.4 в 2019 году вместе с понятием NVM Sets, групп пространств имён с общими характеристиками носителя. Группировка не формальность: наборы собирают пространства с одинаковым профилем нагрузки, например один набор под транзакционную базу, другой под журнал, и пороги задержки назначаются именно набору, а не каждому пространству отдельно. Поддержка видна в структуре identify контроллера: бит Predictable Latency в поле CTRATT, пятый по счёту, рядом с битами NVM Sets и Endurance Groups. Механизм управляется парой признаков с номерами 0x13 и 0x14: первый настраивает конфигурацию окон и порогов, второй запускает новый период определения окон.
Суть контракта простая. Хост делит пространства имён на группы, каждой группе назначает длину окна DTWIN и пороги задержки чтения RWL и записи WWL в микросекундах. Контроллер обязуется выносить накопительную фоновую работу за границы окон, чтобы внутри окна задержки гостевых команд держались в пределах порогов. Если порог пробит, контроллер фиксирует событие DWE, DTWIN Exceeded, поднимает асинхронное уведомление и складывает статистику в агрегированный журнал, откуда хост вычитывает, какая группа и насколько промахнулась. Это договор с измеримой ответственностью, а не тихое обещание прошивки. Список потребителей у него длинный: торговые системы, базы с жёсткими обещаниями по хвосту, диски виртуальных машин с гарантированной задержкой, массивы, где накопители делят между классами нагрузки и каждому классу нужен свой режим.
Окна, пороги и журналы событий
Единица времени здесь окно детерминированного порога, DTWIN, детерминированное пороговое окно, длина которого задаётся хостом. Внутри окна контроллер следит за фактическими задержками чтений и записей, а также за объёмом выполненных в окне операций. Журнал Predictable Latency per NVM Set с идентификатором 0x0B возвращает по каждой группе набор показателей: идентификатор набора, состояние, число окон в периоде, счётчики чтений и записей внутри окон, максимальную, минимальную и типичную длительность окон, установленные пороги и накопленное число событий DWE. Агрегированный журнал событий с идентификатором 0x0A собирает сведения о промахах по группам, и именно его наличие видно в асинхронных уведомлениях. Само уведомление через AER полезно трактовать как сигнал к разбору, а не как приговор: единичный промах после обновления прошивки обычно говорит о новой фоновой активности, серия промахов в одни и те же часы указывает на пересечение с пиками нагрузки, а постоянный рост числа DWE на группе сигналит, что порог выставлен без учёта реальности и его стоит пересчитать по свежим гистограммам.
Показатели длительности окон читаются как кольцевой портрет поведения: максимальная длительность показывает худшее окно периода, минимальная лучшее, типичная отражает середину, а число окон говорит, сколько итераций контроллер успел отработать. Такая структура даёт то, чего не хватает обычному SMART: не общую статистику за жизнь, а разрез по окнам и группам. Если приложение живёт циклами по сто миллисекунд, окна длиной в сто миллисекунд покажут, в какие именно циклы накопитель выбился из контракта, и сводят вопрос прогрессирующей деградации к конкретным временным отрезкам.
Настройка окон и порогов через nvme-cli
Весь путь делается стандартной утилитой. Проверка поддержки, чтение текущей конфигурации, включение режима с буфером настроек и чтение журнала выглядят так:
nvme id-ctrl -H /dev/nvme0 | grep -iE 'ctratt|predict'
nvme get-feature /dev/nvme0 -f 0x13 -H
nvme set-feature /dev/nvme0 -f 0x13 -v 1 --data=/tmp/plm.bin
nvme get-feature /dev/nvme0 -f 0x14 -H
nvme predict-latency-log /dev/nvme0
Значение -v 1 включает режим, а буфер plm.bin несёт записи по группам. Каждая запись занимает двадцать байт: два байта на идентификатор NVM Set, два резервных, затем пороги чтения и записи по четыре байта в микросекундах и по четыре байта счётчиков ожидаемых чтений и записей в окне. Собрать такой буфер можно хоть в python за минуту, и это одна из причин, почему механизм пригоден к эксплуатации, а не только к демонстрациям:
import struct
def group(nvmsetid, rwl_us, wwl_us, dtwin_reads, dtwin_writes):
return struct.pack("<HHIIII", nvmsetid, 0, rwl_us, wwl_us,
dtwin_reads, dtwin_writes)
buf = group(1, 150, 300, 4000, 2000)
open("plm.bin", "wb").write(buf)
Здесь первой группе с набором один назначены пороги в 150 микросекунд на чтение и 300 на запись. Признак 0x14 запускает новый период определения окон и сбрасывает накопленную статистику, что удобно после изменения порогов или перегруппировки наборов.
Практический совет по выбору порогов: брать их из собственных замеров хвоста, например из гистограмм p99.9, полученных на эталонной нагрузке, а не из паспортных значений вендора, потому что порог имеет смысл только относительно реального приложения.
Что происходит внутри окна и за его границами
Контракту есть физическая основа. Флеш-накопитель постоянно выполняет внутреннюю работу: сборку мусора на переполненных блоках, компоновку страниц, перенос данных из SLC-кэша в основной TLC-слой, обслуживание таблиц трансляции. Всё это конфликтует с гостевыми запросами за каналы, параллелизм чипов и буферы, и в момент такого конфликта растёт хвост задержки. PLM не убирает работу, он предписывает её расписание: контроллер планирует сборку и переносы в промежутках между окнами, а внутри окон удерживает фоновую активность на уровне, не мешающем порогам. Для этого контроллеру нужна запасная ёмкость и запас по производительности, поэтому механизм реалистично работает на накопителях с недоиспользованным резервом, а не на заполненных под завязку. Влияют и побочные эффекты классические: рост усиления записи на заполненном носителе, испарение быстрых областей кэша под длительной записью, выгорание параллелизма на старых блоках, и всё это PLM только делает видимым, но не лечит.
Хост со своей стороны тоже обязан понимать ритм. Если приложение нагружает накопитель непрерывно, свободных промежутков для фоновой работы не останется, и либо пороги начнут пробиваться, либо контроллер накопит долг по обслуживанию. Разумная схема чередует окна нагрузки и паузы, и тут механизм неожиданно хорошо сочетается с планированием приложений: батчи обработки и паузы между ними дают контроллеру предсказуемые дыры для сборки мусора. Отдельный пласт работы, который контроллер выносит за окна, это обслуживание энергозависимого кэша: сброс буферов записи в основной носитель должен случаться до начала ответственного окна, иначе потеря питания в середине окна уронит и задержку, и сохранность данных. Прошивки корпоративных моделей выстраивают такой сброс по собственным таймерам, и граница окна DTWIN даёт им публично наблюдаемую черту.
Связь с режимами питания и другими механизмами
PLM живёт в одном ряду с прочими средствами управляемости и требует от них согласованности. Автономные переходы питания APST способны уводить контроллер в глубокие состояния с долгим выходом, и на накопителе, ответственном за детерминированную задержку, их либо ограничивают через признак 0x0C, либо подбирают состояния с коротким выходом, потому что пробуждение из глубокого сна разрушит окно не хуже фоновой сборки. Механизм уровней восстановления чтения RRL ограничивает скорость чтения с проблемных областей и тоже влияет на хвост, так что его настройка идёт в одном пакете с порогами. Сюда же примыкает Host Memory Buffer с признаком 0x0d, отдающий контроллеру часть оперативной памяти хоста: снятая с накопителя буферизация и таблицы трансляции тоже работают на предсказуемость, уменьшая разброс задержек внутри окон. Кэш на стороне хоста и очередь прерываний продолжают обычную работу, но журналы PLM стоит связать с телеметрией узла, чтобы события DWE попадали в те же графики, что и задержки приложений. В ядре подписка на асинхронные уведомления устроена автоматически, событие всплывает в журнале системы, и остаётся лишь привязать обработку к реакции, например к временной замене накопителя из пула.
Свежее развитие идеи живёт в NVMe 2.0 под именем FDP, Flexible Data Placement, признак с номером 0x1d и журнал конфигураций с номером 0x20: вместо расписания фоновых работ хост подсказывает контроллеру, куда класть данные, чтобы сборка мусора вообще не пересекалась с горячими областями. Оба подхода решают одну задачу с разных сторон, и на новых накопителях они дополняют друг друга.
Практика внедрения на ферме
Внедрение детерминированных окон сводится к последовательности:
- проверить поддержку через бит CTRATT в выводе identify и перечень признаков 0x13 и 0x14;
- сгруппировать пространства имён по рабочим нагрузкам, чтобы пороги были осмысленными на группу;
- замерить фактические гистограммы задержек на эталонной нагрузке и выписать цели по p99.9;
- задать окна под цикл приложения и пороги с запасом от измеренных целей;
- связать чтение журнала predict-latency-log и событий DWE с общим мониторингом узла;
- пересматривать конфигурацию после обновления прошивок и изменений профиля нагрузки, потому что оптимальные окна переезжают вместе с нагрузкой.
Ключевой пункт здесь четвёртый: без измеренной гистограммы пороги превращаются в гадание, а контракт с накопителем работает только тогда, когда обе стороны понимают, о чём договорились. Для измерений годится стандартный fio с включёнными перцентилями задержек:
fio --name=lat --rw=randread --bs=4k --iodepth=1 --runtime=60 --time_based --lat_percentiles=1 --output=lat.json --output-format=json
В многопользовательских схемах приём тот же, но порогов больше: каждому классу обслуживания своя группа со своими окнами, и накопитель распределяет фоновую работу между ними по расписанию хоста.
Ограничения и подводные камни
Первое ограничение очевидное: механизм ничего не ускоряет, он лишь дисциплинирует расписание фоновой работы и делает деградацию видимой. Если ёмкость заполнена, а нагрузка непрерывна, накопителю попросту некуда девать сборку мусора, и пороги будут пробиваться независимо от настроек. Второе, поддержка у вендоров неровная, и перечень корпоративных моделей с полноценным PLM стоит проверять по документации конкретной прошивки, потому что встречаются устройства, где поддержка заявлена, но журналы беднее спецификации. Третье, конфигурация хранится на стороне контроллера, и после замены накопителя или сброса её нужно восстанавливать, что в автоматизации фермы решается включением настройки в процедуру провижининга.
Отдельно стоит помнить, что спецификация продолжает развиваться, свежие редакции добавляют журналы и признаки, но базовый контракт PLM с окнами и порогами остался прежним, и инвестиция в его настройку переносится между поколениями накопителей. Итог у механизма такой: Predictable Latency Mode превращает хвостовую задержку накопителя из загадочной случайности в измеряемый контракт с журналом нарушений. Он не заменяет ёмкостный резерв и нормальное проектирование нагрузки, но даёт то, чего в хранилищах всегда не хватало, ответственность по времени: контроллер знает, когда можно заниматься своими делами, а хост узнаёт о каждом нарушении договора по факту, а не по догадкам после инцидента.