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

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

Методология

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

Три цикла​

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

  • Сырой цикл sqlx. sqlx напрямую: цикл, который выполняет те же операторы, что брокер выполняет для той же таблицы, в том же порядке, на пуле, построенном так же. Операторы он берёт у диалекта, которым их строит брокер, поэтому оба цикла выполняют один и тот же SQL.
  • Крейт вручную. Этот крейт и ничего над ним: брокер подключается, InboxQueue открывает подписку, а цикл забирает доставки из её потока, декодирует каждую, читает поле и подтверждает её; или Repository и Routed, превращённые в издателей. Ни обработчика, ни приложения, ни диспетчеризации.
  • Сервис RustStream. Сервис, который пишет пользователь: таблица с #[derive(Inbox)], обработчик #[subscriber] и RustStream::new(..).with_broker(SqlxBroker::new(pool), ..).

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

Сценарии outbox - одно приложение в трёх вариантах, которые занимают три места: приложение без outbox, то же приложение с outbox, написанным вручную на голом sqlx, на соединениях, которые берёт middleware крейта, и то же приложение с outbox этого крейта. Сообщение, опубликованное не из обработчика, в последнем варианте идёт через Outbox::wrap.

Прогон​

  • Новая таблица на каждый прогон. Каждый прогон пересоздаёт свою таблицу и заполняет её, а затем разбирает. Сценарий публикации вместо этого пишет свои строки.
  • Окно открывается на первой обработанной доставке и закрывается на последней, во всех трёх циклах, поэтому запуск сервиса и открытие пула в него не входят. Окно сценария публикации идёт от первой публикации до последней. Каждая обработанная доставка уменьшает счётчик на единицу, и последний отсчёт будит прогон, поэтому прогон узнаёт, что таблица разобрана, не опрашивая её и не засыпая.
  • Число сообщений даёт пробный замер скорости сырого цикла, поэтому измеряемый прогон на любой машине длится не меньше пяти секунд.
  • Циклы чередуются: сырой, крейт, сервис, три круга. Каждый цикл сообщает свой лучший, медианный и худший круг. Главным числом служит лучший, потому что шум только замедляет прогон.
  • Outbox работает поверх Redis Pub/Sub, а сообщения публикует воркер, который держит не больше 1024 непрочитанных сообщений, потому что Pub/Sub теряет то, что не вмещает буфер подписки.

Разница меньше разброса между кругами любого из двух циклов отмечается как «неразличимо» и числом не публикуется. Строка получает пометку «упирается в базу данных», когда обмены с базой данных занимают хотя бы половину времени сообщения. Это арифметика. До кругов пробный замер засекает один оператор, на который отвечает сервер, на одном соединении: медиану 2000 обменов SELECT 1, которая публикуется вместе с машиной. Каждый сценарий объявляет, сколько обменов стоит ему сообщение: каждый оператор и каждое соединение, взятое из пула, которое sqlx проверяет запросом, прежде чем выдать. Строка получает пометку, когда время обмена, умноженное на это число, достигает половины измеренного времени на сообщение.

Сетка пропускной способности​

Каждая ячейка разбирает заполненную таблицу из 20 000 строк на пуле из 16 соединений, при одном, четырёх и восьми воркерах, по одной доставке и пакетами по 64. PostgreSQL и MySQL используют форму блокировки строк; SQLite на локальном файле в режиме WAL использует форму аренды, где захват - один оператор. Сырой цикл запускает n задач на одном пуле, и каждая в цикле захватывает, читает и удаляет, пока её захват не вернётся пустым. Цикл крейта запускает n задач на том же пуле, каждую на собственном брокере, который читает одну подписку. Сервис монтирует обработчик с workers(n) на одной подписке; один воркер - это простое монтирование, которое обрабатывает доставку, прежде чем взять следующую. Три круга, чередуясь, и лучший, медианный и худший круг каждого цикла.

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

Каждый сценарий графика стоимости - сервис, который пишет пользователь, собранный без фичи testing и запущенный на однопоточном рантайме против PostgreSQL из стенда, а рядом считается сырой цикл. Производитель на собственном потоке и собственном рантайме заполняет таблицу между запуском и разбором, поэтому заполнение не входит ни в один счёт. Сценарии outbox работают поверх MemoryBroker, по одному сообщению в пути за раз.

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

Счёт инструкций всё же меняется между прогонами, потому что от того, как часто ответ ещё в пути, когда клиент его опрашивает, зависит, какую часть чтения sqlx берёт сообщение. За пять прогонов одного дерева итог сценария сдвигался примерно до 7%, а его число на сообщение - примерно до 15%; сценарии, которые не обращаются к базе данных, совпадают с точностью до инструкции. Прогон против сохранённого базового замера падает, если сценарий тратит на десять процентов больше инструкций. Выделения памяти не меняются: каждый сценарий объявляет предел, наибольший счёт пяти полных прогонов плюс 0,1%, и каждый прогон выше него падает.

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

just bench

Рецепт поднимает стенд из docker-compose.test.yml (PostgreSQL, MySQL и Redis в Docker в сети хоста), прогоняет каждый сценарий и сетку пропускной способности, останавливает стенд и перезаписывает docs/benchmarks/results.json. Это занимает около получаса, и машина на это время нужна целиком. RUSTSTREAM_BENCH_SECONDS задаёт наименьшую длительность прогона, RUSTSTREAM_BENCH_THROUGHPUT_ROWS - число строк ячейки пропускной способности, а RUSTSTREAM_BENCH_PAIRS - число кругов. Рецепт очищает RUSTFLAGS, поэтому бинарник не привязан к процессору, на котором его собрали. Оба рецепта собирают бенчмарки так же, как собирается выпуск: с полной оптимизацией при компоновке через границы крейтов. Поэтому числа описывают тот код, который получает сервис.

just bench-code

Подсчёт кода поднимает тот же стенд и требует valgrind; исполнитель бенчмарков он устанавливает в той версии, которую закрепляет файл блокировки. just bench-code 5000 измеряет каждый сценарий на пяти тысячах доставок вместо тысячи: число устойчивее, а прогон дольше. just bench-code --save-baseline=main на main записывает базовый замер, а just bench-code --baseline=main на ветке сравнивает с ним.