Представьте, разработчики добавили прозрачное шифрование в файловую систему, собрали ядро и прогнали Fio — пропускная способность не изменилась. Однако после раскатки на продакшен производительность резко упала. Дело не в том, что бенчмарк ошибся: он честно измерил предел быстродействия жесткого диска, а накладные расходы на шифрование остались скрыты за дисковыми задержками. Так и возникает одна из самых типичных ошибок storage-бенчмаркинга. Хранение до сих пор кажется многим инженерам простой подсистемой: файловая система, блочное устройство, метрика throughput. На практике это одна из самых коварных частей стека — с иерархией памяти в 4–8 порядков разницы между самой быстрой и самой медленной операцией, слоями виртуализации (LVM, программные RAID, NFS) и переключениями контекста между ядром и user-space.
Чтобы разобраться, как корректно измерять производительность систем хранения, обратимся к докладу Эреза Цадока, профессора Университета Стони-Брук, возглавляющего Лабораторию файловых систем и хранения данных.
Почему же storage так сложно измерять? Современная система хранения давно перестала быть диском с одной файловой системой. Каждый запрос проходит через десяток уровней, и прежде чем говорить о бенчмарках, стоит понять, через что именно он идет.
Упрощенно иерархию памяти можно представить как пирамиду, где в основе лежат медленные и дешевые жесткие диски, а наверху микроскопические сверхбыстрые регистры процессоров и L1-кэши. Разница в производительности между вершиной и основанием этой пирамиды достигает 4–8 порядков, и при этом ее компоненты постоянно взаимодействуют друг с другом.

К тому же современный storage stack уже давно не строго иерархичен. Сбоку к пирамиде прикручены слои виртуализации: LVM, программные RAID-массивы, а если мы имеем дело с сетевым хранилищем, то добавляются NFS, сеть и связанные с ней задержки. Кроме того, есть еще перенос логики в user-space. Из-за него приходится учитывать не только задержки железа, но и затраты на переключение контекста и копирование данных между ядром и пользовательским пространством.

Эта многослойность — одна из причин того, почему простые тесты редко измеряют те части системы, которые действительно нужно проверить.
Почему ваш бенчмарк ничего не доказывает
Чтобы понять, почему storage-бенчмаркинг почти всегда измеряет не то, что хочется, обратимся, например, к внедрению прозрачного шифрования в файловую систему.
Допустим, модифицированная файловая система с шифрованием показывает пропускную способность (throughput) в 150 МБ/с. Вы запускаете контрольный тест на оригинальной ФС без шифрования — те же 150 МБ/с.
Вроде бы всё хорошо, но запросто может оказаться, что вместо шифрования мы измерили предел быстродействия жесткого диска.
Как так вышло?
Система находилась в режиме I/O bound: пока диск обслуживал запрос, процессору хватало времени и на шифрование, и на остальную работу, а накладные расходы на обслуживание шифрования просто остались в тени задержки накопителя. Однако стоит перенести тот же код на быструю NVMe-конфигурацию, как картина может резко поменяться. Там узким местом уже станет не диск, а CPU. И тогда тот же механизм шифрования, который на HDD выглядел «бесплатным», внезапно начнет снижать пропускную способность хранилища. Поэтому на практике неоптимальная методология замеров часто приводит к трудностям при реализации.
Расхождение между тем, что показывает инструмент, и тем, что происходит на самом деле, встречается не только в синтетических тестах. Хороший пример — измерение скорости записи через dd на уровне блочного устройства. Dd пишет последовательно, одним потоком, крупными блоками и по умолчанию не делает fsync до завершения. Данные уходят в page cache, а dd уже рапортует о результате, пока ядро еще дозаписывает их на диск. База данных поверх той же системы хранения работает совершенно иначе, каждая транзакция генерирует случайный I/O, чтобы найти нужную страницу, прочитать ее, обновить, обновить индексы. В итоге dd легко показывает 500 МБ/с последовательной записи на том же диске, где реальная база данных упирается в 100–200 случайных операций в секунду.
У каждого инструмента свои ограничения, и выбор зависит от того, что именно мы хотим измерить. Чтобы выбирать осознанно, предлагаем разобраться, какие бенчмарки вообще бывают.
Ограничения бенчмарков
Для удобства в своем докладе Цадок делит бенчмарки на несколько категорий. Микробенчмарки хороши, когда нужно изолировать одну функцию, но они ничего не говорят о поведении системы в целом. Макробенчмарки уже ближе к реальности, но все же остаются синтетикой. Трейсы (Trace Replay) выглядят самым правдивым и реалистичным способом тестирования, но быстро устаревают и не всегда хорошо воспроизводятся…
Ни один из этих классов не универсален, поэтому он предлагает рассматривать их не как конкурентов, а как взаимодополняющие инструменты, и комбинировать все три подхода.
Возьмем параметры из таблицы Цадока (см. ниже). Если перемножить их между собой, то число прогонов легко перевалит за тысячу. А если каждый тест надо повторить хотя бы 5 раз, чтобы отсеять случайный шум и получить среднее, продолжительность тестирования вырастает совсем не линейно.

