Как мы написали сервис уведомлений



    Привет, Habr.

    В этой статье расскажу, как мы написали систему уведомлений, которая недавно вышла в VMmanager (платформа управления виртуализацией серверов). Упор будет на архитектуру системы и детали реализации: какой стек технологий использовали, какие решения принимали и почему, что в итоге получилось. Также расскажу, как устроено динамические обновление данных в нашем single-page application.


    Задача


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


    Основные требования к сервису:

    • Простая интеграция с существующими продуктами компании;
    • Умение работать с различными источниками событий;
    • Отправка событий в различные каналы (email, интерфейс и мессенджеры).

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


    Что мы имеем


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




    Схема 1. Обмен обновлениями между BackEnd и FrontEnd

    Все наши продукты представляют из себя single-page application, где за динамическое обновление данных отвечает сервис под названием Notifier.


    Схема работы следующая (схема 1):

    • Пользователь открывает страницу сайта.
    • Frontend (FE) через websocket (WS) подписывается на события определенных сущностей у Notifier. В нашем случае происходит подписка на все события виртуальных машин (VM).
    • Пользователь создает VM.
    • Backend (BE) запускает задачу по созданию VM.
    • BE создает уведомление о создании виртуальной машины для Notifier.
    • Frontend узнает о новой машине через WS.
    • Пользователь в списке виртуальных машин видит новую VM, которая находится в статусе «Устанавливается».

    Интересно, что Frontend не просто узнает о случившемся событии, но и получает полную информацию о сущности. Мы просто убрали лишнее звено из цепочки FE:


    подписываемся → получаем обновление → д̶е̶р̶г̶а̶е̶м̶ ̶B̶E̶ → отрисовываем объект


    При подписке Frontend сообщает Notifier endpoint, который необходимо дернуть при получении обновления. Таким образом, вместе с обновлением FE получает элемент целиком. В результате экономим время на выполнение запроса.



    Результат




    Схема 2. Итоговый вариант сервиса уведомлений

    На схеме 2 мы видим получившийся результат. Давайте разберем его.


    Сервис настроек (alert configurator)


    Входная точка, именно сюда будут попадать настройки уведомлений. Это микросервис, в чьи функции входит:

    • Принимать настройки, состоящие из ряда обязательных полей и дополнительной информации в произвольном формате.
    • Отдавать список настроек по имени конечного алерта.
    • И обрабатывать пару-тройку endpoints, отвечающих парадигме CRUD.

    Формат входных данных:


    {
       "name":"string",
       "metric":{},
       "channel":{},
       "enabled":bool
    }
    

    name — имя конечного алерта для фильтрации.
    metric — объект, который попадает в конечный алерт неизменным. Он содержит данные в произвольном формате, которые различаются от алерта к алерту.
    channel — объект с каналами, в которые необходимо отправить уведомления. Каждый канал содержит в себе получателей, будь то chatID для Telegram или список идентификаторов для отправки по email.
    enabled — поле, отвечающее за активность настройки.


    Обертка конечного алерта (alert wrapper)


    Это исполняемый файл, который запускает конечный алерт, читает его поток вывода (stdout) и пишет в поток ввода (stdin). При этом он забирает на себя всю работу с конфигом и сущностями, необходимым для работы нашего алерта, а также отправку сообщений в notice-center.


    Давайте рассмотрим основные принципы работы обертки на примере диаграммы последовательности.



    Схема 3. Принцип работы alert wrapper

    Для старта wrapper на необходим конфигурационный файл и конечный алерт. В конфиг файле описан путь до исполняемого файла алерта и сущности, с которыми он будет работать.


    Обертка работает в трех основных потоках:

    • Main — в нем при старте wrapper мы читаем конфигурационный файл. Запускаем алерт, получая его stdin и stdout. И также запускаем два следующих потока.
    • AlertWatcher — поток, который получив stdout алерта, будет следить за ним. В случае получения данных они будут обработаны и отправлены в notice center.
    • NotifyWatcher — поток следящий за обновлением информации в notifier. В случае получения обновлений они будут обработаны и отправлены на stdin алерта.


    Зачем так усложнять, спросите вы. Дело в том, что wrapper призван облегчить работу человека или целой команды, которая будет внедрять сервис нотификаций. Тем самым мы решаем основную задачу — простота интеграции. Wrapper берет на себя:

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

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


    • Обработать конфиг присланный wrapper’ом stdin.
    • Реализовать бизнес-логику самого алерта
    • Отдавать на stdout данные в корректном формате

    Notifier


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


    Alert wrapper подписывается на обновления:

    • Конфигурации (alert configurator);
    • Сущности, которую будет обрабатывать алерт (panel);
    • Получателей сообщений (panel).

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


    Сервис отправки сообщений (msg sender)


    Готовый инструмент, благодаря которому наши панели рассылают почту на email пользователям. По первоначальной задумке его было необходимо научить отправлять сообщения не только по почте, но и в любой необходимый канал: telegram, slack или интерфейс панели. Но впоследствии было принято решение об обязательном уведомлении пользователя в интерфейсе, так между alert wrapper и msg sender появился центр уведомлений.


    Центр уведомлений (notice center)


    Cервис, отвечающий за хранение, пересылку и показ уведомлений пользователю.

    • Хранение. Полученные уведомления от wrapper сохраняются, чтобы пользователь имел возможность ознакомится с их историей.
    • Показ. При получении уведомления notice center показывает его пользователю в виде push-сообщения. Если пользователь перешел по нему, мы помечаем уведомление как прочитанное.
    • Пересылка. Если предыдущее условие не было выполнено, то через определенный интервал времени сообщение будет передано в msg sender и отправлено по необходимому каналу.

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


    Заключение


    В результате мы получили рабочий сервис отправки уведомления пользователям. При этом отдельные его части можно использовать вне контекста alert+wrapper, можно напрямую отправлять сообщения в центр нотификации. Msg sender можно и дальше расширять, добавляя поддержку различных каналов.


    Буду рад обсудить архитектуру сервиса в комментах.

    ISPsystem
    Софт для хостинга: ISPmanager, BILLmanager и др.

    Комментарии 4

      0

      вопросики
      1) как понимаю у вас идет запрос к ~api/addnewnotif, они записываются в базу, потом сервис раз в Nt сканирует базу, берет новые и отправляет?
      1.1) если это так как вы решается горизонтальное масштабирования этого сервиса?
      2) есть ли группировка? какую логику используете?

        0
        По первому пункту. Не совсем так. Под Notifier, на самом деле, находится redis stream (изначально был Consul KV, что не подходит под такие задачи). Факт нотификации записывается в него. Сам же Notifier просто отслеживает стрим, раз в Nsec `XRANGE stream prevID +` полученные обновления сопоставляет с подписками, которые пришли на ws и совпадения выкидывает в нужное соединение. Что касается горизонтального масштабирования, могу сказать, что в рамках одного продукта не так много событий, поэтому один redis + notifier прекрасно справляются. Но простор для маневров есть. Redis можно кластеризовать, а Notifier, как его клиент, можно размножить в любом количестве. Останется только решить вопрос распределения клиентов между копиями Notifier.

        По второму вопросу не совсем понял, о чем идет речь.
          0

          второй вопрос про "антиспам", если есть 100 нотификаций в минуту, вы пошлете 100 писем на email?

            0
            В общем случае этот вопрос решили на уровне сервиса конфигурации параметром «частота сообщений». Устанавливается для каждой конкретной настройки уведомлений и есть дефолтное значение для всех.

            Если говорить о конкретной реализации в VMmanager (уведомлять о пороговых значениях виртуальных машин и нод) в настройках есть следующие поля:
            — значение ресурса (о превышении которого нужно сообщать)
            — интервал (время в течении которого держится превышение)
            — частота сообщений
            Т.е. при CPU 80% peak 120s frequency 300s При условии что CPU зашкаливает постоянно первое письмо мы получим через 2мин и затем каждые 5мин.

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

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое