lenec ru

← все посты

Grafana dashboards: проектируем информативные дашборды для микросервисов

19K

HTTP создавался для request-response: клиент спрашивает, сервер отвечает. Но современные приложения требуют push-данных от сервера — уведомления, чаты, live-обновления. Три основных подхода: Long Polling, Server-Sent Events и WebSocket. Каждый со своими trade-offs.

Long Polling: старый добрый hack

Клиент отправляет запрос, сервер держит соединение открытым до появления данных (или таймаута). Получив ответ, клиент немедленно отправляет новый запрос:

// Клиент: рекурсивный long polling
async function poll() {
  try {
    const res = await fetch('/api/events?timeout=30000');
    const data = await res.json();
    handleEvent(data);
  } catch (e) {
    await sleep(1000); // backoff при ошибке
  }
  poll(); // сразу следующий запрос
}

// Сервер: держим запрос до события
app.get('/api/events', async (req, res) => {
  const timeout = Number(req.query.timeout) || 30000;
  const event = await waitForEvent(req.user.id, timeout);
  if (event) {
    res.json(event);
  } else {
    res.status(204).end(); // таймаут, нет данных
  }
});

Overhead: каждый цикл — полный HTTP-запрос с заголовками (1-2 KB). При высокой частоте событий — десятки запросов в секунду на клиента. Когда актуален: legacy-системы без WebSocket-поддержки, корпоративные прокси, которые режут upgrade.

SSE: однонаправленный поток

Server-Sent Events — стандартный механизм push от сервера. Одно HTTP-соединение, данные текут в формате text/event-stream:

// Сервер: SSE endpoint
app.get('/api/stream', (req, res) => {
  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive'
  });

  // Отправка события
  function send(event: string, data: unknown) {
    res.write(`event: ${event}\n`);
    res.write(`data: ${JSON.stringify(data)}\n\n`);
  }

  // Подписка на события
  const unsub = eventBus.subscribe(req.user.id, (event) => {
    send(event.type, event.payload);
  });

  // Heartbeat каждые 30 сек (keep-alive через прокси)
  const heartbeat = setInterval(() => res.write(': ping\n\n'), 30000);

  req.on('close', () => {
    unsub();
    clearInterval(heartbeat);
  });
});

// Клиент: EventSource API (встроен в браузер)
const source = new EventSource('/api/stream');
source.addEventListener('notification', (e) => {
  const data = JSON.parse(e.data);
  showNotification(data);
});
// Автоматический reconnect при обрыве!

Преимущества SSE: автоматический reconnect с Last-Event-ID, работает через HTTP/2 мультиплексирование, не требует специальных библиотек на клиенте. Ограничение: только сервер → клиент.

WebSocket: полный дуплекс

WebSocket начинается как HTTP Upgrade, затем переключается на бинарный фреймовый протокол. Оба направления одновременно:

// Сервер: WebSocket с ws
import { WebSocketServer } from 'ws';

const wss = new WebSocketServer({ server: httpServer });

wss.on('connection', (ws, req) => {
  const userId = authenticate(req);

  ws.on('message', (raw) => {
    const msg = JSON.parse(raw.toString());
    if (msg.type === 'chat') {
      broadcastToRoom(msg.roomId, {
        type: 'chat',
        from: userId,
        text: msg.text,
        ts: Date.now()
      });
    }
  });

  ws.on('close', () => removeFromRooms(userId));

  // Ping/pong для обнаружения мёртвых соединений
  const pingInterval = setInterval(() => {
    if (ws.readyState === ws.OPEN) ws.ping();
  }, 30000);
  ws.on('close', () => clearInterval(pingInterval));
});

// Клиент
const ws = new WebSocket('wss://api.example.com/ws');
ws.onmessage = (e) => {
  const msg = JSON.parse(e.data);
  renderMessage(msg);
};
ws.send(JSON.stringify({ type: 'chat', roomId: '1', text: 'Hello' }));

Сравнение: latency, масштабирование, совместимость

+-------------------+---------------+----------------+----------------+
| Критерий          | Long Polling  | SSE            | WebSocket      |
+-------------------+---------------+----------------+----------------+
| Направление       | server→client | server→client  | bidirectional  |
| Latency           | высокая       | низкая         | минимальная    |
| Overhead          | высокий       | низкий         | минимальный    |
| Reconnect         | ручной        | автоматический | ручной         |
| HTTP/2 multiplex  | нет           | да             | нет (отд. TCP) |
| CDN/proxy         | отлично       | хорошо         | проблемы       |
| Макс. соединений  | 6 per domain  | 6 (HTTP/1.1)   | без лимита     |
| Бинарные данные   | нет           | нет            | да             |
+-------------------+---------------+----------------+----------------+

Масштабирование: SSE и WebSocket держат постоянные соединения. На одном сервере — десятки тысяч. Но при горизонтальном масштабировании нужен sticky sessions или pub/sub (Redis) для broadcast между инстансами.

Когда что выбрать

  • Notifications, live feed, stock prices → SSE. Однонаправленный поток, автоматический reconnect, работает через CDN
  • Chat, collaborative editing, gaming → WebSocket. Нужен двусторонний обмен с минимальной задержкой
  • Legacy, корпоративные прокси → Long Polling. Работает везде, где работает HTTP
  • Редкие обновления (раз в минуту) → обычный polling с интервалом. Не усложняйте

Частая ошибка — использовать WebSocket для уведомлений. SSE проще, не требует библиотек на клиенте, автоматически переподключается и работает через HTTP/2 без дополнительного TCP-соединения. WebSocket оправдан только когда клиент активно отправляет данные серверу.

Начните с SSE для server-push сценариев. Переходите на WebSocket только если нужен полный дуплекс. Long Polling оставьте как fallback для сред, где ничего другое не работает.

Комментарии 0

  • Будьте первым, кто оставит комментарий.

Войдите, чтобы оставить комментарий.