Даже если один прогон занимает 1–2 мин, получится никак не меньше нескольких дней непрерывной работы стенда, а если тест нужно крутить по 10–15 мин, чтобы он вышел на устойчивый режим, эксперимент уже превращается в многонедельный марафон.
Еще одна особенность бенчмаркинга в том, что он выявляет баги. И если во время пары тысяч прогонов ваша система словит критический сбой ядра или произойдет полная остановка системы (kernel panic), то по-хорошему после патча всё нужно начинать сначала.
Поэтому при планировании разработки или оптимизации системы хранения, профессор Цадок рекомендует закладывать на тестирование, дебаггинг и борьбу с регрессиями чуть ли не больше времени, чем на написание кода.
Темное искусство визуализации
Собрать данные тестов — полдела. Самое интересное начинается, когда мы пытаемся визуализировать этот массив.
Взглянем на типичный «ужасный график», из числа тех, что Цадок постоянно видит в присылаемых ему статьях.

Что не так с диаграммой?
И первое, что бросается в глаза, — обрезанная шкала. На столбчатой диаграмме такой прием почти всегда визуально раздувает разницу.
Базовое правило для bar chart — шкалу лучше начинать с нуля. Если же ось обрезана сознательно, например, чтобы показать мелкие колебания, то это нужно явно обозначить в подписи.
Кроме того, авторы «ужасных графиков» используют нечитаемые единицы измерения. Например, числа вроде 10 000 000 байт/сек. Лучше нормализовать шкалу. Допустим, показывать МБ/с, ГиБ/с, мкс или мс. Если диапазон значений слишком велик, можно использовать логарифмическую шкалу. Всё же логарифмический график читается иначе, чем линейный.

Ну и последнее — плохой график заставляет читателя считать вручную. Если над столбцами написаны 8 299.18 и 10 278.98, человек вынужден сам прикидывать, насколько одна система лучше другой. Часто полезнее оставить абсолютные значения на оси, а в аннотации поверх графика дать интерпретацию: «+24%» или «1.24×». Тогда график начинает отвечать на вопрос, а не задавать новую арифметическую задачу.

Цадок предлагает простые приемы, которые снижают когнитивную нагрузку при чтении подобных диаграмм. Во-первых, например, можно явно помечать направление «лучше»: для throughput рост вверх интерпретируется положительно, для latency — наоборот. Это можно показать визуально прямо на оси или в легенде, например, смайликами. Во-вторых, вместо обычных bar chart часто лучше работают box plot, потому что они показывают не только среднее, но и разброс.

