Кирилл Алексеев работает в VK, руководит несколькими командами разработки бэкенда в Почте Mail.ru. Далее, рассказ будет от его имени. Он расскажет, как на запуске RuStore делали пуш-уведомления, а конкретно транспорт на замену Google Firebase для Android. Сделает обзор публичной части сервиса, погрузит в детали архитектуры бэкенда сервиса пушей. Пояснит, как устроены их мобильные SDK, как интегрировали пуши RuStore в Почту Mail.ru и почему они не лягут надолго. Покажет, что у них получилось и как этим можно пользоваться. На запуске бета-версии RuStore было важно, чтобы продукт отвечал требованиям разработчиков и имел минимальную необходимую функциональность. Например, позволял связаться с пользователем с помощью пуш-уведомлений. Для этого активно переиспользовались технологии и знания изнутри.

Зачем делать свой транспорт пушей

Все пуш-уведомления отправляются на мобильные устройства через API операционных систем. У Google и Apple есть свои API. Чтобы послать пуш-уведомление на телефон Android, нужно сделать запрос на бэкенд Google. Дальше бэкенд Google доставит пуш на мобильное устройство.

Возникает очевидная проблема: если Google нас блокирует и не позволяет нам слать пуши, то мы не сможем этого сделать.

Пользователи не в курсе того, кто виноват в том, что сломались пуши. Они и не обязаны это знать. Для них приложение потеряет часть полезной функциональности и будет менее актуальным. Например, в приложение Почты не будут приходить пуши о новых письмах.

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

  • Сервис должен предоставлять возможность подписываться и отписываться от пушей для конкретного приложения на конкретном физическом устройстве.

  • Должна быть возможность с бэкенда отправлять пуши по заданному токену на конкретное приложение на конкретном физическом устройстве, причём пуш должен дойти мгновенно, если устройство находится в сети.

  • Если устройство находится оффлайн, то мы должны сохранить пуш во временном хранилище на диске и доставить его, когда устройство снова появится в сети.

  • Сервис должен быть надёжным. Мы должны переживать уход одного Дата-центра из трёх, при этом насыпать как можно меньше ошибок, терять как можно меньше пушей и не создавать их дубликаты.

  • Сервис должен быть безопасным. Не должно быть возможности с наскока прочитать чужие пуши или отправить пуш в чужое приложение. К примеру, бэкенд Почты отправит пуш в бэкенд Облака. И мы должны быть устойчивы к атакам типа Denial of Service.

  • Простая интеграция в текущую схему работы с пушами. Было бы очень хорошо, если бы наш сервис нёс минимальный overhead на интеграцию в текущую схему, например, чтобы заменить Firebase на наш сервис.

Как выглядит любой пуш-сервис? Есть много похожих сервисов, которые предоставляют такую функциональность. Например, свои у Google, Apple, Huawei. Они все довольно похожи: в каждом есть строка со случайными символами, идентифицирующая конкретное приложение на конкретном физическом устройстве, которая называется пуш-токен.

Сам пуш — это произвольный payload, некий JSON, который пробрасывается на мобильное устройство. Часть полей обрабатываются операционной системой, а часть клиентом, то есть конкретным мобильным приложением.

Например, title: This is a push title. В этом случае на iOS отрисуется пуш с таким title.

Важно, что каждый такой сервис — это некоторая нативная фича операционной системы. Она встроена в операционную систему и пользуется теми возможностями, которые недоступны нам, как сторонним разработчикам.

Сложность задачи состояла в том, чтобы реализовать аналогичный сервис, пользуясь только тем, что доступно. При этом, его производительность по задержке доставки и delivery rate должна быть не хуже .

Для iOS мы не нашли возможности гарантировать какую-либо задержку доставки. Нет возможности запускать наш код на iOS устройстве в фоне через конкретные периоды времени. Поэтому iOS мы решили отложить и сконцентрироваться на Android — в котором такая возможность есть.

Обзор публичной части сервиса

Начнём с веб-интерфейса управления пушами приложения в RuStore. Чтобы включить пуш-уведомления в RuStore, нужно пройти в консоль разработчика и включить там пуши.

