37. SSD, NVMe и почему современные диски такие быстрые

Конец эпохи вращающихся блинов

В предыдущей главе 36. PCIe. Как устройства общаются с CPU мы увидели, как материнская плата превратилась в сверхскоростную коммутируемую сеть, способную прокачивать сотни гигабайт данных в секунду между процессором и шинами расширения.

Однако долгие четыре десятилетия эта фантастическая электронная мощь кремния разбивалась о неумолимые законы классической ньютоновской механики.

Вспомните традиционный серверный жесткий диск (HDD): герметичная банка, внутри которой со скоростью 10 000 или 15 000 оборотов в минуту вращаются зеркальные алюминиевые или стеклянные «блины», покрытые магнитным слоем. Над ними на микроскопической воздушной подушке парит механическое коромысло со считывающей головкой.

Чтобы прочитать хотя бы один сектор данных, сервопривод должен физически разогнать коромысло, спозиционировать его над нужной концентрической дорожкой (время поиска — Seek Time) и дождаться, пока искомый сектор диска пролетит под головкой.

Эта механическая задержка измеряется миллисекундами (обычно 5–10 мс). В итоге самый совершенный серверный HDD физически не способен выдать более 150-200 IOPS (операций ввода-вывода в секунду) при случайном доступе! Когда на сервер базы данных наваливались тысячи клиентов, дисковая подсистема начинала судорожно трещать головками, а время отклика сервиса подскакивало до секунд.

Триумфальное появление твердотельных накопителей (SSD — Solid State Drives) навсегда отправило вращающиеся блины в музей, заменив механику на кремниевые микросхемы. Но чтобы выжать из современных NVMe-дисков миллионы IOPS и ненароком не разрушить их ресурс безграмотным Go-кодом, инженеру необходимо заглянуть в физику ячеек флэш-памяти.


Анатомия NAND Flash: Страницы и Блоки

В оперативной памяти (DRAM) мы можем адресовать и перезаписывать любой произвольный байт или 64-битное слово. Во флэш-памяти типа NAND законы микроэлектроники совершенно иные.

Физически массив энергонезависимой памяти на кристалле кремния организован в строгую двухуровневую иерархию: Страницы (Pages) и Блоки (Blocks).

  • Страница (Page): Минимальная неделимая единица чтения и записи данных. В современных многослойных 3D-NAND кристаллах размер страницы составляет от 4 КБ до 16 КБ.
  • Блок (Block): Крупный физический кластер, объединяющий от 128 до 512 страниц (типичный размер блока в современных SSD составляет от 2 до 8 Мегабайт).

Именно в этой асимметрии кроется фундаментальный парадокс и главное ограничение полупроводниковой флэш-памяти:

  1. Чтение: Мы можем прочитать произвольную физическую страницу (например, 4 КБ) за микросекунды.
  2. Запись: Мы можем записать страницу данных (4 КБ), но исключительно в чистую (пустую) страницу, ячейки которой предварительно сброшены в состояние логической единицы.
  3. Перезапись невозможна: Мы НЕ МОЖЕМ перезаписать уже занятую страницу «поверх» старых данных!

Чтобы изменить хотя бы один-единственный байт в уже заполненной странице, физика требует подать на подложку кристалла высокое напряжение (~20 Вольт) и полностью стереть ВЕСЬ БЛОК целиком (несколько Мегабайт)!

Это железное физическое правило называется Erase-Before-Write (Стирание перед записью). При этом операция стирания блока заметно медленнее записи страницы (в типичной NAND — примерно в 3–20 раз: миллисекунды против сотен микросекунд), а главное — она физически деградирует диэлектрический слой кремниевых ячеек памяти (каждый блок выдерживает строго ограниченное число циклов стирания/записи — P/E cycles, Program/Erase cycles).

FTL: Flash Translation Layer — компьютер внутри диска

Если бы ядро Linux или файловая система напрямую пытались управлять блоками и страницами NAND, любой SQL-запрос с простым UPDATE приводил бы к зависанию всей операционной системы на миллисекунды.

Чтобы изолировать операционную систему от столь суровой физической реальности, каждый современный SSD несет на борту собственный автономный компьютер. Внутри накопителя установлен многоядерный ARM-контроллер, гигабайты собственной скоростной кэш-памяти LPDDR4/DDR5 и сложнейшая операционная система реального времени — FTL (Flash Translation Layer — Уровень трансляции флэш-памяти).

