Математика веб-сокетов: сколько соединений выдержит сервер?

Представь ситуацию: ты проектируешь систему, в которой веб-сокет соединение. У тебя сотни миллионов клиентов в день.

Ты задумываешься: а сколько инстансов нужно системе, чтобы вытянуть нагрузку?

Для WebSocket-сервера узким горлышком почти всегда является не CPU (то есть мощность процессора), а память и лимиты операционной системы (которые держат сами соединения).

Что такое файловый дескриптор?

Грубо говоря, это идентификатор (номер), который операционная система выдает приложению каждый раз, когда оно открывает файл или сетевое соединение (сокет). В Linux всё есть файл. Нет свободных дескрипторов — сервер не сможет принять нового клиента, даже если у него простаивает 90% процессорных мощностей.

Давай разберем математику по дескрипторам от и до.

1. Сколько дескрипторов требует один WebSocket?

Всё очень просто: 1 WebSocket соединение = 1 TCP-сокет = 1 файловый дескриптор (FD) на сервере.

Важный нюанс: пользователи редко ходят на сервер напрямую. Обычно перед серверами стоит балансировщик нагрузки. В таком случае одно подключение клиента стоит два дескриптора в системе: один на балансировщике (Клиент на Балансировщик), второй на самом бэкенде (Балансировщик на Сервер).

2. Сколько дескрипторов может быть на одной машине?

Есть такая цифра 65 535 — это лимит портов (исходящих соединений) на один IP-адрес. Но для входящих соединений сервер не ограничен этим числом. Входящее TCP-соединение идентифицируется кортежем из четырех элементов: (IP источника, Порт источника, IP назначения, Порт назначения). Так как IP и порты клиентов разные, сервер на порту 443 может принять миллионы соединений.

Реальный лимит задается настройками ядра Linux:

  • fs.file-max — глобальный лимит открытых файлов в системе.
  • ulimit -n (или nofile в limits.conf) — лимит на процесс.

Современный Linux легко тюнится на 1 000 000 — 10 000 000 файловых дескрипторов на одну машину. То есть, с точки зрения чисто ОС, сервер может держать миллионы соединений.

3. Во что мы упираемся на самом деле?

Каждый открытый файловый дескриптор (сокет) требует выделения памяти в ядре под TCP-буферы (на чтение и запись), плюс накладные расходы самого приложения.

Буферы нужны, чтобы, например, складывать пакеты, которые пришли раньше. В TCP все пакеты пронумерованы и должны идти друг за другом.
Допустим, пакет номер 3 оказался быстрее пакеты номера 2 (проблемы по сети или что-то еще). TCP положит его в этот самый буфер

  • В хорошо оптимизированном приложении одно висящее WS-соединение потребляет около 10–50 КБ оперативной памяти.
  • Давай возьмем оптимистичные 30 КБ на коннект.

Если мы хотим держать 1 000 000 одновременных WebSocket-соединений (то есть 1 млн открытых FD):
1 000 000 * 30 КБ = 30 ГБ оперативной памяти (только под поддержание самих сокетов, без учета бизнес-логики).

4. На примере реальных чисел

Представим такие цифры

  • Аудитория: 100M DAU. Причем это будет какой-нибудь мессенджер или сервис прослушивания музыки, где люди могут висеть целый день. Значит, нам нужно держать порядка 100 миллионов открытых WS-соединений (100 млн FD на весь кластер).
  • Архитектура: давай представим, что у нас 10 серверов.
  • Нагрузка на 1 сервер: 100 000 000 / 10 = 10 000 000 10 миллионов открытых файловых дескрипторов на одной машине.
  • Память на 1 сервер: 10 000 000 * 30 КБ ≈ 300 ГБ RAM.

Технически собрать машину с 300+ ГБ оперативки и выкрутить лимиты ядра на 10 млн дескрипторов можно. Но архитектурно это очень опасно:

  1. Если один такой сервер упадет (например, переполнение оперативной памяти или сбой сети), 10 миллионов пользователей одновременно отвалятся и пойдут переподключаться, что мгновенно положит оставшиеся 9 серверов. Чтобы этого избежать, клиенты обязательно должны делать переподключение с рандомной задержкой.
  2. Если это не Rust/C++, паузы сборщика мусора на 10 миллионах объектов заморозят сервер на секунды.
  3. Соединения имеют свойство зависать, не уведомляя сервер. Чтобы чистить мертвые сокеты, сервер и клиент шлют друг другу Ping-Pong пакеты. Если 10 миллионов клиентов на сервере пингует его раз в 30 секунд, это генерирует ~330 000 RPS фоновой нагрузки на процессор просто ради поддержания связи. И тут CPU внезапно становится узким местом.

Как это сделать безопаснее:

Обычно на одну WS-ноду закладывают от 250 000 до 500 000 соединений (чтобы сервер оставался легким по памяти, порядка 8-16 ГБ RAM, и быстро перезапускался).

Значит, для 100M DAU нам нужно не 10 серверов, а сотни машин, чтобы безопасно справляться с трафиком