В этом случае генерируется идентификатор проекта, которым будете пользоваться, чтобы указывать его при генерации пуш-токена, отправке пушей и нескольких сервисных токенов.

Сервисные токены — это секреты, сенситивные данные, использующиеся для авторизации на отправке пуша с бэкенда. В нашем случае их можно выписать несколько, чтобы была возможность бесшовно мигрироваться. Чтобы сначала подержать на бэкенде один токен, а потом отозвать старый.

Также у нас к одному приложению можно выписать несколько пуш-проектов, идентификаторов проекта. Так мы можем разделять dev-секреты и prod-секреты. Есть возможность в dev иметь секреты для отправки пуш-токенов на вашем бэкенде и не тащить их с прода, чтобы ни у кого не было варианта их скомпрометировать в dev.

Как устроены наши мобильные SDK

Наше SDK для Android повторяет интерфейс SDK Firebase. Мы стремимся к drop-in replacement для SDK FCM. У нас есть такие же методы, как у Firebase. 

  • GetToken — регистрирует и отдаёт наверх приложению новый пуш-токен, чтобы приложение его отправило на свой бэкенд.

  • DeleteToken — метод, противоположный GetToken, он инвалидирует существующий токен, и после инвалидации пуши больше не будут по нему ходить.

Также определим несколько коллбэков:

  • с onMessageReceived можно перехватить пуши и обработать их произвольным способом;

  • с onNewToken узнать о том, что появился новый пуш-токен и пробросить его на бэкенд; 

  • с onDeletedMessages узнать о потерянных пушах, например, из-за того, что у них истекло время жизни. 

Это всё аналогично Google Firebase.

В случае с API мы тоже стремимся к drop-in replacement. Мы повторяем схему запроса и схему ответа Google Firebase. В Firebase есть два типа пушей:

  1. notification позволяет отправить пуш, который автоматически отрисовывается ОС;

  2. data-пуш позволяет отправить пуш, который не будет автоматически отрисован, но в него можно засунуть какой-то payload, и дальше клиент сам решает, отрисовывать его или нет.

В данном случае будет отправлен пуш с title «Hello, world».

Важно отметить, что у пуша есть какое-то количество предопределённых системой настроек, например, цвет, иконка, картинка. Часть из них мы поддерживаем, всё что я перечислил, отрисовывает наш SDK. Но есть часть полей, которые мы пока не поддерживаем, например, приоритет пушей.

Основные компоненты SDK

Наш мобильный SDK состоит из клиентского и хостового SDK.

Хостовая часть встроена в RuStore. Она собирает все пуши на девайсе и рассылает их конкретным приложениям внутри девайса. Из этого следует логичное ограничение — для юзеров, у которых нет RuStore на девайсе, не будут ходить пуши. Но зато мы можем получать все пуши ровно через один WebSocket, и дальше рассылать их внутри девайса, а не держать отдельный WebSocket для каждого приложения, что могло бы, например, потреблять аккумулятор у юзера.

Клиентский SDK — это SDK, который разработчик встраивает в своё приложение, подобно Google Firebase SDK. Он содержит все те методы, которые я перечислял.

Клиентский и хостовый SDK при инициализации запоминают друг друга по package name. Это нужно, чтобы у злоумышленников не было возможности подставить какое-нибудь третье приложение, которое будет притворяться RuStore и перехватывать пуш-токены у клиентского SDK. 

Как SDK получает пуши 

Когда клиентский SDK выписывает новый пуш-токен, он пробрасывает его приложению, которое перебрасывает его на свой бэкенд. Но также он отправляет этот токен хостовому SDK, чтобы хостовый SDK получал по нему пуш-уведомления.

Когда клиентский SDK передаёт новый пуш-токен хостовому, хостовый SDK добавляет его в свой локальный кэш и начинает по нему получать пуши. Хостовый SDK идёт в наш бэкенд, открывает новый WebSocket и начинает слать запросы на подписку по всем пуш-токенам, которые у него есть. Если запрос на подписку проходит успешно, то дальше в этот WebSocket начинают сыпаться пуши от бэкенда.

