MAATRIX / Блог / Почему случайная запись медленнее последовательной даже там, где нет головок

Почему случайная запись медленнее последовательной даже там, где нет головок

MAATRIX

Если спросить любого, кто застал HDD, почему случайная запись медленнее последовательной, ответ будет мгновенным: головка бегает по диску туда-сюда, а не едет ровно по дорожке. Логично, что на SSD, где механики нет вообще, разница должна исчезнуть. Но на практике вы всё равно видите в бенчмарках, что random write заметно проседает относительно sequential write на том же накопителе. Причина не в механике — она в том, как flash-память физически устроена внутри и как контроллер вынужден с этим жить.

Что на самом деле означает «нет головок»

В HDD задержка складывается из двух физических операций: seek (позиционирование головки над нужной дорожкой) и rotational latency (ожидание, пока нужный сектор подъедет под головку под вращением пластины). Случайный паттерн заставляет повторять обе операции на каждую операцию ввода-вывода, и именно поэтому random I/O на вращающихся дисках был на порядки медленнее sequential.

В SSD нет пластин, нет головок, нет вращения — есть чипы NAND-памяти и контроллер, который адресует их электрически. Теоретически доступ к любой ячейке должен стоить одинаково независимо от её адреса, и в этом смысле «головок нет» — чистая правда. Но у флеш-памяти есть собственное физическое ограничение, которое не имеет отношения к механике и тем не менее делает случайную запись дороже последовательной. Это ограничение — асимметрия между чтением, записью и стиранием на уровне самой ячейки.

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

NAND-flash организована в блоки, а блоки — в страницы. Типичная страница — единица, в которую можно писать данные, а блок — это группа страниц (в современных TLC/QLC накопителях блок может включать сотни страниц). Ключевое физическое ограничение простое: записать данные можно только в чистую (стёртую) страницу, а стереть — только весь блок целиком, а не отдельную страницу внутри него.

Это следствие того, как хранится заряд в ячейке. Запись — это добавление заряда через туннелирование электронов на плавающий затвор транзистора, а стирание — это снятие заряда со всех ячеек блока одновременно высоким напряжением. Электрически операция стирания применяется сразу к целой группе ячеек, поэтому гранулярность стирания на порядок крупнее гранулярности записи. Отсюда фундаментальное правило flash-памяти: erase-before-write — прежде чем перезаписать уже занятую страницу новыми данными, блок, в котором она находится, нужно целиком стереть.

Если бы это правило нужно было соблюдать буквально при каждой записи, SSD были бы медленнее HDD. На практике контроллер обходит проблему за счёт того, что почти никогда не пишет поверх старых данных на месте — вместо этого он пишет в новую, уже чистую страницу, а старую версию помечает как невалидную. Именно этот механизм и есть главный источник разницы между последовательной и случайной записью.

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

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

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

Write amplification и garbage collection: где рождается замедление

Раз SSD не переписывает данные на месте, а всегда пишет «вбок», в блоках со временем накапливаются страницы двух типов: валидные (актуальные данные) и невалидные (устаревшие версии, которые уже никому не нужны, но физически всё ещё занимают место). Когда свободных чистых страниц становится мало, контроллер запускает garbage collection (GC): выбирает блок с большим количеством невалидных страниц, копирует оставшиеся валидные страницы в другое, уже подготовленное место, а затем стирает освободившийся блок целиком, возвращая его в пул чистых.

Здесь и возникает write amplification (WA) — соотношение между тем, сколько данных физически записал контроллер, и тем, сколько данных фактически прислал хост. Если приложение записало 1 МБ, а GC попутно перенёс ещё несколько мегабайт валидных страниц, чтобы освободить блоки, реальный объём записи на кристаллы оказывается кратно больше запрошенного. Мы намеренно не приводим конкретный коэффициент WA — он сильно зависит от контроллера, объёма over-provisioning, заполненности диска и характера нагрузки, и на разных SSD он разный.

Случайная запись усиливает write amplification гораздо сильнее последовательной по одной простой причине: она разбрасывает «протухшие» страницы равномерно по всем блокам. В блоке из сотен страниц, где записи были случайными, может оказаться десяток невалидных страниц вперемешку с ещё живыми — и чтобы освободить такой блок, GC вынужден переносить много валидных данных на каждую единицу освобождённого места. При последовательной записи, наоборот, целые блоки чаще протухают целиком одновременно (например, когда файл перезаписывается заново последовательно), и GC может стереть их без переноса вообще ничего лишнего. Подробнее о том, как накопленная за месяцы работа GC постепенно расходует ресурс ячеек, разбирали в статье про wear leveling и то, как SSD стареет незаметно для пользователя.