И, наконец, цвета стоит подбирать так, чтобы график оставался читаемым не только на экране, но и в печати, в черно-белой цветовой гамме и для людей с нарушениями цветового восприятия.
Но главное здесь — метрики достоверности. Наиболее опасная ошибка, которую можно совершить, — показать средние значения без планок погрешности.
Допустим, на тесте производительность системы упала на 19% относительно базовой, но при этом границы доверительных интервалов обеих систем широки и пересекаются друг с другом. Статистически, если доверительные интервалы пересекаются, истинное среднее значение может находиться, где угодно внутри этого диапазона с заданной вероятностью (например, 50%). Так что на деле система в тесте могла не проиграть 19%, а выиграть 5%.
Если получились гигантские планки погрешностей (error bars), первая мысль — их спрятать и забыть, но, на самом деле, это хороший повод разобраться, почему система ведет себя нестабильно. Возможно, нужно больше прогонов. Возможно, во время теста в фоне проснулся cron и запустил updatedb, разрушив чистоту I/O-операций. А может быть, мы поймали фундаментальный архитектурный дефект, который проявляется только при определенном паттерне нагрузки.

Хорошая визуализация помогает заметить проблему, но не объясняет, почему она возникла. Если график показывает нестабильность или неожиданные задержки, приходится спускаться ниже, к профилированию storage stack.
Как профилировать hot path, не искажая результаты
Когда бенчмарк показывает пересекающиеся доверительные интервалы и широкий разброс результатов, причину, как правило, стоит искать на уровне ядра. Для этого нужно определить, какая именно функция вызывает задержку. И здесь начинаются первые инструментальные сложности.
Стандартный набор инструментов для такой задачи, по мнению Цадока, — это eBPF, tracepoints, blktrace и tcpdump. Они позволяют трассировать путь запроса через весь storage stack, строить гистограммы задержек на уровне блочного устройства и изолировать конкретные фазы I/O.
Однако при профилировании горячего пути внутри ядра возникает одна фундаментальная сложность: инструмент наблюдения сам может повлиять на измерение.
Почему так происходит?
Стоимость самого наблюдения становится частью измеряемой величины, поэтому overhead инструментов и время выполнения операции могут быть сопоставимы. Чем короче измеряемая операция и чем выше частота событий, тем заметнее становится overhead самого наблюдения.
На умеренных нагрузках этим эффектом можно пренебречь. Например, инженер запускает blktrace на диске, который по спецификации должен выдавать 120 IOPS (Input/Output Operations Per Second) на случайных 16K-операциях. Без инструмента картина выглядела бы просто как «диск медленный», а трассировка позволяет увидеть, на каком этапе возникает задержка и почему (подробный разбор здесь).
При высокочастотных нагрузках задача усложняется. На внутренних функциях ядра, которые выполняются за микросекунды и вызываются миллионы раз, overhead наблюдения уже существенно искажает картину. Так, при 20 миллионах I/O-событий в секунду стандартный biolatency (инструмент на базе eBPF) потреблял больше половины ядер сервера только на обработку трейспойнтов. При этом латентность каждого NVMe-запроса составляла десятки микросекунд, а overhead зондов добавлял к ней еще 16 мкс (подробный разбор здесь).
Откуда берется это влияние?
Механика этого искажения видна на примере точек трассировки (tracepoints). Они собирают события и складывают их в lock-free кольцевые буферы в памяти ядра, откуда данные асинхронно вычитывает процесс в user-space. При высокой плотности событий это приводит к потере данных. Демон не успевает обработать поток данных, буфер переполняется, а ядро начинает отбрасывать новые события. В результате статистика становится неполной.
Кроме того, генерация, сериализация и запись тысяч событий в буфер потребляют процессорное время, вытесняют полезные данные из L1/L2-кэшей и меняют паттерны доступа к памяти. Измеряемая система ведет себя иначе, чем без профайлера.
При этом сами события часто и не нужны. Для анализа задержек нужно не среднее арифметическое, а распределение — гистограмма. Вопрос в том, как собрать гистограмму внутри горячего пути ядра с минимальным оверхедом, без аллокаций памяти и без потерь данных.
Цадок предлагает для этого методику, в основе которой лежат внутренние часы процессора и битовые операции.
Стоит отметить, что TSC использовался для профилирования задолго до работ Цадока. Этот подход описан, например, в документации Intel.
Архитектура TSC-профайлера
Логика довольно простая. Перед вызовом функции читается счетчик тактов процессора — TSC (Time Stamp Counter). После выполнения читается еще раз, и разница между значениями дает длительность выполнения операции в тактах:

