Русский
Русский
English
Статистика
Реклама

Оповещение

Из песочницы Многоканальные массовые рассылки на Redis

18.09.2020 00:06:41 | Автор: admin

Вводная


Привет, Хабр! Меня зовут Борис и в этом труде я поделюсь с тобой опытом проектирования и реализации сервиса массовых рассылок, как части объемлющей системы оповещения студентов преподавателями (далее также Ада), которую тоже я осуществляю.



Ада


Нужна затем, чтобы свести на нет число прерываний учебного процесса по следующим причинам:

  1. Преподаватели не хотят делиться личными контактными данными;
  2. Студенты на самом деле тоже у них просто выбора особо нет;
  3. В силу специфики моей альма-матер, многие преподаватели вынуждены или предпочитают использовать мобильные устройства без доступа к сети Интернет;
  4. Если передавать сообщения через старост групп, то в игру вступает эффект испорченного телефона, а также фактор ой, я забыл:(.

Работает примерно так:

  1. Преподаватель через один из доступных ему каналов связи: СМС, Telegram, SPA-приложение передает Аде текст сообщения и список адресатов;
  2. Ада транслирует полученное сообщение всем заинтересованным* студентам по всевозможным каналам связи.

* Доступ к сервису предоставляется в добровольно-заявительном порядке.

Предполагается, что


  1. Общее число пользователей не превысит десяти тысяч;
  2. Соотношение студент преподаватель / член УВП (деканаты, здравпункт, военно-учетный стол и т.д.) будет держаться на уровне 10:1;
  3. Оповещения текстовые по содержанию и носят преимущественно экстренный характер: Моей пары сегодня не будет, Тебя отчисляют))0 и т.д.

Ключевые требования к сервису рассылок


  1. Простота интеграции с другими информационными системами ВУЗа;
  2. Возможность отложенной доставки, принудительная перепланировка времени отправки сообщений, поставленных в очередь в неподобающее для приличных студентов время;
  3. Разделяемая между каналами связи история и ограничения на отправку;
  4. Достоверность и полнота обратной связи: если кому-то чего-то не дойдет, а понять это будет нельзя, то всем будет обидно.

Сие произведение состоит из пяти частей: вводной, подготовительной, концептуальной, предметной и заключительной.

Подготовительную часть можно смело пропускать, если вы знакомы с Redis интерпретацией Pub/Sub шаблона, а также механизмами событий, LUA-скриптинга и обработки устаревших ключей, кроме того, весьма желательно иметь хоть какое-то представление о микросервисной архитектуре ПО.

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

Подготовительная


Очень грубо и сильно абстрактно ~ 5 минут
Redis это открытое [BSD 3-clause] ПО, реализующее хранение данных типа ключ-значение в ОЗУ (преимущественно).

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

Значениями могут выступать строки, хэш-таблицы, списки и множества.

Любую модификацию пространства ключей можно отследить через встроенный механизм событий (отключен по умолчанию в угоду производительности).

В дополнение к транзакциям, пользователь может определять новые операции, что будут выполняться атомарно, используя языковые средства LUA 5.1.

Подробно и из первых уст ~ 15 минут
  1. Pub/Sub Redis. Пробегитесь глазами по первому параграфу, осознайте fire&forget момент, посмотрите, как работают команды PUBLISH, SUBSCRIBE и их паттерн-вариации;
  2. Redis Keyspace Notifications. Первые три параграфа;
  3. EXPIRE Redis. Параграф How Redis expires keys;
  4. Redis 6.0 Default Configuration File. В дополнение к предыдущей ссылке. Строки 939:948 (The default effort of the expire cycle);
  5. EVAL Redis. Отличие EVAL от EVALSHA, а также параграфы Atomicity of scripts, Global variables protection и Available libraries, в последнем нас интересует только cjson;
  6. Redis Lua Scripts Debugger. Не обязательно, но может прилично сэкономить вам слез в будущем. У меня вот кончились пользуюсь каплями;
  7. Исторические аспекты появления микросервисной архитектуры. Тоже не обязательно, но весьма доходчиво и интересно.

Концептуальная


Наивный подход


Самое очевидное решение, которое можно придумать: несколько методов доставки (send_vk, send_telegram и т.д.) и один обработчик, который будет вызывать их с нужными аргументами.

Проблема расширяемости


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

Проблема стабильности


Сломался один из методов = сломался весь сервис.

Прикладная проблема


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

API ВКонтакте работает только через HTTP; у Telegram есть HTTP-шлюз, но он менее стабилен, нежели MTProto и хуже документирован.

Таких различий достаточно много: максимальная длина сообщения, random_id, интерпретация и обработка ошибок и т.д. и т.п.

Как с этим быть?


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

Непонятно? Закажите покушать!


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



  1. Вы нажимаете на большую желтую кнопку Заказать;
  2. Яндекс.Еда находит курьера, сообщает выбранные позиции ресторану и возвращает вам номер заказа, дабы разбавить неопределенность ожидания;
  3. Ресторан по завершении готовки обновляет статус заказа и отдает еду курьеру;
  4. Курьер, в свою очередь, отдает еду вам, после чего помечает заказ как выполненный.

Приятного аппетита!

Вернемся к проектированию


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

Ассоциирующиеся с номером заказа данные будем называть историей, она позволяет в любой момент времени ответить на следующие вопросы:

  1. Кто отправил;
  2. Что отправил;
  3. Откуда;
  4. Кому;
  5. Кто и как получил.

