Минимизация размера фреймов

В протоколе STOMP каждое сообщение передаётся в виде фрейма. Фрейм содержит:

  • команду (SEND, SUBSCRIBE, MESSAGE и другие);
  • набор заголовков;
  • тело сообщения;
  • завершающий нулевой байт.

При интенсивном обмене сообщениями размер каждого фрейма начинает напрямую влиять на:

  • нагрузку на сеть;
  • расход памяти;
  • пропускную способность брокера;
  • скорость сериализации;
  • задержки WebSocket-соединения;
  • производительность JavaScript-приложения.

Особенно критична минимизация фреймов в системах:

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

Даже уменьшение сообщения на 20–30 байт при сотнях тысяч сообщений в минуту может существенно снизить сетевые издержки.


Структура STOMP-фрейма

Типичный STOMP-фрейм выглядит следующим образом:

SEND
destination:/topic/chat
content-type:application/json

{"message":"Hello"}
^@

С точки зрения объёма передаваемых данных фрейм состоит из:

Часть Размер
Команда Небольшой
Заголовки Часто основной источник раздувания
Пустая строка 1 байт
Тело сообщения Основная полезная нагрузка
NULL-терминатор 1 байт

Наибольший вклад в увеличение размера обычно дают:

  • длинные заголовки;
  • verbose JSON;
  • дублирующие поля;
  • текстовые форматы;
  • избыточные метаданные.

Минимизация заголовков

Исключение необязательных заголовков

Многие приложения автоматически добавляют избыточные заголовки:

client.publish({
    destination: '/topic/data',
    headers: {
        'content-type': 'application/json',
        'x-client-version': '1.0.0',
        'x-user-agent': 'browser',
        'x-debug': 'true'
    },
    body: payload
});

В высоконагруженных системах подобные заголовки создают существенный overhead.

Минималистичный вариант:

client.publish({
    destination: '/topic/data',
    body: payload
});

Сокращение имён заголовков

Некоторые брокеры позволяют использовать пользовательские короткие заголовки:

headers: {
    t: 'json',
    v: '1'
}

Вместо:

headers: {
    'content-type': 'application/json',
    'api-version': '1'
}

Особенно полезно при миллионах сообщений.


Отказ от content-type

Если приложение всегда использует один формат данных, заголовок:

content-type:application/json

становится лишним.

Например:

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

Минимизация destination

Слишком длинные пути увеличивают каждый фрейм.

Плохо:

destination:/application/production/europe/chat/messages/general

Лучше:

destination:/chat/gen

На больших объёмах трафика это даёт заметную экономию.


Минимизация JSON

Удаление лишних полей

Плохо:

{
  "userId": 15,
  "userName": "alex",
  "userStatus": "online",
  "userMessage": "hello"
}

Лучше:

{
  "id": 15,
  "n": "alex",
  "s": 1,
  "m": "hello"
}

Использование числовых кодов

Текстовые значения занимают больше места.

Плохо:

{
  "status": "connected"
}

Лучше:

{
  "s": 1
}

Где:

Код Значение
0 disconnected
1 connected
2 reconnecting

Исключение форматирования

Нельзя отправлять prettified JSON:

JSON.stringify(data, null, 2)

Нужно:

JSON.stringify(data)

Разница особенно заметна при больших объектах.


Отказ от длинных ключей

Плохо:

{
  "temperatureValue": 25
}

Лучше:

{
  "t": 25
}

Исключение null-полей

Плохо:

{
  "name": "Alex",
  "email": null,
  "phone": null
}

Лучше:

{
  "name": "Alex"
}

Бинарные данные вместо JSON

Передача компактных строк

Иногда вместо JSON выгоднее использовать компактные строковые протоколы.

JSON:

{
  "x": 10,
  "y": 20,
  "z": 30
}

Компактный формат:

10|20|30

Использование Base64 только при необходимости

Base64 увеличивает размер данных примерно на 33%.

Плохо:

body: btoa(binaryData)

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


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

MessagePack значительно компактнее JSON.

Пример:

import msgpack from '@msgpack/msgpack';