В реальности большую часть пушей мы действительно доставляем через WebSocket, причём мгновенно. Но, если телефон офлайн, то WebSocket может не быть, а на Pub/Sub системе сложно реализовать 100% delivery rate. Пуш может потеряться, пока летит до клиента внутри нашей инфраструктуры, поэтому есть ещё вторичный механизм — HTTP-поллинга. Наш хостовый SDK простым HTTP запросом каждые 5 минут ходит на сервер и просит выгрузить те пуши, которые не были получены. Конечно, эти два механизма синхронизируются между собой по ID пушей, дубли пушей не показываются.

Чтобы мы могли получать пуши в фоне с минимальной задержкой доставки, юзер должен нам дать permission на работу в фоновом режиме. В этом случае для него запускается диалоговое окно «РАЗРЕШИТЬ». После этого мы можем работать в фоне, даже если приложение RuStore на телефоне закрыто.

Детали архитектуры бэкенда сервиса пушей

Бэкенд состоит из 6 основных компонентов:

API написано на Go и разворачивается в Kubernetes. Оно ходит в хранилище пуш-токенов, где лежат все зарегистрированные пуш-токены от всех приложений с метаданными по этим пуш-токенам. Хранилище реализовано на базе Redis Cluster.

Также API ходит в хранилище пушей, где хранятся все пуши на диске с TTL, пока за ними не придёт хостовый SDK. Хранилище пушей реализовано на базе Scylla.

Ещё есть хранилище проектов на базе Redis. Там хранятся все зарегистрированные проекты, их идентификаторы, сервисные токены.

Есть Rate Limits, тоже на базе Redis, а также Pub/Sub шина, через которую доставляются пуши через WebSocket. Это наша собственная разработка, о которой мы ещё поговорим.

Обзор хранилища пуш-токенов 

По сути, пуш-токен — это словарик в Redis. На изображении токены представлены в сокращённом виде. На самом деле они длиннее. Всё это построено на базе Redis Cluster.

У каждого пуш-токена есть следующие поля:

  • Идентификатор проекта, к которому привязан пуш-токен.

  • Syn — монотонно возрастающий счётчик пушей по аналогии с TCP. Когда к нам приходит запрос на отправку нового пуша, мы делаем инкремент syn, получаем следующее значение, и это ID пуша.

  • Syn_stored — по сути это тоже syn, но минимальный syn, который хранится на диске.

Нам нужно было реализовать возможность хранить ограниченное количество пуш-уведомлений, чтобы хранилище не забилось пушами. Для этого мы храним верхнюю границу (syn) и нижнюю границу (syn_stored). Когда к нам приходит запрос на отправку нового пуша, мы можем прямо в памяти из syn вычесть syn_stored. Так мы получаем, сколько у нас сейчас хранится, и решаем, нужно ли удалять пуши, чтобы освободить место. Мы это делаем в памяти, а не идём каждый раз на диск пересчитывать, сколько пушей в реальности хранится.

По статистике на 1 млн пуш-токенов у нас используется порядка 400 МБ в Redis. Для приложения размера Почта Mail.ru на Android потребовалось бы порядка 10-15 ГБ, чтобы сохранить все пуш-токены в памяти.

Обзор хранилища пушей 

Хранилище пушей у нас на базе Scylla, всё хранится на диске. Здесь представлена схема таблицы с пушами. 

Для каждого пуша мы храним:

  • токен, к которому относится этот пуш;

  • его идентификатор, то есть текущее значение syn;

  • payload — это сам пуш;

  • timestamp — это время создания пуша. В основном используется для статистики.

В терминах Scylla наш первичный ключ — это связка токена и syn.

В данном случае токен выступает как partition key, а syn — как clustering key. Это значит, что по каждому токену все пуши хранятся максимально компактно. Они отсортированы в порядке возрастания монотонного счётчика syn. Это позволяет нам делать эффективные чтения по набору токен и syn_range. Когда приходит очередной поллинг и запрашивает все пуши, начиная с какого-то min syn, у нас это работает довольно эффективно в силу специфики хранения этого в Scylla.

В Scylla нет понятия ведущего и ведомого узла. Там можно писать в любой узел, очевидно, из-за этого могут возникать конфликты на записи. Например, мы попробовали записать в первый узел, произошёл таймаут, мы не знаем, удалось записать или нет. Повторяем во второй узел. Здесь мы записали успешно, но на самом деле в первый узел тоже всё записалось.