История создается вместе с заказом как два отдельных Redis ключа, связанных через суффикс:

suffix={Идентификатор пользователя}:{UNIX-время в наносекундах}История=history:{suffix}Заказ=delivery:{suffix}

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

Зрение курьеров работает через подписку на событие DEL ключей по форме delivery:*.

Когда наступает момент доставки, Redis удаляет ключ заказа, после чего курьеры приступают к его обработке.

Так как курьеров несколько велика вероятность возникновения конкуренции на стадии изменения истории.



Избежать её можно, определив соответствующую операцию атомарно в Redis это делается через LUA-скриптинг.

Детали реализации будут подробно рассмотрены в следующей главе. Сейчас же важно получить четкое представление о решении в целом с чем может помочь рисунок ниже.



Отслеживание статуса

Клиент может отследить статус доставки через ключ истории, который генерируется отдельным методом API разрабатываемого сервиса перед постановкой сообщения в очередь (как и номер заказа генерируется Яндекс.Едой в самом начале).

После генерации ключа, на него вешается (опционально и тоже отдельным методом) трекер с тайм-аутом, который будет следить за числом изменений истории курьерами (SET события). Только теперь сообщение ставится в очередь.



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

Зачем мудрить с событиями в Redis, если есть RabbitMQ и Celery


На то есть как минимум пять объективных причин:

  1. Redis уже используется другими сервисами Ады, RabbitMQ/Celery новая зависимость;
  2. Redis нужен нам, в первую очередь, как СУБД, а не средство IPC;
  3. Использования Redisa как хранилища истории защищает нас от SQL-инъекций в текстах сообщений;
  4. Проблема масштабируемости не стоит и в обозримой перспективе не встанет. Кроме того, эта самая масштабируемость в контексте данной задачи достигается скорее за счет увеличения API-лимитов, нежели горизонтального наращивания вычислительных мощностей;
  5. Celery пока что не дружит с asyncio, а программный костяк проекта составляет уже реализованная с основой на asyncio библиотека.

Предметная


Система оповещения (объемлющая) исполнена в виде множества микросервисов. Удобства ради, интерфейсы, методы инициализации слоев данных, текста ошибок, а также некоторые блоки повторяющейся логики были вынесены в библиотеку core, которая, в свою очередь, опирается на: gino (asyncio обертка SQLAlchemy), aioredis и aiohttp.

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


О сущностях ~ 3 минуты
Пользователь человек.

У пользователя есть роль: студент, преподаватель, деканат и т. д., а также почта и имя.

С пользователем может быть связан контакт, где провайдер: ВКонтакте, Telegram, сотовый и т. д.

Пользователи могут состоять в группах [allegiance].

Из групп можно формировать потоки [supergroup].

Группы и потоки могут принадлежать [ownership] пользователям.

Генерация ключа истории


delivery/handlers/history_key/get GitHub

Очередь


delivery/handlers/queue/put GitHub

Обратите внимание на:

  1. Комментарий 171:174;
  2. То, что все манипуляции с Redisом [164:179] завернуты в транзакцию.

Зрение курьеров [94:117]


core/delivery GitHub

Обновление истории курьерами


core/redis_lua GitHub

Инструкции [48:60] отменяют преобразование пустых списков в словари ([] -> {}), так как большинство языков программирования, и CPython в том числе, интерпретируют их иначе, нежели LUA.

ISS: Allow differentiation of arrays and objects for proper empty-object serialization GitHub

Трекер


delivery/handlers/track/post GitHub имплементация.
connect/telegram/handlers/select GitHub [101:134] пример использования в пользовательском интерфейсе.

Курьеры


Всякая доставка из task_stream (@Зрение курьеров) обрабатывается в отдельной asyncio-сопрограмме.

Общая стратегия работы с временными ограничениями прикладных интерфейсов такова: мы не считаем RPS (requests per second), но корректно /реагируем/ на ответы по типу http.TooManyRequests.

Если интерфейс реализует, помимо глобальных (на приложение), еще и пользовательские временные лимиты, то они обрабатываются в порядке очереди, т.е. сначала мы отправляем всем кому можем и только потом начинаем выжидать, если не очень долго.

Telegram


courier/telegram GitHub
Как было замечено ранее, MTProto интерфейс Telegramа выигрывает у HTTP-аналога в стабильности и размере документации. Для взаимодействия с оным мы воспользуемся готовым решением, а именно LonamiWebs/Telethon.

ВКонтакте


courier/vk GitHub
ВКонтакте API поддерживает массовые рассылки через передачу списка идентификаторов в метод messages.send (не более сотни), а также позволяет склеить до двадцати пяти messages.send в одном execute, что дает нам 2500 сообщений за вызов.

Любопытный факт
Многие методы ВКонтакте API, и execute в том числе, наиболее полно описаны в русской версии документации.

Заключительная


В настоящем труде предложен способ организации многоканальной системы массового оповещения. Полученное решение вполне удовлетворяет запросу (@Ключевые требования к сервису рассылок) большинства интересантов, а также предполагает возможность расширения.

Основной недостаток заключается в fire&forget эффекте Pub/Sub, т.е. если удаление ключа заказа придется на момент болезни одного из курьеров, то в соответствующем домене никто ничего не получит, что впрочем будет отражено в истории.
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru