Привет, 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 можно и дальше расширять, добавляя поддержку различных каналов.
Буду рад обсудить архитектуру сервиса в комментах.