Контроллер FTL создает для внешнего мира совершенную иллюзию классического блочного устройства с плоской адресацией секторов (LBA — Logical Block Addressing).

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

  1. Он никогда не пытается стереть старую страницу на месте.
  2. Контроллер берет новые данные и записывает их в новую чистую страницу в совершенно другом, свободном блоке.
  3. Во внутренней таблице маппинга (хранящейся в быстрой RAM контроллера) FTL обновляет трансляцию: виртуальный логический сектор LBA 100 теперь привязан к новому физическому адресу.
  4. Старая физическая страница не стирается — она просто помечается битовым флагом как «Мусор» (Stale / Invalid).
TEXT
ЛОГИЧЕСКИЙ МИР (User Space / Go):
[LBA 100: "Версия 1"] ---> Запись новых данных ---> [LBA 100: "Версия 2"]

ФИЗИЧЕСКИЙ МИР NAND FLASH (Контроллер FTL):
[Физический Блок А: Страница 12] = "Версия 1" (Помечена как МУСОР / STALE)
[Физический Блок Б: Страница 85] = "Версия 2" (АКТИВНАЯ, LBA 100 указывает сюда)

Mechanical Sympathy: Write Amplification (Усиление записи)

Непонимание взаимодействия между размером страниц NAND, блоками и работой FTL порождает одну из самых коварных проблем высоконагруженных хранилищ — Write Amplification (Усиление записи).

Коэффициент усиления записи ($WAF$) показывает, сколько реальных байтов записал контроллер во флэш-память по отношению к тому, сколько байтов попросила записать прикладная программа:
$$WAF = \frac{\text{Байты, физически записанные в NAND Flash}}{\text{Байты, отправленные приложением через syscall write}}$$

Представьте, что каждое событие логгера на Go принудительно сбрасывается на устройство отдельной маленькой записью по 100 байт (скажем, write на файл, открытый с O_DIRECT|O_SYNC, или вызов fsync после каждой записи; оговорка: O_DIRECT обычно требует выровненных буферов и размеров, и 100-байтная запись на многих файловых системах завершится ошибкой EINVAL, а на некоторых пройдёт — для нашей иллюстрации важна лишь арифметика усиления записи):

  1. Ваша программа передает в ядро ОС ровно 100 байт.
  2. Но минимальная неделимая единица записи контроллера SSD — физическая страница размером 4 КБ (или 16 КБ).
  3. Контроллер вынужден записать целую 4-килобайтную страницу во флэш-память!
  4. Вы получаете колоссальное усиление записи: $WAF \approx 40$. Вместо 100 байт износу подверглись 4096 байт кремния!

(Важная оговорка: если просто вызывать write на обычном файле без fsync, то записи сначала попадают в page cache ядра и сливаются в крупные порции, так что WAF будет гораздо меньше. Усиление записи в таких масштабах возникает при немедленном сбросе на устройство.)

Ситуация становится катастрофической при Случайной записи (Random Write) мелкими порциями по всему объему хранилища (характерное поведение классических реляционных баз данных с обновлениями строк на месте).
Контроллер FTL начинает захлебываться: свободные страницы стремительно фрагментируются, фоновый сборщик мусора диска (Garbage Collector) работает без передышки, непрерывно перенося живые страницы из блока в блок.

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


От SATA к NVMe: Программная революция

С физикой кремниевых ячеек NAND всё понятно. Но почему первые поколения SATA SSD выдавали скромные 500 МБ/с и максимум 100 000 IOPS, в то время как современные накопители стандарта NVMe на точно таких же кремниевых кристаллах выдают 14 000 МБ/с (14 ГБ/с) и свыше 2 500 000 IOPS?

Ответ кроется не в кремнии памяти, а в сетевом и программном протоколе связи.

На протяжении десятилетий твердотельные накопители подключались через устаревший интерфейс SATA и протокол AHCI (Advanced Host Controller Interface), разработанный компанией Intel в далеком 2004 году. Этот протокол проектировался для медленных вращающихся магнитных дисков:

  • Протокол AHCI поддерживал ровно одну очередь команд (Queue).
  • Глубина этой единственной очереди составляла всего 32 команды.

Представьте 64-ядерный современный сервер, где сотни параллельных горутин Go одновременно обращаются к файлам на диске. В эпоху AHCI/SATA все эти 64 ядра процессора были вынуждены выстраиваться в одну общую очередь в ядре операционной системы, толкаясь за единственный мьютекс (Lock Contention) и ожидая освобождения 32 слотов в контроллере!

