Обновление данных в режиме реального времени в производстве — история от Джесси
Познакомьтесь с Джесси. Он инженер-программист.
Его наняли в технический отдел дистрибьюторской компании. В первый же день его попросили создать веб-приложение, которое торговые агенты компании используют для выполнения задач.
Джесси поговорил со своим коллегой Уолтером. Уолтер сказал ему, что уже создал серверную службу и выставил конечную точку для выполнения всех задач агента.
Слишком легко. Просто вызовите конечную точку в тот момент, когда агент открывает веб-приложение, верно? Но Джесси хотел идти осторожно. После расследования Джесси понял, что по какой-то причине задачи для агентов генерировались в непредсказуемое время, бедные агенты.
Опрос
Джесси создал веб-приложение, которое звонило на конечную точку Уолтера каждые 15 секунд, чтобы убедиться, что агент получает актуальные задачи. Он протестировал свой код локально и развернул веб-приложение. Торговый агент начал использовать его. У-у-у!
Во время обеденного перерыва Джесси разозлился на телефонный звонок от Уолтера. Высокий показатель RPS в веб-приложении Джесси привел к отключению серверной службы! Джесси увеличил интервал запроса до 2 минут, тем временем ему нужно было искать другое решение для обновления данных в реальном времени.
- Плюсы: опрос — самый простой и наивный способ реализации, и это обычный HTTP-запрос, а не сервер.
- Минусы: если вам нужна низкая задержка, увеличьте частоту запросов, это добавит серверу QPS. Если вы понизите частоту, вы потеряете ощущение реального времени. Это жесткий компромисс.
Долгий опрос
Уолтер обеспокоен, он вмешался, чтобы помочь.
Уолтер когда-то проводил длительные опросы. Клиент открывает HTTP-соединение с сервером, который держит его открытым, всякий раз, когда сервер действительно получает новые данные, он отправляет ответ.
На стороне клиента необходимо отправить запрос, а при получении ответа отправить запрос еще раз.
На стороне сервера сервер не отвечает, пока не произойдет обновление данных. Это может привести к тайм-ауту соединения. Клиенту необходимо восстановить это соединение, чтобы поддерживать активное соединение с сервером.
- Плюсы: по сравнению с опросом частота запросов снижается, но при этом клиенты могут получать обновления данных.
- Минусы: однако требуется, чтобы сервер в основном поддерживал соединение, что увеличивает загрузку ЦП сервера. Если на самом деле нет ответа, он потребляет пропускную способность для ничего такого. Также клиент должен постоянно устанавливать соединение с сервером, каждый раз заново проходя все сетевые уровни.
ССЭ
Джесси нашел еще одного кандидата по имени SSE. Что это?
SSE расшифровывается как Sserver Sent-Events. Из Рекомендации W3C — это API для открытия HTTP-соединения для получения push-уведомлений с сервера в виде событий DOM.
На стороне клиента есть EventSource Interface, который позволяет клиенту запрашивать SSE-соединение с URL-адресом и регистрироваться для прослушивания входящих сообщений:
Поток данных поступает с ключом data
, используя \n
в качестве конца каждого фрагмента сообщения, \n\n
в качестве конца всего фрагмента данных. Есть и другие ключи. Клавиша id
может использоваться для пометки последовательности повторного подключения, клавиша event
может использоваться для классификации различных сообщений.
data: first line\n data: second line\n\n id: 12345\n event: userMessage\n data: {\n data: "msg": "hello world",\n data: "number": 12345\n data: }\n\n
На стороне сервера сервер записывает поток ответов в следующем формате, а ответ имеет тип MIME Content-Type: text/events-stream.
- Плюсы: SSE является рекомендацией W3C, более перспективной и предоставляет преимущества для более эффективного использования сетевых ресурсов. Одним из преимуществ является подключение без подключения, это может привести к значительной экономии времени автономной работы на портативных устройствах.
- Минусы: SSE имеет максимальное ограничение количества подключений EventSource на хост, равное 6, здесьобсуждение об этом, это можно решить с помощью мультиплексирования HTTP/2, но для этого требуется, чтобы ваша сетевая инфраструктура поддерживала HTTP/2. О другом решении мы поговорим позже в этой статье.
Чтобы провести высокоуровневое сравнение между Polling, Long-Polling и SSE, вот графикграфа Бхаратваджа Ганешана из его статьи:
Как видно из графика выше, при опросе клиент устанавливает соединение с интервалами. При длительном опросе клиент переустанавливается после получения ответа от сервера или тайм-аута соединения. С SSE клиент устанавливает соединение только один раз, а затем сервер отправляет поток событий клиенту.
Основываясь на сравнении, Уолтер и Джесси считают, что у SSE более чистая реализация и лучшая стандартная поддержка, есть полифилл EventSource, а также их вариант использования не будет иметь проблем с максимальным ограничением параллелизма. Они предпочли SSE длительному опросу. Наконец-то они внедрили SSE для получения обновлений задач с сервера, и с тех пор они счастливы… правда?
Веб-сокет
Месяц спустя в офис вошел менеджер по продукту Гас. Он сказал Джесси, что ему нужно иметь обновление агента в режиме реального времени, включая заявленный статус задачи.
Джесси был в шоке. То, что у него было, не соответствовало новым требованиям.
- Один из способов решить эту проблему — отправлять HTTP-запрос от клиента к серверу на ack/nack каждый раз, когда клиент получает обновление через SSE.
- Другой способ — переключиться на использование чего-то двунаправленного, например webSocket, что позволит клиенту отвечать серверу, используя то же соединение.
В то время как № 1 требует меньше работы, ack/nack является асинхронным, поэтому не может удовлетворить требования PM почти в реальном времени. Пришлось искать номер 2.
WebSocket – это передовая технология, позволяющая открывать двусторонний интерактивный сеанс связи между браузером пользователя и сервером.
Протокол WebSocket устанавливает рукопожатие HTTP, затем обновляется до TCP и использует TCP для отправки сообщения, заключенного в кадры.
Если вам интересно узнать больше о webSocket, вот веб-сокет RFC, и вот потрясающая статья Армина Ронахера, объясняющая детали.
На стороне клиента мы можем использовать интерфейс WebSocket:
На стороне сервера из-за сложности протокола мы можем использовать существующую реализацию webSocket. В приведенном ниже коде используется пакет node.js ws
.
Все фрагменты кода в этой статье взяты из демонстраций в этом репозитории Github, не стесняйтесь играть с ними =)
- Плюсы: WebSocket — единственный направленный канал среди всех упомянутых механизмов проталкивания. Если вам нужно реализовать функции, требующие, чтобы клиент почти в реальном времени отключал обновления, лучше всего подойдет WebSocket.
- Минусы: по сравнению с HTTP, webSocket имеет более сложную реализацию: рукопожатия HTTP, обновление TCP и протокол на основе кадров TCP. Имейте в виду, если вам действительно нужен WebSocket, иначе это может быть излишним.
Из-за требований к продукту Джесси и Уолтер должны были поддерживать обновление клиент-сервер практически в реальном времени, поэтому они переключились с SSE на webSocket в своих сервисах.
И опять же, это точно не конец…
Система обновления данных в режиме реального времени
За несколько месяцев разработки и тестирования Уолтер и Джесси постепенно построили систему для поддержки push-уведомлений. Он включает в себя следующие механизмы:
- Пульс
На самом деле сетевая среда очень сложна, между клиентом и сервером могут быть прокси-серверы, такие как прокси-сервер балансировки нагрузки. Возможно, что клиент отключается, но прокси все еще подключен к серверу. В этой ситуации сервер не может определить, действительно ли соединение активно.
Распространенным решением является отправка пульса от клиента к серверу.
На графике слева показано, что при использовании SSE клиент отправляет пульсацию на сервер с интервалом, помимо соединения SSE. Если по истечении определенного тайм-аута от клиента не поступает пульс, сервер может считать, что клиент упал замертво.
Сам WebSocket определил пульс пинг-понга, мы могли бы просто следовать протоколу.
- Управление одновременным подключением
Если клиент является веб-сайтом, пользователи могут открывать несколько вкладок или окон, это добавляет избыточные соединения в пул соединений, который должен поддерживать сервер, а также количество push-уведомлений, которые должен выполнить этот сервер. Кроме того, как было сказано выше, у SSE есть максимальное ограничение 6 открытых соединений. Один из способов мультиплексирования — через HTTP2, но для этого ваши серверы, прокси-серверы и инфраструктура маршрутизации должны поддерживать HTTP2. Другой способ — использовать Service Worker.
Service Worker — это скрипт, который запускается в фоновом потоке браузера. Обычное использование — кеширование статических ресурсов для сокращения времени загрузки страницы. Но это также открывает нам дверь для перехвата запроса, мы также можем использовать postMessage
для отправки данных обратно в каждую вкладку/окно.
Идея состоит в том, чтобы использовать Service Worker для выполнения сетевого запроса, например, SSE, а затем транслировать ответ на каждую вкладку/окно.
Я все еще изучаю детали этого решения, буду обновлять пост, когда у меня будет больше информации.
- Прикрепленный сеанс для балансировщика нагрузки
Для более быстрого поиска данных иногда мы хотим сохранить некоторые метаданные в памяти на хосте, к которому подключен клиент. Для этого нам нужно убедиться, что клиент всегда подключается к одному и тому же экземпляру после первого установления соединения.
Закрепленный сеанс относится к функции многих коммерческих решений балансировки нагрузки для веб-ферм, позволяющей направлять запросы для определенного сеанса на тот же физический компьютер, который обслуживал первый запрос для этого сеанс.
Одним из способов реализации липкой сессии является последовательное хэширование. Вот потрясающая статья Сруштики Нилакантам.
- Получение push-данных
Клиент отключается по разным причинам, важно иметь возможность получить предыдущие push-данные. В основном есть два подхода:
- Клиент извлекает предыдущие данные
- Сервер снова нажимает, когда клиент повторно подключается
Для № 1 нам понадобится постоянное хранилище данных. И не выбрасывайте конечные точки get при переходе с pull на push. Каждый раз при подключении клиенты полагаются на эти конечные точки для извлечения сохраненных данных, чтобы ничего не пропустить.
Для № 2. Как показано на графике выше, сервер пытается найти соединение, принадлежащее определенному клиенту, когда запускается push. Если активного соединения нет, сервер временно сохраняет данные, например, используя Redis TTL, данные находятся в памяти и будут удалены по истечении тайм-аута.
Каждый раз, когда клиент подключается к серверу, сервер проверяет, сохранены ли данные для этого клиента, и если есть, выполняет push. Если нет, клиент, вы можете идти.
Что дальше
Эти месяцы были горько-сладкими, но Джесси ценит работу с Уолтером над этим проектом. Он написал этот средний пост, потому что…
«Да технологии, сука!»