Полученное значение не уходит в поток отдельных событий, который нужно выгружать и разбирать, а сразу агрегируется внутри гистограммы прямо в ядре. Это снижает overhead, так как вместо подробного лога каждого вызова накапливается лишь распределение задержек.
Чтобы не плодить тысячи линейных интервалов, используется логарифмическая разбивка по корзинам.
Битовый сдвиг дает достаточно точное приближение log₂ и позволяет быстро определить, в какой диапазон попало измерение. На выходе получается компактная структура: массив примерно из 64 счетчиков (для 64-битного значения TSC). Памяти — меньше килобайта, а overhead в районе нескольких процентов. В докладе Цадок говорит о примерно 4% на старом железе, а на современных CPU этот показатель еще ниже.
Зачем всё это, если можно просто посчитать среднее?
Потому что в storage-системе операции почти никогда не выполняются в одном режиме. Часть уходит в RAM, часть попадает в кэш контроллера, часть идет на диск.
Допустим, у нас есть 1 тыс. быстрых операций по 10 мкс и одна медленная на 10 мс (промах кэша). Эта единственная операция исказит среднюю задержку. На бимодальных и мультимодальных распределениях среднее арифметическое не описывает поведение системы. Так, на графике ниже видно, как гистограмма распадается на несколько пиков и среднее между ними не описывает ни один реальный режим.

Какой срез — такой и результат
Даже если сам тест корректен, результат будет сильно зависеть от того, в какой фазе работы системы его зафиксировали. На холодном старте, в переходном режиме или после прогрева кэшей — одна и та же система может демонстрировать разную производительность.

Цадок подчеркивает, что результаты его бенчмарка сильно зависят от длительности теста, поэтому при оценке производительности критически важно смотреть на полную временную шкалу, а не на отдельные срезы. Без графика с временной шкалой легко выбрать срез, который подтверждает нужный вывод, чем и пользуются отдельные недобросовестные маркетологи.
Закон Цадока
Может показаться, что на фоне бума ИИ вопросы storage-бенчмаркинга отошли на второй план, однако Цадок считает ровно наоборот и в шутку формулирует принцип, который называет своим именем:
«Любая компьютерная система при масштабировании неизбежно становится проблемой хранения данных».
Пока веса модели, KV-cache и рабочие тензоры помещаются в высокоскоростную память, система выглядит быстрой, но как только данные вытесняются в системную RAM, а затем на NVMe, как на первый план выходят задачи, знакомые storage-инженерам уже много лет. Например, как организовать многоуровневое хранение, как скрыть задержки, как оптимизировать перенос данных между слоями памяти. Красивая алгоритмическая задача упирается в распределенный I/O.
Поэтому storage-бенчмаркинг по-прежнему остается актуальным. Чтобы понять, где система действительно теряет производительность, недостаточно просто запустить тест — важно провести замеры так, чтобы сам процесс измерения не исказил результат. А значит, инженерам по-прежнему придется строить корректные Box Plot, собирать репрезентативные данные и искать те самые миллисекундные задержки, которые в конечном счете определяют производительность всей системы. Но если мы хотим добиться прогресса в самых передовых областях, от этой работы никуда не деться.
Litemanager_soft
спасибо, интересно, а я думаю почему это ПК с годами все медленнее и медленнее работает)