const binary = msgpack.encode(data);

client.publish({
    destination: '/topic/bin',
    binaryBody: binary
});

Преимущества:

  • меньший размер;
  • быстрее сериализация;
  • ниже нагрузка на GC;
  • меньше сетевой трафик.

Минимизация тела сообщений

Диффы вместо полных объектов

Плохо:

{
  "id": 10,
  "name": "Alex",
  "online": true,
  "score": 1500,
  "rank": 12
}

Если изменился только score:

{
  "id": 10,
  "score": 1501
}

Передача изменений состояния

В real-time системах выгодно отправлять изменения:

{
  "dx": 2,
  "dy": -1
}

вместо полного состояния:

{
  "x": 1052,
  "y": 440
}

Агрегация событий

Вместо:

send(event1);
send(event2);
send(event3);

лучше:

send([event1, event2, event3]);

Это уменьшает:

  • число STOMP-фреймов;
  • число TCP-пакетов;
  • нагрузку на брокер.

Минимизация служебных кадров

Снижение количества heartbeat

Heartbeat создают постоянный служебный трафик.

Плохо:

heartbeatIncoming: 1000,
heartbeatOutgoing: 1000

Лучше:

heartbeatIncoming: 10000,
heartbeatOutgoing: 10000

или:

heartbeatIncoming: 0,
heartbeatOutgoing: 0

если инфраструктура допускает отключение heartbeat.


Ограничение reconnect spam

При нестабильной сети клиент может генерировать большое число служебных кадров.

Плохо:

reconnectDelay: 100

Лучше:

reconnectDelay: 5000

Минимизация подписок

Уменьшение количества SUBSCRIBE-фреймов

Избыточные подписки увеличивают трафик.

Плохо:

client.subscribe('/topic/a', cb);
client.subscribe('/topic/b', cb);
client.subscribe('/topic/c', cb);

Лучше:

client.subscribe('/topic/all', cb);

с фильтрацией на стороне клиента.


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

Плохо:

component.mount();
client.subscribe(...);

component.unmount();

component.mount();
client.subscribe(...);

Это создаёт лишние:

  • SUBSCRIBE;
  • UNSUBSCRIBE.

Лучше использовать пул подписок или shared subscription manager.


Компрессия WebSocket

Использование permessage-deflate

Большинство WebSocket-серверов поддерживают компрессию:

permessage-deflate

Это позволяет уменьшать размер STOMP-фреймов автоматически.

Особенно эффективно для:

  • JSON;
  • повторяющихся структур;
  • текстовых сообщений.

Ограничения компрессии

Компрессия может быть вредна при:

  • очень маленьких сообщениях;
  • высокой CPU-нагрузке;
  • больших объёмах мелких кадров.

В некоторых случаях сжатие увеличивает latency.


Минимизация ACK/NACK-трафика

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

Плохо:

client.subscribe('/queue/data', callback, {
    ack: 'client'
});

Каждое сообщение требует ACK-фрейма.

Лучше:

client.subscribe('/queue/data', callback, {
    ack: 'auto'
});

если допустима потеря части сообщений.


Batch ACK

Некоторые брокеры позволяют подтверждать сразу несколько сообщений.

Это снижает количество:

  • STOMP-фреймов;
  • системных вызовов;
  • сетевых операций.

Влияние минимизации на производительность

Снижение нагрузки на GC

Меньшие сообщения:

  • быстрее сериализуются;
  • создают меньше временных строк;
  • уменьшают количество объектов;
  • сокращают давление на garbage collector.

Уменьшение fragmentation

Крупные фреймы могут:

  • разбиваться на TCP-пакеты;
  • вызывать fragmentation;
  • увеличивать latency.

Компактные сообщения проходят через сеть эффективнее.


Снижение задержек

Меньшие фреймы:

  • быстрее кодируются;
  • быстрее отправляются;
  • быстрее обрабатываются брокером;
  • быстрее парсятся клиентом.

Практика минимизации в STOMP.js

Компактный publish

Неоптимально:

