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

Нагрузочное тестирование позволяет определить пределы производительности приложения, использующего STOMP.js, а также выявить проблемы масштабирования, утечки памяти, задержки доставки сообщений и деградацию WebSocket-соединений при высокой интенсивности обмена данными.

В системах реального времени нагрузка на STOMP-клиент возникает в нескольких сценариях:

  • массовая публикация сообщений;
  • большое количество подписок;
  • высокая частота входящих кадров;
  • интенсивные переподключения;
  • работа через нестабильные сети;
  • одновременная обработка тысяч событий;
  • параллельная работа множества клиентов.

Нагрузочное тестирование особенно важно для:

  • чатов;
  • торговых платформ;
  • игровых серверов;
  • мониторинговых систем;
  • IoT-платформ;
  • систем уведомлений;
  • коллаборативных приложений.

Типы нагрузочного тестирования

Stress Testing

Проверка поведения системы при экстремальной нагрузке.

Цели:

  • обнаружение предельной пропускной способности;
  • выявление точек отказа;
  • проверка устойчивости брокера;
  • анализ восстановления после перегрузки.

Пример:

10000 сообщений/секунда
5000 активных подписчиков
1000 параллельных подключений

Spike Testing

Проверка реакции на резкий скачок нагрузки.

Типичный сценарий:

100 клиентов → 5000 клиентов за 5 секунд

Проблемы, которые выявляются:

  • блокировка event loop;
  • переполнение очередей;
  • потеря сообщений;
  • таймауты heartbeat;
  • отказ WebSocket.

Endurance Testing

Длительное тестирование под постоянной нагрузкой.

Продолжительность:

6 часов
12 часов
24 часа
72 часа

Цели:

  • поиск утечек памяти;
  • накопление таймеров;
  • деградация reconnect-механизмов;
  • рост CPU;
  • увеличение задержек.

Volume Testing

Тестирование больших объёмов данных.

Проверяются:

  • бинарные сообщения;
  • JSON большого размера;
  • большие batch-пакеты;
  • обработка фрагментированных кадров.

Архитектура нагрузочного тестирования

Основные компоненты

Типичная схема включает:

Load Generator
       ↓
STOMP.js Clients
       ↓
WebSocket
       ↓
STOMP Broker
       ↓
Consumers / Queues

Генераторы нагрузки

В роли генераторов нагрузки могут использоваться:

  • Node.js;
  • headless browser;
  • Docker-контейнеры;
  • Kubernetes pods;
  • специализированные инструменты.

Нагрузочное тестирование в Node.js

Базовая структура клиента

import { Client } fr om '@stomp/stompjs';
import WebSocket from 'ws';

global.WebSocket = WebSocket;

const client = new Client({
    brokerURL: 'ws://localhost:15674/ws',
    reconnectDelay: 0
});

client.onConn ect = () => {
    console.log('connected');
};

client.activate();

Массовое создание клиентов

Параллельные подключения

import { Client } from '@stomp/stompjs';
import WebSocket from 'ws';

global.WebSocket = WebSocket;

const clients = [];

for (let i = 0; i < 1000; i++) {
    const client = new Client({
        brokerURL: 'ws://localhost:15674/ws',
        reconnectDelay: 0
    });

    client.onConn ect = () => {
        console.log(`Client ${i} connected`);
    };

    client.activate();

    clients.push(client);
}

Проблемы массовых подключений

При большом количестве клиентов появляются:

Ограничения файловых дескрипторов

Linux:

ulimit -n 100000

Перегрузка event loop

Признаки:

  • рост latency;
  • зависание heartbeat;
  • увеличение reconnect;
  • задержка callback.

Ограничения WebSocket

Некоторые брокеры ограничивают:

  • количество соединений;
  • число подписок;
  • скорость публикаций;
  • размер кадров.

Нагрузочное тестирование публикации сообщений

Высокочастотная отправка

let sent = 0;

setInterval(() => {
    client.publish({
        destination: '/queue/load',
        body: JSON.stringify({
            index: sent,
            timestamp: Date.now()
        })
    });

    sent++;

    if (sent % 1000 === 0) {
        console.log(`Sent: ${sent}`);
    }
}, 1);

Проблемы интенсивной публикации

Переполнение TCP-буфера

Симптомы:

  • задержка отправки;
  • рост памяти;
  • блокировка сокета.

Давление на GC

Частое создание объектов:

{
    timestamp: Date.now(),
    payload: hugeObject
}

создаёт:

  • короткоживущие объекты;
  • постоянные minor GC;
  • stop-the-world паузы.

Оптимизация публикации

Повторное использование объектов

Плохо:

setInterval(() => {
    client.publish({
        destination: '/queue/test',
        body: JSON.stringify({
            time: Date.now()
        })
    });
}, 1);

