Нагрузочное тестирование при помощи PHP & Redis

28.10.2021

Внезапно образовалась задача: срочно (вот прям сейчас) провести всевозможное тестирование сервера (снаружи web-api) с внутреними разработками, на предмет корректной работы и автономности. Один из видов тестирования, который достался мне - нагрузочное тестирование.

Средства реализации

Так как это нагрузочное тестирование, то необходимо наличие многопоточности.

Моя повседневность это php, а многопоточность из коробки отсутствует, phthreads мёртв, но можно посмотреть в сторону многопроцессности pcntl_fork или на parallel.

Я выбрал многопроцессность, организуемую штатными linux средствами, с которыми я хорошо знаком - запуск фоновых процессов.

Не буду лукавить, на тот момент не было достаточного опыта быстро/на коленке сделать стабильный многозадачный тестер на php (кроме как описанным здесь способом), а вникать в новое просто не было времени. Однако, описанный ниже механизм решил поставленные задачи полностью.

Если говорить о многопроцессности, где процессы должны откуда-то быстро брать информацию для работы и куда-то быстро складывать результаты, при этом главный скрипт должен иметь ко всему этому параллельный доступ, то речь идет о key-values хранилище, а в моем случае именно о Redis.

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

В итоге средства реализации:

  • linux
  • php
  • redis для хранения данных между main и worker процессами
Для полноты картины можно почитать сосдение статьи про php скрипт в systemd и redis pub/sub на php.

Организация тестера

Main

Main - это основной (контролирующий) скрипт с бесконечным циклом (со sleep(1)), который порождает дочерние worker'ов.

Основные задачи main:

  • следить за количеством worker'ов и вовремя их порождать
•••
php
// количество запущенных процессов $iProccess = intval(shell_exec('ps aux | grep receipt-send-worker.php | wc -l'))-1; // если количесво запущенных процессов меньше чем указано в конфиге if($iProccess < $aConfig['max_pproc']) { /* порождаем за текущую итерацию (в текущую секунду) столько процессов, сколько указано в конфиге, при этом не ждем их завершения, а отправляем в фон */ for($i=0; $i<$aConfig['run_proccess_per_sec']; ++$i) exec("php7.3 receipt-send-worker.php > /dev/null 2>&1 &"); }
  • агрегировать статистику, которую генерируют worker'ы и показывать ее в человекопонятном виде
•••
php
// очистка консоли echo chr(27).chr(91).'H'.chr(27).chr(91).'J'; // пример вывода статистики количества запросов и количества готовых чеков, а также их отношение $iSentRequests = intval($oLocalRedis->hGet('sent','requests')); $iSentReceipts = intval($oLocalRedis->hGet('sent','receipts')); $fRequestOnReceipt = round(($iSentReceipts > 0 ? $iSentRequests/$iSentReceipts : 0), 2); $fRequestOnReceiptReverse = round(($iSentReceipts > 0 ? $iSentReceipts/$iSentRequests : 0), 2); echo "Sent requests/receipts: $iSentRequests/$iSentReceipts = $fRequestOnReceipt($fRequestOnReceiptReverse)\n";

Пример статистики отображаемой main скриптом
Пример статистики отображаемой main скриптом

Среди дополнительных задач main скрипта:

  • загрузка данных для worker'ов в Redis
  • актуализация конфига (parse_ini_file хороший вариант), чтобы на каждой итерации иметь свежий конфиг, дабы избежать перезагрузку тестера

Worker

Worker - это порождаемый процесс main скриптом, который выполняет конкретную задачу.

Видов worker'ов было 2 (так как в нашей облачной кассе всего 2 важные операции):

  • фискализация заказа - постановка чека в очередь, если не удастся сразу пробить, и ожидание с периодическим опросом о состоянии статуса фискализации и сном
  • запрос статуса уже готового чека в большом количестве (предмет тестирования - связь различного ПО на сервере, например между целевым скриптом и БД). Однако, эту задачу недостаточно покрыть одним запросом в одном процессе, но multi curl решает эту проблему - один процесс может порождать 2 и более параллельных запроса (на своей рабочей машине удавалось по 200 параллельных запросов).
Задачи решаемые worker'ом:
  • выполнять свою основную функцию в зависимости от вида (суметь фискализировать чек или дождаться ответа от сервера)
  • сгенерировать статистику на основании своей главной задачи (засекать время удобно при помощи hrtime)

Установка среды

Детали установки php можно взять из соседней статьи про Локальный LAMP.

Но если вкратце то:

•••
bash
# ПО для добавления/удаления PPA: $ sudo apt install software-properties-common # добавляем репозиторий с php $ sudo add-apt-repository ppa:ondrej/php $ sudo apt update # устанавливаем php (на момент написания статьи работаю в основном с этой версией) $ sudo apt install php7.3 # устанавливаем модуль redis для php $ sudo apt install php7.3-redis

Для установки redis на linux рекомендую эту инструкцию.

Итог

Как оказалось нагрузочное тестирование с использованием php & redis вполне годно:

  • быстро по:
    • развертываемости, на моей рабочей машине среда уже была, а при портировании на выделенную машину организация среды занимает около 20 минут
    • написанию, первый тестер, в неопределенных условиях был написан за 4 часа
  • удобно по использованию
В итоге со стороны отдела администрирования сервер подкручен (теперь он может выдерживать бОльшие нагрузки), а со стороны отдела разработки выявлены и устранены недостатки внутреннего ПО.

А я получил скилл в тестировании не своего ПО :)