Real-Time Backend

Benchmarking

Discord держит сотни миллионов WebSocket-соединений. Как инженеры знают, что система выдержит следующие 10x роста - и где именно сломается?

  • k6 WebSocket тест выявил, что p99 latency сервера растёт с 8ms до 1.2s при 5000 одновременных соединений - bottleneck оказался в синхронном JSON.stringify на больших payload
  • TechEmpower Framework Benchmarks помогли Discord выбрать Elixir/Phoenix Channels как основу для realtime-слоя: p99 на multi-query оказался стабильнее Node.js при тех же ресурсах
  • uWebSockets.js заявляет 15M req/s - но flame graph реального чата показал, что 60% CPU уходит на бизнес-логику и DB, а не на WS-транспорт
  • Artillery.io сценарий с Socket.io воспроизвёл production-инцидент: при 2000 одновременных пользователей memory leak в event listener накапливал 500MB/час - без нагрузочного теста это обнаружили бы только в prod

C10K C1M

C10K - это задача одновременного обслуживания 10 000 соединений на одном сервере. В 1999 году Дэн Кегел сформулировал её как инженерный барьер эпохи: большинство серверов того времени упирались в thread-per-connection модель и рушились уже при 1-2K соединений. Переход к event-driven I/O (epoll, kqueue) снял это ограничение - сегодня C10K считается решённой задачей.

C1M - следующий рубеж: 1 000 000 одновременных соединений. uWebSockets.js заявляет 15M req/s на синтетических тестах; реальные Production-системы (Slack, Discord) удерживают миллионы WebSocket-соединений через горизонтальное масштабирование и sticky sessions. Один процесс Node.js реалистично обрабатывает 100K-300K idle WS-соединений до упора в память (~32 KB на соединение).

  • C10K (1999) - решается event-loop + epoll/kqueue; актуален только для устаревших blocking-серверов
  • C100K - достижимо на одном узле с tuned Linux (ulimit, tcp_tw_reuse, SO_REUSEPORT)
  • C1M - горизонтальное масштабирование; один процесс ограничен памятью и файловыми дескрипторами

Почему C10K перестал быть проблемой для современных серверов?

Latency Percentiles

Среднее время ответа (avg) - худшая метрика для realtime-систем: оно маскирует хвосты распределения. Если p50 = 5 ms, а p99 = 800 ms, средняя выйдет ~12 ms - и кажется всё нормально, пока 1% пользователей не жалуется на лаги. В production принято смотреть p99 (99-й перцентиль) и p99.9 (999-й) - именно они определяют реальный worst-case опыт.

TechEmpower Framework Benchmarks публикует p99 для сотен фреймворков в стандартизированных условиях. uWebSockets.js стабильно попадает в топ-5 по throughput и p99 среди Node/JS окружений. Сравнивать результаты разных инструментов напрямую некорректно: wrk2 и k6 используют разные модели нагрузки.

Для WebSocket-систем latency измеряется от отправки сообщения клиентом до получения ответа (round-trip). k6 позволяет писать сценарии с ws.send/ws.on и собирает перцентили автоматически. Artillery.io заточен под Socket.io и HTTP/2 - удобен для смешанных сценариев с think-time между запросами.

Сервис показывает avg latency 10 ms и p99 latency 950 ms. Что это означает?

Flame Graphs

Flame graph - визуализация CPU-профиля: ось X - суммарное время (не хронологическое), ось Y - глубина стека вызовов. Широкие прямоугольники вверху - функции, которые потребляют больше всего CPU. Brendan Gregg разработал формат в Netflix для диагностики production-проблем без остановки сервиса.

На flame graph realtime-сервера нужно искать: (1) широкие блоки JSON.parse/stringify - признак избыточной сериализации; (2) синхронные crypto-вызовы - блокируют event-loop; (3) unexpected GC frames - тюнинг --max-old-space-size и уменьшение аллокаций; (4) широкие блоки в libuv/poll - нормально для idle WS сервера.

