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

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

Backpressure в WebSocket: когда клиенты не успевают

Суть Как медленные клиенты заполняют TCP-окна, раздувают очереди отправки на уровне приложения и обрушивают серверы — и паттерн high-water mark, который это предотвращает.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на middle-высоте — в небе
◷ 12 min

Ваш WebSocket broadcast-сервер на тестах справляется с 10 000 клиентами. В production 100 клиентов на медленном мобильном интернете начинают отставать. Память сервера растёт. GC-паузы удлиняются. Потом срабатывает OOM killer и укладывает весь сервис. Это классический режим отказа WebSocket — и он не имеет никакого отношения к протоколу.

TCP-окно приёма с точки зрения приложения

TCP даёт каждому сокету окно приёма — количество байт, которые получатель готов принять, прежде чем отправитель должен приостановиться. Когда приложение на принимающей стороне читает байты, окно открывается; когда отстаёт — окно сжимается до нуля и отправитель останавливается.

С точки зрения сервера:

  1. Сервер вызывает send(socket, message).
  2. Ядро копирует сообщение в буфер отправки сокета.
  3. TCP передаёт из буфера отправки с учётом рекламируемого клиентом окна приёма.
  4. Если окно клиента равно нулю (его буфер чтения заполнен), TCP прекращает передачу.
  5. Вызов send() ядра блокируется, если буфер отправки заполнен, или возвращает EAGAIN в неблокирующем режиме.

Для broadcast-сервера с тысячами клиентов это создаёт критическую асимметрию: быстрые клиенты опустошают буферы за миллисекунды; медленные оставляют байты лежать в буферах отправки ядра, и когда те заполняются — сервер застревает.

Очередь отправки на уровне приложения

Production WebSocket-серверы никогда не блокируются на send(). Вместо этого они поддерживают очередь отправки уровня приложения на соединение. Когда сервер рассылает сообщение, он ставит его в очередь для каждого соединения и сразу возвращает управление. Фоновый поток записи опустошает каждую очередь в ядро так быстро, как позволяет TCP-окно клиента.

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

Рассмотрим сценарий:

  • Сервер рассылает сообщение 100 КБ каждые 10 мс.
  • 100 из 10 000 клиентов медленные — запросы к БД блокируют их поток чтения.
  • Очередь каждого медленного клиента растёт: 100 сообщений/секунду × 100 КБ = 10 МБ/секунду.
  • Через 10 секунд: 100 медленных клиентов × 10 МБ × 10 с = 10 ГБ RAM в очередях.
  • Срабатывает OOM killer.
Математика отказа от backpressure
Скорость рассылки (пример)
100 КБ каждые 10 мс = 10 МБ/с на клиента
Рост RAM на медленного клиента при такой скорости
10 МБ/с
RAM при 100 медленных клиентах через 10 с
10 ГБ
Рекомендуемый целевой p95 глубины очереди
< 5 сообщений
Рекомендуемый порог медленного клиента
< 0,1% соединений
Рекомендуемый потолок суммарных байт в очереди
< 5% кучи

Паттерн high-water mark

Решение — high-water mark: ограничение очереди на соединение, триггерирующее вытеснение или backpressure.

Когда очередь соединения превышает high-water mark (например, 100 сообщений или 10 МБ), сервер выбирает:

Вариант A — вытеснить соединение. Принудительно закрыть медленного клиента с кодом 1013 (“try again later”). Клиент переподключится когда сеть освободится. Другие клиенты не затронуты. Стандартный выбор для broadcast-сервисов.

Вариант B — дропать сообщения. Для идемпотентных данных (биржевые цены, снимки игрового состояния), где последнее значение заменяет предыдущее — дропать старые сообщения и оставлять только N последних на соединение. Медленный клиент получит актуальное состояние при переподключении, а не устаревшую историю.

Вариант C — приостановить продюсера. Для peer-to-peer стримов (видеоконференции, передача файлов) — сигнализировать отправляющему приложению приостановиться до опустошения очереди ниже low-water mark. Это полное распространение backpressure.

Режим отказа из-за idle timeout прокси

Есть второй распространённый режим отказа: прокси между клиентом и сервером имеет 60-секундный idle timeout. Если в течение 60 секунд через TCP-соединение не проходят байты, прокси закрывает его — без отправки WebSocket close-фрейма.

Клиент и сервер об этом не подозревают, пока сервер не попытается отправить следующее сообщение: send() вернёт EPIPE или ECONNRESET. Клиент увидит событие close с кодом 1006 (аномальное закрытие). Соединение мертво.