Лучше:

const payload = {
    time: 0
};

setInterval(() => {
    payload.time = Date.now();

    client.publish({
        destination: '/queue/test',
        body: JSON.stringify(payload)
    });
}, 1);

Batch-отправка

const messages = [];

for (let i = 0; i < 100; i++) {
    messages.push({
        id: i,
        value: Math.random()
    });
}

client.publish({
    destination: '/queue/batch',
    body: JSON.stringify(messages)
});

Нагрузочное тестирование подписчиков

Массовые подписки

for (let i = 0; i < 5000; i++) {
    client.subscribe(`/topic/channel-${i}`, () => {});
}

Проблемы большого количества подписок

Рост памяти

Каждая подписка хранит:

  • callback;
  • subscription id;
  • destination;
  • ссылки на замыкания.

Замедление маршрутизации

Брокер начинает тратить больше времени на:

  • сопоставление destination;
  • fan-out;
  • ACL-проверки;
  • сериализацию.

Измерение производительности

Throughput

Количество сообщений в секунду.

Формула:

Throughput =

Пример:

const start = Date.now();
let messages = 0;

client.subscribe('/topic/load', () => {
    messages++;

    const elapsed = (Date.now() - start) / 1000;

    console.log(messages / elapsed);
});

Latency

Задержка между отправкой и получением.

Формула:

Latency = T_{receive} - T_{send}

Пример:

client.subscribe('/topic/test', message => {
    const data = JSON.parse(message.body);

    const latency = Date.now() - data.timestamp;

    console.log(`Latency: ${latency} ms`);
});

Percentile Latency

Средние значения часто бесполезны.

Пример:

Average: 20ms
P95: 400ms
P99: 3000ms

Это означает наличие серьёзных задержек у части сообщений.


Мониторинг памяти

Использование heap

setInterval(() => {
    const memory = process.memoryUsage();

    console.log({
        rss: memory.rss,
        heapUsed: memory.heapUsed,
        heapTotal: memory.heapTotal
    });
}, 5000);

Признаки утечки памяти

Постоянный рост heapUsed

100 MB
150 MB
220 MB
400 MB

без снижения после GC.


Причины утечек

Неудалённые подписки

Плохо:

client.subscribe('/topic/test', callback);

Лучше:

const subscription =
    client.subscribe('/topic/test', callback);

subscription.unsubscribe();

Накопление таймеров

setInterval(() => {
    reconnect();
}, 1000);

без очистки:

clearInterval(timer);

Анализ CPU

Высокая загрузка процессора

Причины:

  • JSON.parse;
  • JSON.stringify;
  • компрессия;
  • сериализация;
  • heartbeat;
  • reconnect storms.

Профилирование Node.js

Запуск:

node --inspect load-test.js

или:

node --prof load-test.js

Тестирование reconnect

Искусственный разрыв соединения

client.webSocket.close();

Массовые переподключения

const client = new Client({
    brokerURL: 'ws://localhost:15674/ws',
    reconnectDelay: 1000
});

Reconnect Storm

Проблема возникает, когда тысячи клиентов переподключаются одновременно.

Последствия:

  • DDoS-подобная нагрузка;
  • исчерпание сокетов;
  • перегрузка брокера;
  • отказ accept loop.

Randomized Reconnect

Правильный подход:

const reconnectDelay =
    Math.random() * 5000;

const client = new Client({
    brokerURL: 'ws://localhost:15674/ws',
    reconnectDelay
});

Тестирование heartbeat

Heartbeat под нагрузкой

const client = new Client({
    brokerURL: 'ws://localhost:15674/ws',
    heartbeatIncoming: 4000,
    heartbeatOutgoing: 4000
});

Проблемы heartbeat

Под высокой нагрузкой:

  • heartbeat задерживается;
  • event loop блокируется;
  • появляются false disconnect;
  • запускаются лишние reconnect.

Диагностика heartbeat

client.debug = str => {
    if (str.includes('PING') ||
        str.includes('PONG')) {
        console.log(str);
    }
};

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

Web STOMP

В RabbitMQ используется плагин Web STOMP:

rabbitmq_web_stomp

Ограничения RabbitMQ

Под высокой нагрузкой возможны:

  • memory alarm;
  • disk alarm;
  • queue overflow;
  • channel exhaustion.

Проверка очередей

rabbitmqctl list_queues

Метрики RabbitMQ

Ключевые показатели:

  • messages_ready;
  • messages_unacknowledged;
  • consumers;
  • memory;
  • disk_free;
  • socket usage.

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

Особенности ActiveMQ

Apache ActiveMQ активно использует:

  • dispatch queues;
  • prefetch buffers;
  • persistence store;
  • advisory messages.

Проблемы под нагрузкой

Slow Consumer

Если подписчик не успевает:

Producer > Broker > Consumer

начинается накопление сообщений.


Producer Flow Control

Брокер может блокировать publisher:

Usage Manager Memory Lim it reached

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

Ограничения браузеров

Браузеры имеют:

  • лимиты WebSocket;
  • ограничения памяти;
  • throttling таймеров;
  • особенности background tabs.

Тестирование во вкладках

Проблемы:

  • pause timers;
  • задержка heartbeat;
  • suspend network;
  • падение FPS.

Инструменты нагрузочного тестирования

Artillery

Популярный инструмент для WebSocket-нагрузки.

Конфигурация:

config:
  target: "ws://localhost:15674/ws"

scenarios:
  - engine: ws
    flow:
      - send: "CONNECT"

k6

Поддерживает WebSocket-нагрузку.

Пример:

import ws from 'k6/ws';

export default function () {
    ws.connect(
        'ws://localhost:15674/ws',
        {},
        socket => {
            socket.send('CONNECT');
        }
    );
}

autocannon

Используется для HTTP-нагрузки вокруг STOMP-инфраструктуры.


Метрики нагрузочного тестирования

Client Metrics

На стороне клиента анализируются:

  • throughput;
  • latency;
  • reconnect count;
  • dropped messages;
  • heap size;
  • CPU usage.

Broker Metrics

На стороне брокера:

  • queue depth;
  • socket count;
  • disk I/O;
  • routing latency;
  • memory pressure.

Типичные узкие места

JSON-сериализация

Особенно тяжела при:

  • больших payload;
  • глубокой вложенности;
  • частой публикации.

Compression

permessage-deflate способен:

  • снижать трафик;
  • резко увеличивать CPU.

Event Loop Blocking

Опасный код:

while (true) {}

или:

heavySyncOperation();

приводит к:

  • heartbeat timeout;
  • reconnect;
  • packet loss.

Практика масштабирования

Горизонтальное масштабирование

Вместо:

1 process × 100000 clients

лучше:

10 processes × 10000 clients

Worker Threads

import {
    Worker
} from 'worker_threads';

позволяют:

  • распределять нагрузку;
  • изолировать CPU-heavy операции;
  • уменьшать блокировку event loop.

Кластеризация Node.js

import cluster from 'cluster';

используется для:

  • многопроцессной нагрузки;
  • распределения подключений;
  • симуляции большого числа клиентов.

Проверка потери сообщений

Счётчики сообщений

Publisher:

let sent = 0;

Subscriber:

let received = 0;

Сравнение

console.log({
    sent,
    received,
    lost: sent - received
});

Проверка порядка сообщений

Sequence ID

client.publish({
    destination: '/topic/order',
    body: JSON.stringify({
        seq: counter++
    })
});

Обнаружение нарушения порядка

let previous = -1;

client.subscribe('/topic/order', msg => {
    const current =
        JSON.parse(msg.body).seq;

    if (current <= previous) {
        console.log('Order violation');
    }

    previous = current;
});

Анализ стабильности

Признаки нестабильной системы

  • рост latency;
  • увеличение reconnect;
  • падение throughput;
  • рост памяти;
  • потеря heartbeat;
  • накопление очередей.

Практические сценарии

10000 сообщений в секунду

Проверяется:

  • скорость маршрутизации;
  • загрузка CPU;
  • стабильность WebSocket.

50000 подписчиков

Проверяется:

  • fan-out;
  • память брокера;
  • масштабирование topic exchange.

24-часовой тест

Проверяется:

  • утечка памяти;
  • reconnect stability;
  • degradation patterns.

Автоматизация нагрузочных тестов

CI/CD интеграция

Нагрузочные тесты могут запускаться:

  • nightly;
  • перед релизом;
  • после изменений брокера;
  • после обновления STOMP.js.

Regression Testing

Позволяет обнаружить:

  • ухудшение latency;
  • падение throughput;
  • увеличение памяти;
  • рост reconnect frequency.

Частые ошибки

Отсутствие cleanup

client.deactivate();

не вызывается после тестов.


Нереалистичная нагрузка

Проблема:

1 publisher
0 subscribers

не отражает production.


Игнорирование сети

В production влияют:

  • packet loss;
  • jitter;
  • bandwidth;
  • proxy;
  • TLS latency.

Подход к построению реалистичных тестов

Реалистичная модель нагрузки должна учитывать:

  • реальные интервалы сообщений;
  • burst-patterns;
  • reconnect пользователей;
  • разные размеры payload;
  • mix publish/subscribe;
  • географическую задержку;
  • мобильные сети;
  • TLS handshake;
  • нестабильные подключения;
  • разные типы брокеров;
  • ограничения браузеров и серверов.