awesome-everything EN
↑ Обратно к восхождению

Сети и протоколы

WebSocket в production: прокси, безопасность и распределённая архитектура

Суть Как настроить Nginx и ALB для долгоживущих upgrade, защититься от DoS и cache-poisoning, и масштабировать WebSocket-кластеры с pub/sub и sticky sessions.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на senior-высоте — в орбите
◷ 14 min

Вы деплоите WebSocket-сервер. Тесты проходят. В production соединения случайно обрываются через 60 секунд тишины, браузеры сообщают о 1006 в idle-периоды, а load balancer иногда возвращает 502 на upgrade. Ни одна из этих проблем — не баг в вашем приложении. Это неправильные настройки прокси, невидимые пока не знаешь куда смотреть.

Неправильные настройки прокси и load balancer

Прокси вроде Nginx, HAProxy и AWS ALB проектировались для HTTP — коротких запрос-ответных диалогов в миллисекундах. Постоянное WebSocket-соединение — чужеродно для них. Распространённые неправильные настройки:

Проблема 1 — Idle timeout закрывает тихие соединения.

  • Nginx по умолчанию: proxy_read_timeout 60s (закрывает при отсутствии данных 60 секунд).
  • AWS ALB по умолчанию: idle_timeout.timeout_seconds = 60.
  • Решение: поднять до минимум 3 600 секунд (1 час).
location /ws {
  proxy_pass http://backend;
  proxy_http_version 1.1;
  proxy_set_header Upgrade $http_upgrade;
  proxy_set_header Connection "Upgrade";
  proxy_read_timeout 3600s;
  proxy_send_timeout 3600s;
  proxy_buffering off;
}

Проблема 2 — L7-буферизация задерживает ответ 101. Nginx по умолчанию буферизирует HTTP-ответы. Ответ 101 Switching Protocols удерживается в буфере до «завершения» — но WebSocket никогда не завершает ответ. Это может задержать upgrade на сотни миллисекунд. Решение: proxy_buffering off.

Проблема 3 — Прокси не понимает Upgrade-заголовок. Некоторые прокси вырезают или игнорируют Connection: Upgrade. Бэкенд видит запрос без Upgrade-заголовков и возвращает обычный HTTP-ответ вместо 101. Решение: явно установить оба заголовка как показано выше.

Проблема 4 — HTTP/2-прокси не пробрасывает WebSocket upgrade. Если прокси HTTP/2, а бэкенд HTTP/1.1, прокси может не знать как обработать WebSocket extended CONNECT. Решение: явно тестировать WebSocket через стек прокси при деплое, а не только HTTP-трафик.

Чеклист настройки прокси
Nginx: поднять read/send timeout
proxy_read_timeout 3600s
AWS ALB: поднять idle timeout
idle_timeout.timeout_seconds = 3600
Nginx: отключить буферизацию ответа
proxy_buffering off
Интервал ping сервера против idle timer прокси
каждые 25–30 с
ALB sticky session (для stateful WS серверов)
Включить target group stickiness
Проверка Origin-заголовка (на стороне сервера)
Обязательно — блокировать неавторизованные origin

Безопасность: проверки Origin, DoS и атаки медленного читателя

Проверка Origin. Браузер всегда отправляет заголовок Origin при WebSocket upgrade. Сервер обязан его проверять:

if request.headers["Origin"] not in ALLOWED_ORIGINS:
    respond 403 Forbidden
    return

WebSocket-клиент не в браузере не отправляет Origin-заголовок по умолчанию — если сервер его требует, небраузерные клиенты должны добавлять его явно. Это основная защита от cross-site WebSocket hijacking (CSWSH), где вредоносная страница на attacker.com открывает WebSocket к api.yoursite.com, используя cookies жертвы.

Rate limiting при handshake. Ботнеты могут исчерпать SYN-backlog TCP попытками соединения. Ограничивайте на load balancer: максимум попыток соединения с IP в секунду (обычно 5–20). Применяйте SYN cookies на уровне ОС (net.ipv4.tcp_syncookies = 1).

Атака медленного читателя (Slow Loris). Вредоносный клиент завершает WebSocket handshake, но никогда не читает из своего сокета. Очередь отправки сервера для этого соединения заполняется. Митигация: закрывать соединения без активности в течение 30 секунд (не отправлено и не получено ни одного сообщения, нет pong-ответа на ping).

Ограничение размера сообщения. Клиент, отправляющий сообщение 100 МБ, вынуждает сервер буферизировать 100 МБ на подписчика. Задавайте максимальный размер сообщения (обычно 64 КБ–1 МБ) и закрывайте соединение с кодом 1009 (“message too big”) при превышении.

Горизонтальное масштабирование: pub/sub и sticky sessions

Один WebSocket-сервер достигает потолка при 500k–2 M соединений. Дальше нужно несколько серверов с общим messaging backbone.

Pub/sub (Redis Streams или RabbitMQ). Каждый сервер подписывается на нужные каналы. Когда приложение публикует сообщение, все подписанные серверы рассылают его своим локально подключённым клиентам:

