Ядро
Общие переменные
Переменные variables: в perfscale и выходные данные шагов неизменяемы и
существуют в рамках одного VU — один VU не видит значений другого, и ничего
нельзя обновить в процессе прогона. Общие переменные закрывают этот пробел:
единое хранилище «ключ/значение» с областью видимости на весь прогон и
атомарными операциями, которое изменяется и читается из шагов через
std/set_shared_variable@v1 / std/get_shared_variable@v1. Они выражают
паттерны координации, недоступные переменным уровня VU:
- Общие счётчики — подсчёт событий по всем VU (
increment). - Очереди «производитель/потребитель» — один VU добавляет элементы работы,
остальные извлекают их через
popв порядке FIFO. - Барьеры — потребитель блокируется через
wait_for, пока не накопится N элементов или не изменится флаг.
Каждая операция — одно атомарное обращение: нет пары шагов «чтение-изменение-запись», которая могла бы привести к гонке, и нет примитива блокировки, который мог бы вызвать взаимную блокировку.
Эта страница — руководство. Параметры и выходные данные шагов описаны в справочнике действий.
Объявление (обязательно)
Общие переменные объявляются в файле конфигурации, рядом с variables:,
в виде отображения «имя → начальное JSON-значение». Тип выводится из
начального значения и определяет, какие операции допустимы:
# config.yaml
vus: 8
duration: 30s
shared_variables:
pending_orders: [] # list → append / pop / length_gte
approved_count: 0 # number → increment
last_error: null # any → set / get
Шаг, ссылающийся на необъявленное имя, или операция op, несовместимая с
объявленным типом (increment для списка), завершается ошибкой валидации
конфигурации ещё до запуска любого VU — опечатка никогда не превратится в
молчаливый null посреди теста. Динамического создания нет: шаги используют
только объявленные имена. При старте прогона объявленные имена (пере)устанавливаются
в свои начальные значения.
Очередь «производитель/потребитель»
Один VU (или один тестовый файл) производит элементы работы, остальные их
потребляют. append возвращает новую длину списка; pop извлекает и
возвращает первый элемент (null для пустого списка — проверяйте это через
check:, когда пустота является ошибкой, или используйте wait_for, чтобы
вместо этого заблокироваться):
# config.yaml
shared_variables:
pending_orders: []
# producer.yaml — every iteration enqueues one order
steps:
- name: enqueue order
use: std/set_shared_variable@v1
with:
name: pending_orders
op: append
value: { id: "ord-${seq}", total: "${randf(10,100,2)}" }
# consumer.yaml — block up to 10s for an item, then take it
steps:
- name: dequeue order
use: std/get_shared_variable@v1
with:
name: pending_orders
op: pop
wait_for: { length_gte: 1, timeout_ms: 10000 }
extract: { order_id: $.id, total: $.total }
check:
body_contains: ord-
Полезные данные интерполируются, как в любом другом шаге, — ${seq},
${randf(10, 100, 2)}, ${{ vars.* }}, выходные данные предыдущих шагов.
extract извлекает поля из значения тем же синтаксисом путей через точку
($.foo.bar, $.[0].x), что и extract в std/llm@v1; каждый
извлечённый ключ попадает на верхний уровень выходных данных шага, поэтому
outputs: + ${{ name.order_id }} работают дальше по цепочке.
Барьер: накопление N элементов
Потребитель, который не должен начинать работу, пока N производителей не
внесли свою часть, — классический рандеву нагрузочного тестирования.
wait_for блокирует выполнение (опрашивая значение с небольшой асинхронной
задержкой, без активного ожидания), пока условие не станет истинным:
steps:
- name: wait for batch of 10 orders
use: std/get_shared_variable@v1
with:
name: pending_orders
wait_for: { length_gte: 10, timeout_ms: 30000 }
wait_for принимает ровно одно условие — exists, equals: <json> или
length_gte: <int> — плюс timeout_ms (по умолчанию 5000). По истечении
таймаута шаг завершается с ошибкой и строкой [err], в которой указано
последнее наблюдавшееся значение (тот же контракт, что и у таймаута подписки
в std/pubsub@v1): отставший производитель — это именно тот сигнал, для
которого вы подбираете размер теста, а не молчаливый успех. Время ожидания
попадает в waited_ms и в сэмпл метрики shared_variable_wait_ms, поэтому
медленные барьеры видны в сводке.
Общий счётчик естественно сочетается с барьером — increment возвращает
новое значение:
# config.yaml
shared_variables:
approved_count: 0
steps:
- name: approve
use: std/set_shared_variable@v1
with: { name: approved_count, op: increment, value: 1 }
- name: proceed once 50 approvals are in
use: std/get_shared_variable@v1
with:
name: approved_count
wait_for: { equals: 50, timeout_ms: 60000 }
Драйверы: memory против redis
Хранилище подключается через драйвер, который выбирается для каждого шага:
| Драйвер | Хранилище |
|---|---|
memory (по умолчанию) | Глобальное для процесса отображение — общее для всех VU в одном процессе, внешний сервис не требуется |
Вендорные драйверы — Redis, … — это pro-capability, регистрируемые на том
же драйверном интерфейсе; неизвестное значение driver завершает шаг ошибкой
со списком зарегистрированных драйверов, так что всегда ясно, какую сборку
вы запускаете.
Хранилище memory локально для процесса: когда распределённый прогон
распределяет VU по нескольким агентам perfscaled, у каждого агента своё
хранилище — append на агенте A невидим для агента B. Межагентная
координация (очередь или барьер на весь кластер) требует сетевого драйвера,
например Redis; в пределах одного агента memory — самый быстрый вариант,
не требующий никаких запущенных сервисов.
Ограничения
- Нет динамического создания — необъявленные имена являются ошибками
конфигурации, поэтому всё общее состояние теста полностью перечислено в
shared_variables:. popработает по принципу FIFO, и просмотра по индексу (peek) нет; проверки полей полезных данных на стороне потребителя выполняются черезextractили шагstd/check@v1.wait_for— это опрос (polling), а не push-подписка, — держитеtimeout_msнебольшим в горячих циклах, чтобы зависший производитель быстро приводил к ошибке.