url pushing что это
HTTP/2 Server Push не так прост, как я думал
Фото найдено на просторах Википедии
Привет! Меня зовут Макс Матюхин, я работаю PHP-программистом в Badoo. Мы постоянно изучаем различные возможности по ускорению работы нашего приложения и самыми интересными находками, конечно, делимся в нашем блоге на Хабре.
Вторая версия протокола HTTP обещает нам много улучшений, и одной из любопытных особенностей HTTP/2 является поддержка push. Теоретически эта возможность позволяет ускорить загрузку приложения. Недавно Jake Archibald написал большую статью, в которой проанализировал особенности реализации push в различных браузерах, и оказалось, что таких особенностей довольно много.
Мы уже публиковали пост, описывающий базовый функционал HTTP/2 Server Push, а этот будет хорошим дополнением, рассказывающим, как в реальности обстоят дела с поддержкой HTTP/2 Server Push в различных браузерах.
Я много раз слышал фразу «HTTP/2 Server Push справится с этим», когда дело касалось проблем с загрузкой страницы, но я мало что понимал в этой теме и потому решил разобраться подробнее.
HTTP/2 Server Push оказалась более сложной и низкоуровневой, чем я думал, но по-настоящему меня удивило то, насколько отличается её взаимодействие с различными браузерами – я-то всегда считал, что это полностью отработанная фича, готовая к использованию в production.
Я не хочу сказать, что HTTP/2 Server Push – это бесполезная ерунда; думаю, это действительно мощный инструмент, который со временем станет лучше. Но я уже не считаю его «серебряной пулей из золотого ружья».
Схема извлечения данных
Между вашей страницей и целевым сервером есть целая серия кешей и функций, которые могут перехватить запрос:
Указанная выше модель напоминает блок-схемы, которые используются для описания Git или её характерных признаков. Те, кто знаком с темой, убеждаются в правильности своих знаний, остальные – только пугаются. Прошу прощения, если это так, и надеюсь, следующие разделы помогут вам разобраться.
Как работает HTTP/2 Server Push
Страница: Привет, example.com, могу я получить твою домашнюю страницу? 10:24
Сервер: Конечно! О, пока она отправляется, вот ещё таблица стилей, изображения, JavaScript и JSON. 10:24
Страница: Вау, класс! 10:24
Страница: Я читаю HTML, и, похоже, мне понадобится таблица сти… О, ты уже отправил мне её, круто! 10:25
Отвечая на запрос, сервер может включить дополнительные ресурсы, такие как заголовки запросов, которые браузер сможет сопоставить позже. Они находятся в кеше до тех пор, пока браузер не запросит ресурс, соответствующий описанию.
Повышение производительности происходит благодаря тому, что ресурсы отправляются ещё до того, как браузер запросит их. Теоретически это означает, что страница должна загружаться быстрее.
Вот практически всё, что я знал об HTTP/2 Server Push. Звучало это довольно легко, но на деле всё оказалось совсем не так просто…
Что угодно может использовать push-кеш
HTTP/2 Server Push – это низкоуровневая сетевая фича: всё, что использует сетевой стек, может использовать и её.
Но любая фича полезна, если она непротиворечива и предсказуема. Я протестировал HTTP/2 Server Push по этим показателям, запушив ресурсы и попробовав их собрать с помощью:
Способы взаимодействия сервисов друг с другом. Пулинг/пуш. Достоинства/недостатки. Выбор
Курьер доставил заказ. По смене статуса заказа надо уведомить заинтересованные стороны об этих событиях.
Клиент отправляет сообщение в чат поддержки. Нужно уведомить сервисы поддержки о поступивших данных от клиента.
Построение отчёта завершено. Ожидающий отчёт пользователь может его загрузить. Надо его уведомить об этом.
Знакомые/типовые ситуации. Одному сервису надо уведомить другой (другие) о происшедших событиях.
Давайте немного усложним:
Какие способы уведомления есть?
Активность со стороны сервера
Это, в общем-то, типовое решение. Сервер держит список заинтересованных сторон. По мере появления событий выполняет HTTP-запросы к клиентам.
Подвариант этого решения: Websocket. Сервер отправляет события в сокеты всем подписанным сторонам.
Повторы, обработка ошибок
Рано или поздно любой TCP/HTTP-канал сталкивается с недоступностью другой стороны. Что делать после возникновения ошибки? Повторять запросы? Что делать с вновь поступающими запросами? Ждать, пока успешно выполнятся предыдущие?
Рассмотрим виды ошибок:
Получив неустранимую ошибку, клиент может только записать её в лог. То есть, если полная остановка доставки сообщений не приемлема, то, получив неустранимую ошибку, типовым решением будет считать, что «уведомление доставлено», и переходить к доставке следующих уведомлений. Вероятно, это единственный нормальный путь.
Идя по этому пути, надо постоянно и внимательно следить за мониторами таких ошибок. Анализировать трафик на тему «почему возникла неустранимая ошибка?» и «можно ли жить дальше с этой ошибкой».
Но это не самая большая проблема.
Более интересными являются проблемы:
500-е ошибки
Мы выполняем запрос-передачу данных для сервера X. Происходит 500-я ошибка. Что это?
Возможны два варианта:
Сервис-приёмник данных по какой-то причине именно сейчас не работает (перегружается, переключается БД итп). В этом случае повтор запроса в дальнейшем приведёт нас к успеху.
В сервисе допущена ошибка, приводящая к 500. В этом случае, сколько бы повторов мы ни сделали, до исправления кода в приёмнике ситуация не изменится.
То есть, по повторяемости запросов ошибки у нас делятся на три вида:
Те, которым повтор поможет (сетевые, устранимые 500-ки).
Те, которым повтор не поможет, но выглядят как те, которым поможет (неустранимые 500-ки).
Те, которым повтор не поможет (например 40x-ки).
Разрабатывая политику повторов, помимо указанной проблемы, имеем ещё множество других проблем:
Как часто повторять запросы?
Не будем ли мы «укладывать» внешний сервис, повторяя запросы?
Не будем ли сами «укладываться», если одна из внешних систем по какой-то причине имеет некорректный TCP-стек ( iptables DROP )?
Если посмотреть на систему повторов запросов, то обнаружится, что практически в каждом случае она выбирается индивидуально.
Если сервис, генерирующий событие, и занимается доставкой его до заинтересованных сторон, то имеем
минимальный лаг доставки
минимальная нагрузка на хранилище сообщений;
необходимость повторов в случае неуспеха доставок
необходимость ведения реестра, кому что доставлено и кому что нужно доставить
двусмысленность некоторых ошибок: непонятно, можно (нужно) ли повторять, или нет
система повторов может быть причиной DDoS для клиентских сервисов.
Также есть некоторое количество организационных минусов:
После того, как клиент прекратил де-факто работу (тут два варианта: сервера выключены, сервера не выключены), система продолжает доставлять ему уведомления.
Вебсокет в режиме клиент-сервер
Часть описанных проблем решает постоянное соединение, инициируемое клиентом. Однако именно часть.
Пулинг
Достоинства пулинга
Максимально быстрое восстановление работоспособности после факапов.
Недостатки пулинга
минимальный лаг доставки сообщений равен интервалу пулинга, который обычно выбирается ненулевым
множество сервисов пулящих один создают существенно бОльшую нагрузку, нежели случай с активным сервисом. Сервисы, для которых нет сейчас никаких сообщений, всё равно создают нагрузку на подсистему доставки сообщений.
Ещё один неочевидный, организационный недостаток пулинга: часто способ получения новой порции данных связан со структурой хранения данных.
отсутствие двусмысленности, описанной выше
наиболее быстрое восстановление работоспособности после сбоя
максимальная независимость от сетевого стека TCP на клиенте
нет необходимости хранить/майнтенить список клиентов.
Лаг доставки
Как правило, запросы для пулинга формулируются как «есть ли данные для меня; если есть, то какие?». Такие запросы (в случае, если они некорректно спроектированы) зачастую имеют следующие проблемы:
при перегрузках количество данных в ответе может расти, или время выполнения запроса ухудшаться.
В случае, если получение очередной порции данных сопровождается простой выборкой из BTREE индекса, то и ответ на вопрос «есть ли данные?», как правило, сравнительно бесплатен. Об индексах поговорим ниже.
А сейчас давайте рассмотрим алгоритм работы традиционного пулера.
Обрабатываем полученные данные
Пауза соответствующая интервалу
Если рассматривать этот алгоритм, то видим, что переменная index и есть то, что связывает нас со структурой хранения данных.
Такой алгоритм, как правило, используют новички и. приводят себя к трудноустранимой проблеме: запросы с большими значениями index сделать индексируемыми крайне сложно. Почти невозможно.
Почему разработчик попадает в такую ситуацию? Потому что проектирует БД и API отдельно друг от друга. А нужно посмотреть на все компоненты в целом и на влияние их друг на друга.
Проблема состоит в том, что в БД, как правило, данные хранятся в виде плоских таблиц. Когда мы получаем очередную порцию данных с одними и теми же условиями фильтрации, то приходится делать что-то вроде следующего:
Как сделать план независящим от положения смещения? Использовать вместо смещения выборку из индекса:
Первичная инициализация. index := 0
Выполняем запрос limit данных, передавая в запрос index
Пауза, соответствующая интервалу
В системе с такой архитектурой, как правило, уже нет существенных препятствий к снижению интервала до минимальных значений (вплоть до нуля).
Но давайте ещё порефлексируем над архитектурой. Что плохого в ней?
Алгоритм привязан к структуре данных
Выполняется практически полностью на стороне клиента
Вследствие предыдущей проблемы сложно, например, централизованно модифицировать его на иную работу после факапов/проблем.
Пользователь может сам подставлять в index произвольные значения. Иногда это может быть нежелательно или приводить к багам, которые разработчику сервера сложно понять.
Давайте ещё раз модифицируем алгоритм. Заменим index на state и управлять им будем с сервера:
Выполняем запрос limit данных, передавая в запрос значение state
Что мы получили? Гибкость.
Переменная state определяется только сервером и не обязана быть привязанной к числу смещения. При желании в этой переменной можем хранить JSON со многими полями.
Если в переменной state хранится не только позиция окна, а, например, и значения фильтров и криптоподпись, то эту переменную имеет смысл называть курсором. Переименуем переменную ещё раз и избавимся от постоянных задержек:
Если данные были, перейти к шагу 2
Таким образом, получаем алгоритм, минимизирующий число запросов, если данных для клиента нет, и запрашивающий данные с максимальной производительностью, если таковые имеются.
Рекомендации по работе с курсорами:
Поскольку хранением курсора между запросами озадачен клиент, то имеет смысл хранить в курсоре и версию ПО сервера. В этом случае можно написать дополнительный код, обеспечивающий обратную совместимость (конвертацию форматов курсоров).
Во избежание трудных багов весь набор фильтров, полученных в первом запросе, хорошо хранить в курсоре и в последующих запросах игнорировать параметры фильтрации не из курсора. Перфекционисты могут даже выделить инициализацию курсора в отдельный запрос.
Во избежание введения в соблазн пользователей использовать в своём коде какие-то данные из курсора, лучше не использовать человекочитаемую строку в значении курсора. JSON, пропущенный через base64-кодирование (и криптоподписанный) подходит идеально.
Пример. Изменение алгоритма после сбоя.
Любая система гарантированной доставки сообщений из точки А в точку B в случае факапов будет накапливать пул недоставленных сообщений. После восстановления работоспособности будет период времени, когда приёмник данных сильно отстаёт от источника.
В случае, если порядок доставки сообщений возможно нарушать, то обработчик запроса с курсором может (продетектировав значительное отставание) начать возвращать два потока данных: тот, на который подписан клиент, и тот же, но с более актуальными данными.
Таким образом, пользователи, запросившие отчёт прямо во время факапа, продолжат его ждать (и дождутся). А пользователи, запросившие отчёт после факапа, получат его с небольшой задержкой.
Пример алгоритма серверной стороны, включающего второй поток в случае сильного отставания, приведён на рисунке.
Пофантазировав, схему можно дополнить не одним, а несколькими фолбеками.
Курсорная репликация
Описанные курсоры можно использовать для репликации данных с сервиса на сервис.
Часто один сервис должен иметь у себя кеш/реплику части данных другого сервиса. При этом требований синхронности к этой реплике нет. Поменялись данные в сервисе A. Они должны максимально быстро поменяться и в сервисе B.
Например, мы хотим реплицировать табицу пользователей с сервиса на сервис.
Для такой репликации можно использовать что-то готовое из инструментария баз данных, а можно сделать небольшой «велосипед». Предположим, что пользователи хранятся в БД PostgreSQL. Тогда делаем следующее:
модифицируем изменяющие пользователей запросы, чтобы на каждое изменение записи пользователя значение lsn устанавливалось бы из растущей последовательности
строим по полю lsn (уникальный) BTREE индекс.
В этом случае обновление записи пользователя будет выглядеть примерно так:
А запрос для работы курсора будет выглядеть как-то так:
Итоги
Почти во всех случаях, когда применяется активная система уведомлений зависимых сервисов, её можно заменить описанной курсорной подпиской.
При этом проблемы доступности клиентов, настроек, работоспособности TCP-стека останутся у клиентов
Максимально быстрое и простое восстановление после простоя/сбоев. Отсутствие двусмысленностей в кодах ошибок.
Web PUSH Notifications быстро и просто
Добрый день. В этой небольшой заметке я хочу рассказать как быстро и просто настроить push-уведомления на вашем сайте. Эта статья ни в коем случае не претендует на звание исчерпывающего руководства, но, я надеюсь, что она даст точку старта для дальнейшего изучения.
Информации по этой теме в интернете полно, но она фрагментирована, разбросана по разным ресурсам и перемешена с уведомлениями для мобильных устройств с примерами на Java, C++ и Python. Нас же, как веб-разработчиков, интересует JavaScript. В этой статье я постараюсь саккумулировать всю необходимую и полезную информацию.
Я думаю, вы уже знаете что такое push-уведомления, но я всё же напишу коротко о главном.
Пользователь, заходя на сайт, вытягивает (pull) с него данные. Это удобно и безопасно, но с развитием интернет ресурсов, появилась необходимость оперативно доставлять информацию пользователям не дожидаясь пока те сами сделают запрос. Так и появилась технология принудительной доставки (push) данных с сервера клиенту.
Push-уведомления работают только если у вас на сайте есть HTTPS.
Без валидного SSL сертификата запустить не получится. Так что если у вас еще нет поддержки HTTPS, то пришло время её сделать. Рекомендую воспользоваться Let’s Encrypt.
Для запуска на localhost нужно прибегать к хитростям. Я же тестировал скрипты на Github Pages.
Оглавление
Хорошие уведомления
Сразу хочу оговориться, что push-уведомления не для рекламных рассылок. Отправлять нужно только то, что действительно нужно конкретному пользователю и на что он действительно должен оперативно отреагировать.
Плохие примеры тоже требуют уведомления, но на них не нужно реагировать оперативно. Эти уведомления можно отправить на почту. Вообще, все важные уведомления рекомендуется дублировать на почту, так-как push-уведомления могут не дойти до пользователя по разным, не зависящих от вас, причинам. Также важным фактором является актуальность события. Об этом я еще поговорю чуть позже. Рекомендую к прочтению:
Вернемся к нашим баранам. Так как же всё это работает? Для начала немного теории.
Теория
Среди непосвященных бытует мнение что push-уведомления это простая технология, не требующая для реализации особых ресурсов. В действительности же это целый пул технологий.
Для начала небольшая схема того как все это работает (анимированная схема):
К сожалению, мне не удалось выяснить кто и как создает ID устройства и как сервер сообщений привязывается к конкретному устройству. Я использовал сервер сообщений Firebase Cloud Messaging от Google и его библиотеку. К сожалению, я не смог выяснить можно ли его заменить на свой сервер и как это сделать.
Изначально для отправки сообщений использовали:
Cloud to Device Messaging
Потом его заменили на:
Google Cloud Messaging
А потом еще раз поменяли на:
Firebase Cloud Messaging
Интересно, что дальше.
Что же происходит на стороне клиента?
Google рекомендует использовать переключатель для подписки и отписки от уведомлений. Таким образом, инициация процедуры подписки на уведомления исходит от пользователя, а не от сайта.
Принудительно подписывать на уведомления каждого приходящего пользователя, это плохая практика. Не делайте так.
Это все выглядит очень сложно, но на сервере все не проще.
Сложности на серверной стороне
Практика
Наконец-то, мы перешли к самому главному. Как я уже говорил ранее, в качестве сервера сообщений мы будем использовать Firebase Cloud Messaging, поэтому мы начинаем с регистрации и создания проекта на Firebase.
Можно еще покопаться в настройках и поиграться с разделением прав доступа, но, в общем-то, работа с сайтом Firebase закончена.
Приступаем к написанию клиента
Начнем с того что создадим Service Worker для получения push-уведомлений.
Создаем файл firebase-messaging-sw.js с следующим содержимым.
Файл Service Worker-а должен называться именно firebase-messaging-sw.js и обязательно должен находиться в корне проекта, то есть доступен по адресу https://example.com/firebase-messaging-sw.js. Путь к этому файлу жестко прописан в библиотеке Firebase.
Написанного кода достаточно для того чтобы показывать уведомления. О дополнительных возможностях поговорим чуть позже. Теперь добавим библиотеку Firebase и скрипт подписки в наш шаблон страницы.
Добавляем на страницу кнопку для подписки на уведомления
Подписка на уведомления
Вот и все. Это весь код который требуется для получения push-уведомлений.
Отправка уведомлений с сервера
В общем виде отправка уведомления выглядит так:
Все поля по порядку:
Это пример отправки одного уведомления одному получателю. Можно отправить одно уведомление сразу нескольким получателям. Вплоть до 1000 получателей за раз.
Пример ответов от сервера сообщений:
Мы не привязаны к какому-то конкретному языку программирования и для простоты примера будем использовать PHP с расширением cURL. Скрипт отправки уведомления нужно запускать из консоли.
messaging.onMessage
Обработчик messaging.onMessage стоит отдельного упоминания, так как он относится как раз к категории подводных камней. В примерах от Firebase я не видел примера использование этого обработчика. О нем мне рассказал FluorescentHallucinogen, за что ему отдельное спасибо, но он не упомянул о некоторых особенностях его использования.
Что же это за обработчик и как он работает. Из документации мы знаем, что этот обработчик вызывается если мы получаем push-уведомление и находимся в этот момент на странице сайта с которого отправлено уведомление (желающие использовать нативное решение могут посмотреть пример реализации). Эта функциональность очень полезна тем, что мы можем отобразить уведомление на странице сделав красивую модалку или еще что-то. У меня такой необходимости нет, потому я просто отображу стандартное уведомление.
Вроде все просто, но есть подводный камень. Дело все в том что на мобильных устройствах запрещено использовать конструктор Notification. И для решения этой проблемы нужно использовать ServiceWorkerRegistration.showNotification() и обработчик в этом случае будет иметь виде:
Теперь уведомления работают и на мобильных устройствах. Казалось бы уже все, но нет. Не смотря на заверения некоторых, ServiceWorker не должен быть пустым. Мы же хотим, что бы по клику пользователь переходил на нужную нам страницу. Для этого нам нужно добавить обработчик клика по уведомлению в ServiceWorker.
Сохраняем параметры уведомления для доступа свойству click_action в ServiceWorker-е.
Обрабатываем клик по уведомлению в ServiceWorker-е.
TTL и дополнительный контроль над уведомлением
Важным свойством для уведомления может является время его актуальности. Это зависит от ваших бизнес процессов. По умолчанию время жизни уведомлений 4 недели. Это очень много для уведомлений такого характера. Например, уведомление «Ваша любимая передача начинается через 15 минут» актуально в течении 15 минут. После этого сообщение уже не актуально и показываться не должно. За контроль над временем жизни отвечает свойство time_to_live со значением от 0 до 2419200 секунд. Подробней читать в документации. Сообщение с указанным TTL будет иметь вид:
Сообщение вида «Ваша любимая передача начинается через 15 минут» актуально в течении 15 минут, но уже через минуту после отправки оно станет не корректным. Потому что передача начнется не через 15 минут, а уже через 14. Контролировать такие ситуации нужно на стороне клиента.
Для этого мы поменяем отправляемое с сервера сообщение:
Вот таким незамысловатым образом мы получили полный контроль над уведомлением. Что самое интересное, пользователю мы показываем время уведомления в его часовом поясе. Это актуально для сервисов который работают по всему миру или регионах с широким разбросом часовых поясов как у матушки-России.
Заключение
А теперь поговорим о грустном. Не смотря на все прелести технологии, у неё есть ряд недостатков:
Библиотека Firebase скрывает в себе много тайн и её исследование могло бы дать ответы на некоторые вопросы, но это уже выходит за рамки этой статьи.
Поиграться
Проект на GitHub Pages
Так как для запуска Service Worker-а нужен HTTPS, то самым простым решением было разместить проект на GitHub Pages, что я и сделал.
Проект представляет из себя полноценное приложение для отправки и получения уведомлений. Для того что бы получить уведомление надо:
Можно отправить уведомление через любой инструмент для отправки HTTP запросов. Можно использовать сURL, я предпочитаю приложение Postman для Chrome.
Запрос такой же как и описанный ранее:
Вот и все. Получаем уведомление и радуемся жизни.
Ссылки
Updated at 2018-06-09
Обнаружились некоторые «особенности» в работе уведомлений.
Дубликаты уведомлений
Ко мне несколько раз обращались с вопросом: «Как исправить дублирующиеся уведомления?»
Проявляется эта проблема если открыть сайт отправляющий уведомления одновременно в нескольких вкладках. В этом случае Service Worker отправляет уведомление в обе вкладки и в обоих вкладках срабатывает метод messaging.onMessage. Наблюдать эту проблему можно на моем Demo проекте.
Могу порекомендовать для этих целей библиотеку pamelafox/lscache.
Если у вас есть другой метод решения проблемы, напишите в комментариях.
Картинки в уведомлениях
Сегодня ко мне обратился пользователь CTterorist, заметивший, что не отображаются картинки (image) в уведомлениях.
То есть, если вы отправите сообщение в таком виде, то Firebase потеряет картинку.
Обработчики показа уведомления такие же как в примерах выше.