Как решать капчу в нагрузочных тестах k6 через низкоуровневый API

Шаг решения капчи в k6 должен проходить через низкоуровневый HTTP API, потому что k6 не является Node, и в него нельзя установить SDK. Это самая простая часть: два вызова, отправка и опрос. По-настоящему важно то, где именно выполняется решение капчи. Поместите его в функцию default, и каждый виртуальный пользователь будет решать капчу на каждой итерации, что измеряет не ваше приложение, а сам решатель, и перегружает машину, которая для этого не предназначена. Поместите его в стадию setup, и вы получите чистые цифры, с одним честным ограничением, о котором стоит знать.
Что понадобится
- k6, любая недавняя версия. Устанавливать пакеты не нужно, потому что в k6 просто некуда их устанавливать.
- Sitekey и URL страницы защищённого эндпоинта, который вы тестируете.
- CapSkip, запущенный в режиме Local mode, если k6 работает на той же машине, что и решатель, или в режиме Server mode, если он работает на генераторе нагрузки или в CI. Оба варианта описаны в разделе Настройки подключения.
Когда решать капчу действительно стоит
Скажем прямо, прежде чем писать хоть одну строку кода. Если тестируемый сайт ваш собственный, обычно лучше просто пропустить генератор нагрузки мимо капчи: добавьте его адрес в allowlist или отправляйте заголовок, который распознаёт ваше staging-окружение, и полностью пропускайте виджет. Вы пытаетесь измерить своё приложение, а каждая решённая капча добавляет задержку, которая на самом деле принадлежит кому-то другому.
Решать капчу стоит в двух случаях. Либо нужный вам эндпоинт защищён, а вы не контролируете эту защиту, либо сам защищённый путь и есть то, что тестируется, и его пропуск означал бы тестирование маршрута, по которому реальный трафик никогда не идёт. Оба случая реальны, и весь остаток статьи посвящён именно им.
Почему здесь не работает SDK
Скрипты k6 выглядят как JavaScript, но выполняются не на Node. Реализация require в k6 своя собственная, и документация прямо говорит об ограничении: она загружает только встроенные модули k6, локальные файлы и удалённые скрипты и не поддерживает алгоритм разрешения модулей Node. Ни npm, ни node_modules, ни fs, ни crypto. Поэтому пакет CapSkip импортировать нельзя, как и что-либо ещё, к чему вы могли бы прибегнуть.
Это стоит меньше усилий, чем кажется на слух. API совместим с 2captcha и имеет всего два эндпоинта, поэтому собственный модуль http в k6 покрывает это примерно пятнадцатью строками кода. Если ваш генератор нагрузки в итоге окажется обычным процессом Node, обратитесь к странице сервиса распознавания капчи для Node.js и найдите там клиента с SDK, который вам подойдёт.
Шаг 1: напишите решение капчи как обычную функцию
Отправьте запрос на in.php, получите ID, затем опрашивайте res.php, пока не придёт ответ. Запрашивайте JSON, чтобы читать поля напрямую, а не разбирать строки по символу вертикальной черты.
// No install step. Both modules are built into k6.
import http from 'k6/http';
import { sleep } from 'k6';
const SOLVER = 'http://127.0.0.1:8080';
const KEY = 'YOUR_API_KEY';
function solve(sitekey, pageurl) {
const submitted = http.post(SOLVER + '/in.php', {
key: KEY,
method: 'userrecaptcha',
googlekey: sitekey,
pageurl: pageurl,
json: '1',
});
return poll(submitted.json('request')); // the captcha ID
}Обратите внимание на название параметра. reCAPTCHA требует googlekey, а Turnstile с методом turnstile требует sitekey. Отправка не того параметра обычно и есть причина ответа ERROR_GOOGLEKEY.
function poll(id) {
const url = SOLVER + '/res.php?key=' + KEY +
'&action=get&json=1&id=' + id;
// Roughly three minutes of headroom at five seconds a try.
for (let i = 0; i < 36; i++) {
sleep(5);
const res = http.get(url);
if (res.json('status') === 1) {
return res.json('request'); // the token
}
}
throw new Error('solve did not finish in time');
}Ответ, который ещё не готов, возвращается как CAPCHA_NOT_READY со статусом ноль, поэтому цикл проверяет именно поле status, а не считает готовым любой ответ. Результат можно прочитать только один раз, поэтому сохраняйте токен сразу, как только его получите.
Шаг 2: решайте капчу на стадии setup
k6 выполняет setup один раз, до старта любого виртуального пользователя, и передаёт всё, что она вернула, в функцию default. Именно такая форма здесь и нужна. Решайте капчу там, раздавайте токены, и ни один VU не будет платить временем решения внутри собственной итерации.
const PAGE = 'https://example.com/page-with-recaptcha';
const SITEKEY = 'YOUR_SITEKEY';
export const options = {
vus: 10,
duration: '90s',
setupTimeout: '5m', // the 60s default expires mid solve
};
export function setup() {
// One token per VU. They are single use.
const tokens = [];
for (let i = 0; i < 10; i++) {
tokens.push(solve(SITEKEY, PAGE));
}
return { tokens };
}Строка setupTimeout важнее, чем кажется на первый взгляд. По умолчанию k6 даёт стадии setup 60 секунд, а одно решение reCAPTCHA само по себе может занять большую часть этого времени. Десять решений подряд туда не влезут, стадия будет убита, и ошибка укажет на setup, а не на то место, куда вы бы стали смотреть в первую очередь.
Ограничение, о котором стоит знать, прежде чем строить на этом решение
Токены одноразовые и остаются действительными примерно две минуты. Обе половины этого ограничения важны. Одноразовость означает, что токенов нужно не меньше, чем защищённых запросов, поэтому тест, который бьёт по защищённому эндпоинту тысячу раз, требует тысячи решений и уже перестаёт быть похож на нагрузочный тест. Две минуты означают, что токены, решённые на стадии setup, уже начинают устаревать к моменту старта первого VU, поэтому долгий soak-тест большую часть времени будет отправлять просроченные токены.
Поэтому такой паттерн подходит для короткого всплеска нагрузки на защищённый эндпоинт, а не для получасового soak-теста. Отдельный пост про срок жизни токена reCAPTCHA содержит точные тайминги. Если вам нужна постоянная нагрузка через виджет, используйте способ с allowlist, упомянутый ранее: это единственный честный вариант получить её.
Шаг 3: не допускайте решатель в свои метрики
Всё, что вы отправляете через http-модуль k6, попадает в http_req_duration, включая вызовы решателя. p95, в который незаметно затесался пятнадцатисекундный опрос, не годится в качестве числа, на которое можно ориентироваться. Помечайте тегами важные для вас запросы и указывайте thresholds именно для этого тега.
export const options = {
vus: 10,
duration: '90s',
setupTimeout: '5m',
thresholds: {
// Measure the app, not the solve.
'http_req_duration{target:app}': ['p(95)<500'],
},
};
export default function (data) {
const token = data.tokens[__VU - 1];
http.post(PAGE, { 'g-recaptcha-response': token }, {
tags: { target: 'app' },
});
}Переменная __VU нумерует виртуальных пользователей начиная с единицы, поэтому индексирование массива токенов по ней даёт каждому VU свой собственный токен. Теги представляют собой более чистое решение, чем прятать решение капчи туда, куда k6 не заглядывает, потому что запросы решения всё равно остаются видны в выводе, когда вам нужно узнать, сколько времени они заняли.
Где должен находиться решатель
Генераторы нагрузки редко совпадают с вашим рабочим компьютером. k6 работает в CI, на выделенной машине или в управляемом сервисе, и адрес обратной петли на любой из них не совпадает с машиной, на которой стоит ваш решатель.
| Режим | Прослушивает | Когда использовать |
|---|---|---|
| Локально | 127.0.0.1, только это устройство | k6 и решатель на одной машине |
| Сервер | Ваш сетевой адрес или публичный IP | CI, парк генераторов нагрузки, управляемый раннер |
Server mode решает всё, что перечислено во второй строке: смените адрес прослушивания в приложении, укажите в скрипте этот хост, и все генераторы будут использовать общий решатель. Статический публичный IP рекомендуется, если вызывающие стороны находятся вне вашей сети. Это по-прежнему ваше оборудование и по-прежнему безлимитное решение, поэтому меняется только то, где выполняется решение, и ничего больше. CapSkip представляет собой Windows-приложение, так что это один компьютер с Windows, к которому обращаются генераторы. Один практический момент: не размещайте его за тем же балансировщиком нагрузки, который вы тестируете, иначе вы будете дважды измерять собственное узкое место.
Частые ошибки
| Что вы видите | Причина | Исправить |
|---|---|---|
| Не удаётся найти модуль ‘capskip’ | k6 не умеет разрешать npm-пакеты | Вызывайте API через http-модуль k6 |
| Истекло время выполнения setup() | Решения заняли больше времени, чем 60 секунд по умолчанию | Увеличьте setupTimeout, чтобы хватило на все решения |
| p95 огромен, хотя ничего не тормозит | Вызовы решателя учитываются в http_req_duration | Пометьте тегами запросы приложения и фильтруйте порог по тегу |
| Поздние итерации отклоняются, ранние проходят нормально | Токены устарели, срок их жизни истёк | Сократите прогон или решайте капчу позже и в меньшем количестве |
| Каждый VU получает один и тот же отказ | Один токен используется повторно во всех VU | Решайте по одному токену на VU и индексируйте по __VU |
Полный список кодов и параметров, которые принимает каждый метод, есть в документации CapSkip API.
FAQ
Могу ли я установить SDK CapSkip в k6?
Нет. k6 реализует собственный загрузчик модулей, который обрабатывает встроенные модули, локальные файлы и удалённые скрипты, и намеренно не следует алгоритму разрешения модулей Node, поэтому npm-пакеты недоступны. API совместим с 2captcha, поэтому эти два эндпоинта покрывают всё, что сделал бы за вас SDK, примерно пятнадцатью строками собственного http-модуля k6.
Стоит ли вместо этого решать капчу внутри функции default?
Только если само решение капчи и есть то, что вы тестируете. Функция default выполняется один раз за итерацию на каждый VU, поэтому двадцать VU в течение двух минут дают сотни решений, и ваши цифры задержки превращаются в измерение опроса. Решайте капчу в setup, раздавайте токены и оставляйте в итерации только тот запрос, который вам действительно важен.
Мой k6 работает в управляемом сервисе. Может ли он достучаться до решателя?
Да, в режиме Server mode. Решатель слушает сетевой адрес, а не адрес обратной петли, и генераторы обращаются к нему через API, как к любому другому внутреннему сервису. Статический публичный IP делает это стабильным, если раннеры находятся вне вашей сети. Включите проверку ключа и выдайте каждому окружению собственный ключ, чтобы можно было отозвать один ключ, не затрагивая остальные.
Как провести часовой нагрузочный тест через виджет?
Никак, по крайней мере не через настоящие токены. Одноразовость в сочетании с двухминутным сроком жизни означает, что для часа устойчивой нагрузки нужен непрерывный поток решений, а в этот момент тестируемой системой становится сам решатель. Для длительного прогона на собственном приложении освободите генератор нагрузки от капчи и тестируйте путь за ней.
Коротко
Используйте http-модуль k6 для запросов к in.php и res.php, решайте капчу в setup, увеличив setupTimeout, помечайте тегами запросы вашего приложения, чтобы пороги оставались осмысленными, и держите прогон достаточно коротким, чтобы токены оставались живыми. О стороне краулинга читайте на странице сервиса распознавания капчи для парсинга. Именно в нагрузочном тестировании оплата за каждое решение быстрее всего доходит до абсурда, потому что один-единственный всплеск может потребовать сотен токенов, не приносящих бизнесу никакой пользы. Вся разница заключается в модели ценообразования: обход капчи на оборудовании, которое у вас уже есть, стоит одинаково, запускаете вы один тест или сорок.
