Выберите продукт

WebSocket и HAProxy: правильные timeouts без 502

WebSocket за HAProxy часто рвётся с 502 из-за неверных таймаутов, HTTP/2 и неудачных health-check-ов. В статье — рабочая схема: какие timeouts действуют после 101, как читать логи, где спотыкается Upgrade и как настроить stick-table для липкости и защиты.
WebSocket и HAProxy: правильные timeouts без 502

Если ваш WebSocket за HAProxy падает в 502 Bad Gateway, почти всегда виноваты timeouts, неверный Upgrade, HTTP/2 на фронте или избыточная оптимизация keep-alive. Разберём, как работает WebSocket в HAProxy, какие таймауты реально играют после 101 Switching Protocols, где чаще всего рождается 502, и как применять stick-table для липких сессий и защиты от перегруза. Для wss понадобится TLS: здесь пригодятся SSL-сертификаты.

Как WebSocket проходит через HAProxy

WebSocket начинается как обычный HTTP/1.1 GET с заголовками Connection: Upgrade и Upgrade: websocket. Бэкенд отвечает 101 Switching Protocols, после чего прокси переводит подключение в туннель (поток байтов без HTTP-фрейминга). В HAProxy этот момент критичен: перестают действовать часть HTTP-таймаутов, зато вступают в силу timeout tunnel, а также общие timeout client и timeout server. Для устойчивости полезно включать option tcpka на длинных соединениях.

Отсюда выводы:

  • Если не настроить timeout tunnel, соединения будут обрываться по более коротким HTTP-таймаутам.
  • Если фронт говорит с клиентом по HTTP/2, а вы ждёте HTTP/1.1 Upgrade, возможны неожиданные обрывы и 502.
  • Проверки живости (health checks) не должны ходить по WebSocket-эндпойнту: ответ 101 ломает многие политики проверки и выбивает сервер из пула.

Где рождается 502 и как распознать причину

Типичные источники 502 при WebSocket через HAProxy:

  • Сработал таймаут: слишком короткий timeout tunnel, timeout server или timeout client. Например, приложение шлёт ping раз в минуту, а туннель стоит 30 секунд.
  • HTTP/2 на фронте и попытка Upgrade: клиент договорился на h2 по ALPN, но Upgrade для WebSocket нужен именно на HTTP/1.1 (если не используется RFC 8441).
  • Заголовки Upgrade/Connection не проходят или переписываются: агрессивные правила могут сломать апгрейд.
  • Health-check-и ожидают 200, а бэкенд отвечает 101 при Upgrade — сервер помечается как down и отдаёт 502.
  • Не тот режим балансировки в stateful-приложении: клиент прыгает между инстансами, теряет состояние и инициирует повторное соединение; на пике это может проявляться как 502.

По логам HAProxy удобно отличать обрыв по таймауту от закрытия со стороны бэкенда: смотрите длительность, termination state и кто закрыл первым. Захватывайте заголовки Upgrade/Connection, чтобы убедиться, что апгрейд случился.

Рабочая базовая конфигурация (HTTP mode + Upgrade)

Минимальный, но практичный каркас для WebSocket через HAProxy 2.x. Он разделяет путь для WebSocket, настраивает корректные таймауты и не допускает HTTP/2 на фронте там, где нужен Upgrade.

# haproxy.cfg (фрагменты)

global
  log 127.0.0.1 local0 info
  # При необходимости увеличьте буферы, если крупные заголовки
  # tune.bufsize 65536
  # tune.maxrewrite 8192

defaults
  mode http
  log global
  option httplog
  option dontlognull
  option tcpka
  timeout connect 5s
  timeout client 1m
  timeout server 1m
  timeout http-request 10s
  timeout http-keep-alive 1m
  timeout tunnel 1h