Flash Translation Layer: почему контроллеру проще с предсказуемым паттерном

Операционная система и приложения обращаются к SSD так же, как к любому блочному устройству — по логическим адресам блоков (LBA), как будто внутри действительно есть последовательно пронумерованные секторы, которые можно перезаписывать на месте. Но физически, как мы разобрали выше, перезаписи на месте почти никогда не происходит. Разрыв между этими двумя картинами мира закрывает слой контроллера, который называется Flash Translation Layer (FTL).

FTL хранит и постоянно обновляет карту соответствия «логический адрес → физический адрес» (map table). Каждая запись хоста, даже перезапись уже существующего LBA, на деле уходит в новую физическую страницу, а запись в map table просто указывает: теперь этот логический адрес живёт вот здесь. Старая физическая страница помечается невалидной и ждёт своей очереди на GC.

Именно нагрузка на этот механизм и объясняет разницу в скорости на уровне, невидимом пользователю напрямую:

  • Случайная запись порождает случайные, разбросанные по всему пространству адресов обновления map table. FTL приходится чаще модифицировать метаданные в непредсказуемом порядке, что для многих реализаций (особенно там, где карта или её часть держится не полностью в быстрой SRAM, а частично подгружается) означает дополнительные операции чтения-модификации-записи самой карты — это отдельная, невидимая пользователю нагрузка поверх «полезной» записи данных.
  • Последовательная запись даёт предсказуемый, монотонно растущий диапазон LBA. Часть FTL-реализаций умеет сворачивать такие диапазоны эффективнее (крупными кластерами вместо записи каждого адреса по отдельности), а главное — данные физически ложатся компактно, блок за блоком, без перемешивания живых и мёртвых страниц внутри одного блока. Это напрямую снижает будущую нагрузку на garbage collection, о которой шла речь выше.

Иными словами, «головок нет» — верно на уровне физического позиционирования, но неверно на уровне «стоимость операции не зависит от паттерна». FTL — это программно-аппаратный слой, у которого своя внутренняя структура данных (карта соответствия) и свои алгоритмы её обслуживания, и, как у любой структуры данных, у неё есть паттерны доступа, с которыми она работает эффективнее, и паттерны, которые создают ей дополнительную работу.

Что дополнительно видит операционная система: очереди, TRIM, over-provisioning

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

Глубина очереди команд. Современный NVMe поддерживает множество параллельных очередей с большой глубиной каждая, и SSD умеет обрабатывать много запросов одновременно за счёт параллелизма между несколькими NAND-каналами и кристаллами внутри накопителя. При случайной записи с достаточной параллельной нагрузкой (много одновременных запросов) разница с последовательной записью может сокращаться — потому что контроллер распределяет случайные запросы по разным каналам параллельно, а не последовательно один за другим. А вот однопоточная случайная запись с глубиной очереди 1 обычно проседает заметнее всего, потому что не даёт контроллеру возможности перекрыть задержки внутренних операций параллелизмом. Подробнее о том, как устроены очереди команд и почему один поток не выжимает из накопителя заявленную производительность, — в статье про очереди NVMe и то, почему один поток не вытягивает диск на максимум.

TRIM. Команда TRIM сообщает контроллеру, какие LBA больше не используются файловой системой (например, после удаления файла), чтобы соответствующие физические страницы можно было заранее пометить невалидными, не дожидаясь, пока хост туда что-то допишет. Без своевременного TRIM контроллер вынужден предполагать, что все когда-либо записанные страницы всё ещё содержат нужные данные, и переносить их при GC, даже если файловая система давно считает это место свободным. Это напрямую увеличивает write amplification и, соответственно, замедляет запись — в том числе последовательную. Реальный пример того, как забытый или не отработавший вовремя TRIM выливается в ощутимые паузы записи, разбирали в статье про фоновый TRIM и пятнадцатисекундные зависания базы.

