К основному содержимому
performance⁠-⁠testing.ru

Тест, который падает сам: пороги в k6

Как превратить нагрузочный тест из отчёта, который никто не открывает, в проверку, роняющую пайплайн без участия человека.

k6infra-cimethodologyslo

Нагрузочный тест, результаты которого кто-то смотрит глазами, — это не проверка, а отчёт. А отчёт можно и не открыть.

Разница видна на пайплайне: юнит-тест либо зелёный, либо красный, и просить кого-то посмотреть на него не нужно. Нагрузочный тест выдаёт графики, а решение «прошло или нет» принимает человек, у которого сегодня релиз и полтора часа до конца дня. Угадайте, что он решит.

Порог как условие выхода

В k6 критерий приёмки живёт в самом сценарии.

export const options = {
  thresholds: {
    http_req_failed: ['rate<0.01'],
    http_req_duration: ['p(95)<200'],
  },
};

Здесь написано, что доля неуспешных запросов должна остаться ниже одного процента, а девяносто пятый перцентиль времени ответа — ниже двухсот миллисекунд. Нарушено хотя бы одно условие, и k6 завершается с ненулевым кодом: пайплайн краснеет сам, без человека в цепочке.

Код возврата тут вполне конкретный — в файле errext/exitcodes/codes.go он объявлен так:

// ThresholdsHaveFailed indicates that one or more thresholds have failed.
ThresholdsHaveFailed ExitCode = 99

Знать это число полезнее, чем кажется: рядом живут ScriptException со значением 107 и InvalidConfig со значением 104. Большинству CI хватает деления на ноль и не-ноль, но если вы собираете статистику по сборкам, различие между 99 и 107 отделяет настоящую деградацию производительности от сломанного скрипта — а это две разные проблемы и две разные команды.

Несколько порогов на одну метрику

Одним выражением дело не ограничено.

export const options = {
  thresholds: {
    http_req_duration: ['p(90) < 400', 'p(95) < 800', 'p(99.9) < 2000'],
  },
};

Такая запись описывает форму распределения, а не одну точку. Она допускает, что хвост длиннее середины, и запрещает ему уходить дальше двух секунд. Один порог по p(95) этого не выражает: он одинаково пропускает и ровное распределение, и то, где десятая доля процента запросов висит по минуте.

Не ждать конца теста

Часовой soak-тест, который на пятой минуте показал p99 в десять секунд, оставшиеся пятьдесят пять минут занимает стенд впустую.

thresholds: {
  http_req_duration: [
    { threshold: 'p(99) < 10', abortOnFail: true, delayAbortEval: '10s' },
  ],
}

delayAbortEval тут не украшение: первые секунды теста врут всегда. Пул соединений пустой, JIT не прогрет, кеши холодные — и p99 на второй секунде показывает цифру, не имеющую отношения к установившемуся режиму. Без задержки abortOnFail будет глушить тесты, которые на самом деле в порядке.

У облачного запуска есть отдельная оговорка, и она в документации написана прямым текстом. Пороги там вычисляются раз в шестьдесят секунд, поэтому остановка может опоздать на минуту.

Чего порогом не проверить

Не всякая метрика умеет всё.

Тип метрики Что можно писать в пороге
Counter count, rate
Gauge value
Rate rate
Trend avg, min, max, med, p(N), где N от 0 до 100

Практическое следствие одно, зато неприятное: перцентиль есть только у Trend. Если своя метрика заведена как Counter, а порог хочется по p(95), менять придётся тип метрики, а не выражение порога.

Где пороги держать

Их место в сценарии, а не в настройках CI.

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

Возражение против этого известно: разным окружениям нужны разные пороги, и на dev-стенде двести миллисекунд недостижимы. Но решается оно переменными окружения внутри сценария, а не переносом всего критерия наружу.

Чего пороги не делают

Они не говорят, почему тест упал.

Порог отвечает на вопрос «прошло или нет» и молчит про причину. Красный пайплайн с кодом 99 означает, что p95 уехал за двести миллисекунд, и ничего больше. Дальше нужны трассировка, профилировщик и человек, который умеет их читать. Автоматизация приёмки не заменяет разбор, она освобождает время на разбор, забирая себе ту часть, где человек и так ошибается чаще машины.

Источники

  1. Thresholds — официальная документация k6(Сайт)
  2. k6, errext/exitcodes/codes.go — коды возврата в исходнике(Репозиторий)

Доклады по теме