Scylla разруливает такие конфликты автоматически по алгоритму LWW (Last Write Wins), то есть остаётся только та запись, у которой timestamp новее. Этот алгоритм не подходит в общем случае для задачи разрешения конфликтов мастер-мастер, но в нашем случае это позволяет by design не иметь дубликаты пушей на повторах.

Логика в API на отправке пуша 

Вот пример запроса на отправку пуша.

Здесь:

  • header Authorization, который содержит тот самый сервисный токен (сокращённый);

  • пуш-токен, по которому отправляется пуш;

  • идентификатор проекта (в url);

  • title — это «Hello, world!».

Когда отправляется пуш, мы должны провести некоторое количество валидаций.

Первая валидация когда API идёт в хранилище проектов и проверяет, что заданный сервисный токен соответствует тому проекту, в который отправляется пуш. Если эта валидация пройдена, то нужно проверить, что указанный проект соответствует тому пуш-токену, на который пуш отправляется. Для этого мы у пуш-токенов храним project_id. В противном случае злоумышленник мог бы создать свой проект, выписать для него сервисные токены, потом найти пуш-токен, допустим, юзера Почты, и со своим сервисным токеном успешно отправить пуш юзеру почты. Конечно, такого быть не должно.

Если все валидации успешно пройдены, то дальше мы идём уже в хранилище пуш-токенов, делаем инкремент. В Redis это работает атомарно в силу однопоточности. Получаем новый ID пуша. Также мы в той же самой транзакции, в той же самой lua’шке можем проверить разницу между верхней и нижней границей, и понять консистентно с инкрементом счётчика, нужно ли удалять какой-то пуш из хранилища. Соответственно, lua’шка пробрасывает на бэкенд информацию о том, нужно ли удалять один пуш, и, в том числе, значение нового счётчика.

С этим значением нового счётчика API идёт в хранилище пушей и сохраняет там новый пуш с заданным временем жизни. Кстати, в API время жизни задаёт его клиент.

Асинхронно с этим мы идём в Pub/Sub шину и засылаем туда пуш. Если устройство подключено к интернету, есть WebSocket, то пуш будет мгновенно доставлен.

Так выглядит запрос на получение пушей:

Это публичная API, она в интернете, в неё входят все мобилки. Но пользоваться ей напрямую не придётся. Она инкапсулирована за SDK, наш SDK с ней взаимодействует. 

Здесь так же есть идентификатор проекта, пуш-токен, для которого нужно получить пуши, и минимальный syn, с которого API должна вернуть все пуш-уведомления.

Когда в API приходит такой запрос, API идёт в хранилище пуш-токенов и смотрит, а есть ли в системе какие-то пуши и какое текущее значение syn. Если syn равен min_syn, значит никаких новых пушей не приходило и нет смысла идти в хранилище пушей, чтобы их получить. Если же syn больше, чем min_syn значит, какие-то пуши приходили, можно их забрать.

На самом деле это довольно важная оптимизация, которая полагается на то, что syn — это монотонно возрастающий счётчик. По нашей статистике примерно 98-99% запросов в Get обрабатываются без похода в хранилище пушей, то есть без похода в диск, а просто из памяти. Если же пуши всё-таки есть, то мы идём в хранилище пушей, забираем все эти пуши и отдаём клиенту. Далее мы асинхронно удаляем те пуши, которые считаем подтверждёнными, чтобы не хранить их на диске.

Важно отметить, что подтверждёнными считаются не те пуши, которые мы только что отправили, а пуши, у которых syn не больше, чем min_syn. Контракт между клиентом и API состоит в том, что если клиент присылает указанный min_syn, то мы считаем, что все пуши до этого min_syn он уже видел и их можно удалять.

Как не забить хранилище неактуальными пуш-токенами 

Есть разные причины, по которым пуш-токен может «протухнуть»:

  • приложение удалили с устройства; 

  • телефон потеряли или продали;

  • другие причины, которые нельзя предусмотреть заранее.