# Отдельный фронт для WSS с гарантированным HTTP/1.1 (без h2)
frontend fe_wss
  bind :443 ssl crt /etc/haproxy/certs/site.pem alpn http/1.1
  http-response set-header X-Proxy Via-HAProxy
  # Для диагностики: логируем заголовки апгрейда
  log-format "%ci:%cp [%tr] %ft %b/%s %TR/%Tw/%Tc/%Tr/%Tt %ST %B %ac/%fc/%bc/%sc/%rc %sq/%bq %{+Q}r upg=%[req.hdr(Upgrade)] conn=%[req.hdr(Connection)]"

  acl is_websocket hdr(Upgrade) -i websocket
  acl is_upgrade   hdr(Connection) -i upgrade

  use_backend be_app_ws if is_websocket is_upgrade
  default_backend be_app_http

backend be_app_http
  balance roundrobin
  option httpchk GET /health HTTP/1.1\r\nHost:\ app.local\r\nConnection:\ close
  server app1 10.0.0.10:8080 check inter 2s fall 3 rise 2
  server app2 10.0.0.11:8080 check inter 2s fall 3 rise 2 slowstart 5s

backend be_app_ws
  balance source
  option http-keep-alive
  option tcpka
  # Для stateful сокетов желательно прилипание по IP
  stick-table type ip size 200k expire 1h
  stick on src
  # Важно: НЕ включать httpclose/http-server-close здесь
  server app1 10.0.0.10:8080 check inter 2s fall 3 rise 2
  server app2 10.0.0.11:8080 check inter 2s fall 3 rise 2

Комментарии к настройкам:

  • alpn http/1.1 на фронтенде исключает HTTP/2 для WebSocket-эндпойнта. Это снимает половину загадочных 502, связанных с Upgrade.
  • timeout tunnel 1h критичен: после 101 ваша сессия живёт в туннеле. Если приложение отправляет ping раз в 30–60 секунд, часовой туннель не разрывает соединения преждевременно.
  • balance source и stick on src обеспечивают «липкость» к серверу для stateful-приложений.
  • option httpchk в HTTP-бэкенде проверяет обычный путь /health, а не WebSocket-эндпойнт. Ответ должен быть 200/204, а не 101.

Какие timeouts действительно важны для WebSocket

Разложим по полочкам ключевые таймауты, влияющие на стабильность WebSocket:

  • timeout http-request — ожидание полного HTTP-запроса клиента до апгрейда. Слишком короткий таймаут даст 408/502 на старте.
  • timeout client и timeout server — общий простой чтения/записи на сторонах. После апгрейда они учитываются, но при наличии timeout tunnel именно он определяет поведение туннеля.
  • timeout tunnel — главный параметр после 101. Если трафик молчит дольше этого значения, HAProxy разорвёт соединение. Практично ставить 30–120 минут.
  • timeout http-keep-alive — влияет до апгрейда; слишком маленькое значение может оборвать соединение перед Upgrade.
  • timeout connect — важно при пиках/холодном старте; маленькое значение даст 502 при установке серверного соединения.

Практическое правило: держите timeout tunnel заведомо больше интервала пингов приложения и TCP keepalive. Если клиент шлёт ping раз в 25–30 секунд, ставьте минимум 5–10 минут; для фоновых сокетов — 30–60 минут.

Диагностика и разбор логов

Для ускоренной диагностики включите расширенный формат логов с захватом Upgrade/Connection — это покажет, были ли заголовки у клиента и прошли ли они до бэкенда:

log-format "%ci:%cp [%tr] %ft %b/%s %TR/%Tw/%Tc/%Tr/%Tt %ST %B %ac/%fc/%bc/%sc/%rc %sq/%bq %{+Q}r upg=%[req.hdr(Upgrade)] conn=%[req.hdr(Connection)]"

