MAATRIX / Блог / NVMe и очереди: почему один поток не выжимает диск

NVMe и очереди: почему один поток не выжимает диск

MAATRIX

Купили сервер с NVMe, в характеристиках производитель обещает несколько гигабайт в секунду, а простой тест — записать файл дд или прогнать один поток запросов — показывает цифру в разы скромнее. Первая мысль: диск бракованный или сервер обманывает. На самом деле чаще всего диск исправен и работает ровно так, как спроектирован — просто тест меряет не то, для чего NVMe создавался. Разберёмся, почему так происходит и как замерить производительность так, чтобы цифры имели отношение к реальности.

Что такое глубина очереди и почему это не абстрактный термин

Когда приложение отправляет диску запрос на чтение или запись, оно может действовать двумя способами. Первый: отправить запрос, дождаться ответа, отправить следующий. Это и есть очередь глубиной 1 (queue depth = 1, QD1) — в моменте времени диску известно максимум об одном незавершённом запросе. Второй способ: отправить сразу несколько запросов, не дожидаясь ответа на каждый по отдельности, и получать результаты по мере готовности. Это очередь большей глубины — диск в моменте времени "видит" сразу пачку работы и может её планировать.

Разница принципиальная не для приложения, а для контроллера накопителя. При QD1 контроллер простаивает большую часть времени в ожидании: он обработал команду, отправил ответ и ждёт следующую, пока хост-система формирует новый запрос и пока сигнал идёт по шине туда и обратно. При глубокой очереди контроллер почти никогда не простаивает — он держит внутри себя очередь работы и постоянно занят: одни NAND-каналы читают, другие пишут, внутренний планировщик диска балансирует нагрузку между несколькими независимыми линиями памяти одновременно.

Именно вторая ситуация — параллельная обработка множества независимых команд — источник тех впечатляющих цифр пропускной способности, которые производитель указывает в характеристиках накопителя. И именно её проверяют производители при замере "до X ГБ/с" — почти всегда это цифра при большой глубине очереди и нескольких параллельных потоках, а не при последовательной отправке команд одна за другой.

SATA/AHCI против NVMe: разница в архитектуре, а не в маркетинге

Чтобы понять, почему NVMe вообще так спроектирован, полезно вспомнить, откуда он пришёл. Протокол AHCI, на котором работают SATA SSD, разрабатывался в середине 2000-х под вращающиеся жёсткие диски — устройства, для которых параллельная обработка большого числа запросов не давала особого выигрыша, потому что механическая головка физически может обслуживать операции только последовательно. AHCI поддерживает Native Command Queuing (NCQ) — единственную очередь команд глубиной до 32 запросов. Для HDD и даже для SATA SSD этого было достаточно, потому что сама шина SATA и протокол AHCI изначально не рассчитывались на то, что диск сможет параллельно перемалывать тысячи запросов одновременно.

NVMe (Non-Volatile Memory Express) спроектирован с нуля под флеш-память, у которой параллелизм — фундаментальное свойство архитектуры: внутри одного накопителя работают десятки независимых NAND-каналов, каждый из которых может обрабатывать команды независимо от других. Протокол NVMe поддерживает до 65535 очередей ввода-вывода (плюс отдельная административная очередь), и каждая такая очередь может содержать до 65536 ожидающих команд. На практике конкретный контроллер накопителя обычно поддерживает не максимум спецификации, а какое-то практическое число — но даже типичные бюджетные потребительские NVMe спокойно держат десятки одновременных очередей с глубиной в несколько десятков команд каждая, что уже на порядки больше, чем 32 команды в единственной очереди SATA/AHCI.

Таблица ниже показывает разницу на уровне протокола, без привязки к конкретной модели:

ПараметрSATA/AHCI (NCQ)NVMe
Число очередей команд1до 65535
Глубина одной очередидо 32 команддо 65536 команд
Путь команды до контроллерачерез AHCI-контроллер на шине SATAнапрямую по PCIe, минуя лишний уровень трансляции
Прерыванияограниченное число векторовMSI-X, отдельные векторы под очереди

Из этой разницы в архитектуре и растёт разница в заявленной пропускной способности. Дело не в том, что NAND-память в NVMe физически быстрее той же NAND-памяти в SATA SSD — часто это буквально одни и те же чипы флеш-памяти от одного производителя. Дело в том, сколько параллельных операций протокол умеет прогнать через контроллер одновременно.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Как устроены очереди NVMe: submission queue, completion queue, doorbell

На уровне механики NVMe работает через пары очередей в памяти хоста: submission queue (очередь отправки, куда драйвер кладёт команды) и completion queue (очередь завершения, куда контроллер накопителя кладёт результаты). Драйвер операционной системы формирует команду в буфере памяти, пишет её в submission queue и "звонит в дверной звонок" (doorbell register) — специальный регистр на контроллере, сигнализирующий "появилась новая работа в очереди номер N". Контроллер забирает команду, когда готов, обрабатывает её на своём внутреннем уровне (обращение к нужному NAND-каналу, чтение или запись страницы флеш-памяти, ECC-коррекция при необходимости) и, закончив, кладёт результат в completion queue, сигнализируя прерыванием.