Over-provisioning. Производители резервируют часть физической ёмкости SSD (недоступную пользователю в адресном пространстве) специально для нужд GC — как буфер чистых блоков, в которые можно писать, пока идёт освобождение других. Чем больше свободного пространства (как зарезервированного производителем, так и просто незанятого пользователем), тем меньше давление на GC и тем меньше write amplification при случайной записи. Ресурс накопителя, который расходуется всей этой внутренней работой, измеряется в TBW (terabytes written) — сколько ресурс SSD подробнее считается и от чего он тает быстрее, чем кажется, разбирали отдельно в статье про ресурс SSD и то, куда уходит TBW.

Практические следствия для проектирования нагрузки на сервер

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

Почему лог-структурированные СУБД и хранилища построены на последовательной записи. LSM-based движки (используются во многих современных key-value и колоночных СУБД) намеренно никогда не переписывают данные на месте: новые записи всегда уходят последовательно в конец активного сегмента (memtable → SSTable на диске), а устаревшие версии данных схлопываются позже отдельным фоновым процессом — компакцией. Это архитектурное решение осознанно повторяет ту же идею, что реализует FTL внутри SSD: превратить произвольные логические изменения в предсказуемый, последовательный физический поток записи и вынести уборку «мусора» в отдельную фоновую фазу. В результате нагрузка на GC внутри самого SSD снижается, потому что СУБД уже отдаёт данные компактными последовательными сегментами, а не разрозненными точечными правками.

Batching вместо точечных записей. Там, где приложение может группировать мелкие записи в более крупные последовательные блоки (буферизация перед flush, WAL с групповым commit, объединение мелких файлов), это снижает и число операций ввода-вывода, и степень фрагментации адресного пространства, с которой потом работает GC.

Выравнивание записи по границе блока. Запись, не выровненная по внутренней гранулярности накопителя (странице/блоку), вынуждает контроллер читать-модифицировать-записывать частично занятые страницы — это тот же принцип, что и «запись одного байта стоит килобайты», только применительно к flash-памяти. Проверка выравнивания разделов и файловых систем — недорогая практика, которая снижает паразитную нагрузку без изменения архитектуры приложения.

Запас свободного места на диске. Поскольку GC работает эффективнее при наличии запаса чистых блоков, диск, заполненный на 90%+, при случайной записи ведёт себя заметно хуже того же диска с существенным запасом свободного пространства — именно потому, что контроллеру физически негде маневрировать между «стереть» и «записать». Для нагруженных БД и хранилищ с интенсивной случайной записью разумно закладывать запас свободного пространства сверх того, что нужно чисто по объёму данных.

Выбор класса накопителя под профиль нагрузки. Если профиль нагрузки на сервере предполагает интенсивную случайную запись (транзакционные БД, очереди сообщений с диска, кеши на диске), это одна из тех характеристик, ради которых стоит смотреть не только на заявленную последовательную скорость в маркетинговых цифрах, а на архитектуру контроллера, объём over-provisioning и класс накопителя (потребительский vs. серверный, DWPD). Мы намеренно не называем конкретных моделей и их измеренных цифр производительности — они меняются от поколения к поколению и зависят от конкретной прошивки, поэтому единственный надёжный способ — тестировать нужный профиль нагрузки (fio с нужным паттерном random write) на конкретном железе перед принятием решения, а не полагаться на цифры из чужого бенчмарка.

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

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

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

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

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

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

Если у SSD нет головок, почему вообще существует разница между sequential и random write?

Потому что физическое ограничение flash-памяти не в позиционировании, а в асимметрии между записью (постранично) и стиранием (целыми блоками). Случайная запись хуже утилизирует эту гранулярность и создаёт больше работы для garbage collection и для карты соответствия внутри FTL, чем последовательная.

Уменьшится ли разница между случайной и последовательной записью, если увеличить глубину очереди запросов (параллелизм)?

Как правило да, потому что параллельные запросы контроллер может распределить по разным NAND-каналам и кристаллам одновременно, перекрывая внутренние задержки. Но это сглаживает разницу, а не устраняет её полностью — write amplification от случайного паттерна никуда не девается, просто его последствия менее заметны при высокой параллельной нагрузке.

Помогает ли отключение TRIM ускорить запись?

Нет, обычно наоборот. Без TRIM контроллер дольше не узнаёт, какие страницы фактически свободны, и переносит при GC больше «мёртвых» данных, которые файловая система уже считает удалёнными. Это увеличивает write amplification, а не уменьшает.

Это касается только SSD, или на NVMe то же самое?

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

Можно ли полностью убрать случайную запись из своей нагрузки?

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

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

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

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