Бенчмарки¶
Прослойка между клиентом Redis и типом сообщения стоит времени на каждой доставке: чтение,
декодирование, ack. Эта страница говорит, сколько именно. Точкой отсчёта служит та же работа,
написанная руками на fred.
Один процесс выполняет сценарий тремя способами. Сырой клиент - цикл, написанный руками на
fred. ruststream-fred - такой же цикл, написанный руками на этом крейте: брокер, подписка,
поток доставок и ack, без сервиса, без обработчика и без диспетчеризации. Сервис RustStream -
сервис, который пишет пользователь, запущенный через настоящий рантайм.
Отсюда две разницы, и отвечают они на разные вопросы. Прослойка против сырого клиента - это то, во что обходится собственный потребитель этого крейта сверх клиента, который он оборачивает; за это число отвечает данный репозиторий. Сервис против сырого клиента - это то, что пользователь платит целиком. Промежуток между ними - это то, во что обходится рантайм поверх именно этого брокера. Он публикуется по брокерам потому, что все прослойки тонкие, и если доля рантайма у брокеров разная, она живёт в том, как эти двое стыкуются: как поток отдаёт элементы, как приходят доставки, как обратное давление доходит до потребителя.
Всё остальное держится одинаковым: пул и его размер, группа потребителей и имя потребителя, команды
чтения с их COUNT и BLOCK, место подтверждения, декодирование в тот же тип, байты тела, рантайм
tokio и сборка. Процедура принадлежит самому фреймворку и описана в разделе
методики; здесь
опубликовано то, что она дала на этой машине.
Измеряются три формы потребителя, по одной на каждую форму доставки этого крейта: группа
потребителей Redis Streams с подтверждением каждой записи, надёжная очередь заданий на списке и
канал Pub/Sub. Каждая снимается против трёх форм сервера, к которым умеет подключаться крейт:
одиночный сервер, кластер и мастер за Sentinel. Подписка на всех трёх одна и та же, различается
только маршрутизация клиента под ней, поэтому разница между такими строками говорит именно о ней.
Единственное намеренное исключение: строка Pub/Sub на кластере шардированная, SSUBSCRIBE и
SPUBLISH, то есть та форма, с которой кластер и используют, тогда как классический PUBLISH
рассылается на каждый узел.
Числа¶
Лучший из трёх чередующихся раундов, в скобках медианный. Больше значит лучше. Разница, которая меньше разброса между прогонами, публикуется как «неразличимо», а не процентом.
Таблица читается из документа, указанного ниже, при каждом открытии страницы, поэтому показывает последний прогон и ничего кроме него.
Подтверждение доставки в Redis стоит отдельной команды - XACK для записи потока, LREM для записи
списка, - а на сервере, до которого идут через локальную петлю, такая команда стоит десятки
микросекунд. Это на порядок больше того, что доставка проводит внутри этого крейта. Потребитель
проводит своё окно в ожидании сокета, поэтому обе эти строки несут отметку «ограничено брокером»:
всё, что лежит над сокетом, делает свою работу внутри ожидания, за которое потребитель платил и
так. Для потребителя, который подтверждает каждое сообщение, это настоящий результат и одновременно
нижняя граница этих расходов, а не их измерение.
Pub/Sub ничего не подтверждает, и это та строка, где работе над сокетом есть где проявиться: доставка там - это чтение из сокета, снятие конверта и декодирование, и стоит она на порядок меньше доставки из потока.
Машинная форма того же прогона, из которой сайт фреймворка строит сводную таблицу по брокерам, лежит
по адресу
benchmarks/results.json.
Собственный код крейта¶
Вторая таблица - собственная цена сообщения в этом крейте, подсчитанная, а не засечённая по часам:
инструкции под callgrind и выделения памяти под DHAT. Каждый сценарий - сервис в том виде, в каком
его пишет пользователь: он построен на RedisBroker и запущен против одиночного сервера стенда,
поэтому каждая его команда - та, что отправляет настоящий сервис. Сценарий потока читает группу
потребителей RedisStream командой XREADGROUP, подтверждает доставку XACK и отправляет ответ
XADD через RedisPublish. Надёжный список переносит каждую запись командой LMOVE и
подтверждает её LREM. Сценарий Pub/Sub читает канал или шаблон и ничего не подтверждает. Сценарий
с .pipeline() отправляет подтверждения одной выборки вместе, а его обработчик добавляет по одной
своей команде INCR на доставку. .pipeline().atomic() заключает подтверждение каждой доставки в
собственный MULTI / EXEC.
Сервис работает на однопоточном рантайме tokio, и fred обслуживает свои соединения в том же
потоке выполнения. В подсчёт входит всё, что выполняется в этом потоке выполнения: фреймворк, этот
крейт и fred, который пишет команды и разбирает ответы. Сервер - отдельный процесс, и в число он
не входит; не входит и работа ядра при обращении к сокету. Сообщения дописываются в поток Redis из
другого потока выполнения до начала измеряемой выборки, поэтому их производство тоже не считается.
Инструкции и выделения даны на сообщение в установившемся режиме: это наклон между прогоном на 1000 доставок и прогоном на 2000. Последняя колонка - то, во что один раз обошлись подключение пула, создание группы потребителей, открытие подписки и первая доставка. Числа абсолютные, собственная цена фреймворка входит в них; отдельно её публикует ядро на своей странице бенчмарков.
Сервис говорит с настоящим сервером, поэтому подсчёт между прогонами немного плавает: за шесть
прогонов итоговые инструкции сценария расходились не больше чем на 0,4%, а выделения - на несколько
блоков. Поэтому порог каждого сценария - наибольшее увиденное число плюс запас в
0,1%. just bench-code падает, если выделений больше объявленного сценарием порога, а с
--baseline=main - ещё и на приросте инструкций больше двух процентов; запрос на слияние, который
меняет цену, приводит свои числа.
Машина¶
Флаги сборки публикуются вместе с числами, потому что они эти числа меняют. Бинарник, собранный с
-C target-cpu=native, даёт результат, который не воспроизвести на другой машине, поэтому рецепт
очищает переменную перед сборкой.
Чего эти числа не значат¶
Это один потребитель, один ключ, маленькое тело и сервер на локальной петле. Измеряется то, во что доставка обходится внутри этого крейта и в рантайме над ним, а не то, сколько способен пронести Redis. Строку отсюда нельзя сравнивать со строкой, опубликованной для другого брокера: транспорты выполняют на сообщение разную работу.
Окно прогона открывается на первой доставке и закрывается на последней, одинаково во всех трёх циклах, поэтому одно подтверждение за весь прогон остаётся за пределами числа везде.
Путь публикации в эти строки не входит. Все три цикла кормит один и тот же конвейерный издатель на
fred, чтобы всё, чем они различаются, оставалось на стороне потребления. Во что обходится
собственный издатель этого крейта - отдельное измерение.
Серверы стенда работают в сети хоста и без сохранения на диск: между клиентом и сервером нет прокси порта, нет журнала append-only, нет снимков. Измеряется стоимость доставки, а не мост перед сервером и не диск под ним. Сервис, который оставляет сохранение включённым, платит за него, и платит одинаково с обеих сторон.
Число Pub/Sub снято при издателе, который никогда не ждёт потребителя. Redis Pub/Sub выбрасывает то, что потребитель не готов взять, а не ставит в очередь, поэтому эта строка говорит о доставках в секунду у насыщенного потребителя.
Числа - снимок одной машины в один день. Их переснимают вручную на машине, отданной одному этому замеру: разница, о которой говорит эта страница, меньше шума машины, которую делят с другими задачами.
Как повторить¶
Рецепт поднимает стенд из docker-compose.test.yml, прогоняет все сценарии, гасит стенд и
переписывает docs/benchmarks/results.json тем, что намерил. Это занимает около десяти минут и
требует машину целиком. Число сообщений не фиксировано: пробный прогон подбирает его так, чтобы
каждый измеряемый прогон длился не меньше пяти секунд на той машине, где его снимают.
Рецепт поднимает тот же стенд, подсчитывает таблицу кода под valgrind против его одиночного
сервера, гасит стенд и переписывает раздел code того же документа. Ему нужны valgrind и
исполнитель бенчмарков: cargo install --locked gungraun-runner --version =0.19.4.