Сценарии и трактовка:

  • В логах upg=websocket, conn=upgrade, но большой %Tt и %ST=502 — вероятен таймаут туннеля или закрытие бэкендом. Проверьте timeout tunnel и логи приложения.
  • Пустые upg/conn при обращении к WebSocket-пути — клиент не отправил Upgrade, либо фронт принудил h2.
  • 502 сразу после попытки — возможно, бэкенд помечен как down. Проверьте health-check-и и маршрут именно к WS-бэкенду.

HTTP/2 и WebSocket: как не наступить на грабли

Частая причина 502 — ALPN с h2 на фронте, когда приложение и клиент используют HTTP/1.1 Upgrade. Чтобы исключить конфликт, выделите отдельный bind/фронтенд под WebSocket с alpn http/1.1 (как в примере выше). Альтернативы:

  • Разделить по SNI: один сертификат/бандл и два bind — с h2 для обычного трафика и без h2 для домена/поддомена, где живёт WebSocket.
  • Разделить по пути: направлять /ws, /socket.io/ и т. п. на фронт без h2.

Если хотите глубже в тему h2 и совместимость прокси, посмотрите разбор нюансов HTTP/2 и gRPC в HAProxy. Для классического WebSocket Upgrade оставайтесь на HTTP/1.1.

Логи HAProxy с заголовками Upgrade/Connection и полями termination state

Health-check-и для пулов с WebSocket

Ловушка — проверять живость по WebSocket-пути. Бэкенд будет отвечать 101, а не 200, и серверы выпадут из пула. Решение — обычный HTTP-здоровый эндпойнт, например /health в отдельном HTTP-бэкенде:

backend be_app_http
  option httpchk GET /health HTTP/1.1\r\nHost:\ app.local\r\nConnection:\ close
  server app1 10.0.0.10:8080 check inter 2s fall 3 rise 2

Держите разделение: Upgrade-трафик идёт в be_app_ws, проверки живости — в be_app_http. Если у приложения один порт, пропускайте checks на обычный путь, а WebSocket обслуживайте на другом URL.

FastFox SSL
Надежные SSL-сертификаты
Мы предлагаем широкий спектр SSL-сертификатов от GlobalSign по самым низким ценам. Поможем с покупкой и установкой SSL бесплатно!

Защита и липкость с помощью stick-table

Stick-table в HAProxy дают две вещи для WebSocket: прилипаемость клиента к инстансу и контроль нагрузки (лимиты). Ниже примеры трекинга и rate-limit на рукопожатие:

frontend fe_wss
  bind :443 ssl crt /etc/haproxy/certs/site.pem alpn http/1.1
  acl is_ws hdr(Upgrade) -i websocket
  acl is_up hdr(Connection) -i upgrade

  stick-table type ip size 200k expire 30m store conn_cur,conn_rate(30s),http_req_rate(30s)
  http-request track-sc0 src if is_ws is_up
  # Не более 50 одновременных сокетов с одного IP
  http-request deny deny_status 429 if { sc_conn_cur(0) gt 50 }
  # Не более 20 апгрейдов за 10 секунд
  http-request deny deny_status 429 if { sc_http_req_rate(0) gt 20 }

  use_backend be_app_ws if is_ws is_up
  default_backend be_app_http

Эти лимиты прикрывают аномалии и помогают сохранить доступность бэкенда под всплесками соединений. Подробнее о возможностях — в материале подробный разбор stick-table и rate-limit.

Stick-table в HAProxy для лимитов одновременных соединений и частоты апгрейдов

Типовые ловушки и анти-паттерны

  • Включён httpclose/http-server-close в WS-бэкенде. После 101 соединение потоковое; принудительное закрытие на уровне HTTP рушит туннель.
  • Слишком маленькие буферы. Если заголовки крупные (много cookies), повышайте tune.bufsize и tune.maxrewrite.
  • TCP keepalive выключен. Для длинноживущих сокетов включайте option tcpka.
  • Смешение h2 и Upgrade на одном bind без явной логики. Если оставляете h2, маршрутизируйте пути на фронт с принудительным HTTP/1.1.
  • Ожидание 200 от health-check-а на WS-пути. Всегда проверяйте отдельный HTTP-эндпойнт.