Для WebSocket серверов под нагрузкой полезен async flame graph (собирает через async_hooks): показывает полную цепочку от входящего сообщения до ответа включая ожидание I/O. Инструменты: clinic.js flame (автоматически), или 0x с флагом --kernel-tracing для системных вызовов.

На flame graph Node.js сервера широкий блок JSON.stringify занимает 40% CPU. Какой вывод?

Load Testing

Инструменты нагрузочного тестирования realtime-систем отличаются по поддерживаемым протоколам и модели нагрузки. wrk2 - золотой стандарт для HTTP: поддерживает constant throughput (не constant arrival rate как wrk), что даёт корректные перцентили. k6 - скриптовый, поддерживает WebSocket через ws API, собирает p95/p99 по умолчанию. Artillery.io специализируется на Socket.io и HTTP/2, удобен для realtime-сценариев с state.

TechEmpower Framework Benchmarks - стандарт индустрии для сравнения фреймворков: тестирует JSON serialization, single DB query, multiple DB queries, plaintext. uWebSockets.js стабильно показывает 15M+ req/s на plaintext - но это синтетика. В реальных условиях (DB, auth, business logic) цифры падают на порядок. Benchmarks полезны для выбора основы, но не заменяют профилирование под реальную нагрузку.

  1. Определить метрики успеха ДО теста: p99 < X ms, throughput > Y req/s, error rate < 0.1%
  2. Прогреть систему (warm-up фаза) - JIT и connection pools должны устаканиться
  3. Тестировать с реалистичным payload: размер сообщений, частота, паттерны сессий
  4. Собрать flame graph во время нагрузки - не после, не в изоляции
  5. Проверить p99 и p99.9 - не только avg/p50

Высокий throughput в синтетическом бенчмарке означает, что система готова к production-нагрузке

Синтетические бенчмарки (TechEmpower, uWebSockets.js 15M req/s) тестируют изолированные компоненты без реального бизнес-логики, DB и auth. Production throughput обычно ниже на порядок.

Синтетика исключает самые дорогостоящие операции: запросы в БД, сериализацию сложных объектов, JWT-валидацию, межсервисные вызовы. Бенчмарк нужен для сравнения фреймворков в одинаковых условиях, а не для прогноза production-производительности.

Команда выбирает между wrk и k6 для нагрузочного тестирования WebSocket API. Что предпочтительнее?

Итоги

  • C10K решена event-driven I/O; C1M требует горизонтального масштабирования - один процесс Node.js реалистично держит 100K-300K idle WS-соединений
  • Смотреть на p99 и p99.9, не на avg: хвосты распределения определяют реальный пользовательский опыт в realtime-системах
  • Flame graph под нагрузкой показывает CPU-bottleneck; JSON.stringify, синхронный crypto и GC паузы - главные кандидаты на оптимизацию
  • wrk2 для HTTP с constant throughput, k6 для WebSocket, Artillery.io для Socket.io сценариев - правильный инструмент под протокол

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

Бенчмаркинг пересекается с несколькими ключевыми областями realtime-архитектуры:

  • WebSocket Scaling — C1M соединений требует горизонтального масштабирования WS-серверов и sticky sessions
  • Event Loop & Non-blocking I/O — Flame graphs на Node.js сервере напрямую отражают эффективность event loop и отсутствие блокирующих операций
  • Protocol Selection — Результаты бенчмарков (TechEmpower) - один из критериев выбора между HTTP/2, WebSocket и gRPC

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

  • Какие метрики нагрузочного тестирования наиболее значимы для вашей системы - throughput, p99 latency или количество одновременных соединений?
  • Как flame graph мог бы изменить подход к оптимизации по сравнению с code review или интуитивными предположениями?
  • Если синтетический бенчмарк показывает 10x лучший результат, чем production-профилирование, - что это говорит об архитектуре системы?

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

  • alg-01-big-o
Benchmarking

0

1

Войти