client.publish({
    destination: '/application/production/chat/general',
    headers: {
        'content-type': 'application/json',
        'x-app-version': '1.5.0',
        'x-platform': 'web'
    },
    body: JSON.stringify({
        userIdentifier: 100,
        userName: 'Alex',
        userCurrentStatus: 'online',
        userMessageText: 'Hello world'
    }, null, 2)
});

Оптимизированный вариант:

client.publish({
    destination: '/c/g',
    body: JSON.stringify({
        i: 100,
        n: 'Alex',
        s: 1,
        m: 'Hello world'
    })
});

Анализ экономии

Исходный вариант

destination:/application/production/chat/general
content-type:application/json
x-app-version:1.5.0
x-platform:web

{
  "userIdentifier":100,
  "userName":"Alex",
  "userCurrentStatus":"online",
  "userMessageText":"Hello world"
}

Размер может превышать 250 байт.


Оптимизированный вариант

destination:/c/g

{"i":100,"n":"Alex","s":1,"m":"Hello world"}

Размер — менее 70 байт.

Экономия:

  • более 70%;
  • меньше TCP-трафика;
  • ниже latency;
  • выше throughput.

Комбинирование методов

Максимальный эффект достигается при сочетании:

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

Типичные ошибки

Чрезмерная минификация

Слишком агрессивное сокращение ухудшает поддержку кода.

Плохо:

{
  "a": 1,
  "b": 2,
  "c": 3
}

Без документации такие структуры быстро становятся нечитаемыми.


Сжатие маленьких сообщений

Компрессия 20-байтных сообщений может:

  • увеличить CPU usage;
  • не дать выигрыша;
  • повысить задержки.

Огромные aggregated payload

Чрезмерный batch приводит к:

  • росту latency;
  • скачкам памяти;
  • крупным GC pause;
  • блокировкам event loop.

Избыточная сериализация

Плохо:

JSON.stringify(JSON.stringify(data))

Это:

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

Архитектурные подходы

Словари полей

Можно заранее договориться:

Ключ Поле
i id
n name
s status
t timestamp

Это позволяет резко уменьшить payload.


Версионирование протокола

При компактных схемах важно хранить версию:

{
  "v": 2,
  "d": {...}
}

Иначе изменение структуры приведёт к несовместимости клиентов.


Клиентские декодеры

Минифицированные структуры требуют декодирования:

function decode(data) {
    return {
        id: data.i,
        name: data.n,
        status: data.s
    };
}

Это сохраняет читаемость внутреннего API приложения.


Оптимизация STOMP.js на уровне клиента

Отключение debug

Плохо:

client.debug = console.log;

Логирование:

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

Лучше:

client.debug = () => {};

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

Плохо:

client.publish({
    destination: '/topic/data',
    body: JSON.stringify(createNewObject())
});

Лучше использовать object pooling при экстремальных нагрузках.


Минимизация промежуточных строк

Избыточная конкатенация строк:

body: '{ "x": ' + x + ', "y": ' + y + '}'

может создавать лишние временные объекты.

Предпочтительнее:

body: JSON.stringify({ x, y })

Метрики эффективности

Для оценки минимизации обычно измеряют:

Метрика Значение
bytes per message размер сообщения
messages per second throughput
latency задержка
GC pauses паузы сборщика
bandwidth usage расход сети
reconnect frequency стабильность
broker CPU usage нагрузка брокера

Реальные сценарии

Финансовые терминалы

Используются:

  • бинарные payload;
  • короткие ключи;
  • delta updates;
  • batch-сообщения.

Причина — экстремальные требования к latency.


Онлайн-игры

Часто применяются:

  • битовые маски;
  • бинарные координаты;
  • state diff;
  • tick aggregation.

IoT

Для устройств с плохой связью критичны:

  • минимальный размер кадров;
  • низкий расход трафика;
  • уменьшение heartbeat;
  • короткие команды.

Мобильные приложения

Компактные STOMP-фреймы:

  • снижают расход батареи;
  • уменьшают потребление мобильного трафика;
  • ускоряют работу при нестабильной сети.