Два окна страницы прогона pacod-benchmark, светлое и тёмное, наложенные друг на друга, в каждом предупреждение, показатели прогона и таблица ячеек с вызовами в секунду, задержками, собственной задержкой шлюза и накладными расходами.
Завершённый прогон днём и ночью: управляемый режим против прокси на демо-шлюзе, девять готовых ячеек и собственное предупреждение прогона, что его цифры показывают поведение, а не пропускную способность.

pacod-benchmark измеряет pacod-gw (его режимы managed и proxy, минимальный шлюз, варианты развёртывания) и другие шлюзы — во многих конфигурациях за один прогон. Каждая ячейка измеряется относительно базовой линии, которая вызывает mock-провайдер напрямую, так что остаток — это то, что добавил шлюз. Интерфейс составляет наборы, следит за прогонами секунда за секундой и сравнивает их; отчёты уходят в консоль шлюза.

Бенчмарки шлюза

Возможности

Наборы с осями

Одна базовая конфигурация и поля, которые она меняет, названные по пути через точку. Каждая комбинация становится ячейкой; то, что не может выполниться, отбрасывается с указанием причины.

Базовая линия для каждой ячейки

Та же нагрузка от тех же агентов напрямую к моку. Накладные расходы ячейки — её задержка минус задержка базовой линии, включая то, чего не видит собственный таймер шлюза.

Сцена, которую ставят и убирают

На управляемом pacod-gw каждый прогон создаёт свою временную организацию, рабочие пространства, ключи и mock-провайдеры, а потом удаляет их, начиная с ключей.

Любой шлюз как цель

pacod-gw-lite, другая установка или другой продукт как внешняя цель — с ключом, который вставляется для каждого прогона и никогда не сохраняется.

Поведение или пропускная способность

Каждый прогон говорит, что показывают его цифры и почему — где стояли агенты, хватало ли им процессора, делил ли кто-то ещё шлюз или мок.

Подобное с подобным

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

Отчёты по контракту шлюза

Отчёт прогона — в собственном формате pacod-gw и проходит собственные проверки pacod-gw. Опубликуйте его в консоль или скачайте и загрузите туда.

Очередь и расписания

Один прогон за раз, остальные — в очереди, которую можно переупорядочить. Расписания запускают набор ежедневно или каждые несколько часов и могут опубликовать отчёт по окончании.

Командная строка

pacod-bench run выполняет набор на цели из терминала — без базы данных, входа и интерфейса — и записывает отчёт в файл.

Диаграмма платформы с выделенным pacod-benchmark и приглушёнными остальными проектами, коралловая пунктирная линия соединяет его с pacod-gw, а золотая точечная дуга — с pacod-id.
Место pacod-benchmark: внутри платформы, но вне пути вызовов любого продукта. Коралловая линия — нагрузка, которую он подаёт на pacod-gw, и отчёты, которые он там публикует, а золотая дуга — его собственный вход через pacod-id; приглушены вызовы LLM продуктов через pacod-gw (зелёный), вход и роли через pacod-id (золотой) и их использование людьми (серый).

Скриншоты

Светлый экран наборов pacod-benchmark с двумя наборами Managed against proxy mode, в версиях 2 и 1, с их осями и комбинациями, над формой создания нового набора и полем для импорта.
Наборы. Каждый — это одна конфигурация и оси, которые он меняет, здесь нагрузка и режим в шести комбинациях; новый набор начинается с примера или импортируется в YAML или JSON.
Светлый экран прогонов pacod-benchmark с одним завершённым прогоном на demo-gateway, запущенным admin@pacod.demo, и кнопками запуска прогона из набора и сравнения прогонов.
Прогоны, новые первыми: каждый — набор на цели, с ячейками (здесь шесть и три базовые линии), статусом и тем, кто его запустил. Сравнить можно от двух до шести.
Светлый экран агентов нагрузки pacod-benchmark с пояснением о поведении и пропускной способности и одним агентом local — свободен, в стеке, вызывает http://pacod-gw:8080, 2 процессора, его лимиты и версия контракта.
Агенты нагрузки: агенты самого pacod-gw, которые делают каждый вызов ячейки. Этот работает в стеке демо, поэтому его цифры показывают поведение; для пропускной способности нужны агенты на других машинах.
Тёмная панель pacod-benchmark с таблицей последних прогонов и блоком агента local — свободен, в стеке, 2 процессора — и ограничениями в 256 ячеек и 4 часа на прогон.
Панель ночью: последние прогоны и агенты нагрузки — свободны ли они, где стоят и каковы ограничения прогона.
Тёмный экран бенчмарков консоли pacod-gw со списком опубликованного отчёта и формой загрузки другого.
Отчёты pacod-benchmark, опубликованные в консоли в разделе Platform → Testing → Benchmarks.

Зачем нужен бенчмарк

Шлюз оценивают по двум вещам: что он добавляет к каждому вызову и как ведёт себя, когда много вызовов приходит одновременно. Ни то ни другое не видно в одном нагрузочном тесте с одной настройкой. Большую часть времени вызова занимает провайдер; доля шлюза невелика, и она меняется с режимом, диалектом, кэшем, режимом аудита, нагрузкой и развёртыванием. pacod-benchmark измеряет эту долю по всем этим параметрам за один прогон, и каждый результат говорит, можно ли считать его пропускной способностью шлюза или только описанием его поведения.

Собственной нагрузки он не создаёт. Вызовы делают агенты нагрузки pacod-gw — те же, которыми пользуется тестер консоли для своих нагрузочных тестов, — а отчёты имеют формат, который определяет и проверяет pacod-gw. Бенчмарк планирует, готовит сцену, запускает агентов одновременно, суммирует их секунды и говорит, о чём могут свидетельствовать цифры.

Как он устроен

браузер ──► pacod-caddy ──► bench-ui (SvelteKit, вход через pacod-id)
                                │ только на сервере, с токеном человека
                                ▼
                        bench-api (pacod-bench serve) ──► PostgreSQL "bench"
                          │  токен админа     ──► API управления pacod-gw (сцена, живые цифры, отчёты)
                          │  токен агентов    ──► агенты нагрузки pacod-gw ──► шлюз или внешний URL
                          │                                            └─► мок напрямую (базовые линии)
                          └  сервисный клиент ──► pacod-id (роли, фид, ключи)
pacod-bench run (командная строка): тот же контроллер, без базы, без интерфейса, файл отчёта
  • Контроллер — это библиотека, pacod-bench-engine, без базы данных и без сервера. Сервис и командная строка выполняют один и тот же код.
  • Сервис, pacod-bench serve, содержит API, очередь прогонов, планировщик и базу PostgreSQL bench. Он слушает только во внутренней сети pacod-admin-net: его вызывает лишь сервер интерфейса.
  • Интерфейс — фронтенд на SvelteKit, который вызывает API со своего сервера с токеном вошедшего человека. Страница прогона следит за ним вживую по потоку server-sent events.
  • Агенты, мок и типы отчёта берутся из pacod-gw, зафиксированные на определённом коммите. Изменение контракта агента или отчёта начинается в pacod-gw и переносится сюда.

Набор

Набор — это базовая конфигурация и оси, которые он меняет, в YAML или JSON. Вот пример examples/suites/modes.yaml:

name: Managed against proxy mode
description: The same calls in both modes at three load levels, behind the same mock.
base:
  request: { interface: openai, mode: managed, stream: true, prompt_words: 60, max_tokens: 64 }
  mock: { family: openai, delay_ms: 200, chunk_delay_ms: 20, chunks: 20 }
  workspace: { cache: "off", audit: on_request }
  load:
    mode: concurrency
    stages: [{ duration_s: 0, target: 10 }, { duration_s: 60, target: 10 }]
    timeout_s: 120
    warmup_s: 5
axes:
  request.mode: [managed, proxy]
  load.target: [10, 50, 100]

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

Часть Поля
request Интерфейс (OpenAI, Anthropic, Gemini, собственный, эмбеддинги), режим managed или proxy, потоковый ответ или целиком, длина промпта, max_tokens, HTTP/1.1 или HTTP/2, дополнительные заголовки
mock Диалект, на котором шлюз говорит с провайдером, и задержка мока: до заголовка, между чанками, число чанков в ответе
workspace Кэш ответов (off, shadow, enforce), режим аудита, хранилище ответов, что происходит при заполненном спуле использования
key Инспекция (full или none — слепая пересылка в режиме proxy), политика учёта использования, предел одновременных вызовов
load Замкнутый цикл вызывающих (concurrency) или открытый поток поступлений (rate), его этапы, тайм-аут вызова, предел числа вызовов, разогрев, не входящий в цифры

Ось называет поле по пути через точку; load.target масштабирует каждый этап, так что рост остаётся ростом и при другой вершине. Комбинации, которые не могут выполниться, отбрасываются, и план говорит почему: режиму proxy нужен мок его собственного диалекта, собственный интерфейс есть только в режиме managed, кэш никогда не отвечает в режиме proxy и на эмбеддинги, а одна и та же конфигурация дважды остаётся одной. Порядок grouped держит вместе ячейки, которым нужна одна и та же конфигурация шлюза, чтобы шлюз перезагружался как можно реже; shuffled распределяет их в фиксированном порядке, чтобы медленный дрейф машины не совпал с одной из осей. Повторы выполняют каждую конфигурацию заново.

То, что решает только развёртывание (аналитика вкл. или выкл., хранилище кэша, схема масштабирования, минимальный шлюз, железо), осью не является. Каждое такое развёртывание регистрируется как отдельная цель и получает тот же набор. Примеры в репозитории охватывают оба режима, три задержки провайдера (20, 200 и 1000 мс), кэш ответов, ступени вызовов в секунду, трансляцию между диалектами и внешнюю цель. Полный формат описан в docs/suite-format.md.

Прогон шаг за шагом

  1. План. Набор разворачивается для цели. Внешняя цель отклоняет оси, которые бенчмарк не может там задать (mock, workspace, key). Прогон хранит свой план вместе с копиями набора, цели и агентов.
  2. Проверка агентов. Каждый агент должен отвечать, говорить по этому контракту, быть свободным и допускать свою долю каждой ячейки.
  3. Подготовка сцены на управляемом шлюзе: прочитать его отпечаток, создать собственные mock-провайдеры бенчмарка, временную организацию bench-<run>, по рабочему пространству на вариант с явной привязкой кэша и выдать доступ к mock-моделям. Затем дождаться, пока каждый экземпляр шлюза применит конфигурацию.
  4. Выполнение каждой ячейки. Задать профиль мока, если он изменился, и создать ключ ячейки (только её режим и модель, срок — 15 минут после планового конца ячейки), затем снова дождаться экземпляров. Дождаться, пока агенты освободятся, и около трёх секунд наблюдать за вызовами шлюза в полёте (их не ожидается). Запустить всех агентов одновременно. Раз в секунду собирать новые секунды каждого агента, каждые две секунды — собственные цифры шлюза. Затем остановить агентов, удалить ключ, подвести итог и отметить, что стоит знать читателю.
  5. Разбор сцены: сначала ключи, остальное — от новых к старым. То, что не удалось удалить, перечисляется как остаток, который уберёт администратор. Отмена идёт тем же путём, а если API остановится во время прогона, агенты остановятся сами, и следующий запуск уберёт то, что прогон создал.

Базовые ячейки выполняются первыми: тот же запрос и та же нагрузка от тех же агентов напрямую к моку, без всякого шлюза. Их ключ сравнения охватывает только то, что доходит до мока, поэтому ячейки managed и proxy делят одну базовую линию. Пока идёт нагрузка ячейки, в шлюз ничего не записывается, и тесты это проверяют.

Как читать цифры

Страница прогона показывает для каждой ячейки устойчивое окно — от конца разогрева до конца последнего этапа:

Цифра Что это
Вызовов в секунду Среднее по устойчивому окну
Первый байт, весь вызов Перцентили задержки, как их видели агенты
Шлюз Собственная задержка шлюза (x-gw-delay-ms, до заголовка ответа); n/a, если цель её не присылает
Накладные расходы Задержка ячейки минус задержка её базовой линии: что добавила цель, включая то, чего не видит собственная цифра шлюза, например пересылку потока
Отметки Агент в стеке шлюза, с нехваткой процессора или с отставанием; другой трафик на шлюзе; изменение конфигурации во время ячейки; тайм-ауты; короткое устойчивое окно

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

Ключ сравнения ячейки — это SHA-256 всей её конфигурации в каноническом JSON. Ячейки с одним ключом выполняли одну и ту же конфигурацию — в любом прогоне и на любой цели. Run again на завершённом прогоне ставит в очередь его собственную копию набора на той же цели с теми же агентами, так что ключи его ячеек совпадают с ключами первого прогона.

Цели и агенты

Цель Что там делает бенчмарк Как она добавляется
Управляемый pacod-gw Готовит сцену каждого прогона через API управления, с токеном администратора платформы В интерфейсе администратором, с токеном; или из конфигурации (BENCH_GW_ADMIN_URL, BENCH_GW_TARGETS)
Внешняя цель Ничего: отправляет вызовы на URL и модель с ключом, который вставляется для каждого прогона В интерфейсе администратором

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

Агенты — это агенты нагрузки pacod-gw, перечисленные в BENCH_AGENTS или добавленные администратором в интерфейсе. Агент выполняет один тест за раз, так что бенчмарк и тестер консоли никогда не нагружают одного агента одновременно; ячейка ждёт занятого агента, а затем пропускается с отметкой.

Где работает агент Как
В собственном стеке бенчмарка bench-load-agent
В стеке pacod-gw Его gw-load-agent (docker-compose.load.yml)
На другой машине deploy/load-agent/ из pacod-gw, нацеленный на публичную плоскость данных шлюза
В Kubernetes Helm-чарт pacod-gw, loadAgent.enabled

Каждый агент объявляется как in_stack, same_host или remote, и только удалённые агенты могут дать прогон пропускной способности.

Отчёты

Отчёт завершённого прогона, pacod-bench-report/1, собирается собственным крейтом pacod-gw и проверяется его собственной валидацией. В нём цель; факты о развёртывании, каждый либо наблюдаемый у шлюза, либо объявленный оператором; агенты и где они стояли; поведение или пропускная способность и почему; и каждая ячейка с цифрами, гистограммами, секундами и отметками. Секретов в нём нет, и он называет того, кто выполнил прогон, а не того, кто его опубликовал, так что скачанный и опубликованный отчёт — один и тот же.

  • Publish to pacod-gw отправляет его в API управления шлюза; он появляется в разделе Platform → Testing → Benchmarks.
  • Download отдаёт те же байты для загрузки в любую консоль pacod-gw. Шлюз хранит отчёт один раз на SHA-256, так что один и тот же отчёт дважды — это один отчёт.
  • pacod-bench validate report.json проверяет файл так же, как шлюз.

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

Роли и безопасность

Роли принадлежат pacod-id, описаны в authz/bench.yaml и передаются в pacod-id при каждом запуске. Неявных ролей нет: учётная запись без роли видит «no access».

Роль Может
bench.viewer Читать агентов, цели, наборы, прогоны и результаты; скачивать отчёты
bench.operator А также писать наборы, запускать и отменять прогоны, публиковать отчёты
bench.admin А также добавлять шлюзы, внешние цели и агентов, менять настройки прогонов, удалять прогоны, убирать остатки, выдавать роли

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

  • Список разрешённого. У клиента API управления есть метод на каждый вызов, который делает прогон, и нет общего. Он удаляет только внутри временной организации самого прогона и пишет провайдеров и модели только под mock-именами бенчмарка.
  • Запечатанные токены. Токен шлюза, добавленного в интерфейсе, шифруется собственным ключом данных (AES-256-GCM), который обёрнут ключом BENCH_KEK из окружения или файла, но никогда не из базы. Идентификатор цели и её API управления вшиты в шифрование: токен, скопированный на другую цель, или токен, у которого в базе поменяли адрес, не откроется. Ни ответ, ни прогон, ни отчёт, ни лог его не содержат.
  • Вне досягаемости. API слушает только в pacod-admin-net, и Caddy никогда его не публикует. Журнал аудита шлюза называет бенчмарк при каждом его изменении.
  • Ключи. Вставленный ключ внешней цели остаётся в памяти и никогда не записывается. Временный ключ ячейки разрешает только её режим и модель.
  • Агенты. Агент сравнивает токен за постоянное время, а сам токен — не короче 32 символов. API говорит с агентами по обычному HTTP, поэтому агентам на других машинах нужна частная сеть или VPN.
  • Расписания срабатывают с полномочиями своего создателя: при каждом срабатывании бенчмарк проверяет, может ли создатель всё ещё запускать прогоны, и иначе отключает расписание.

Интерфейс входит как конфиденциальный клиент pacod-id с PKCE. API принимает только токены, выпущенные для его собственной аудитории, и отклоняет их, если его копия ролей старше минуты.

Развёртывание и эксплуатация

Сервис Что
bench-api pacod-bench serve: API, очередь прогонов и планировщик; метрики на порту 9190
bench-ui Интерфейс, за Caddy
bench-load-agent, bench-load-mock Только в автономном стеке: агент нагрузки и mock-провайдер pacod-gw
bench-gw-lite Профиль dev-gateway: pacod-gw-lite перед моком, для регистрации как внешней цели

docker-compose.yml содержит собственные сервисы бенчмарка; docker-compose.standalone.yml добавляет его копии общих компонентов (PostgreSQL, pacod-id, Caddy, Prometheus), а также собственные агент и мок. На ноутбуке make dev-up запускает стек на http://localhost:18380 (вход как admin@pacod-bench.test, пароль pacod-bench-dev), нацеленный на стек разработки pacod-gw на той же машине. Бенчмарк управляет этим шлюзом, как только получает один из его токенов администратора платформы — в интерфейсе или в .env.

pacod-demo загружает только первый файл, рядом с другими продуктами, и направляет его на демо-шлюз, его агент нагрузки и его мок; интерфейс доступен на bench.demo.pacod.io. Собственный Helm-чарт запланирован.

Одновременно идёт один прогон; остальные ждут в очереди, которую переупорядочивает страница Runs. Расписания запускают набор на управляемом шлюзе по UTC — ежедневно в заданное время по выбранным дням недели или каждые несколько часов. Срабатывание пропускается, пока предыдущий прогон расписания ещё ждёт или идёт, а срабатывание, пропущенное больше чем на 15 минут, не наверстывается. Настройки администратора могут опустить пределы прогона (ячейки, секунды, ожидание занятого агента) ниже пределов окружения, по умолчанию 256 ячеек и четыре часа.

Каждый сервис пишет одно событие на строку в stdout, в JSON или текстом. Runbook описывает конфигурацию, токен шлюза, цели, агентов, ротацию BENCH_KEK и поиск неисправностей.

Командная строка

pacod-bench run выполняет набор из терминала и записывает отчёт. Файл цели называет, где лежат секреты, но никогда не сами секреты:

name = "lite on the laptop"
product = "pacod_gw_lite"          # pacod_gw, pacod_gw_lite, third_party
[external]
base_url = "http://127.0.0.1:18381"
model = "openai/gpt-mock"
key_env = "BENCH_TARGET_KEY"
paid = false
mock = { family = "openai", delay_ms = 200, chunk_delay_ms = 20, chunks = 20 }
[declared]
layout = "lite"
arch = "arm64"

Управляемый шлюз вместо этого получает [managed] — с его API управления, переменной с его токеном и моком, каким его видит шлюз. Ctrl-C отменяет прогон, и сцена сначала разбирается. scripts/build-binaries.sh собирает командную строку как нативный бинарный файл для Linux (статический) и macOS.

Что проверено и где пределы

  • Контроллер тестируется против имитации API управления и агента внутри процесса: управляемый прогон готовит сцену, измеряет и разбирает её, ничего не записывая во время нагрузки ячейки; внешний прогон ничего не готовит и хранит лишь подсказку о ключе; занятый агент останавливает прогон до того, как что-либо начнётся.
  • API тестируется против PostgreSQL: набор от API до конца, отмена во время прогона, шлюз, добавленный в интерфейсе и никогда не показывающий свой токен, перезапуск, который убирает оставленное прерванным прогоном, очередь, настройки, агенты, добавленные в интерфейсе, и расписания, которые срабатывают, публикуют и никогда не наверстывают пропущенное срабатывание.
  • Контракты агента нагрузки и отчёта тестируются на собственных фикстурах pacod-gw, а make vendor-check отклоняет копию pacod-gw, взятую не из коммита.
  • В pacod-demo каждый прогон на демо-шлюзе показывает поведение, а не пропускную способность: агент работает в стеке шлюза на той же машине, а мок общий. Для цифр пропускной способности нужны агенты на других машинах и собственный мок.
  • Токен прогона — токен администратора платформы: более узкой роли для него в pacod-gw пока нет.
  • Ячейка с кэшем измеряет попадание, а не смесь попаданий и промахов: каждый вызов ячейки отправляет одно и то же тело.
  • Собственного Helm-чарта пока нет.

Документ об архитектуре показывает, как устроен код — от плана до отчёта.

Быстрый старт

Стек рядом со стеком разработки pacod-gw

make dev-up
open http://localhost:18380

Командная строка, без Docker

make plan SUITE=examples/suites/modes.yaml
cargo run -p pacod-bench -- run --suite examples/suites/modes.yaml --target target.toml --agents local=http://127.0.0.1:18096 --out report.json
cargo run -p pacod-bench -- validate report.json

Работает с

  • pacod-gw: Шлюз, который он измеряет. Он управляет агентами нагрузки и моком pacod-gw, готовит сцену каждого прогона через API управления и публикует отчёты в разделе Platform → Testing → Benchmarks.
  • pacod-id: Вход в интерфейс идёт через pacod-id, а API проверяет каждого вызывающего по ролям из authz/bench.yaml, которые передаёт в pacod-id при каждом запуске.
  • pacod-demo: Работает на bench.demo.pacod.io рядом с другими продуктами, управляет демо-шлюзом с токеном, который выпускает bootstrap, и использует собственные агент нагрузки и мок шлюза.