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

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

Методология

Как ruststream-zeromq измеряет себя: три цикла, узел в том же процессе вместо брокера, арифметика «упирается в транспорт» и подсчёт кода. Процедура общая для всего фреймворка (методология RustStream), а эта страница говорит, как она выглядит на ZeroMQ.

Три цикла​

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

  • Сырой клиент. Сокеты zeromq, напрямую: получить, декодировать.
  • Крейт брокера. Этот крейт и ничего над ним: брокер подключается, открывается подписка, а цикл забирает сообщения из её потока, декодирует их и подтверждает.
  • RustStream. Сервис, который пишет пользователь: обработчик #[subscriber] и приложение вокруг него.

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

Тело сообщения - 512 байт JSON. Оно декодируется в структуру из двух целочисленных полей, поэтому декодирование ничего не выделяет, и число говорит об этом крейте, а не о парсере JSON.

Узел в том же процессе​

Каждый другой крейт брокера во фреймворке измеряет себя против сервера: запускается контейнер, клиент к нему подключается, и строка показывает, что крейт добавляет поверх этого клиента. Узлы ZeroMQ общаются напрямую. Второй конец каждого цикла - сокет PUSH в том же процессе с параметрами по умолчанию, и прогон не запускает ничего, кроме самого бинарника.

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

Прогон​

  • Новый адрес на каждый прогон: новый порт на loopback или новый путь ipc://, поэтому прогон никогда не встречает соединение, оставленное предыдущим.
  • Окно открывается на первом сообщении и закрывается, когда декодировано последнее, во всех трёх циклах. Привязка и рукопожатие в него не входят.
  • Число сообщений даёт пробный замер скорости сырого цикла, поэтому измеряемый прогон на любой машине длится не меньше пяти секунд.
  • Циклы чередуются: сырой, крейт, RustStream, три круга. Каждый цикл сообщает свой лучший, медианный и худший круг.

Строка получает пометку «упирается в транспорт», когда сообщение большую часть времени ждало транспорт, и работа над ним шла внутри ожидания, за которое уже заплачено. Это арифметика: круговая задержка одного обмена на том же транспорте, умноженная на число таких обменов на сообщение, против измеренного времени на сообщение. Круговая задержка измеряется парой REQ/REP вне всех циклов и публикуется вместе с машиной. Потребитель PUSH/PULL ничего не отправляет обратно, поэтому сообщение стоит ноль круговых обменов, и ни одна строка не упирается в транспорт.

Подсчёт кода​

Каждый сценарий графика стоимости - сервис, который пишет пользователь, на рабочем брокере, привязанном к TCP-порту на loopback, собранный без фичи testing. Узел - сырой сокет zeromq в собственном потоке, который пишет три кадра этого крейта.

Узел пишет все сообщения до начала подсчёта, пока сервис не читает, и сообщения ждут в буферах ядра. PUSH/PULL хранит их для потребителя, который не читает, поэтому сценарии чтения и пакета построены на нём. Сценарий ответа построен на DEALER/ROUTER, где ответ возвращается тому узлу, который спросил, и его подсчёт идёт, пока узел не прочтёт все ответы.

Инструкции даёт callgrind, выделения памяти - DHAT. Сценарий прогоняется на одном сообщении, затем на 1000, затем на 2000. Разность двух последних - установившаяся стоимость сообщения, а одно сообщение - холодный старт.

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

Как запустить самому​

just bench

Рецепт прогоняет оба сценария сравнения и перезаписывает docs/benchmarks/results.json. Стенд он не поднимает, потому что узел - сокет в том же процессе. Это занимает минуты, и машина на это время нужна целиком. Рецепт очищает RUSTFLAGS, поэтому бинарник не привязан к процессору, на котором его собрали. Оба рецепта собирают замеры так же, как собирается выпуск: с полной оптимизацией при компоновке через границы крейтов. Поэтому числа описывают тот код, который получает сервис.

just bench-code

Подсчёт кода занимает секунды и требует valgrind. Исполнитель бенчмарков рецепт устанавливает сам, той версии, которую закрепляет Cargo.lock выпуска. just bench-code --save-baseline=main на main записывает базовый замер, а just bench-code --baseline=main на ветке сравнивает с ним. just bench-code 2000 считает каждый сценарий на двух тысячах сообщений вместо тысячи.