Пользователь A (подключён к Серверу 1) отправляет сообщение в чат.
Сервер 1 публикует { roomId, message, msgId } в Redis stream "room:42".
Сервер 2 (тоже подписан на "room:42") читает из stream.
Сервер 2 рассылает своим локально подключённым пользователям комнаты 42.

Это развязывает отправителей от получателей и позволяет горизонтальный масштаб без требования всем клиентам быть на одном сервере.

Sticky sessions. Когда load balancer маршрутизирует одного клиента на один и тот же сервер при реконнектах, in-flight состояние (неподтверждённые сообщения, частичные подписки) сохраняется без полной Redis-репликации. AWS ALB реализует это как target group stickiness (1-часовой cookie по умолчанию). Минус: сбой сервера отправляет всех sticky-клиентов переподключаться к другим серверам одновременно — мини thundering herd на каждый упавший инстанс.

Почему это работает

Почему самая сложная часть — не WebSocket, а управление состоянием. Протокол WebSocket прямолинеен. Сложная инженерная задача: что происходит когда соединение пользователя мигрирует на другой сервер при горизонтальном масштабировании, деплое или сбое сервера? Всё in-flight состояние — подписки, частичные загрузки, позиция игровой синхронизации — должно либо жить во внешнем хранилище (Redis, БД), либо восстанавливаться с нуля через протокол реконнекта + восстановления сообщений. Discord, Slack и все сервисы chat-масштаба тратят больше инженерного времени на репликацию состояния и консистентность под сбоями, чем на WebSocket-проводку.

Observability: метрики которые нужно экспортировать

WebSocket-сервис без этих метрик тихо умрёт от OOM:

МетрикаЦелевое значение
Количество активных соединенийАлерт при неограниченном росте (connection leak)
Глубина send-очереди на соединение (p95/p99)Цель < 5 сообщений
Суммарные байты в очередиЦель < 5% кучи
Количество медленных клиентовЦель < 0,1% соединений
Задержка сообщения p99Цель < 100 мс (end-to-end)
Распределение close-кодов (1000/1006/1013)Скачок 1006 = сетевое событие
Скорость реконнектов в минутуСкачок = перезапуск сервера или авария

Инструменты: netstat -an / ss -s для подсчёта состояния сокетов, tcpdump для трассировки на уровне пакетов, Prometheus для метрик приложения, eBPF-программы для размеров буферов сокетов и частоты ретрансмитов.

Викторина

Финансовая торговая платформа должна пушить 1000 обновлений цен в секунду 50k браузеров в разных географиях (US, Европа, Азия; RTT 10–300 мс). Какая архитектура правильная?

Спроектируй

Спроектируйте чат-приложение для 1 миллиона одновременных пользователей в US, EU и APAC. Требования: гарантия доставки (at-least-once, без дубликатов), реконнект с синхронизацией истории, p99 задержка < 200 мс для межрегиональных сообщений, graceful degradation при отключении региона. Стек: Redis Streams, PostgreSQL, CDN с edge compute.

  • Задержка p99 < 200 мс даже для межрегиональных сообщений.
  • Без потери сообщений (at-least-once доставка).
  • Без дубликатов даже при реконнекте клиентов.
  • Поддержка 1M одновременных соединений.
  • Graceful degradation при отключении региона.
Вспомните перед уходом
  1. 01
    Назовите три распространённые неправильные настройки Nginx, ломающие долгоживущие WebSocket-соединения, и решение для каждой.
  2. 02
    Что такое cross-site WebSocket hijacking (CSWSH) и как проверка Origin-заголовка защищает от него?
  3. 03
    Почему pub/sub (например, Redis Streams) необходим для горизонтального масштабирования WebSocket, и какова роль sticky sessions рядом с ним?
Итог

Production WebSocket-деплои чаще всего ломаются не из-за протокола, а от неправильных настроек прокси: idle timeout (60с по умолчанию) закрывает тихие соединения, буферизация ответа удерживает 101 бесконечно, Upgrade-заголовки вырезаются. Решение: поднять timeout до 3600с, отключить буферизацию, явно установить Upgrade-заголовки — и отправлять серверный ping каждые 25–30 секунд для сброса idle-таймеров прокси. Безопасность требует: проверки Origin (защита от CSWSH), rate limiting при handshake (защита от ботнетов), slow-read timeout на соединение (защита от Slow Loris), ограничений размера сообщений. Горизонтальный масштаб сверх одного сервера требует pub/sub backbone (Redis Streams, RabbitMQ), чтобы сообщения с любого сервера достигали клиентов на любом другом, плюс sticky sessions для сохранения per-connection in-flight состояния. Самая сложная инженерная задача — репликация состояния и консистентность под сбоями серверов, а не WebSocket-проводка. Ключевые цели observability: медленных клиентов ниже 0,1%, суммарных байт в очереди ниже 5% кучи, p99 задержки сообщений ниже 100 мс.

Связанные уроки
встречается в258
Продолжить восхождение ↑WebSocket: тест с выбором ответа
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources4
expand
  1. 01
  2. 02
  3. 03
  4. 04

Trademarks belong to their respective owners. Editorial reference only.