Раздел 3. Как клиент держит связь с сервером
Постоянное соединение (WebSocket / long-lived TCP) против polling; gateway- и connection-серверы; как сервер понимает, к какому узлу подключён получатель.
В Разделе 1 мы выяснили: мессенджеру нужна постоянная связь, потому что сервер должен сам толкать сообщения тому, кто не спрашивает. Этот раздел — про то, как именно клиент держит эту связь: почему не годится опрос, что такое WebSocket, как устроены серверы соединений и как сервер вообще находит, куда доставлять сообщение.
Почему не опрос (polling)
Наивная идея — пусть клиент сам периодически спрашивает «нет ли мне новых сообщений». Это называется опрос (polling), и он плох в обоих вариантах:
- Частый опрос (short polling): клиент дёргает сервер каждую секунду. Получается лавина пустых запросов — почти всегда «ничего нового», а сеть, процессор и батарея тратятся впустую. На миллионах пользователей это неподъёмно.
- Редкий опрос: дёргаем реже — экономим ресурсы, но получаем задержку до интервала опроса. Сообщение пришло сразу после запроса — пользователь увидит его только через минуту.
- Long polling: компромисс — запрос «висит», пока не появятся данные. По задержке лучше, но соединение одноразовое (пришёл ответ — нужен новый запрос), инициатива по-прежнему у клиента, и держать массу висящих запросов дорого.
Вывод: опрос не решает задачу. Нужен канал, по которому сервер сам пушит данные, как только они появились.
WebSocket: постоянный двусторонний канал
Этот канал и даёт WebSocket. Технически это «апгрейд» обычного HTTP-соединения: клиент и сервер один раз делают рукопожатие, после чего поверх одного TCP-соединения открывается постоянный двусторонний (full-duplex) канал. Дальше обе стороны шлют друг другу данные в любой момент, не открывая соединение заново.
Есть родственная технология — SSE (Server-Sent Events), но она односторонняя (только сервер → клиент). Чату нужен обмен в обе стороны, поэтому берут WebSocket.
Аналогия из Раздела 1: опрос — это бумажные письма «есть что-нибудь? — нет», а WebSocket — телефонный звонок: соединился один раз и говоришь в обе стороны, пока не положил трубку.
Мобильная обвязка: heartbeat и переподключения
На мобильных соединение нестабильно, поэтому вокруг WebSocket навешивают обвязку:
- Heartbeat (ping-pong): устройство и сервер обмениваются короткими сигналами «я жив». Пропали сигналы — связь считают мёртвой и закрывают. (Из этого же факта бесплатно получается онлайн-статус — Раздел 7.)
- Переподключение с backoff и jitter: связь рвётся регулярно; клиент переподключается, но не мгновенно и не все разом — с нарастающей паузой (backoff) и случайным разбросом (jitter), чтобы не устроить «шторм».
- Догрузка пропущенного: после переподключения клиент забирает из очереди то, что пришло, пока он был offline.
И важная граница: живая связь существует, только пока приложение открыто. Свернул или закрыл — операционная система рвёт соединение ради экономии батареи, и тогда в дело вступают push-уведомления (Раздел 5).
Серверы соединений: stateful против stateless
Соединения держит не Message-сервис, а отдельный слой — серверы соединений (Conn-серверы, gateway). Они держат открытые сокеты и гоняют по ним кадры, а вся бизнес-логика живёт в Message-сервисе.
Зачем такое разделение? Потому что у этих двух частей разная природа:
- сервер соединений stateful — он привязан к конкретным живым сокетам, и его тяжело масштабировать и перезапускать;
- логику хотят держать stateless — её легко размножать и обновлять, любой узел обрабатывает любой запрос.
Разделив их, мы масштабируем тяжёлый «держатель соединений» отдельно от лёгкой логики.
Масштаб тут серьёзный: задача «держать много открытых соединений» известна как C10K, а в больших мессенджерах это уже C10M — миллионы соединений. Каждое соединение ест память и файловый дескриптор, поэтому используют эффективные механизмы вроде epoll и тюнят ОС. Один узел тянет примерно 500 тыс — 1 млн соединений, так что на пик в десятки миллионов нужны десятки-сотни узлов.
Как найти получателя: Реестр сессий и pub/sub
Соединение Боба висит на каком-то одном узле, а сообщение пришло на узел Алисы. Как Message-сервис узнаёт, куда доставлять? Двумя инструментами:
- Реестр сессий — справочник адресов. При подключении узел записывает «пользователь/устройство → на каком узле он сейчас висит» в быстрое общее хранилище (типа Redis, живёт в памяти), с TTL и обновлением при переподключении. При отправке Message-сервис смотрит в реестр, находит узел получателя и пересылает сообщение туда по внутренней сети. К реестру обращаются на каждое сообщение, поэтому он сам — нагруженный компонент, его тоже шардируют и реплицируют.
- Шина публикаций-подписок (pub/sub) — для рассылки многим. Когда получателей много (группа, канал), искать каждого в реестре невыгодно; удобнее, чтобы узлы подписывались на «каналы» своих пользователей, а отправитель публиковал сообщение один раз — и все нужные узлы получали его сами.
Коротко: реестр — для точечной адресации одному, шина — для рассылки многим. На практике их сочетают (подробнее об операционной стороне — Раздел 12).
Балансировка постоянных соединений
Обычные запросы балансировщик раскидывает по одному, а постоянное соединение так не получится: его нельзя перекидывать на другой узел на каждом кадре. Поэтому соединение балансируют один раз при установке и держат «прилипшим» (sticky) к своему узлу на всё время жизни. Работает это на транспортном уровне (L4) — балансировщику не нужно понимать содержимое, только распределить соединение.
Опасный момент — шторм переподключений: когда узел падает или его выкатывают, все его соединения рвутся разом и клиенты ломятся заново все сразу. Защита: плавный вывод узла (drain) при выкатке, разброс и нарастающая пауза (jitter + backoff) у клиентов, и расчёт на то, что данные не теряются — пропущенное лежит в очереди и догрузится после переподключения.
Вопросы интервьюера (с разбором)
«Почему для мессенджера не подходит опрос (polling), и что используешь вместо него?» Образец: «Частый опрос — это лавина почти всегда пустых запросов, бьёт по сети, процессору и батарее; редкий опрос экономит ресурсы, но даёт задержку до интервала. Long polling лучше по задержке, но соединение одноразовое и инициатива всё равно у клиента. Мне же нужно, чтобы сервер сам толкал данные, как только они появились, поэтому беру постоянное двустороннее соединение — WebSocket: одно рукопожатие, дальше full-duplex канал.»
«Зачем выделять серверы соединений отдельно от Message-сервиса?» Образец: «Потому что у них разная природа. Сервер соединений stateful — он держит конкретные живые сокеты, его тяжело масштабировать и перезапускать. Логику же хочу держать stateless, чтобы легко размножать и обновлять. Разделив их, я масштабирую тяжёлый держатель соединений отдельно от лёгкой логики. Плюс держать миллионы соединений (C10M) — это отдельная инженерная задача с памятью и дескрипторами на каждое, её удобно решать в выделенном слое.»
«Как сервер понимает, к какому узлу подключён получатель?» Образец: «Через Реестр сессий — быстрое общее хранилище в памяти (типа Redis), где при подключении пишется, на каком узле сейчас висит пользователь или устройство. При отправке Message-сервис смотрит в реестр, находит узел получателя и пересылает сообщение туда по внутренней сети. Для рассылки многим вместо поиска каждого использую шину публикаций-подписок: узлы подписаны на своих пользователей, отправитель публикует один раз. Реестр — для адресации одному, шина — для многих.»
#мессенджер #websocket #постоянное-соединение #connection-серверы #маршрутизация #балансировка #подготовка-к-интервью