Когда замер на стенде расходится с поведением в продакшене, виноват почти никогда не сам инструмент. Инструмент честно измерил то, что ему подсунули: нагрузку не той формы, метрику не той стороны, один прогон вместо серии. Ломается протокол — порядок действий вокруг замера. Поэтому честный бенчмарк начинается не с выбора утилиты, а с ответа на вопрос: какое решение зависит от результата и при каких условиях вы готовы считать этот результат ответом.
Тупик: дефолтный прогон и вывод «бенчмарки врут»
Что пробуют первым. Поднимают сервис на стенде, берут нагрузочный генератор с настройками по умолчанию, давят до отказа и записывают две цифры: пиковую пропускную способность и среднюю задержку. Сравнивают варианты — у кого цифры лучше, того и берут. Быстро, без приготовлений, и результат выглядит объективным, потому что получен машиной.
Почему это не помогает. Первая причина — в устройстве генератора. Режим по умолчанию у большинства инструментов — закрытый цикл: клиент отправляет запрос, ждёт ответа и лишь потом отправляет следующий. Пока сервис справляется, этого не заметно. Но стоит сервису замедлиться — и клиент автоматически сбавляет темп: в образовавшуюся паузу не уходит ни одного запроса. Нагрузка подстраивается под деградацию, и момент перегруза в замер не попадает вовсе. Вы измеряете не «как сервис ведёт себя под давлением», а «как быстро он отвечает, когда давление ослабевает вместе с ним» — режим, которого в реальности не существует. У эффекта есть устоявшееся имя (coordinated omission), но важнее имени — проверка: шлёт ли ваш генератор запросы по расписанию, не оглядываясь на ответы.
Вторая — среднее. Из арифметики среднего следует, что горстка медленных запросов тонет в массе быстрых. А очереди, блокировки и сборка мусора живут именно в хвосте распределения. Средняя задержка может стоять на месте, пока хвост растёт в разы — а пользователи и алерты живут в хвосте. Решение по средней — решение по чужой задаче.
Третья — один прогон. Цифра без повторов не имеет погрешности: неизвестно, повторится ли она завтра. Частоты процессора плавают, соседи по машине шумят, кэши греются и остывают. Пока одна и та же версия не прогнана несколько раз и собственный разброс не виден, у слова «быстрее» нет опоры — не с чем его сравнивать.
Четвёртая — точка вместо кривой. Система ведёт себя по-разному в разных режимах: пока есть свободные ресурсы, задержка почти не растёт с нагрузкой; за переломом очередь копится быстрее, чем обслуживается, и задержка уходит вверх. «Пиковая» точка лежит на краю этого перелома, у неё нет окрестности: сдвиньте нагрузку немного — и поведение меняется качественно. Два варианта могут быть близнецами до перелома и разойтись после, и точечный замер этого не покажет ни при какой аккуратности.
Куда этот путь заводит. Цифры выходят уверенные, продакшен с ними расходится, и дорога сворачивает в настоящий тупик: вывод «бенчмарки врут, мерить бессмысленно», после которого измерения бросают и решают по интуиции. Вывод неверен. Инструмент не соврал — он честно измерил то, что ему подсунули: нагрузку, которая подстраивалась под сервис, метрику, прячущую хвост, один прогон без погрешности, точку без кривой. Сломан не замер как идея, а протокол. Выход из тупика — не отказ от измерений, а протокол, собранный с другого конца: сначала решение, потом модель нагрузки, потом замер.
Решение до цифры
У бенчмарка всегда есть заказчик — решение, ради которого его затевают: сменить реализацию, поставить кэш, поднять реплики. Пока решения нет, цифру не с чем сравнивать, кроме красивой предыдущей цифры. До первого прогона запишите: какой вопрос закрывает замер; какой ответ какое действие вызывает («новая версия быстрее — мигрируем, иначе — остаёмся»); и порог, за которым разница считается настоящей. Порог берётся из экономики решения, а не из замера: если переход — это недели миграции, минимальный выигрыш не окупается; если флаг в конфигурации — стоит брать и скромный.
И арифметика шума. Любой прогон дрожит, и если ожидаемый эффект сравним с этим дрожанием, его либо не вытащить разумным числом повторов, либо сначала придётся снижать шум — фиксировать частоты, убирать соседей. Иначе честный ответ — «разницы не видно». Это самый дешёвый результат бенчмарка: он экономит миграцию, которая ничего бы не дала.
Мерить кривую, а не точку
Одна точка не описывает систему, потому что режимов у неё минимум два. Снимайте несколько уровней интенсивности, от заведомо лёгкой до насыщения, и на каждом — задержку в перцентилях, с той стороны, откуда приходит запрос. Место, где задержка начинает расти заметно быстрее нагрузки, — колено кривой — и есть настоящая граница применимости ваших цифр: снятое по одну сторону колена не переносится на другую. Решение «что раскатывать» часто сводится к вопросу «по какую сторону колена мы будем жить», и точечный бенчмарк на него не отвечает в принципе.
Для самопроверки пригодится арифметика из теории очередей: в устоявшемся режиме среднее число запросов внутри системы равно произведению скорости их поступления на среднее время пребывания запроса в системе. Это тождество (закон Литтла), и им удобно сверять показания генератора: подставьте заявленную интенсивность и среднее время пребывания — получите, сколько запросов должно быть «в полёте». Не сходится с наблюдаемым — где-то врёт учёт: запросы теряются, дублируются или считаются не там, где ходят. Сверка копеечная, а сломанные сетапы отсеивает исправно.
Про место замера. Тайминги на сервере показывают, где время уходит внутри кода, но не то, что чувствует вызывающий: очередь перед вашим кодом и сеть в них не входят. Для решений про пользовательский опыт меряйте перцентили с клиента; серверную разбивку держите для атрибуции. «Кто виноват» и «что видит пользователь» — разные вопросы.
И про стенд. Полную копию продакшена вы не получите: объёмы данных, состояние кэшей, соседи и конфигурация будут отличаться. Задача бенчмарка — не скопировать прод, а воспроизвести режим, важный для решения: если решаете про поведение под пиком, нужен режим насыщения, а не уютный полупустой стенд.
Как сравнивать версии
Машина дрейфует между прогонами: нагрев, масштабирование частот, фоновые задания, состояние кэшей. Прогнали вариант утром, альтернативу вечером — измерили в том числе разницу между утром и вечером. Честная схема — чередование в одной сессии: A, B, A, B, несколько пар подряд. Сравнивайте не отдельные цифры, а медианы пар вместе с их разбросом. Если разброс прогонов одной версии больше разницы между версиями, эффект лежит ниже шумового пола — и правильная запись результата «не различимо», а не докручивание прогонов до победного.
Ловушки, которые портят сравнение изнутри:
- Прогрев. Холодный и прогретый сервис — разные системы: первые запросы идут по холодным путям кода и данных. Оба режима легитимны, но отвечают на разные вопросы; зафиксируйте, какой меряете, и держите его одинаковым для обеих версий.
- Наблюдатель. Логи, метрики и трейсинг на горячем пути — часть измеряемой системы. Если в одной версии трейсинг включён, а в другой нет, вы сравниваете не версии, а трейсинг. Уравняйте или выключите.
- Микробенчмарки. В компилируемых языках оптимизатор может выбросить измеряемую работу, если её результат никому не нужен; аллокаторы и кэши греются и льстят цифре. Микробенчмарк хорош как вопрос «где уходит время», но как ответ «что быстрее в проде» годится редко. Мы уже разбирали, где Rust в сервисах окупается, а где нет, — там ответ тоже упирается в доверие к собственному замеру горячей петли.
Протокол: что делать руками
- Запишите решение и порог: «если X быстрее Y не меньше чем на Z в условиях W — переходим, иначе остаёмся». Порог — из экономики решения.
- Измерьте собственный шум: прогоните одну версию несколько раз подряд и посмотрите разброс. Всё, что меньше него, вы измерить не можете — это ваш пол чувствительности.
- Проверьте режим генератора по документации: отправка по расписанию или по факту ответа. Открытый цикл нужен, когда клиенты друг друга не ждут; закрытый честен, если клиенты — фиксированный пул воркеров, но тогда размер пула — часть модели нагрузки.
- Снимите кривую: несколько уровней от лёгкой до насыщения, на каждом — перцентили с клиента. Отметьте колено.
- Чередуйте версии: A, B, A, B в одной сессии, несколько пар; сравнивайте медианы пар и их разброс; «не различимо» — валидный ответ.
- Сверьте арифметику: интенсивность, умноженная на среднее время пребывания, должна сходиться со средним числом запросов в полёте. Расхождение — баг учёта.
- Опишите условия: прогрев, размер данных, класс машины, соседи, настройки сборщика мусора — в тело отчёта, не в сноски. Цифра без условий не переносится никуда.
Позиция редакции
Мы считаем, что бенчмарк без заранее записанного правила решения — демонстрация, а не измерение: цифру подгонят под уже принятое решение, и произойдёт это незаметно для самих себя. Минимальный честный протокол: вопрос и порог до запуска, открытая модель нагрузки, перцентили с клиента, чередование версий с разбросом, условия в отчёте. Свой замер на своих данных всегда весомее чужой цифры: чужие бенчмарки годятся как указатель направления, но не как основание для миграции.
Наша позиция неверна, когда ожидаемый эффект кратно больше любого правдоподобного шума: если варианты различаются в разы, полный протокол превращается в церемонию, и грубый прогон даст тот же ответ. Она неверна и там, где цена ошибки мала, — например, для дымовых тестов на регрессию. Мерить стоит ровно настолько тщательно, насколько дорого обойдётся ошибка.