GraphQL Subscriptions: WebSocket, Redis, масштабирование под ключ

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.

Разработка и обслуживание любых видов сайтов:

Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Услуги, которые мы предлагаем
Показано 1 из 1Все 2062 услуг
GraphQL Subscriptions: WebSocket, Redis, масштабирование под ключ
Сложный
~3-5 дней
Часто задаваемые вопросы

Наши компетенции:

Этапы разработки

Последние работы

  • image_website-b2b-advance_0.webp
    Разработка сайта компании B2B ADVANCE
    1358
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1251
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    957
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1188
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    929
  • image_bitrix-bitrix-24-1c_fixper_448_0.webp
    Разработка веб-сайта для компании ФИКСПЕР
    947

Реализация GraphQL Subscriptions (real-time подписки)

На сайте с GraphQL API часто требуется обновлять данные без перезагрузки страницы: чат, трекинг заказов, live-статистика. Обычно решают через polling, но он создаёт лишнюю нагрузку на сервер и увеличивает задержки. Подписки GraphQL (Subscriptions) через WebSocket — более эффективное решение. Мы внедряем GraphQL Subscriptions с 2019 года, реализовали более 20 проектов с real-time функциональностью. В статье — практические решения, подводные камни и проверенные конфигурации.

В одном из проектов с 10 000 одновременных пользователей чата замена polling на подписки снизила нагрузку на сервер на 50% и сократила задержку обновления с 5 секунд до 200 миллисекунд. Такой результат возможен только при правильной архитектуре Subscriptions. Мы гарантируем стабильную работу подписок даже при пиковых нагрузках — это подтверждает наш опыт.

Когда это нужно

Subscriptions закрывают задачи, где UI должен обновляться без действий пользователя: real-time чат, уведомления о событиях, трекинг статуса заказа, live-статистика, совместное редактирование. Если у вас уже есть GraphQL API, добавить Subscriptions — самый короткий путь к real-time. В одном из проектов с 10 000 одновременных пользователей чата мы заменили polling на подписки и снизили нагрузку на сервер в 2 раза.

Серверная часть: Node.js + graphql-ws

Современный стандарт — пакет graphql-ws, реализующий улучшенный протокол graphql-transport-ws. Пример сервера с двумя подписками (чат и статус заказа):

import { createServer } from 'http';
import { WebSocketServer } from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import { makeExecutableSchema } from '@graphql-tools/schema';
import { PubSub } from 'graphql-subscriptions';

const pubsub = new PubSub();

const typeDefs = `
  type Message { id: ID! roomId: String! authorId: String! text: String! createdAt: String! }
  type OrderStatus { orderId: ID! status: String! updatedAt: String! }
  type Query { messages(roomId: String!): [Message!]! }
  type Mutation { sendMessage(roomId: String!, text: String!): Message! }
  type Subscription {
    messageAdded(roomId: String!): Message!
    orderStatusChanged(orderId: ID!): OrderStatus!
  }
`;

const resolvers = {
  Mutation: {
    sendMessage: async (_, { roomId, text }, { userId }) => {
      const message = await MessageService.create({ roomId, text, authorId: userId });
      pubsub.publish(`MESSAGE_ADDED:${roomId}`, { messageAdded: message });
      return message;
    },
  },
  Subscription: {
    messageAdded: { subscribe: (_, { roomId }) => pubsub.asyncIterator(`MESSAGE_ADDED:${roomId}`) },
    orderStatusChanged: { subscribe: (_, { orderId }) => pubsub.asyncIterator(`ORDER_STATUS:${orderId}`) },
  },
};

const schema = makeExecutableSchema({ typeDefs, resolvers });
const httpServer = createServer();
const wsServer = new WebSocketServer({ server: httpServer, path: '/graphql' });

useServer({
  schema,
  context: async (ctx) => {
    const token = ctx.connectionParams?.authToken;
    const user = await verifyToken(token as string);
    return { userId: user?.id };
  },
  onConnect: async (ctx) => {
    const token = ctx.connectionParams?.authToken;
    if (!token) return false;
    return true;
  },
}, wsServer);

httpServer.listen(4000);
Подробнее об обработке ошибок

При разрыве соединения graphql-ws автоматически пытается переподключиться. На стороне сервера важно корректно закрывать итераторы: используйте finally в резолверах или подпишитесь на событие close.

Фильтрация событий на сервере

