В распределенной системе эффективное перемещение данных между службами — задача не из легких. Это может быть особенно сложно для веб-приложения внешнего интерфейса, которое опирается на опрос данных из многих внутренних служб.
Недавно я исследовал решения этой проблемы для Gray Matter, в частности, как мы могли бы уменьшить трафик для некоторых наиболее востребованных сервисов в нашей сети. Наше веб-приложение имело следующие характеристики:
-
Опросы сервисов на обновления каждые 5 секунд.
-
Не нуждается в двунаправленной связи.
-
Не нужно поддерживать устаревшие браузеры.
-
Общается со службами через прокси на основе посланника.
Я посмотрел на два серверных решения — WebSockets и Server-Sent Events (SSE). В конце концов, я понял, что WebSockets был излишним для нашего случая использования, и решил сосредоточиться на SSE, главным образом потому, что это просто старый HTTP и позволяет нам использовать некоторые действительно интересные функции HTTP / 2.
Быстрый учебник по SSE
SSE является частью стандарта HTML5 EventSource и в основном просто предоставляет нам API для управления и анализа событий из длительных HTTP-соединений.
Для реализации клиента SSE создайте экземпляр объекта EventSource и начните прослушивать события. Когда этот код загружается в браузер, он открывает TCP-соединение и отправляет HTTP-запрос на сервер, сообщая ему о необходимости отправлять события в очередь, когда они у них есть. Мы даже можем слушать нестандартные события, такие как шутки пап:
JavaScript
xxxxxxxxxx
1
const source = new EventSource(`https://localhost:7080/events`, {
2
withCredentials: true
3
});
4
source.addEventListener("dadJoke", function(event) {
6
console.log(event.data)
7
});
Если соединение обрывается, EventSource отправит сообщение об ошибке и автоматически попытается восстановить соединение!
Реализация сервера также очень проста. Чтобы сервер стал сервером SSE, ему необходимо:
-
Установите соответствующие заголовки (текст / событие-поток).
-
Следите за подключенными клиентами.
-
Вести историю сообщений, чтобы клиенты могли наверстать упущенное (необязательно).
-
Отправлять сообщения в определенном формате - блок текста, оканчивающийся парой строк:
Простой текст
xxxxxxxxxx
1
id: 150
2
event: dadJoke
3
retry: 10000
4
data: They're making a movie about clocks. It's about time.
Вот и все! Есть много из SSE серверных пакетов там , чтобы сделать вещи еще проще, или вы можете свернуть свой собственный.
Вам также может понравиться:
Использование HTTPS в Mule .
Проблемы с SSE
Есть несколько общих недостатков, о которых вы узнаете в SSE:
-
Нет встроенной поддержки бинарных типов.
-
Одностороннее общение.
-
Нет поддержки IE.
-
Нет способа добавить заголовки с объектом EventSource.
-
Максимум 6 клиентских подключений с одного хоста.
Первые 4 очка не были для нас проблемой. Нам не нужно отправлять в браузер ничего, кроме JSON. Клиенту не нужно отправлять данные на сервер. Для IE существуют полифилы, и мы можем прикреплять любые заголовки или данные, относящиеся к конкретному маршруту, используя наш прокси.
Последнее очень важно. Одним из самых значительных ограничений SSE является ограничение максимального количества параллельных подключений в браузере, которое в большинстве современных браузеров установлено на 6 для домена в соответствии со спецификацией HTTP / 1.1 . Я прочитал, что HTTP / 2 решает эту проблему, поэтому я немного углубился, чтобы понять, как это сделать.
От подтягивания клиента к выталкиванию сервера
В стандартном HTTP клиент запрашивает у сервера ресурс, и сервер отвечает. Это сделка 1: 1.
Когда появился HTTP / 1.1, он ввел концепцию «конвейерной передачи», когда несколько запросов HTTP отправляются по одному TCP-соединению . Таким образом, клиент может запросить кучу ресурсов с сервера одновременно, но ему нужно только открыть одно соединение.
Однако с реализацией возникла огромная проблема - сервер должен был отвечать в том порядке, в котором он получал запросы от клиента, поскольку не было другого способа определить, какой запрос был отправлен с каким ответом. Это приводит к так называемой проблеме блокировки заголовка (HOL) , когда клиент отправляет запросы A, B и C, но запрос A требует много ресурсов сервера, B и C блокируются до тех пор, пока не завершится A.
Вместе с HTTP / 2, который был разработан для преодоления проблем с производительностью HTTP / 1.x. Это дает нам сжатые заголовки HTTP, приоритеты запроса. Самое главное, это позволяет нам мультиплексировать несколько запросов по одному TCP-соединению. Подождите, у нас уже не было этого последнего с HTTP / 1.1? Да и нет; из-за таких проблем, как проблема HOL, в браузерах отсутствовала надежная конвейерная поддержка, поэтому сообщество веб-разработчиков приняло собственные решения для повторного использования соединений - шардинг домена, конкатенация, спрайтинг и встраивание ресурсов.
HTTP / 2 - реальное решение для мультиплексирования из-за того, как работает базовый «слой кадрирования». Вместо текстовых данных HTTP / 1.x сообщения HTTP / 2 разбиваются на более мелкие части, такие как заголовки и тела запросов / ответов. Затем они кодируются.
Эти небольшие кодированные блоки называются «кадрами» и содержат идентификатор, поэтому они могут быть связаны с конкретным сообщением. Это означает, что их не нужно отправлять сразу - кадры, принадлежащие разным сообщениям, могут чередоваться. Когда эти кадры достигают места назначения, они могут быть повторно собраны и декодированы в стандартное сообщение HTTP с помощью HTTP / 2-совместимых серверов.
Пока соединения находятся за одним и тем же именем хоста, включение HTTP / 2 даст нам столько запросов, сколько мы хотим через одно и то же соединение!
Здесь есть еще один смысл для SSE. Поскольку поток SSE - это просто длительный HTTP-запрос, у нас может быть столько отдельных потоков SSE, сколько мы хотим через одно соединение. Мы можем отправлять сообщения с сервера на клиент и с клиента, а также с клиента на сервер и с него.
SSE и посланник
Пока у нас есть приятная настройка - HTTP / 2 обеспечивает эффективный уровень передачи данных, в то время как SSE предоставляет нам собственный веб-API и формат обмена сообщениями для клиента.
Теперь мне было любопытно, смогу ли я заставить это работать в распределенной системе, где сервер (ы) и клиент развернуты в отдельных контейнерах за шлюзом или «пограничным» прокси. Этот прокси обрабатывает все входящие запросы и направляет их в соответствующее место, что важно, потому что он позволяет нам размещать все за тем же именем хоста («example.com» на диаграмме ниже). Таким образом, мы можем воспользоваться мультиплексным соединением. Вот настройки:
Вы можете видеть, что у нас есть два потока событий, мультиплексированных по одному соединению, и мы также получаем статические активы (index.html) по этому соединению!
Я воспроизвел эту настройку в небольшом наборе докеров - два сервера SSE, выдающие анекдоты папам каждые 10 секунд, и клиент, который отображает события в браузере. Попробуйте сами, клонируя репозиторий здесь, а затем перейдя по адресу https: // localhost: 8080 с открытыми инструментами разработчика.
Оболочка
xxxxxxxxxx
1
git clone https://github.com/kaitmore/simple-sse
2
cd simple-sse
3
docker-compose up -d
На вкладке «Сеть» вы должны увидеть запросы к нашим двум серверам событий, использующим протокол h2, а также ответы, представленные на странице (вам может потребоваться немного подождать):
Вам может быть интересно, откуда вы знаете, что запросы / ответы мультиплексируются? Я вижу два потока, перечисленных там!
Посмотрите на столбец «Идентификатор соединения» и обратите внимание, что они все одинаковые: 260121. Элементы, перечисленные на вкладке «Сеть», являются запросами, а не TCP-соединениями. Если бы мы запустили этот же docker-compose с отключенным HTTP / 2, вы бы увидели, что у каждого запроса свой идентификатор соединения *:
* Ну ... вроде. localhost (index.html) и первый поток событий на самом деле имеют один и тот же идентификатор соединения, потому что после возвращения index.html это соединение освобождается для повторного использования в следующем запросе.
Конфигурация посланника
Настройка Envoy для работы с SSE потребовала некоторых экспериментов. Вы можете увидеть окончательную конфигурацию здесь .
Включение HTTP / 2
Первое, что мне нужно было сделать, это включить HTTP / 2. В соответствии с документами Envoy , их alpn_protocolsfield
необходимо установить в любом месте, чтобы они могли tls_context*
принимать соединения HTTP / 2. Я также обнаружил, что необходимо настроить http2_protocol_options
каждый кластер, который хочет HTTP / 2, хотя я не указывал никаких опций.
Настройка тайм-аутов
Как только я включил http / 2, я быстро заметил, что происходит что-то интересное. Казалось, что новый поток событий создается каждые 10–15 секунд. Водопад показывает, что соединение действительно разорвано, поэтому браузер пытается восстановить соединение:
После некоторого поиска в Google я наткнулся на эту маленькую жемчужину в FAQ по документам посланника :
«Этот тайм-аут [на уровне маршрута] по умолчанию равен 15 секундам, однако он не совместим с потоковыми ответами (ответами, которые никогда не заканчиваются) и должен быть отключен. Тайм-ауты простоя потоков должны использоваться в случае потоковых API, как описано в другом месте на этой странице ».
Обновление всех определений маршрута для отключения тайм-аутов решило проблему:
Простой текст
xxxxxxxxxx
1
routes:
2
match:
3
prefix: "/"
4
route:
5
cluster: client
6
timeout: 0s # Disable the 15s default timeout
Другой тайм-аут, на который следует обратить внимание - это stream_idle_timeout , который определяет количество времени, в течение которого соединение может оставаться открытым без получения каких-либо сообщений. По умолчанию установлено значение 5 м, и его можно отключить так же, как и время ожидания на уровне маршрута выше.
Завершение
Отправленные сервером события дают нам надежную альтернативу опросу с помощью встроенного веб-API, автоматического повторного подключения, пользовательских событий и поддержки HTTP / 2. Соединение SSE с Envoy в качестве шлюза позволяет нам воспользоваться преимуществами поддержки HTTP / 2 за счет прокси-серверов на разные потоковые серверы под одним именем хоста, что снижает сетевую тряску и ускоряет наш пользовательский интерфейс.
Сноска: HTTP / 2-серверный Push против SSE
Когда я впервые узнал о HTTP / 2 и SSE, я продолжал читать о «продвижении сервера HTTP / 2», и я не совсем понимал, как эти вещи связаны. Они одинаковы? Это конкурирующие технологии? Могут ли они использоваться вместе?
Оказывается, они разные. Push-сервер - это способ отправки активов клиенту до того, как они его попросят. Типичным примером может служить интерфейс, который запрашивает index.html. Сервер отправляет обратно этот запрошенный файл, но вместо того, чтобы браузер анализировал и запрашивал другие ресурсы в index.html, сервер уже знает, чего хочет клиент, и «выталкивает» оставшиеся статические ресурсы, такие как style.css и bundle. JS. Вместо 3-х вызовов к серверу для каждого из этих активов, это делается через одно соединение HTTP. Нажим сервера использует те же базовые технологии, но сценарий использования немного отличается.