WebSocket — протокол, который начинается как обычный HTTP-запрос, а затем превращается в постоянный двусторонний канал. Именно из-за этого превращения Nginx требует нескольких неочевидных директив: без них соединение либо не устанавливается, либо обрывается через минуту простоя. Ниже — рабочая конфигурация обратного прокси, разбор тайм-аутов и способы понять, кто именно закрывает соединение.

Чем WebSocket отличается от обычного запроса

Клиент отправляет GET с заголовками Upgrade: websocket и Connection: Upgrade, а также случайный ключ Sec-WebSocket-Key. Сервер отвечает кодом 101 Switching Protocols и заголовком Sec-WebSocket-Accept. С этого момента по тому же TCP-соединению идут кадры WebSocket, а HTTP-парсер больше не участвует. Для прокси это означает, что соединение должно оставаться открытым часами, а не завершаться после первого ответа.

Отсюда два практических следствия. Во-первых, балансировщик обязан поддерживать HTTP/1.1 на участке до бэкенда. Во-вторых, все тайм-ауты, рассчитанные на короткие запросы, становятся опасными: они рвут канал в самый неподходящий момент.

Минимальная рабочая конфигурация

Сначала определим переменную для заголовка Connection. Без неё нельзя одновременно обслуживать обычные запросы и апгрейд: пустое значение должно превращаться в close, а не в upgrade.

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

upstream ws_backend {
    server 127.0.0.1:3000;
    keepalive 32;
}

server {
    listen 443 ssl;
    server_name example.com;

    location /ws/ {
        proxy_pass http://ws_backend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Директива proxy_http_version 1.1 обязательна: по умолчанию Nginx говорит с бэкендом по HTTP/1.0, где заголовок Upgrade не поддерживается, и приложение вернёт ошибку или просто закроет соединение. Заголовок Host тоже стоит передавать явно, иначе виртуальные хосты приложения могут выбрать не тот сайт.

Тайм-ауты: что и когда срабатывает

Главный параметр — proxy_read_timeout. Он отсчитывается не от начала сессии, а от последнего успешного чтения. Если приложение раз в 30 секунд отправляет ping или любое служебное сообщение, значения по умолчанию в 60 секунд обычно достаточно. Если трафик бывает только по действию пользователя, тайм-аут нужно увеличить, иначе Nginx разорвёт канал после паузы.

location /ws/ {
    proxy_pass http://ws_backend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;

    proxy_read_timeout 3600s;
    proxy_send_timeout 3600s;
    proxy_socket_keepalive on;
    proxy_buffering off;
}

proxy_send_timeout ограничивает паузу при записи в сокет бэкенда, а не общую длительность передачи. proxy_socket_keepalive включает TCP keepalive для соединения с бэкендом и помогает заметить мёртвый узел.

Отдельно проверьте keepalive_timeout. Он относится к клиентским HTTP-соединениям до апгрейда и после ответа 101 уже не действует, поэтому увеличивать его до часа ради WebSocket бессмысленно. Не путайте proxy_read_timeout с временем жизни сессии: приложение может держать канал сколько угодно, пока идут обмены.

Несколько бэкендов и липкие сессии

Если WebSocket-серверов больше одного и состояние хранится в памяти процесса, обычный round-robin приведёт к тому, что повторное подключение попадёт на другой узел и потеряет контекст. Для небольших установок помогает ip_hash, для более аккуратного распределения — hash по идентификатору пользователя из cookie или токена.

upstream ws_backend {
    hash $cookie_session consistent;
    server 10.0.0.11:3000;
    server 10.0.0.12:3000;
    keepalive 64;
}

Параметр consistent включает кольцо с ограниченным влиянием: при выпадении узла перераспределяется только часть ключей. Keepalive в upstream экономит рукопожатия для сопутствующего HTTP-трафика; для самого WebSocket соединение всё равно занято на всю сессию.

Буферы, заголовки и размеры

Длинные cookie и токены авторизации иногда не помещаются в стандартные буферы заголовков. Тогда клиент получает 400 ещё до апгрейда, а в error.log появляется запись о слишком большом заголовке. Помогает увеличение large_client_header_buffers, но злоупотреблять им не стоит: это память на каждое соединение.

large_client_header_buffers 4 16k;
client_header_timeout 20s;
client_body_timeout 20s;

Эти директивы касаются только этапа рукопожатия. После ответа 101 они перестают влиять на трафик. Если приложение отдаёт большие бинарные сообщения, следите за памятью воркеров и не увеличивайте proxy_buffers наугад.

Диагностика: кто закрывает соединение

Начните с рукопожатия через curl. Код 101 и заголовок Upgrade подтверждают, что Nginx и бэкенд договорились. Если приходит 400, 426 или 502, проблема в конфигурации или в недоступном приложении, а не в тайм-аутах.

curl -i -N \
  -H "Connection: Upgrade" \
  -H "Upgrade: websocket" \
  -H "Sec-WebSocket-Version: 13" \
  -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
  https://example.com/ws/

Дальше смотрите access.log: успешный апгрейд виден как статус 101, а время запроса в логе будет коротким, потому что фиксируется только рукопожатие. Для наблюдения за живыми сокетами используйте ss и фильтр по порту. В error.log ищите сообщения upstream timed out и client prematurely closed connection: первое указывает на бэкенд, второе — на клиента или промежуточное оборудование.

sudo tail -f /var/log/nginx/error.log
ss -tnp | grep ':443'
sudo nginx -t && sudo systemctl reload nginx

В браузере вкладка Network показывает статус 101 и дальнейшие кадры, а закрытие с кодом 1006 означает обрыв без корректного close-кадра.

Нагрузка, ограничения и безопасность

Каждое проксируемое соединение занимает два сокета в worker_connections: клиентский и upstream. При лимите 1024 и одном воркере это максимум около 512 одновременных WebSocket-клиентов, а не 1024. Для тысяч подключений поднимайте worker_connections, число воркеров и проверяйте лимиты файловых дескрипторов через ulimit и systemd.

Апгрейд — удобная точка для ограничений:

  • limit_conn на зону по IP не даёт одному клиенту открыть сотни каналов;
  • limit_req сдерживает шторм переподключений;
  • TLS обязателен для внешнего трафика;
  • проверку Origin и авторизацию делайте в приложении.

Не открывайте /ws/ в обход аутентификации и не логируйте токены целиком.

Итог

Для WebSocket в Nginx достаточно пяти вещей: HTTP/1.1 до бэкенда, корректные заголовки Upgrade и Connection, разумный proxy_read_timeout, понимание лимита worker_connections и проверка рукопожатия через curl. Тайм-ауты считайте от последнего обмена, а не от старта сессии, и тогда долгие каналы перестанут рваться «сами собой».

Насколько публикация полезна?

Нажмите на звезду, чтобы оценить!

Средняя оценка 0 / 5. Количество оценок: 0

Оценок пока нет. Поставьте оценку первым.

Поделиться: Telegram ВКонтакте