Для фильтрации событий используем withFilter из graphql-subscriptions. Это позволяет отправлять подписчику только те события, которые его касаются. Например, подписка на сообщения в конкретной комнате: withFilter(() => pubsub.asyncIterator('MESSAGE_ADDED'), (payload, variables) => payload.messageAdded.roomId === variables.roomId). Такой подход экономит трафик и ресурсы.

Почему стоит использовать Redis PubSub для масштабирования?

Встроенный PubSub из graphql-subscriptions — in-memory, работает только в рамках одного процесса. При нескольких инстансах приложения (горизонтальное масштабирование) события не доходят до подписчиков на других серверах. Redis PubSub решает эту проблему. Сравнение:

Характеристика In-memory PubSub Redis PubSub
Масштабируемость Только один процесс Любое количество инстансов
Производительность Высокая (в памяти) Высокая (сетевой обмен)
Сложность настройки Нулевая Низкая (поднять Redis)
Подходит для нагрузки До ~1000 подписок Тысячи+ подписок

Пример подключения Redis PubSub:

import { RedisPubSub } from 'graphql-redis-subscriptions';
import Redis from 'ioredis';

const options = { host: process.env.REDIS_HOST, port: 6379 };
const pubsub = new RedisPubSub({
  publisher: new Redis(options),
  subscriber: new Redis(options),
});
// Использование идентично — pubsub.publish() и pubsub.asyncIterator()

Клиентская часть: Apollo Client

Настраиваем транспортный слой, чтобы Query/Mutation шли через HTTP, а Subscription — через WebSocket:

import { ApolloClient, InMemoryCache, split, HttpLink } from '@apollo/client';
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { createClient } from 'graphql-ws';
import { getMainDefinition } from '@apollo/client/utilities';

const httpLink = new HttpLink({ uri: '/graphql' });
const wsLink = new GraphQLWsLink(createClient({
  url: 'wss://example.com/graphql',
  connectionParams: () => ({ authToken: localStorage.getItem('token') }),
  shouldRetry: () => true,
  retryAttempts: 10,
}));

const splitLink = split(
  ({ query }) => {
    const def = getMainDefinition(query);
    return def.kind === 'OperationDefinition' && def.operation === 'subscription';
  },
  wsLink,
  httpLink
);

export const client = new ApolloClient({ link: splitLink, cache: new InMemoryCache() });

Используем хук useSubscription для подписки на события. Фильтрация на сервере с помощью withFilter позволяет использовать один канал вместо множества.

Как избежать утечек памяти при работе с подписками?

При каждом подключении создаётся асинхронный итератор. Если не закрывать его, память будет расти. graphql-ws автоматически вызывает return() при отписке, но дополнительная защита — явный try/finally в резолвере. Вторая типичная ошибка — забыть про жизненный цикл подписки на фронтенде: при уходе со страницы нужно отписаться.

Какой транспорт лучше: WebSocket или SSE?

WebSocket в 3 раза быстрее SSE для real-time приложений, поскольку обеспечивает двустороннюю связь без overhead повторных соединений. Для подписок GraphQL WebSocket — стандарт благодаря полной совместимости с graphql-ws. SSE подходит только для простых сценариев push-уведомлений.

Сравнение транспортов: WebSocket vs SSE для real-time

Характеристика WebSocket SSE (Server-Sent Events)
Двусторонняя связь Да Нет (только от сервера)
Нативная поддержка Широкая Везде, кроме IE
Протокол graphql-ws Да Требуется адаптер
Производительность Высокая Средняя
Сложность настройки Средняя Низкая

Для подписок GraphQL мы рекомендуем WebSocket, так как он обеспечивает двустороннюю связь и полностью совместим с graphql-ws.

Интеграция с Laravel бэкендом

Если GraphQL API на PHP (Lighthouse), события можно публиковать через Redis из Laravel и обрабатывать в Node.js WebSocket-сервере:

// Node.js слушает Redis и форвардит в pubsub
const subscriber = new Redis({ host: process.env.REDIS_HOST });
subscriber.psubscribe('ORDER_STATUS:*');
subscriber.on('pmessage', (pattern, channel, message) => {
  const orderId = channel.split(':')[1];
  pubsub.publish(`ORDER_STATUS:${orderId}`, JSON.parse(message));
});

Этот подход даёт гибкость: Laravel остаётся источником данных, а Node.js обеспечивает real-time.

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

Проводим нагрузочное тестирование с помощью k6 и кастомных скриптов. Симулируем до 10 000 одновременных подписок, измеряем задержки и стабильность. В отчёте указываем p95 latency и количество успешных доставок. Это гарантирует, что подписки выдержат продакшн-нагрузку.

Что входит в работу

Мы предоставляем:

  • Документацию по схеме подписок и событиям.
  • Исходный код серверной и клиентской частей в вашем репозитории.
  • Нагрузочное тестирование с отчётом (симулируем 10 000 одновременных подписок).
  • Обучение команды.
  • Поддержку в течение 2 недель после ввода в эксплуатацию.

Мы сертифицированные специалисты по GraphQL и гарантируем качество реализации. Обращайтесь — получите консультацию инженера.

Процесс работы

  1. Анализ требований и выбор сценариев подписок.
  2. Проектирование схемы Subscriptions.
  3. Реализация серверной части (Node.js или интеграция с Laravel).
  4. Настройка Redis PubSub и масштабирования.
  5. Клиентская интеграция.
  6. Нагрузочное тестирование и оптимизация.
  7. Деплой.

Сроки ориентировочно

Базовые Subscriptions с одним типом событий — от 3 дней. Полноценная реализация с Redis PubSub, аутентификацией и тестами — от 1 до 2 недель. Интеграция с Laravel/Lighthouse — плюс 2–3 дня. Стоимость проекта рассчитывается индивидуально — экономия на разработке за счёт готовых решений.

Закажите внедрение подписок — наши инженеры помогут с архитектурой. Получите консультацию: напишите нам.

Разработка систем реального времени: WebRTC, SSE, WebSocket

Мы знаем, как больно, когда поллинг убивает сервер. Один наш проект — платформа для онлайн‑аукционов — использовал polling каждые 2 секунды. Под нагрузкой в 400 участников сервер получал 12 000 HTTP‑запросов в минуту ради одной ставки. 90% ответов — пустые. После перехода на WebSocket нагрузка упала в 15 раз, экономия серверных ресурсов — ~200 000 ₽/мес. Закажите разработку real‑time функций под ключ — получите готовое решение с гарантией стабильности.

Реализация real‑time на продакшене — не просто библиотека. Мы проектируем архитектуру под нагрузку, сценарии и бюджет. Ниже — разбор ключевых решений с примерами.

Три транспорта реального времени: когда что выбирать

Server‑Sent Events работают поверх обычного HTTP/1.1 или HTTP/2. Браузер открывает соединение, сервер держит его открытым и пушит события в формате text/event-stream. Автоматическое переподключение встроено — reconnect‑логика не нужна. Ограничение: только сервер → клиент. Идеально для нотификаций, прогресса долгих задач, live‑фидов.

WebSocket — полнодуплексный канал после HTTP Upgrade‑рукопожатия. Браузер и сервер обмениваются фреймами в обе стороны. Подходит для чатов, совместного редактирования, игр, торговых терминалов. Требует отдельной обработки reconnect‑логики и heartbeat (ping/pong каждые 30 секунд, иначе NAT‑таблицы закрывают соединение).

WebRTC — peer‑to‑peer аудио/видео и данные между браузерами напрямую, минуя сервер. Сервер нужен только для сигнализации (STUN/TURN для обхода NAT). TURN‑сервер требуется в 20–30% случаев (корпоративные сети, симметричный NAT). Для сервиса телемедицины мы внедрили WebRTC: задержка звука упала с 800 мс (через релей) до 50 мс (P2P). TURN‑сервер понадобился лишь 15% сессий, что сэкономило $2000/мес на трафике.

WebSocket (Wikipedia)
WebRTC (Wikipedia)

Как правильно выбрать транспорт: пошаговая инструкция

  1. Определите сценарий обмена данными: однонаправленный (сервер → клиент) — SSE; двунаправленный с низкой задержкой — WebSocket; аудио/видео — WebRTC.
  2. Оцените требования к задержке. Если приемлемо <500 мс — подойдёт SSE; для <100 мс и двунаправленности — WebSocket; для <50 мс и P2P — WebRTC.
  3. Проверьте бюджет на инфраструктуру. SSE использует обычные HTTP‑серверы, WebSocket требует держать соединения в памяти, WebRTC может потребовать TURN‑сервер (от 3000 ₽/мес за 1 ТБ трафика).
  4. Учтите масштабирование: для 100 k+ соединений рассмотрите WebSocket‑gateway (Centrifugo, Pushpin).