Решение: сервер отправляет ping-фрейм каждые 25–30 секунд. Ping сбрасывает таймер idle прокси. Клиент должен ответить pong в разумное время (сервер может вытеснять клиентов, не ответивших pong в течение 10 секунд, как очистку устаревших соединений).

Граничные случаи

Сообщение 1 МБ, 10k клиентов, 1% медленных. Сервер рассылает сообщение 1 МБ. Для 9 900 быстрых клиентов TCP буферизирует и доставляет за миллисекунды. Для 100 медленных сообщение лежит в очереди приложения. Если сервер рассылает 50 таких сообщений до обнаружения медленных клиентов: 100 клиентов × 50 сообщений × 1 МБ = 5 ГБ раздутой памяти. Production-серверы непрерывно мониторят количество медленных клиентов и вытесняют их когда их доля превышает 0,5% соединений или суммарные байты в очереди превышают 5% кучи — что наступит раньше.

Проследи
1/5

Трассируйте инцидент с backpressure на WebSocket broadcast-сервере.

1
Step 1 of 5
Сервер рассылает сообщение 100 КБ 10 000 клиентам. Что делает TCP-стек сервера для быстрых клиентов?
2
Locked
100 из 10 000 клиентов медленные (запрос к БД блокирует их поток чтения). Их буферы приёма ядра заполняются. Что видит сервер при вызове send() для медленного клиента?
3
Locked
Что делают production-серверы вместо блокировки?
4
Locked
Сервер ставит 100 КБ на сообщение в очередь, рассылает каждые 10 мс, 100 клиентов медленные. Сколько RAM раздувается в секунду?
5
Locked
Какую метрику должен мониторить сервер для обнаружения проблемы до краша?
Найди ошибку

Метрики приложения WebSocket broadcast-сервиса под backpressure

log
2026-05-15T14:32:00Z broadcast_service[5234]:
active_connections: 50234
total_queued_bytes: 8589934592  (8 ГБ)
slow_client_count: 127
p95_queue_depth: 156 сообщений
p99_queue_depth: 341 сообщений
broadcast_latency_p99: 18432мс
memory_usage: 15.2 ГБ / 16 ГБ кучи
gc_pause_time: 1247мс

2026-05-15T14:32:15Z broadcast_service[5234]:
active_connections: 49891  (-343 за 15с)
total_queued_bytes: 10737418240  (10 ГБ)
slow_client_count: 412
p95_queue_depth: 287 сообщений
p99_queue_depth: 502 сообщений
broadcast_latency_p99: 32568мс
memory_usage: 15.8 ГБ / 16 ГБ кучи
gc_pause_time: 2100мс

2026-05-15T14:32:30Z broadcast_service[5234]: OOM killer вызван; процесс убит

Сервис рассылает 1 МБ в секунду 50k клиентам. Глубина очереди растёт, куча на 15,8 ГБ. Каков режим отказа и немедленное решение?

Викторина

Прокси между клиентом и сервером имеет 60-секундный idle timeout. За 59 секунд не отправлялось ни одного сообщения. На 60-й секунде прокси закрывает TCP-соединение без WebSocket close-фрейма. Какой close-код генерирует WebSocket-реализация клиента?

Вспомните перед уходом
  1. 01
    Почему очередь отправки уровня приложения решает проблему блокировки, но создаёт новую — и как high-water mark решает обе?
  2. 02
    Каков правильный интервал ping'а против idle timeout прокси и что делать если клиент не ответил pong?
  3. 03
    Назовите три стратегии вытеснения медленных клиентов и условия применения каждой.
Итог

Основной режим отказа WebSocket — backpressure: когда клиенты не успевают читать данные со скоростью отправки сервером, TCP-окна приёма сжимаются до нуля, буфер отправки ядра заполняется, и — если сервер использует очереди приложения для избегания блокировки — они растут неограниченно до исчерпания RAM процессом. Решение — high-water mark на соединение, триггерирующий вытеснение (код 1013), дроп сообщений или приостановку продюсера в зависимости от семантики данных приложения. Второй режим отказа — idle timeout прокси: прокси с 60-секундным таймером закрывают тихие WebSocket-соединения без close-фрейма, генерируя close-код 1006. Ping-фреймы каждые 25–30 секунд это нейтрализуют. Ключевые метрики для мониторинга: количество медленных клиентов (целевое значение ниже 0,1% соединений), суммарные байты в очереди (целевое значение ниже 5% кучи) и p99 глубины очереди на соединение (целевое значение ниже 5 сообщений для интерактивных приложений).

Связанные уроки
встречается в258
Продолжить восхождение ↑Реконнект: jittered backoff, thundering herd, восстановление сообщений
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.