[Python, Анализ и проектирование систем] Многоканальные массовые рассылки на Redis
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Вводная
Привет, Хабр! Меня зовут Борис и в этом труде я поделюсь с тобой опытом проектирования и реализации сервиса массовых рассылок, как части объемлющей системы оповещения студентов преподавателями (далее также — Ада), которую тоже я осуществляю.
Ада
Нужна затем, чтобы свести на нет число прерываний учебного процесса по следующим причинам:
- Преподаватели не хотят делиться личными контактными данными;
- Студенты на самом деле тоже — у них просто выбора особо нет;
- В силу специфики моей альма-матер, многие преподаватели вынуждены или предпочитают использовать мобильные устройства без доступа к сети Интернет;
- Если передавать сообщения через старост групп, то в игру вступает эффект «испорченного телефона», а также фактор «ой, я забыл:(».
Работает примерно так:
- Преподаватель через один из доступных ему каналов связи: СМС, Telegram, SPA-приложение — передает Аде текст сообщения и список адресатов;
- Ада транслирует полученное сообщение всем заинтересованным* студентам по всевозможным каналам связи.
* Доступ к сервису предоставляется в добровольно-заявительном порядке.
Предполагается, что
- Общее число пользователей не превысит десяти тысяч;
- Соотношение студент — преподаватель / член УВП (деканаты, здравпункт, военно-учетный стол и т.д.) будет держаться на уровне 10:1;
- Оповещения текстовые по содержанию и носят преимущественно экстренный характер: «Моей пары сегодня не будет», «Тебя отчисляют))0» и т.д.
Ключевые требования к сервису рассылок
- Простота интеграции с другими информационными системами ВУЗа;
- Возможность отложенной доставки, принудительная перепланировка времени отправки сообщений, поставленных в очередь в неподобающее для приличных студентов время;
- Разделяемая между каналами связи история и ограничения на отправку;
- Достоверность и полнота обратной связи: если кому-то чего-то не дойдет, а понять это будет нельзя, то всем будет обидно.
Сие произведение состоит из пяти частей: вводной, подготовительной, концептуальной, предметной и заключительной.
Подготовительную часть можно смело пропускать, если вы знакомы с Redis интерпретацией Pub/Sub шаблона, а также механизмами событий, LUA-скриптинга и обработки устаревших ключей, кроме того, весьма желательно иметь хоть какое-то представление о микросервисной архитектуре ПО.
В предметной части обозревается код на Питоне, но я верю, что информации достаточно, чтоб вы могли написать подобное на чем угодно.
Подготовительная
Очень грубо и сильно абстрактно ~ 5 минут
SPL
Redis — это открытое [BSD 3-clause] ПО, реализующее хранение данных типа «ключ-значение» в ОЗУ (преимущественно).
С ключом может быть ассоциировано время жизни, по истечении которого он будет удален.
Значениями могут выступать строки, хэш-таблицы, списки и множества.
Любую модификацию пространства ключей можно отследить через встроенный механизм событий (отключен по умолчанию в угоду производительности).
В дополнение к транзакциям, пользователь может определять новые операции, что будут выполняться атомарно, используя языковые средства LUA 5.1.
Подробно и из первых уст ~ 15 минут
SPL
- Pub/Sub — Redis. Пробегитесь глазами по первому параграфу, осознайте fire&forget момент, посмотрите, как работают команды PUBLISH, SUBSCRIBE и их паттерн-вариации;
- Redis Keyspace Notifications. Первые три параграфа;
- EXPIRE — Redis. Параграф «How Redis expires keys»;
- Redis 6.0 Default Configuration File. В дополнение к предыдущей ссылке. Строки 939:948 (The default effort of the expire cycle…);
- EVAL — Redis. Отличие EVAL от EVALSHA, а также параграфы «Atomicity of scripts», «Global variables protection» и «Available libraries», в последнем нас интересует только cjson;
- Redis Lua Scripts Debugger. Не обязательно, но может прилично сэкономить вам слез в будущем. У меня вот кончились — пользуюсь каплями;
- Исторические аспекты появления микросервисной архитектуры. Тоже не обязательно, но весьма доходчиво и интересно.
Концептуальная
Наивный подход
Самое очевидное решение, которое можно придумать: несколько методов доставки (send_vk, send_telegram и т.д.) и один обработчик, который будет вызывать их с нужными аргументами.
Проблема расширяемости
Если мы захотим добавить новый метод доставки, то будем вынуждены модифицировать существующий код, а это — ограничения программной платформы.
Проблема стабильности
Сломался один из методов = сломался весь сервис.
Прикладная проблема
API разных каналов связи значительно отличаются друг от друга в плане взаимодействия. Например, ВКонтакте поддерживает массовые рассылки, но не более чем для сотни пользователей за вызов. Telegram же нет, но при этом разрешает больше вызовов в секунду.
API ВКонтакте работает только через HTTP; у Telegram есть HTTP-шлюз, но он менее стабилен, нежели MTProto и хуже документирован.
Таких различий достаточно много: максимальная длина сообщения, random_id, интерпретация и обработка ошибок и т.д. и т.п.
Как с этим быть?
Было принято решение разделить процесс постановки сообщений в очередь и процессы отправки (далее — курьеры) на организационном уровне, причем так, чтобы первый даже не подозревал о существовании последних и наоборот, а связующим звеном между ними будет выступать Redis.
Непонятно? Закажите покушать!
А пока ждете — позвольте познакомить вас с моей интерпретацией сего благородного действа, начиная с оформления и заканчивая закрытой за курьером дверью.
- Вы нажимаете на большую желтую кнопку «Заказать»;
- Яндекс.Еда находит курьера, сообщает выбранные позиции ресторану и возвращает вам номер заказа, дабы разбавить неопределенность ожидания;
- Ресторан по завершении готовки обновляет статус заказа и отдает еду курьеру;
- Курьер, в свою очередь, отдает еду вам, после чего помечает заказ как выполненный.
Приятного аппетита!
Вернемся к проектированию
Возможно, что приведенная в параграфе ранее модель не вполне соответствует действительности, но именно она легла в основу разработанного решения.
Ассоциирующиеся с номером заказа данные будем называть историей, она позволяет в любой момент времени ответить на следующие вопросы:
- Кто отправил;
- Что отправил;
- Откуда;
- Кому;
- Кто и как получил.
История создается вместе с заказом как два отдельных Redis ключа, связанных через суффикс:
suffix={Идентификатор пользователя}:{UNIX-время в наносекундах}
История=history:{suffix}
Заказ=delivery:{suffix}
Заказ определяет — когда курьеры единожды увидят историю, чтобы, по завершении отправки, соответствующим образом изменить ответ на вопрос “Кто и как получил”.
“Зрение” курьеров работает через подписку на событие DEL ключей по форме delivery:*.
Когда наступает момент доставки, Redis удаляет ключ заказа, после чего курьеры приступают к его обработке.
Так как курьеров несколько — велика вероятность возникновения конкуренции на стадии изменения истории.
Избежать её можно, определив соответствующую операцию атомарно — в Redis это делается через LUA-скриптинг.
Детали реализации будут подробно рассмотрены в следующей главе. Сейчас же важно получить четкое представление о решении в целом с чем может помочь рисунок ниже.
Отслеживание статуса
Клиент может отследить статус доставки через ключ истории, который генерируется отдельным методом API разрабатываемого сервиса перед постановкой сообщения в очередь (как и номер заказа генерируется Яндекс.Едой в самом начале).
После генерации ключа, на него вешается (опционально и тоже отдельным методом) трекер с тайм-аутом, который будет следить за числом изменений истории курьерами (SET события). Только теперь сообщение ставится в очередь.
Если курьер не находит контактов получателей в своем домене — канале связи, то он вызывает искусственное событие SET через команду PUBLISH, тем самым показывая что он “в порядке” и ждать дальше не надо.
Зачем мудрить с событиями в Redis, если есть RabbitMQ и Celery
На то есть как минимум пять объективных причин:
- Redis уже используется другими сервисами Ады, RabbitMQ/Celery — новая зависимость;
- Redis нужен нам, в первую очередь, как СУБД, а не средство IPC;
- Использования Redis’a как хранилища истории защищает нас от SQL-инъекций в текстах сообщений;
- Проблема масштабируемости не стоит и в обозримой перспективе не встанет. Кроме того, эта самая масштабируемость в контексте данной задачи достигается скорее за счет увеличения API-лимитов, нежели горизонтального наращивания вычислительных мощностей;
- Celery пока что не дружит с asyncio, а программный костяк проекта составляет уже реализованная с основой на asyncio библиотека.
Предметная
Система оповещения (объемлющая) исполнена в виде множества микросервисов. Удобства ради, интерфейсы, методы инициализации слоев данных, текста ошибок, а также некоторые блоки повторяющейся логики были вынесены в библиотеку core, которая, в свою очередь, опирается на: gino (asyncio обертка SQLAlchemy), aioredis и aiohttp.
В коде можно увидеть разные сущности, например, User, Contact или Allegiance. Связи между ними представлены на диаграмме ниже, краткое описание — под спойлером.
О сущностях ~ 3 минуты
SPL
Пользователь — человек.
У пользователя есть роль: студент, преподаватель, деканат и т. д., а также почта и имя.
С пользователем может быть связан контакт, где провайдер: ВКонтакте, Telegram, сотовый и т. д.
Пользователи могут состоять в группах [allegiance].
Из групп можно формировать потоки [supergroup].
Группы и потоки могут принадлежать [ownership] пользователям.
Генерация ключа истории
delivery/handlers/history_key/get — GitHub
Очередь
delivery/handlers/queue/put — GitHub
Обратите внимание на:
- Комментарий 171:174;
- То, что все манипуляции с 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 сообщений за вызов.
Любопытный факт
SPL
Многие методы ВКонтакте API, и execute в том числе, наиболее полно описаны в русской версии документации.
Заключительная
В настоящем труде предложен способ организации многоканальной системы массового оповещения. Полученное решение вполне удовлетворяет запросу (@Ключевые требования к сервису рассылок) большинства интересантов, а также предполагает возможность расширения.
Основной недостаток заключается в fire&forget эффекте Pub/Sub, т.е. если удаление ключа заказа придется на момент “болезни” одного из курьеров, то в соответствующем домене никто ничего не получит, что впрочем будет отражено в истории.
===========
Источник:
habr.com
===========
Похожие новости:
- [Python, XML] Модуль для работы с XML файлами
- [Python, API] Чертыре способа получить аудио вк или «это не баг, а фича»
- [Анализ и проектирование систем, Высокая производительность, Промышленное программирование, Распределённые системы] Выбор архитектурного стиля (часть 2)
- [Open source, Системное администрирование, Виртуализация, Разработка под Linux] Разработка и тестирование Ansible-ролей с использованием Molecule и Podman
- [Java, Open source, Анализ и проектирование систем, Программирование] Добавляем ORM в проект за четыре шага
- [Python, Обработка изображений] OpenCV в Python. Часть 1
- [Python, Тестирование веб-сервисов] Как читать файлы конфигурации в тестах с Selenium на Python (перевод)
- [Python, Астрономия, Суперкомпьютеры, Экология] Астрономам порекомендовали меньше использовать суперкомпьютеры и Python из-за вреда экологии
- [DevOps, Kubernetes, Интервью, Серверное администрирование] Команда поддержки систем хранения данных Bloomberg полагается на открытый исходный код и SDS (перевод)
- [Java, Scala, Анализ и проектирование систем, Высокая производительность] Как построить надежное приложение на базе Event sourcing?
Теги для поиска: #_python, #_analiz_i_proektirovanie_sistem (Анализ и проектирование систем), #_studenty (студенты), #_prepodavateli (преподаватели), #_opoveschenie (оповещение), #_informirovanie (информирование), #_python, #_analiz_i_proektirovanie_sistem (
Анализ и проектирование систем
)
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 12:04
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Вводная Привет, Хабр! Меня зовут Борис и в этом труде я поделюсь с тобой опытом проектирования и реализации сервиса массовых рассылок, как части объемлющей системы оповещения студентов преподавателями (далее также — Ада), которую тоже я осуществляю. Ада Нужна затем, чтобы свести на нет число прерываний учебного процесса по следующим причинам:
Работает примерно так:
* Доступ к сервису предоставляется в добровольно-заявительном порядке. Предполагается, что
Ключевые требования к сервису рассылок
Сие произведение состоит из пяти частей: вводной, подготовительной, концептуальной, предметной и заключительной. Подготовительную часть можно смело пропускать, если вы знакомы с Redis интерпретацией Pub/Sub шаблона, а также механизмами событий, LUA-скриптинга и обработки устаревших ключей, кроме того, весьма желательно иметь хоть какое-то представление о микросервисной архитектуре ПО. В предметной части обозревается код на Питоне, но я верю, что информации достаточно, чтоб вы могли написать подобное на чем угодно. Подготовительная Очень грубо и сильно абстрактно ~ 5 минутSPLRedis — это открытое [BSD 3-clause] ПО, реализующее хранение данных типа «ключ-значение» в ОЗУ (преимущественно).
С ключом может быть ассоциировано время жизни, по истечении которого он будет удален. Значениями могут выступать строки, хэш-таблицы, списки и множества. Любую модификацию пространства ключей можно отследить через встроенный механизм событий (отключен по умолчанию в угоду производительности). В дополнение к транзакциям, пользователь может определять новые операции, что будут выполняться атомарно, используя языковые средства LUA 5.1. Подробно и из первых уст ~ 15 минутSPL
Концептуальная Наивный подход Самое очевидное решение, которое можно придумать: несколько методов доставки (send_vk, send_telegram и т.д.) и один обработчик, который будет вызывать их с нужными аргументами. Проблема расширяемости Если мы захотим добавить новый метод доставки, то будем вынуждены модифицировать существующий код, а это — ограничения программной платформы. Проблема стабильности Сломался один из методов = сломался весь сервис. Прикладная проблема API разных каналов связи значительно отличаются друг от друга в плане взаимодействия. Например, ВКонтакте поддерживает массовые рассылки, но не более чем для сотни пользователей за вызов. Telegram же нет, но при этом разрешает больше вызовов в секунду. API ВКонтакте работает только через HTTP; у Telegram есть HTTP-шлюз, но он менее стабилен, нежели MTProto и хуже документирован. Таких различий достаточно много: максимальная длина сообщения, random_id, интерпретация и обработка ошибок и т.д. и т.п. Как с этим быть? Было принято решение разделить процесс постановки сообщений в очередь и процессы отправки (далее — курьеры) на организационном уровне, причем так, чтобы первый даже не подозревал о существовании последних и наоборот, а связующим звеном между ними будет выступать Redis. Непонятно? Закажите покушать! А пока ждете — позвольте познакомить вас с моей интерпретацией сего благородного действа, начиная с оформления и заканчивая закрытой за курьером дверью.
Приятного аппетита! Вернемся к проектированию Возможно, что приведенная в параграфе ранее модель не вполне соответствует действительности, но именно она легла в основу разработанного решения. Ассоциирующиеся с номером заказа данные будем называть историей, она позволяет в любой момент времени ответить на следующие вопросы:
История создается вместе с заказом как два отдельных Redis ключа, связанных через суффикс: suffix={Идентификатор пользователя}:{UNIX-время в наносекундах}
История=history:{suffix} Заказ=delivery:{suffix} Заказ определяет — когда курьеры единожды увидят историю, чтобы, по завершении отправки, соответствующим образом изменить ответ на вопрос “Кто и как получил”. “Зрение” курьеров работает через подписку на событие DEL ключей по форме delivery:*. Когда наступает момент доставки, Redis удаляет ключ заказа, после чего курьеры приступают к его обработке. Так как курьеров несколько — велика вероятность возникновения конкуренции на стадии изменения истории. Избежать её можно, определив соответствующую операцию атомарно — в Redis это делается через LUA-скриптинг. Детали реализации будут подробно рассмотрены в следующей главе. Сейчас же важно получить четкое представление о решении в целом с чем может помочь рисунок ниже. Отслеживание статуса Клиент может отследить статус доставки через ключ истории, который генерируется отдельным методом API разрабатываемого сервиса перед постановкой сообщения в очередь (как и номер заказа генерируется Яндекс.Едой в самом начале). После генерации ключа, на него вешается (опционально и тоже отдельным методом) трекер с тайм-аутом, который будет следить за числом изменений истории курьерами (SET события). Только теперь сообщение ставится в очередь. Если курьер не находит контактов получателей в своем домене — канале связи, то он вызывает искусственное событие SET через команду PUBLISH, тем самым показывая что он “в порядке” и ждать дальше не надо. Зачем мудрить с событиями в Redis, если есть RabbitMQ и Celery На то есть как минимум пять объективных причин:
Предметная Система оповещения (объемлющая) исполнена в виде множества микросервисов. Удобства ради, интерфейсы, методы инициализации слоев данных, текста ошибок, а также некоторые блоки повторяющейся логики были вынесены в библиотеку core, которая, в свою очередь, опирается на: gino (asyncio обертка SQLAlchemy), aioredis и aiohttp. В коде можно увидеть разные сущности, например, User, Contact или Allegiance. Связи между ними представлены на диаграмме ниже, краткое описание — под спойлером. О сущностях ~ 3 минутыSPLПользователь — человек.
У пользователя есть роль: студент, преподаватель, деканат и т. д., а также почта и имя. С пользователем может быть связан контакт, где провайдер: ВКонтакте, Telegram, сотовый и т. д. Пользователи могут состоять в группах [allegiance]. Из групп можно формировать потоки [supergroup]. Группы и потоки могут принадлежать [ownership] пользователям. Генерация ключа истории delivery/handlers/history_key/get — GitHub Очередь delivery/handlers/queue/put — GitHub Обратите внимание на:
Зрение курьеров [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 сообщений за вызов. Любопытный фактSPLМногие методы ВКонтакте API, и execute в том числе, наиболее полно описаны в русской версии документации.
Заключительная В настоящем труде предложен способ организации многоканальной системы массового оповещения. Полученное решение вполне удовлетворяет запросу (@Ключевые требования к сервису рассылок) большинства интересантов, а также предполагает возможность расширения. Основной недостаток заключается в fire&forget эффекте Pub/Sub, т.е. если удаление ключа заказа придется на момент “болезни” одного из курьеров, то в соответствующем домене никто ничего не получит, что впрочем будет отражено в истории. =========== Источник: habr.com =========== Похожие новости:
Анализ и проектирование систем ) |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 12:04
Часовой пояс: UTC + 5