Ядро

Нагрузочное тестирование GraphQL

perfscale нагружает GraphQL-эндпоинты через встроенный движок шагов: одна операция на шаг, запросы и мутации по HTTP POST (или опционально GET), причём запрос валидируется по схеме самого эндпоинта ещё до того, как будет отправлен.

Никакой кодогенерации и клиентских заглушек: документ — это обычный текст в YAML (или файл .graphql), переменные — JSON, а ответы возвращаются в виде структурированных data/errors, из которых следующие шаги извлекают данные подстановкой ${{ … }}.

Эта страница — руководство. Параметры и выходные данные шага описаны в справочнике действий; готовый к запуску сценарий — examples/graphql.test.yaml.

Одно действие: std/graphql@v1

steps:
  - name: fetch viewer
    use: std/graphql@v1
    with:
      url: https://api.example.com/graphql
      query: |
        query GetViewer($id: ID!) {
          viewer(id: $id) { id name }
        }
      variables: { "id": "${{ vars.viewer_id }}" }
    check:
      status: 200
    outputs: viewer

  - name: rename widget
    use: std/graphql@v1
    with:
      url: https://api.example.com/graphql
      query: |
        mutation Rename($id: ID!, $name: String!) {
          renameWidget(id: $id, name: $name) { id }
        }
      variables: { "id": "${{ viewer.data.viewer.id }}", "name": "w-${seq}" }
  • query задаётся инлайн (многострочный YAML хорошо читается); query_file указывает на файл .graphql — эти два параметра взаимоисключающие.
  • variables — JSON-объект. Подстановка ${{ … }} работает везде внутри with:, поэтому значения, извлечённые на предыдущих шагах, попадают сюда напрямую, а токены-генераторы с одинарными фигурными скобками (${uuid}, ${rand}, ${now}) раскрываются при каждом выполнении — каждая итерация может отправлять новое имя или id.
  • Документ с несколькими операциями требует operation: GetViewer, чтобы выбрать одну из них, — это тот же operationName, который получает сервер.
  • method: GET переносит операцию в query-параметры URL для чтений, кешируемых на CDN. Мутации остаются на POST (по умолчанию).

Валидация по схеме

Каждый запрос разбирается перед отправкой и — когда схема доступна — валидируется по ней. Опечатка заваливает шаг (и perfscale lint) с подсказкой «возможно, вы имели в виду» вместо того, чтобы тратить запросы на целевой сервис:

fetch viewer: query validation failed: unknown field 'viewr' on type 'Query' — did you mean 'viewer'?

Два источника схемы:

  • Introspection (по умолчанию) — движок один раз за прогон отправляет introspection-запрос на эндпоинт, кеширует схему на весь процесс и валидирует по ней каждый запрос. Один сетевой обход на эндпоинт, независимо от числа VU.
  • schema_file: schema.graphql — валидация по локальному SDL-файлу. Это запасной вариант для эндпоинтов, которые запрещают introspection (типично для продакшена): если introspection не удался и schema_file не задан, шаг всё равно выполняется — без валидации, с одной строкой [sys] в логе об этом. introspection: false полностью отключает получение схемы.

perfscale lint применяет тот же барьер: синтаксис — всегда (офлайн), схема — когда эндпоинт доступен или задан SDL-файл. perfscale lint --offline пропускает сетевую проверку.

Что считается ошибкой

Ошибки GraphQL приходят в теле ответа со статусом 200 OK, поэтому одного HTTP-статуса для вердикта недостаточно:

  • HTTP-статус ≥ 400 → шаг падает.
  • есть errors, а data равно null или отсутствует → шаг падает (ничего не разрешилось).
  • частичные data плюс errors → шаг проходит — сервер разрешил то, что смог, — а ошибки учитываются в graphql_errors.

Стандартные проверки check: работают без изменений: status, duration_ms_lt, body_contains (по сырому телу ответа). Проверки на уровне полей читают декодированную нагрузку через шаг std/check@v1:

  - name: viewer id present
    use: std/check@v1
    with:
      on: viewer.data.viewer
      message_contains: "id"   # …or assert via outputs interpolation

Метрики

  • graphql_req_duration — HDR-гистограмма в мс (процентили в сводке); время оборота каждой операции, один сэмпл на реально отправленный запрос, при успехе и при ошибке. Задавайте пороги через std/thresholds@v1: "graphql_req_duration": ["p99<200"].
  • graphql_req_failed — доля ошибок, вычисляемая из graphql_req_duration: один сэмпл 0/1 на вызов (1 = шаг упал: транспортная ошибка, HTTP-статус с ошибкой или errors без data — см. выше). Используйте её для SLO-гейтов: graphql_req_failed: ["rate<0.01"]. Подробности вычисления: metrics.md.
  • graphql_errors — счётчик, суммируется за прогон; ошибки уровня GraphQL, включая те, что при частичных данных не заваливают шаг. Эмитируется всегда (0 на чистых ответах), поэтому проверки graphql_errors: ["count==0"] разрешаются и на здоровых прогонах.
  • graphql_op_<operationName>_duration — HDR-гистограмма в мс; задержка по каждой операции, публикуется только когда операция именованная (явный operation или единственная именованная операция), поэтому кардинальность метрик ограничена определением теста. Каждая именованная операция получает и производную долю graphql_op_<operationName>_failed.
  • Запрос также попадает в стандартные агрегаты http_req_duration / http_req_failed / http_reqs, как любой HTTP-шаг.

Пул соединений

pool: per-vu (по умолчанию) привязывает шаг к шарду HTTP-клиента виртуального пользователя — то же поведение keep-alive, что и у std/http@v1, поэтому VU переиспользует свои тёплые соединения между итерациями. pool: shared сажает всех VU на один глобальный для процесса клиент: максимальное переиспользование соединений к одному эндпоинту ценой конкуренции за блокировку пула при очень большом числе VU.

Ограничения

  • Подписки не поддерживаются (транспорт — HTTP запрос/ответ); документ, содержащий подписку, не пройдёт валидацию.
  • Батчинг запросов (массивы операций) и инкрементальная доставка (@defer/@stream) не поддерживаются — одна операция на шаг.
  • query_file и schema_file — это доступ к файловой системе: они требуют allow_file_actions в конфигурации прогона и подчиняются ограничению fs_root.