Перейти к основному содержимому
Нормативная версия документации - английская

Эта страница переведена с английского языковой моделью. При любом расхождении верен английский оригинал.

Методология

Как каждый крейт RustStream измеряет себя, чтобы число одного брокера значило то же, что число другого.

Процедура ниже общая для всех адаптаций библиотек брокеров. Адаптация, которая от неё отступает, сообщает об этом на своей странице.

Три цикла​

Прогон измеряет один сценарий трижды. Циклы различаются только тем, через что проходят сообщения:

  • Сырой клиент. Собственный клиент брокера, напрямую.
  • Адаптация библиотеки брокера. Консьюмер и издатель самой адаптации (дескриптор подписки, поток подписчика, его подтверждение, его издатель), которые крутит цикл внутри бенчмарка, без рантайма.
  • RustStream. Сервис целиком, как его пишет пользователь: обработчик, приложение, диспетчеризация.

Из трёх циклов получаются две разности:

  • адаптация минус клиент - сколько адаптация библиотеки брокера стоит поверх клиента. За эту разность отвечает сама адаптация;
  • RustStream минус адаптация - сколько рантайм добавляет поверх адаптации. Её публикуют для каждой адаптации, потому что адаптации передают сообщения рантайму по-разному: как поток отдаёт доставки, приходят ли они пакетами, как до консьюмера доходит обратное давление.

Всё остальное во всех трёх циклах одинаково:

  • Клиент и его конфигурация. Предвыборка, режим ack, consumer group, долговечность, число соединений и любые настройки брокера. Фреймворк настраивает брокер через RustStream, но итоговые настройки клиента обязаны совпасть.
  • Позиция ack. RustStream отправляет ack после возврата из обработчика, поэтому два других цикла отправляют ack там же. Пакетный ack в конце прогона измеряет другой протокол, а не другой фреймворк.
  • Декодирование в тот же тип. Циклы без обработчика десериализуют полезную нагрузку в ту же структуру тем же кодеком и читают поле через std::hint::black_box. Без этого проще всего получить неверное число: оптимизатор выбрасывает декодирование, результат которого не используется, и сырая сторона незаметно перестаёт декодировать.
  • Полезная нагрузка, байт в байт. Тела сообщений всем циклам выдаёт один генератор.
  • Рантайм. Режим tokio (flavor), число рабочих потоков и число одновременно обрабатываемых сообщений.
  • Сборка. Профиль, RUSTFLAGS и аллокатор совпадают, а фичи наблюдаемости (logging, metrics, otel) выключены или включены во всех трёх циклах сразу. Числа с -C target-cpu=native в окружении другая машина не воспроизведёт, поэтому флаги публикуются вместе с результатами.

Прогон​

  • Консьюмер подключается до публикации первого сообщения. Иначе одна сторона разбирает то, что уже накопилось у брокера очереди, а другая принимает сообщения по мере публикации. В большинстве брокеров очереди это два разных пути.
  • Каждый прогон получает свои имена. Новый субъект, очередь, поток или consumer group на каждый прогон, чтобы прогон N не увидел остатков прогона N-1.
  • Окно открывается на первом принятом сообщении и закрывается на ack последнего. Перед измеряемыми прогонами идёт калибровочный, его результат выбрасывается. Установка соединения, регистрация консьюмера и первые выделения памяти - стоимость старта, а не сообщения.
  • Прогон длится не меньше пяти секунд. Под это подбирается число сообщений, чтобы переходные процессы старта и разрешение таймера оставались в пределах шума.
  • Циклы чередуются, а не идут блоками. Сырой, адаптация, фреймворк, снова сырой, адаптация, фреймворк, всего три круга. Если прогнать один цикл целиком, а потом следующий, весь дрейф машины (нагрев, фоновая нагрузка, страничный кеш) достанется последнему.

Отчёт​

Три круга. Одно число из одного прогона результатом не считается. Каждый цикл сообщает три круга: лучший, медианный и худший. Шум на машине только замедляет прогон, поэтому самый быстрый круг ближе всего к стоимости без помех. Медианный круг - типичный. Самый медленный показывает, насколько машина была далека от покоя.

Ячейка таблицы. В таблице печатается лучший круг, в скобках медианный. Худший остаётся в документе, но в ячейку не попадает: по нему считается разброс, расстояние от лучшего круга до худшего. Разница меньше разброса публикуется как неразличимо, а не процентом: величина ниже шума между прогонами изображала бы точность, которую никто не измерял.

Насыщенный консьюмер. Если сырая сторона провела прогон в ожидании брокера очереди, строка получает пометку broker-bound.

Окружение публикуется вместе с числами: процессор и число ядер, ядро системы, способ запуска брокера очереди (образ, контейнер, хост), версия rustc, версии крейтов, профиль сборки и флаги. Без этого число нельзя ни воспроизвести, ни признать устаревшим.

Измерение кода​

График стоимости кода каждый крейт собирает собственной командой бенчмарков. Её называет страница бенчмарков крейта. В ruststream это just bench. Для команды нужны valgrind и раннер бенчмарков той версии, на которую крейт ссылается в зависимостях.

В ruststream команда just bench 5000 измеряет каждый сценарий на пяти тысячах доставок вместо тысячи: число устойчивее, прогон дольше. Опубликованный документ остаётся на значении по умолчанию.

Что измеряется. Каждый сценарий - сервис, написанный как у пользователя. Его запускает настоящий рантайм с выключенной тестовой обвязкой, то есть код, который попадает в релиз. В ruststream транспорт внутрипроцессный, поэтому числа не зависят от сокета, нагрузки на сервер и сети: это абсолютная собственная стоимость фреймворка. Адаптация библиотеки брокера измеряет себя на настоящем транспорте, а что он добавляет к числу, сообщает её страница бенчмарков. Сравнение с чужим клиентом - график сравнения, его делают адаптации библиотек брокеров.

Где идёт сбор. Только внутри измеряемой области. Очередь наполняется до её начала, поэтому сценарий измеряет установившуюся доставку, а не подключение, подписку и первые выделения памяти. Подготовка и уборка работают в том же процессе и через тот же код фреймворка. Если захватить и их, наполнение очереди сойдёт за стоимость её разбора.

Три прогона. Первый прогон - одна доставка, второй и третий - длинные, и третий вдвое длиннее второго. В ruststream это тысяча и две тысячи доставок. У адаптации библиотеки брокера число доставок указано на её странице бенчмарков. Прогон с одной доставкой - холодный старт. Разница между двумя другими - стоимость сообщения на запущенном сервисе. Посреди прогона ничего не выключают, поэтому способ годится и для счётчика выделений памяти: выключателя у него нет.

Два числа. Инструкции считает callgrind, выделения памяти - DHAT. Оба числа точны: инструкции сравнивают, а выделения ограничивает пол, заявленный сценарием. В ruststream отдельный бенчмарк измеряет время по часам на собственных сценариях. Оно шумное, и just bench печатает его для справки.

Сравнение, а не порог. С аргументом --baseline=main команда измеряет ветку против тех же бенчмарков на целевой ветке. Автор пул-реквеста запускает сравнение сам и приводит его числа.

Провал - это рост инструкций в сценарии сверх допуска крейта или выделения памяти сверх заявленных сценарием. Допуск у каждого крейта свой. В ruststream он равен двум процентам, а допуск адаптации библиотеки брокера указан на её странице бенчмарков. Допуск относительный, поэтому действует только при сравнении с базовым прогоном. Пол выделений памяти проверяется на каждом прогоне. Изменение, которое удешевляет сценарий, в том же пул-реквесте опускает и его заявленный пол. Холодный старт проверяется так же.