Поэтому мы все пуш-токены создаём в хранилище пуш-токенов с наперёд заданным TTL. Спустя указанное время пуш-токен удаляется из Redis автоматически, но только если он действительно не нужен приложению. Мы это понимаем по периодическим GET-запросам.

У поллинга есть несколько вторичных функций. Если к нам периодически приходят GET, то мы автоматически продлеваем время жизни токена. Мы делаем это не на каждый запрос, а раз в сколько-то часов или дней, чтобы не создавать лишнюю нагрузку на мастера хранилища пуш-токенов, потому что TTL мы задаём довольно долгий (порядка месяца).

Почему мы не ляжем (надолго)

Отказоустойчивость на уровне API

Наше API эксплуатируется в Kubernetes. Трафик на него заводится через публичный виртуальный IP-адрес. С этого виртуального IP-адреса нагрузка роутится на несколько машин, на которых стоит связка NGINX + Envoy.

Соответственно, NGINX принимает запрос, проксирует его в Envoy, который дальше его раскидывает по Kubernetes. Kubernetes живёт в 5 Дата-центрах, поэтому уход одного нам точно не страшен. На Envoy настроены health-чеки, он чекает все поды. Если какой-то под оказался на плохой Кубер-ноде, на которой, например, высокий LA, то health-чек сфейлится и под будет выкинут из нагрузки, и она распределится по нормальным подам.

Redis Cluster для хранилища пуш-токенов

Хранилище пуш-токенов у нас на базе Redis Cluster. Благодаря этому мы довольно легко масштабируемся горизонтально. Если кончается запас по CPU или по памяти, то мы пользуемся автоматическим решардингом в Redis. Просто добавляем ноды и кластер автоматически ребалансится.

В случае с чтениями всё даже проще. Мы большую часть чтений направляем на реплики. Поэтому если у нас кончается запас по чтениям, мы просто в Redis Cluster добавляем ещё реплики и начинаем балансировать нагрузку между ними.

Redis Cluster устойчив к уходу одного Дата-центра (конечно, если не засовывать все тачки в один Дата-центр) благодаря механизму автоматического failover. Если какой-то мастер-узел уходит, то оставшееся множество мастеров проводит выборы и из доступных реплик старого и выбирает нового. То есть какая-то реплика промоутится до мастера. Так кластер будет в даунтайме, по крайней мере, на запись, пока не выберется новый мастер. Опыт на практике показывает, что выборы занимают от 15 секунд до минуты и через минуту автоматически кластер снова доступен на запись.

В случае с чтениями всё даже лучше. Есть несколько реплик, они стоят в разных Дата-центрах. Поэтому прямо в том же самом запросе, если ушла какая-то реплика, мы можем просто повторить её в другой Дата-центр и без даунтайма отдать результат юзеру.

Scylla для хранилища пушей

В хранилище пушей, реализованном на базе Scylla, тоже всё хорошо с масштабированием. Scylla легко масштабируется горизонтально. Просто добавляется новая нода, и данные раскидываются по ней из кластера.

В случае с уходом узла Дата-центра всё ещё лучше, чем с Redis, потому что в Scylla нет понятия ведущего или ведомого узла, поэтому и не может быть даунтайма, пока выбирается новый мастер. Если у нас сфейлился запрос на запись или чтение в один Дата-центр, то мы можем просто повторить его в другой. В итоге даунтайма нет.

Мы в Почте Mail.ru активно используем Scylla. Можно посмотреть мой доклад с конференции Scylla Summit. Там я рассказывал о том, как мы добились нагрузки в 250 тысяч запросов в секунду на запись на небольшом кластере Scylla с HDD (у нас было всего лишь 8 узлов).

10 миллионов WebSocket и Go

Лет 5 назад мы разработали сервис Notifier, который отправляет пуши через WebSocket. Мы используем его, чтобы браузерным вкладкам, на которых открыта вкладка со списком писем, доставлять сообщения о новых письмах. Это позволяет нам:

  1. Снижать задержку доставки, то есть юзер раньше узнаёт о том, что ему пришло письмо.

  2. Уменьшить нагрузку на наш сторадж, то есть не приходится поллить сторадж.