Ключевой момент: пока контроллер обрабатывает одну команду из очереди, драйвер может продолжать класть в ту же (или в другую) submission queue следующие команды — они просто ждут своей очереди на обработку. Контроллер решает сам, в каком порядке и с использованием каких внутренних ресурсов их выполнять, и может честно делать это параллельно, распределяя работу между независимыми NAND-каналами. Именно эта возможность держать "в полёте" сразу много команд, ожидающих обработки, и называется глубиной очереди.

Современные NVMe-накопители дополнительно используют несколько очередей одновременно — обычно операционная система создаёт отдельную пару submission/completion очередей на каждое процессорное ядро (или на группу ядер), чтобы разные ядра CPU могли отправлять команды диску без блокировок друг на друга. Это ещё один уровень параллелизма поверх глубины отдельной очереди — множество очередей, работающих одновременно, каждая из которых может нести свою пачку команд.

Почему один поток с глубиной очереди 1 не может раскрыть диск

Теперь возвращаемся к исходному вопросу. Если тест устроен так: приложение отправляет одну команду записи, ждёт подтверждения от диска, потом отправляет следующую — это и есть QD1 в один поток. В каждый момент времени у контроллера накопителя есть ровно одна команда для обработки. Как только он её выполнил и отправил результат, он простаивает, пока не придёт следующая — а следующая придёт только после того, как ответ дойдёт до приложения, приложение обработает его и решит отправить новую команду.

Это время ожидания — задержка одной отдельной операции (latency) — у современных NVMe действительно низкое, обычно десятки-сотни микросекунд для чтения из кэша или сотни микросекунд — единицы миллисекунд для операций с флеш-памятью напрямую (точные цифры зависят от модели и не будем их выдумывать — у конкретного накопителя они свои, смотрите характеристики или мерьте сами). Но при последовательном выполнении множества таких операций одна за другой пропускная способность математически ограничена этой самой задержкой: если одна операция занимает условно 0.1 миллисекунды и вы выполняете их строго последовательно, то за секунду пройдёт максимум 10 000 таких операций — независимо от того, что диск теоретически способен обработать в 50 раз больше запросов, если бы они пришли одновременно.

Грубо это можно описать законом Литтла из теории очередей: пропускная способность примерно равна глубине очереди, делённой на среднюю задержку одной операции. При глубине очереди 1 пропускная способность жёстко привязана к задержке одной операции и не может вырасти, сколько бы параллельных NAND-каналов ни было внутри диска физически. При глубине очереди 32 (то есть 32 запроса "в полёте" одновременно) та же самая задержка одной операции даёт теоретически в 32 раза больше пропускной способности — потому что пока одна команда обрабатывается, ещё 31 уже стоят в очереди и не ждут, когда предыдущая освободит место.

Отсюда практический вывод: паспортная цифра "до 7000 МБ/с" на коробке диска — это почти всегда результат замера при большой глубине очереди и нескольких параллельных потоках. Замер тем же диском в один поток с QD1 покажет цифру, которая отражает задержку одной операции, а не совокупную пропускную способность параллельной архитектуры. Диск не сломан и не обманывает — он просто работает в режиме, где его главное архитектурное преимущество физически не может проявиться.

Латентность и пропускная способность — это разные метрики

Путаница между этими двумя метриками — частая причина разочарования при бенчмарках. Латентность (задержка) — это время выполнения одной отдельной операции, от отправки команды до получения ответа. Пропускная способность (throughput) — это суммарный объём данных или число операций в секунду при определённом уровне параллелизма. Диск с низкой латентностью и диск с высокой пропускной способностью — не всегда одно и то же устройство, хотя у современных NVMe обычно хороши обе характеристики.

При QD1 вы измеряете практически чистую латентность — насколько быстро диск отвечает на одну команду без конкуренции с другими операциями. При высокой глубине очереди вы измеряете пропускную способность в условиях, приближенных к реальной многопользовательской или многопоточной нагрузке. Обе цифры полезны, но они отвечают на разные вопросы. Если вас интересует "насколько быстро выполнится один синхронный запрос" (например, при зачем важна скорость диска для баз данных в контексте одиночной синхронной транзакции) — смотрите на латентность при низкой очереди. Если вас интересует "сколько запросов в секунду выдержит диск под реальной нагрузкой множества клиентов" — нужна пропускная способность при глубине очереди, соответствующей вашей нагрузке.