Когда имеет смысл TCP mode

Иногда WS-путь проще отдать через mode tcp, особенно если нужен «прозрачный» туннель без HTTP-логики на фронте, а маршрутизация делается по SNI/порту. Минусы — нет анализа заголовков и HTTP-rate-limit; плюсы — минимальная вовлечённость HAProxy в протокол. Пример простого TCP-фронта с длинными таймаутами:

frontend fe_wss_tcp
  mode tcp
  bind :8443 ssl crt /etc/haproxy/certs/site.pem
  option tcplog
  option tcpka
  timeout client 1h
  default_backend be_wss_tcp

backend be_wss_tcp
  mode tcp
  balance source
  option tcpka
  timeout server 1h
  server app1 10.0.0.10:8080 check
  server app2 10.0.0.11:8080 check

TCP-режим используйте, если не полагаетесь на HTTP Upgrade-логику и не делаете маршрутизацию по заголовкам/пути. Для стабильности продакшена удобнее управлять такими нагрузками на выделенном VDS.

FastFox VDS
Облачный VDS-сервер в России
Аренда виртуальных серверов с моментальным развертыванием инфраструктуры от 195₽ / мес

Чек-лист быстрых проверок при 502

  • Путь WebSocket обслуживается фронтом с alpn http/1.1 (без h2)?
  • В логах фронта видны Upgrade: websocket и Connection: upgrade?
  • timeout tunnel достаточно велик и согласован с ping/pong вашего приложения?
  • Health-check-и проверяют обычный HTTP-эндпойнт, а не WS-путь?
  • Нет ли httpclose/http-server-close в WS-бэкенде?
  • Буферы (tune.bufsize) покрывают размер заголовков?
  • Включён ли option tcpka для долгоживущих соединений через NAT/фаерволы?
  • Для stateful-приложения включены липкие сессии (balance source + stick on src)?

Итоги

Причина 502 при WebSocket за HAProxy почти всегда в деталях конфигурации: неверные или недостаточные timeouts, конфликт HTTP/2 с Upgrade, агрессивные закрытия соединений или ошибочные health-check-и. Настройте фронт под HTTP/1.1 для WS, дайте туннелю щедрый timeout tunnel, отделите проверку живости от Upgrade-пути и используйте stick-table для липкости и защиты. Такая схема стабильно переживает NAT и сетевые колебания без 502, а у вас остаются прозрачные точки наблюдения и рычаги управления нагрузкой.

Поделиться статьей

Вам будет интересно

systemd-resolved: NXDOMAIN, negative caching, TTL и DNSSEC (SERVFAIL) — диагностика и лечение OpenAI Статья написана AI (GPT 5)

systemd-resolved: NXDOMAIN, negative caching, TTL и DNSSEC (SERVFAIL) — диагностика и лечение

Частая проблема на Linux/VDS: внезапные NXDOMAIN в systemd-resolved, «залипание» из-за negative caching и stale cache, влияние SOA ...
FRR Linux: BGP multihoming, communities и защита от route leak — практический шаблон OpenAI Статья написана AI (GPT 5)

FRR Linux: BGP multihoming, communities и защита от route leak — практический шаблон

Собираем практичную конфигурацию FRR для eBGP multihoming с двумя аплинками: строгий экспорт только своих префиксов, импорт defaul ...
LVM cache (dm-cache) в Linux: ускоряем HDD с помощью SSD/NVMe без миграции данных OpenAI Статья написана AI (GPT 5)

LVM cache (dm-cache) в Linux: ускоряем HDD с помощью SSD/NVMe без миграции данных

LVM cache (dm-cache) позволяет подключить SSD/NVMe как кэш к медленному HDD-томy и ускорить I/O без переноса данных. Разберём cach ...