Опытным путём доказано, что этот сервис устойчив к уходу одного Дата-центра. Можно почитать статью на Хабре от первоначального разработчика этого сервиса Сергея Камардина «Миллион WebSocket и Go», но с тех пор у нас нагрузка уже несколько выросла.

Защита от DoS на генерации пуш-токенов

Это ещё одна проблема, которую нам нужно было решить. Наше API на генерацию пуш-токенов доступно в интернете, чтобы любая мобилка могла пойти и сгенерировать себе пуш-токен. Из этого следует очевидная проблема — кто-то может вооружиться curl’ом и пойти забивать нам базу пуш-токенов. Apple и Google тоже решают у себя такую проблему. Наши исследования показали, что они используют для этого секреты, зашитые в физические устройства, проводят по ним авторизацию. У нас такой возможности не было, но у нас есть RuStore на телефоне, там есть авторизация юзера, которой мы решили воспользоваться. 

Когда мобильное приложение хочет выписать себе новый пуш-токен, оно берёт o2-токен юзера из RuStore и идёт с ним в специальный сервис Auth-Proxy. Auth-Proxy берёт этот o2-токен и идёт в авторизацию RuStore. Авторизация проверяет токен, если он валидный, то возвращает «ОК». Вместе с «ОК» возвращается user_ID, к которому относится этот o2-токен.

Далее Auth-Proxy идёт в специальный сервис, который генерирует коротко живущие одноразовые токены. Генерирует токен и прикладывает к нему в payload user_ID. Дальше этот промежуточный токен прокидывается по всей цепочке в мобильное приложение.

Теперь мобильному приложению, чтобы выписать себе пуш-токен, нужно взять этот промежуточный токен и пойти в API. API пойдёт в сервис короткоживущих токенов, проверит его, причём запрос будет не идемпотентный. Поскольку токен одноразовый, его нельзя два раза использовать. Если токен валидный, то возвращается «ОК». Вместе с «ОК» возвращается payload, который был в токене. В данном случае это user_ID, к которому относился o2-токен. Дальше API генерирует пуш-токен, пробрасывает его на мобилку.

Таким образом за счёт проверки авторизации мы, как минимум, защитились от того, что кто-то возьмёт curl и пойдёт генерировать нам нагрузку. Но если кто-то задался целью, ему будет не сложно достать свой o2-токен из RuStore и с ним ходить к нам, создавать проблемы.

Поэтому у нас во всей цепочке используется user_ID. На API он сохраняется в рейтлимитилку. Если кто-то с одним и тем же o2-токеном будет ходить и пытаться генерировать пуш-токены, то он просто упрётся в лимиты.

Как мы интегрировали пуши RuStore в Почту Mail.ru

Не сюрприз, что первым большим потребителем разработанного сервиса стала Почта Mail.ru. Мы решили посмотреть, нормально ли работает то, что мы разработали. Мы стали параллельно запускать пуши по 2 каналам: по каналу с Firebase и по нашему каналу пуш-уведомлений RuStore. Мы замеряли, сколько пушей приходит через каждый из каналов.

Первые результаты не очень порадовали. Мы долго чинили баги в аналитике, которая считает, сколько к нам пушей приходит, а также баги в доставке пушей. Пока дочинили не до конца. UPD: дочинили!

Следующим этапом мы собираемся показывать первым тот пуш, который пришёл раньше. То есть если пуш придёт в Firebase, то покажем юзеру его, если в RuStore, то его, а на клиенте будет  производиться дедупликация по ID пуша.

Тут же мы планируем замерять задержку доставки, насколько кто быстрее доставляет в среднем по какому-то перцентилю. Опыт подсказывает, что какие-то проблемы возникнут и нужно будет их чинить.

Мы, разработчики Почты Mail.ru, выступали для своего сервиса пуш-уведомлений RuStore потребителями, и могли оценить, насколько сложно его интегрировать.

Для включения пушей достаточно просто сделать несколько кликов в веб-интерфейсе, так сгенерируются ID проекта и сервисные токены.

Интерфейс нашего SDK повторяет интерфейс SDK FCM, встраивать его было несложно. А вот в случае с API получилось, что мы переиспользовали коннектор от Firebase, заменили в нём хост, и у нас всё заработало из коробки.