Кстати, тот же корень путаницы часто встречается и в вопросе что такое IOPS на самом деле и почему цифра недостижима — заявленные IOPS почти всегда посчитаны при определённой (обычно немаленькой) глубине очереди, и без этого контекста цифра сама по себе мало что говорит о вашем конкретном сценарии.

Как правильно замерить производительность под свою нагрузку

Главный практический вывод простой: чтобы получить осмысленную оценку производительности диска под вашу задачу, тестировать нужно с параметрами глубины очереди и числа параллельных потоков, которые соответствуют вашей реальной прикладной нагрузке — а не паспортным максимумам и не первому попавшемуся дефолту тестового инструмента.

Инструмент fio (Flexible I/O Tester) — стандартный выбор для таких замеров на Linux, потому что он даёт явный контроль над обоими параметрами через ключи --iodepth (глубина очереди) и --numjobs (число параллельных потоков/заданий). Пример job-файла, эмулирующего однопоточную последовательную запись логов — паттерн, близкий к QD1:

[log-write-qd1]
ioengine=libaio
rw=write
bs=4k
direct=1
iodepth=1
numjobs=1
size=1G
runtime=30
time_based

И пример, эмулирующий нагрузку СУБД с множеством параллельных соединений — множество независимых клиентов одновременно читают и пишут небольшими блоками:

[db-like-random-rw]
ioengine=libaio
rw=randrw
rwmixread=70
bs=8k
direct=1
iodepth=32
numjobs=8
size=4G
runtime=60
time_based

Обратите внимание на direct=1 — это отключает кэширование страниц операционной системы и заставляет запросы идти напрямую к диску, иначе вы измерите скорость оперативной памяти, а не накопителя. Ключевой момент: два этих теста на одном и том же физическом диске дадут совершенно разные цифры, и оба будут "правдой" — просто про разные сценарии использования.

Логика подбора параметров под свою нагрузку такая:

  • СУБД с пулом соединений (PostgreSQL, MySQL с десятками активных клиентов) — естественный параллелизм, тестируйте с iodepth от 16 до 64 и numjobs от 4 до 16, в зависимости от реального числа одновременно активных соединений.
  • Однопоточное приложение с последовательной записью логов или архива — паттерн близкий к QD1, iodepth=1, numjobs=1, и здесь как раз важна именно латентность одной операции, а не пиковая пропускная способность.
  • Веб-сервер, отдающий статику или обрабатывающий много мелких независимых запросов — обычно где-то посередине, iodepth 4-16, numjobs 2-8, но точное число зависит от вашей конкурентности на практике — измеряйте на живом трафике через iostat -x 1 (колонка aqu-sz показывает фактическую среднюю глубину очереди, которую генерирует ваша реальная нагрузка) и подставляйте это значение в fio.

Тестирование не тем паттерном, что даёт реальная нагрузка, обманывает в обе стороны: замер с большой глубиной очереди для по сути однопоточного приложения покажет вам цифру, которую вы никогда не увидите на практике — и вы напрасно понадеетесь на производительность, которой не будет. А замер QD1 для сервиса, который на практике создаёт естественный параллелизм из десятков соединений, наоборот, занизит ожидания и может привести к выбору более дорогого железа, чем реально нужно для задачи.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Означает ли низкая скорость при QD1-тесте, что диск бракован?

Нет, если цифра в пределах разумного для латентности одной операции (обычно доли миллисекунды — единицы миллисекунд для NVMe). Сравните с паспортной латентностью конкретной модели, а не с паспортной пропускной способностью — это разные метрики.

Как узнать, какая глубина очереди у меня на практике на уже работающем сервере?

Запустите iostat -x 1 во время типичной нагрузки и посмотрите на колонку aqu-sz (average queue size) — это и есть фактическая средняя глубина очереди, которую генерирует ваше приложение прямо сейчас.

NVMe в виртуалке (VPS) ведёт себя так же, как физический диск на выделенном сервере?

Архитектура очередей та же на уровне протокола, но в виртуализированной среде добавляется слой гипервизора и возможное разделение диска между соседями по хосту, поэтому даже при правильной глубине очереди цифры на VPS будут менее предсказуемы, чем на выделенном сервере с прямым доступом к NVMe.

Стоит ли гнаться за максимальной iodepth в тестах, раз она даёт цифры побольше?

Нет — цифра, полученная на нереалистичной для вас глубине очереди, не имеет практической ценности и может ввести в заблуждение при планировании мощностей. Ориентируйтесь на измеренную реальную нагрузку, а не на максимум, который выдаёт тестовый инструмент.

Может ли SATA SSD с NCQ показывать похожие цифры на большой глубине очереди?

До определённого предела да, потому что NCQ тоже даёт какой-то параллелизм (до 32 команд), но упирается в пропускную способность самой шины SATA (около 600 МБ/с) и меньшее число внутренних очередей — NVMe масштабируется дальше за счёт PCIe и большего числа независимых очередей.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →