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

Ми знаємо, як боляче, коли полінг вбиває сервер. Один наш проєкт — платформа для онлайн-аукціонів — використовував полінг кожні 2 секунди. Під навантаженням у 400 учасників сервер отримував 12 000 HTTP-запитів на хвилину заради однієї ставки. 90% відповідей — пусті. Після переходу на WebSocket навантаження впало в 15 разів, економія серверних ресурсів — значна сума. Замовте розробку 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% сесій, що зекономило значні кошти на трафіку.

WebSocket (Wikipedia) WebRTC (Wikipedia)

Як правильно вибрати транспорт: покрокова інструкція

  1. Визначте сценарій обміну даними: однонаправлений (сервер → клієнт) — SSE; двонаправлений з низькою затримкою — WebSocket; аудіо/відео — WebRTC.
  2. Оцініть вимоги до затримки. Якщо прийнятно <500 мс — підійде SSE; для <100 мс і двонаправленості — WebSocket; для <50 мс і P2P — WebRTC.
  3. Перевірте бюджет на інфраструктуру. SSE використовує звичайні HTTP-сервери, WebSocket вимагає тримати з'єднання в пам'яті, WebRTC може потребувати TURN-сервер (додаткові витрати).
  4. Врахуйте масштабування: для 100k+ з'єднань розгляньте 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+ з'єднань на одній ноді. Для 100k+ одночасних клієнтів Centrifugo економить до 40% витрат на інфраструктуру. Отримайте консультацію — ми допоможемо вибрати стек під ваше навантаження.

Строки

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

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