Стандарт NVMe (Non-Volatile Memory Express) был спроектирован с чистого листа специально для многоядерных архитектур и прямого подключения к шине PCIe:

  1. До ~64K независимых очередей: Спецификация NVMe допускает до 65 535 очередей ввода-вывода (Submission Queues) и столько же очередей завершения (Completion Queues), не считая служебной административной очереди. (Реальные контроллеры обычно поддерживают гораздо меньше — десятки или сотни.)
  2. До ~64K команд в каждой очереди: Каждая очередь может вмещать до 65 536 записей (одновременно в полной очереди — до 65 535 команд; реальная глубина, объявляемая контроллером в поле CAP.MQES, как правило, существенно меньше).
  3. Прямое соединение через PCIe: Полное исключение медленных мостовых контроллеров SATA/PCH.
flowchart TD
    classDef process  fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
    classDef success  fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
    classDef error    fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
    classDef warning  fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
    classDef entry    fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
    classDef aux      fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
    classDef system   fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
    classDef data     fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
    classDef network  fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
    classDef accent   fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
    subgraph AHCI_SATA ["Эпоха AHCI / SATA (Узкое горлышко)"]
        CPU1["CPU Core 0"]:::system & CPU2["CPU Core 1"] & CPUN["CPU Core N"] --> LOCK{"Синхронизация ядра<br>(Spinlock Contention)"}
        LOCK --> Q1["Единственная очередь<br>(Максимум 32 команды)"]
        Q1 --> SATA["SATA Контроллер"]
    end

    subgraph NVMe_PCIe ["Эпоха NVMe / PCIe (Параллельная магистраль)"]
        CPU_0["CPU Core 0"]:::data --> SQ0["Очередь 0 (Submission Queue)<br>До 64K команд"]
        CPU_1["CPU Core 1"]:::data --> SQ1["Очередь 1 (Submission Queue)<br>До 64K команд"]
        CPU_N["CPU Core N"]:::data --> SQN["Очередь N (Submission Queue)<br>До 64K команд"]
        SQ0 & SQ1 & SQN --> NVMe_CTRL["Многоканальный NVMe Контроллер<br>Параллельный DMA обмен"]
    end
  

Архитектура Lockless (Без блокировок)

Драйвер NVMe в ядре Linux реализован с безупречным следованием принципу Mechanical Sympathy: он выделяет ровно одну независимую пару аппаратных очередей (Submission Queue и Completion Queue) на каждое логическое ядро процессора.

Когда горутина на ядре CPU Core 0 выполняет системный вызов чтения или записи, ядро Linux помещает дескриптор команды в Очередь 0 — кольцевой буфер, который лежит в обычной оперативной памяти хоста, а контроллер диска забирает команды оттуда сам по DMA.
Никаких глобальных блокировок! Никакого ожидания других процессорных ядер!

Регистрация новой команды для диска сводится к простой записи в регистр-звонок (Doorbell Register), отображенный в адресное пространство (MMIO) контроллера: в него записывается лишь новый индекс хвоста очереди — сами команды в doorbell не передаются. Каждое ядро процессора общается с диском по своему собственному скоростному выделенному коридору.


Итог

Подведем итоги нашего погружения в мир твердотельных накопителей и протокола NVMe:

  1. Фундаментальная асимметрия NAND Flash: Флэш-память читается и пишется страницами (4–16 КБ), но стирается только гигантскими блоками (2–8 МБ) по правилу Erase-Before-Write.
  2. Интеллект FTL: Прошивка контроллера диска маппит виртуальные логические секторы на физические страницы памяти «на лету» и непрерывно выполняет собственный сборщик мусора (Garbage Collector) для очистки невалидных страниц.
  3. Опасность случайных записей: Мелкие случайные обновления данных (Random Writes) увеличивают коэффициент усиления записи (Write Amplification), снижают IOPS и ускоряют износ ячеек кремния.
  4. Торжество LSM-деревьев: Архитектурный отказ баз данных от модификаций страниц на месте в пользу последовательной записи (Append-Only SSTables) раскрывает 100% потенциала твердотельных накопителей.
  5. Параллелизм NVMe: Революционный скачок производительности обеспечен отказом от узкой одиночной очереди AHCI и переходом на множество (по спецификации — до ~64K) параллельных аппаратных очередей (Submission Queues), привязанных к ядрам процессора в архитектуре Lockless.

Мы изучили архитектуру компьютера от кремниевых вентилей процессора до очередей команд NVMe-диска. Но чтобы все эти знания стали действенным инженерным инструментом в повседневной разработке на Go, нам необходимо сложить их в единую, кристально ясную систему координат.

Какова цена доступа к кэшу L1 по сравнению с оперативной памятью? Сколько тактов стоит сетевой пинг до соседнего дата-центра?

В следующей лекции мы соберем главную шпаргалку любого бэкенд-архитектора: 38. Latency Numbers Every Programmer Should Know.