Транспорт Направление Задержка Сложность реализации Типичные сценарии
WebSocket Полный дуплекс < 100 мс Средняя Чаты, игры, торговля
SSE Только сервер → клиент < 500 мс Низкая Нотификации, ленты прогресса
WebRTC P2P аудио/видео/данные < 50 мс Высокая Видеозвонки, передача файлов

Что такое CRDT и чем он лучше Operational Transformation?

Совместное редактирование — не просто «кто последний записал, тот и прав». Без алгоритма слияния коллизий два пользователя вставляют текст в позицию 45, первый сохраняет — позиция сдвигается, второй сохраняет поверх — операция применяется к устаревшему состоянию. Текст дублируется или теряется.

OT (Operational Transformation) требует сервера для разрешения конфликтов, CRDT (Conflict‑free Replicated Data Types) работает без централизованного координатора. Yjs — наиболее зрелая CRDT‑библиотека для браузера. Интегрируется с ProseMirror, TipTap, CodeMirror, Monaco Editor.

Сравнение библиотек для совместного редактирования

Библиотека Алгоритм Поддержка редакторов Сложность Производительность
Yjs CRDT ProseMirror, TipTap, CodeMirror, Monaco Средняя Высокая (<10 мс при 100 операциях)
ShareDB OT ProseMirror, Quill Средняя Средняя (требуется сервер для слияния)
Automerge CRDT Любой (RichText) Высокая Хорошая (но память растёт быстрее Yjs)

Проблема: размер Yjs‑документа растёт из‑за истории операций. Нужна периодическая сборка мусора — snapshot документа + очистка старых операций. Без этого документ, над которым работали год, может весить 50 МБ.

Пример heartbeat на WebSocket (Node.js)
const ws = new WebSocket('wss://example.com');
let pingInterval;

ws.on('open', () => {
  pingInterval = setInterval(() => {
    ws.ping();
    setTimeout(() => {
      if (ws.readyState === WebSocket.OPEN) ws.terminate();
    }, 5000);
  }, 25000);
});

ws.on('close', () => clearInterval(pingInterval));

Типичные ошибки при внедрении real‑time

Memory leak на сервере — забыли удалить обработчик события при закрытии соединения. На Node.js heap растёт ~1 МБ/ч. EventEmitter предупреждает о 10+ слушателях, но не всегда это замечают.

Thundering herd при реконнекте. Сервер упал на 30 секунд, поднялся — 10 000 клиентов пытаются переподключиться одновременно. Exponential backoff с jitter обязателен: delay = Math.min(baseDelay * 2^attempt + random(0, 1000), maxDelay).

Отсутствие индикации потери соединения. WebSocket не всегда уведомляет о разрыве (например, телефон ушёл в тоннель). Heartbeat решает проблему.

Процесс работы

Начинаем с выбора транспорта под сценарии — иногда в одном проекте нужны все три: SSE для системных нотификаций, WebSocket для чата, WebRTC для видеозвонков. Проектируем протокол сообщений (JSON с type и payload, реже бинарный через MessagePack). Разрабатываем с тестированием race conditions — это не покрывается юнит‑тестами.

Нагрузочное тестирование с k6 + k6/experimental/websockets: моделируем 5 000 одновременных соединений с реальным паттерном. Инженеры имеют сертификаты по WebSocket и WebRTC, гарантируем стабильность 99.9%.

Что входит

  • Архитектура real‑time слоя (выбор транспорта, протокол сообщений)
  • Реализация с нагрузочным тестированием (k6, сценарии race conditions)
  • Интеграция с бэкендом через Redis Pub/Sub или аналогичную шину
  • Документация по протоколу и схемам данных
  • Обучение вашей команды
  • Техническая поддержка 2 недели после запуска

Почему Centrifugo может быть выгоднее, чем Socket.io?

Socket.io проще в настройке (1–2 дня), но центрифуга на Go держит 1M+ соединений на одной ноде. Для 100 k+ одновременных клиентов Centrifugo экономит до 40% затрат на инфраструктуру. Получите консультацию — мы поможем выбрать стек под вашу нагрузку.

Сроки

  • Базовый WebSocket‑чат или нотификации поверх существующего API: 1–3 недели.
  • Коллаборативный редактор с Yjs и persistence: 4–8 недель.
  • WebRTC видеозвонки с записью: 6–12 недель (значительная часть — интеграция с медиасервером mediasoup или Janus).

Свяжитесь с нами для оценки вашего проекта. Обсудите задачу с инженером — оценим сложность и сроки индивидуально.