Какая польза от интеграции? За счёт старта экспериментов с интеграцией в Почту Mail.ru мы:

  1. Частично защищены на случай, если Google заблокирует нам пуши совсем. Мы сможем продолжать слать пуши о новых письмах юзерам через канал пушей RuStore.

  2. Можем повышать delivery rate за счёт отправки по двум каналам параллельно, а не по одному. Таким образом мы можем суммарно повышать delivery rate всех пушей, которые приходят на устройство юзерам.

  3. Провели нагрузочное тестирование пушей RuStore в реальных условиях. Это позволило найти проблемы, часть которых мы уже зафиксили.

Промежуточные результаты (VKPNS vs FCM)

Эксперименты всё ещё идут, но я уже готов поделиться промежуточными результатами.

  • Количество успешно отправленных пушей за сутки:

~99.999% vs 99.993%

Недавно команда выкатила фикс на бэке, который позволил на целый процент повысить количество успешных ответов на отправку пуша. До этого мы были на 1 % хуже, чем Google, а сейчас у нас на одну девятку больше. Это данные по статистике, которая собирается по отправке пушей от Почты.

Спойлер: проблема была в том, что в коннекторе к Redis, который мы используем, был дедлок, из-за этого у нас в какой-то момент начинали копиться коннекты, что приводило к OOM. Если пользуетесь Radix, то рекомендую обновиться.

  • Среднее время ответа API send: 13ms vs 45ms

Время ответа нашего API уже в 3 раза ниже, чем у Google. Это было ещё до фиксов OOM. Здесь приведено среднее время ответа, максимальное также ниже.

  • Доставляемость пушей за сутки:
    Октябрь 2022 года  ~60% vs 100%
    UPD: Декабрь 2022 года ~101% vs 100%

Пока что наша основная зона роста — это доставляемость пушей. Мы уже доставляем 60% от того, что доставляет Google через свой нативный канал. По нашей статистике для довольно большой части юзеров пуши не доставляются совсем, что говорит о том, что есть какой-то баг, из-за которого происходит рассинхронизация клиентского и хостового SDK. При этом те юзеры, кому мы пуши доставляем, получают примерно столько же, сколько и от Google — где-то мы доставляем чуть меньше, где-то чуть больше. Поэтому считаем, что текущее значение delivery rate вызвано не тем, что нас как-то режет ОС, то есть не непреодолимыми проблемами. В течение нескольких недель мы надеемся зафиксить баг, о котором я сказал, и тогда delivery rate существенно вырастет. UPD: пофиксили в декабре, актуальный delivery rate можно увидеть выше.

Планы на будущее

Мы стартовали совсем недавно и с довольно узким подмножеством функциональности, потому что нам надо было быстро запускаться. В ближайшее время мы собираемся:

Приблизиться по функциональности к FCM

Как минимум, мы должны сделать рассылки по топикам (массовые рассылки в Firebase), поддержать ещё больше настроек пуша. Я говорил, что мы поддерживаем иконку, картинку, цвет пуша, но пока что не все поля. В консоли разработчиков в Firebase можно видеть метрики доставляемости, сколько там пушей отправлено, сколько доставлено, сколько открыто. Мы пока эту статистику не выводим, но это в планах.

Научиться получать пуши через другие приложения VK на устройстве пользователя

Я говорил о том, что пуш-уведомления приходят юзеру, только если у него стоит RuStore. Понятно, что это ограничение для роста нашего сервиса. Поэтому мы хотим сделать так, чтобы пуш-уведомления могли приходить через любой из продуктов ВК, который стоит на Android-устройстве юзера. Это должно существенно повысить нам охват. При этом важно, что мы должны продолжать держать только одно соединение, а не из каждого приложения ВК (из Маруси отдельно WebSocket, из Облака отдельно WebSocket) — это нам не подходит.

Добиться значения health-метрик не хуже, чем у Google FCM

Поскольку есть куда расти, как минимум, по delivery rate, и те значения API, которые я показывал, тоже нужно сохранить, важный вектор нашей работы — это стабилизация нашего сервиса. По основным health-метрик нам нужно быть, как минимум, не хуже, чем Google.

Комментарии (7)