Webrium
Подписаться

Как мерить производительность, чтобы себе не соврать

Почему типовой прогон на стенде расходится с продакшеном и как собрать протокол замера: от решения через модель нагрузки до кривой вместо точки

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

Когда замер на стенде расходится с поведением в продакшене, виноват почти никогда не сам инструмент. Инструмент честно измерил то, что ему подсунули: нагрузку не той формы, метрику не той стороны, один прогон вместо серии. Ломается протокол — порядок действий вокруг замера. Поэтому честный бенчмарк начинается не с выбора утилиты, а с ответа на вопрос: какое решение зависит от результата и при каких условиях вы готовы считать этот результат ответом.

Тупик: дефолтный прогон и вывод «бенчмарки врут»

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

Почему это не помогает. Первая причина — в устройстве генератора. Режим по умолчанию у большинства инструментов — закрытый цикл: клиент отправляет запрос, ждёт ответа и лишь потом отправляет следующий. Пока сервис справляется, этого не заметно. Но стоит сервису замедлиться — и клиент автоматически сбавляет темп: в образовавшуюся паузу не уходит ни одного запроса. Нагрузка подстраивается под деградацию, и момент перегруза в замер не попадает вовсе. Вы измеряете не «как сервис ведёт себя под давлением», а «как быстро он отвечает, когда давление ослабевает вместе с ним» — режим, которого в реальности не существует. У эффекта есть устоявшееся имя (coordinated omission), но важнее имени — проверка: шлёт ли ваш генератор запросы по расписанию, не оглядываясь на ответы.

Вторая — среднее. Из арифметики среднего следует, что горстка медленных запросов тонет в массе быстрых. А очереди, блокировки и сборка мусора живут именно в хвосте распределения. Средняя задержка может стоять на месте, пока хвост растёт в разы — а пользователи и алерты живут в хвосте. Решение по средней — решение по чужой задаче.

Третья — один прогон. Цифра без повторов не имеет погрешности: неизвестно, повторится ли она завтра. Частоты процессора плавают, соседи по машине шумят, кэши греются и остывают. Пока одна и та же версия не прогнана несколько раз и собственный разброс не виден, у слова «быстрее» нет опоры — не с чем его сравнивать.

Четвёртая — точка вместо кривой. Система ведёт себя по-разному в разных режимах: пока есть свободные ресурсы, задержка почти не растёт с нагрузкой; за переломом очередь копится быстрее, чем обслуживается, и задержка уходит вверх. «Пиковая» точка лежит на краю этого перелома, у неё нет окрестности: сдвиньте нагрузку немного — и поведение меняется качественно. Два варианта могут быть близнецами до перелома и разойтись после, и точечный замер этого не покажет ни при какой аккуратности.

Куда этот путь заводит. Цифры выходят уверенные, продакшен с ними расходится, и дорога сворачивает в настоящий тупик: вывод «бенчмарки врут, мерить бессмысленно», после которого измерения бросают и решают по интуиции. Вывод неверен. Инструмент не соврал — он честно измерил то, что ему подсунули: нагрузку, которая подстраивалась под сервис, метрику, прячущую хвост, один прогон без погрешности, точку без кривой. Сломан не замер как идея, а протокол. Выход из тупика — не отказ от измерений, а протокол, собранный с другого конца: сначала решение, потом модель нагрузки, потом замер.

Решение до цифры

У бенчмарка всегда есть заказчик — решение, ради которого его затевают: сменить реализацию, поставить кэш, поднять реплики. Пока решения нет, цифру не с чем сравнивать, кроме красивой предыдущей цифры. До первого прогона запишите: какой вопрос закрывает замер; какой ответ какое действие вызывает («новая версия быстрее — мигрируем, иначе — остаёмся»); и порог, за которым разница считается настоящей. Порог берётся из экономики решения, а не из замера: если переход — это недели миграции, минимальный выигрыш не окупается; если флаг в конфигурации — стоит брать и скромный.

И арифметика шума. Любой прогон дрожит, и если ожидаемый эффект сравним с этим дрожанием, его либо не вытащить разумным числом повторов, либо сначала придётся снижать шум — фиксировать частоты, убирать соседей. Иначе честный ответ — «разницы не видно». Это самый дешёвый результат бенчмарка: он экономит миграцию, которая ничего бы не дала.

Мерить кривую, а не точку

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

Для самопроверки пригодится арифметика из теории очередей: в устоявшемся режиме среднее число запросов внутри системы равно произведению скорости их поступления на среднее время пребывания запроса в системе. Это тождество (закон Литтла), и им удобно сверять показания генератора: подставьте заявленную интенсивность и среднее время пребывания — получите, сколько запросов должно быть «в полёте». Не сходится с наблюдаемым — где-то врёт учёт: запросы теряются, дублируются или считаются не там, где ходят. Сверка копеечная, а сломанные сетапы отсеивает исправно.

