Real-Time Backend

uWebSockets.js

15 миллионов WebSocket сообщений в секунду на одном ядре против 300 тысяч у стандартного ws. Разница в 50 раз - не маркетинговый слайд, а benchmark на одном CPU. uWebSockets.js достигает этого не хитрым алгоритмом, а выходом за пределы JavaScript.

  • **Онлайн-игры:** realtime мультиплеер с 10000+ игроками требует рассылки состояния 60 раз в секунду. На ws это ~18M сообщений/сек, что невозможно на одном процессе. uWS справляется благодаря C++ pub/sub и cork.
  • **Финтех:** trading платформы отправляют котировки тысячам клиентов с задержкой менее 1ms. Zero-copy и минимальные аллокации снижают tail latency - именно то, что критично для трейдинга.
  • **Умные дома:** IoT hub с тысячами устройств. Postman использует uWS в своей платформе Postbot; Socket.IO в режиме высокой нагрузки рекомендует uWS как транспорт.

uWebSockets.js: когда ws недостаточно быстр

Создатель uWebSockets.js Алекс Хулин однажды опубликовал benchmark: его библиотека обрабатывает 15 миллионов сообщений в секунду на одном ядре, тогда как стандартный `ws` упирается в 300 тысяч. Разница в 50 раз достигается без магии - только через архитектурные решения: C++ ядро (libuv + OpenSSL), zero-copy где возможно, отказ от лишних аллокаций. uWebSockets.js (uWS) оборачивает этот C++ движок тонким слоем Node.js bindings. Это не pure JavaScript - это нативный модуль, который использует N-API. Следствие: бинарники платформо-специфичны и требуют правильной версии glibc (Ubuntu 24.04 vs Alpine).

uWS совмещает HTTP и WebSocket сервер в одном event loop, в отличие от Express + ws, где HTTP и WS - разные слои с дополнительными накладными расходами. Порт принимает как HTTP upgrade запросы, так и обычный HTTP. Это критично для production: один процесс, один порт, максимальная утилизация ресурсов.

Почему uWebSockets.js значительно быстрее pure-JavaScript реализаций WebSocket?

Производительность: zero-copy и memory model

В обработчике `message` uWS передаёт `ArrayBuffer` - это прямой указатель на внутренний буфер C++ ядра. Buffer жив только во время вызова обработчика. Если данные нужны после - необходимо явное копирование через `Buffer.from(message).slice()` или `message.slice(0)`. Это zero-copy: JS-код читает данные без аллокации новой памяти. Разработчики из ws-мира часто получают пустой буфер, потому что сохраняют ссылку на `message` без копирования.

uWS поддерживает pub/sub на уровне C++ через механизм топиков. `ws.subscribe('room:42')` и `app.publish('room:42', data)` работают без O(n) перебора подписчиков в JS. Внутренний индекс на C++ стороне. Это особенно важно для fan-out к тысячам клиентов: broadcast в uWS быстрее broadcast через Set.forEach.

Почему нельзя сохранить ссылку на `message` в обработчике uWS для использования в setTimeout?

Backpressure: управление перегрузкой

Когда сервер отправляет данные быстрее, чем клиент успевает их принять, буфер TCP растёт. uWS не скрывает эту проблему, как делает большинство библиотек - он явно сигнализирует backpressure. Метод `ws.send()` возвращает значение: 0 (BACKPRESSURE - буфер заполнен), 1 (SUCCESS - отправлено или буферизовано), 2 (DROPPED - соединение закрыто). Обработчик `drain` вызывается, когда буфер освободился. Игнорирование backpressure приводит к неограниченному росту памяти и OOM.

Метод `ws.getBufferedAmount()` возвращает байты в очереди на отправку. Если значение растёт без остановки - клиент не успевает читать. Стратегии: throttle отправки, закрыть соединение при превышении порога, перевести клиента в slow-path очередь. Многие realtime игры используют последний подход: медленным клиентам отправляются снапшоты реже.

Что означает возвращаемое значение 0 (BACKPRESSURE) метода ws.send() в uWebSockets.js?

Pub/Sub и cork: батчинг на уровне ядра

uWS реализует pub/sub на уровне C++ без перебора подписчиков в JS. `app.publish('topic', data)` вызывает один C++ метод, который итерирует связный список подписчиков и записывает в их сокеты через writev() - один системный вызов на клиента. Никакого JavaScript в горячем пути. `ws.cork(callback)` - аналог Nagle's algorithm для приложений: буферизует все send() внутри callback и отправляет одним TCP сегментом. Без cork каждый send() - потенциально отдельный системный вызов.

Топики в uWS - иерархические через wildcard: subscribe('room:#') подписывает на все топики вида 'room:123', 'room:456'. publish('room:42', data) доставляет только тем, кто подписан на точный топик или wildcard. Это позволяет строить broadcast-иерархии без дополнительного слоя роутинга.

uWebSockets.js можно использовать как прямую замену библиотеке ws без изменений в коде

API uWS существенно отличается: message приходит как ArrayBuffer (не Buffer/string), отсутствует EventEmitter интерфейс, backpressure API обязателен

uWS намеренно выбрал отличный от ws API для достижения максимальной производительности. Это не wrapper над ws, а независимая реализация с другими trade-offs. Миграция требует переписывания обработчиков.

Зачем использовать ws.cork() при отправке нескольких сообщений подряд?

Ключевые идеи

  • **Zero-copy message**: `message` в обработчике - ArrayBuffer на C++ память, валидный только внутри обработчика. Для сохранения нужен `Buffer.from(message).slice()`.
  • **Backpressure API**: `ws.send()` возвращает 0/1/2. Значение 0 (BACKPRESSURE) требует паузы отправки до события `drain`. Игнорирование ведёт к OOM.
  • **Pub/Sub и cork**: топики обрабатываются C++ ядром без JS-перебора. `ws.cork()` батчит несколько send() в один системный вызов - критично при высокочастотной отправке.

Связанные темы

uWebSockets.js - альтернатива ws при высокой нагрузке:

  • ws (Node.js) — Стандартная альтернатива с более простым API и pure-JavaScript реализацией. Выбор между uWS и ws зависит от требований к throughput
  • Backpressure и flow control — Backpressure в WebSocket - частный случай общей проблемы управления потоком данных в realtime системах

Вопросы для размышления

  • uWS передаёт `message` как ArrayBuffer на C++ память. Какие паттерны кода в production могут привести к чтению освобождённой памяти, и как их обнаружить?
  • Pub/sub в uWS работает без JS-слоя, но ограничен одним процессом. Как масштабировать pub/sub на несколько Node.js процессов, и где тут место Redis?
  • cork() батчит несколько send() в один syscall. В каких сценариях cork() может навредить latency, даже если он улучшает throughput?

Связанные уроки

  • rt-05 — uWebSockets.js is a bare-metal WebSocket server; protocol knowledge is mandatory
  • rt-09 — Socket.IO is the abstracted alternative; comparing them makes the uWebSockets.js tradeoffs concrete
  • rt-08 — uWebSockets.js is where binary serialization pays off - binary frames are first-class
  • rt-12 — ws vs uWebSockets.js: pure JS vs C++ binding - same goal, different performance ceiling
  • net-15-tcp-basics
uWebSockets.js

0

1

Войти