Про место замера. Тайминги на сервере показывают, где время уходит внутри кода, но не то, что чувствует вызывающий: очередь перед вашим кодом и сеть в них не входят. Для решений про пользовательский опыт меряйте перцентили с клиента; серверную разбивку держите для атрибуции. «Кто виноват» и «что видит пользователь» — разные вопросы.

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

Как сравнивать версии

Машина дрейфует между прогонами: нагрев, масштабирование частот, фоновые задания, состояние кэшей. Прогнали вариант утром, альтернативу вечером — измерили в том числе разницу между утром и вечером. Честная схема — чередование в одной сессии: A, B, A, B, несколько пар подряд. Сравнивайте не отдельные цифры, а медианы пар вместе с их разбросом. Если разброс прогонов одной версии больше разницы между версиями, эффект лежит ниже шумового пола — и правильная запись результата «не различимо», а не докручивание прогонов до победного.

Ловушки, которые портят сравнение изнутри:

  • Прогрев. Холодный и прогретый сервис — разные системы: первые запросы идут по холодным путям кода и данных. Оба режима легитимны, но отвечают на разные вопросы; зафиксируйте, какой меряете, и держите его одинаковым для обеих версий.
  • Наблюдатель. Логи, метрики и трейсинг на горячем пути — часть измеряемой системы. Если в одной версии трейсинг включён, а в другой нет, вы сравниваете не версии, а трейсинг. Уравняйте или выключите.
  • Микробенчмарки. В компилируемых языках оптимизатор может выбросить измеряемую работу, если её результат никому не нужен; аллокаторы и кэши греются и льстят цифре. Микробенчмарк хорош как вопрос «где уходит время», но как ответ «что быстрее в проде» годится редко. Мы уже разбирали, где Rust в сервисах окупается, а где нет, — там ответ тоже упирается в доверие к собственному замеру горячей петли.

Протокол: что делать руками

  1. Запишите решение и порог: «если X быстрее Y не меньше чем на Z в условиях W — переходим, иначе остаёмся». Порог — из экономики решения.
  2. Измерьте собственный шум: прогоните одну версию несколько раз подряд и посмотрите разброс. Всё, что меньше него, вы измерить не можете — это ваш пол чувствительности.
  3. Проверьте режим генератора по документации: отправка по расписанию или по факту ответа. Открытый цикл нужен, когда клиенты друг друга не ждут; закрытый честен, если клиенты — фиксированный пул воркеров, но тогда размер пула — часть модели нагрузки.
  4. Снимите кривую: несколько уровней от лёгкой до насыщения, на каждом — перцентили с клиента. Отметьте колено.
  5. Чередуйте версии: A, B, A, B в одной сессии, несколько пар; сравнивайте медианы пар и их разброс; «не различимо» — валидный ответ.
  6. Сверьте арифметику: интенсивность, умноженная на среднее время пребывания, должна сходиться со средним числом запросов в полёте. Расхождение — баг учёта.
  7. Опишите условия: прогрев, размер данных, класс машины, соседи, настройки сборщика мусора — в тело отчёта, не в сноски. Цифра без условий не переносится никуда.

Позиция редакции

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

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

С чем это связано

Метка над заголовком говорит, зачем туда идти: продолжить тему, перейти к практике или увидеть возражение.

Сетка сервисов: закрашены лишь три ячейки — случаи, где Rust окупается; рядом счётчик сэкономленных машинЭкономия машин
Усиление

Rust в сервисах: где окупается, а где нет

Спор о Rust в сервисах разбирают без самообмана: скорость там давно не главное — решают два фактора, и оба нетехнические.

Из трёх причин медленной работы за шкалу выходит ожидание, а не вычислениеожидание
Предыстория

Python тормозит: что действительно ускоряет

База под остальное: пока три вида «медленно» не разделены, любое ускорение остаётся угадыванием, включая переписывание на другом языке.

За порогом срока жизни системы цена содержания перевешивает скорость первой версиисрок жизницена содержания
Предыстория

Java никуда не делась: где она осталась и почему

Что учесть заранее: решение о языке принимает тот, кто платит за содержание системы годами, а не тот, кто её начинает.

Письмо по вторникам

Один разбор недели и короткий список того, что изменилось. Без дайджестов на сорок ссылок.

Начните вводить — материалы появятся здесь.

выбрать · Enter открыть · Esc закрыть