Многоканальные массовые рассылки на Redis

Вводная


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



Ада


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

  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. Использования Redis’a как хранилища истории защищает нас от 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, т.е. если удаление ключа заказа придется на момент “болезни” одного из курьеров, то в соответствующем домене никто ничего не получит, что впрочем будет отражено в истории.

Средняя зарплата в IT

110 500 ₽/мес.
Средняя зарплата по всем IT-специализациям на основании 7 087 анкет, за 2-ое пол. 2020 года Узнать свою зарплату
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

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

    +1

    Честно говоря, выглядит как сильно переусложненное решение простой задачи. Представляю себе, каково будет это поддерживать. По моему, для рассылок хватило бы БД SQL на несколько таблиц, простой формочки на PHP для создания рассылки и пары крон-скриптов на PHP для собственно отправки. Redis кажется неудачным выбором, так как в обычной БД есть SQL-запросы для произвольной выборки данных, а в нем нет, данные ищутся только по ключу.

      –2
      Осталось только выучить PHP. Зачем-то.

      Что до остального — действительно можно было решить проще через крон-скрипты и SQL.
      И… я скорее согласен с тем, что сам придумал проблему ради проблемы.

      В любом случае — опыт интересный и результат получился не то чтобы плохой.
        +1

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


        Вы в посте заикнулась про микросервисную архитектуру, а потом снизу говорите о том, что взяли эти инструменты только потому что вы не хотите использовать другие. Один из бонусов микросервисной архитектуры это то, что вы можете использовать инструменты ПОДХОДЯЩИЕ для задачи и будучи не ограниченным стеком оставшемся от предыдущих задач.


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


        Не нравится php или хочется реалм-тайм взаимодействия, выкините питон возьмите ГО, Котлин (если гонитесь за модой), храните данные в любой БД (хоть sqlite) и выгружайте горячии данные (те что нужно обработать) в память.

          0
          Так же стоит задуматься почему реляционная база данных называется реляционной. У вас четко прослеживаются отношения между таблицами, вы даже нам схему начертили, но вдруг решили хранить данные в редисе?


          В Редисе хранится лишь часть данных: коды подтверждения почты, телефонов, а также описанные в статье история и заказ. Остальное — в PostgreSQL.

          Мне нравится Go, но на нём разработка заняла бы больше времени и не то чтобы в чем-то от этого выиграла в силу отсутствия проблемы масштабируемости.

          Учить PHP ради PHP особого смысла не вижу.
          +1

          Крон-скрипты можно писать на любом языке, не обязательно PHP.

        • НЛО прилетело и опубликовало эту надпись здесь
            +1
            Более того, как раз рассылки через SQL или даже, извините, через MongoDB делаются удобнее, чем через Редис. Так как вот с этой самой «нотификацией студентов» есть еще масса проблем, которые в этой статье не рассмотрены.
            +5

            Господи, что за поток сознания? Причем тут вообще яндекс еда? Я сколько силился понять — не смог.


            Redis уже используется другими сервисами Ады, RabbitMQ/Celery — новая зависимость;

            ОК


            Redis нужен нам, в первую очередь, как СУБД, а не средство IPC;

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


            Использования Redis’a как хранилища истории защищает нас от SQL-инъекций в текстах
            сообщений;

            ЛОЛ. Да, SQL инъекций в редисе действительно нет, но открытый на интернет редис ломают только так, даже с паролем. Ну, и у редиса есть свои уязвимости в протоколе. Поэтому — не аргумент.


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

              0
              Причем тут вообще яндекс еда? Я сколько силился понять — не смог

              Яндекс.Еда должна была сделать концепт решения понятнее.

              Да, SQL инъекций в редисе действительно нет, но открытый на интернет редис ломают только так, даже с паролем. Ну, и у редиса есть свои уязвимости в протоколе. Поэтому — не аргумент.

              На интернет открыты только интерфейсы пользователей. Redis и разработанный сервис доступны только в пределах хоста.

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

              :3
              0
              Отдельная история, что сделать по-настоящему надежную очередь на редисе, с мастер-слейв или кластером редиса… проще застрелиться.

              Redis Streams нормально заходят в этом случае и нет Pub/Sub проблем.

                –1
                Каналы в Телеграме полностью подходят под ТЗ, заметно удобнее, гораздо надежнее и без глупых ограничений. А еще их разрабатывать не надо. Все само работает.
                Преподавателей без интернета на телефоне отправляем на пенсию.

                Можно даже много каналов и постинг через бота. Тогда вообще красиво все будет.
                  0
                  Нельзя их отправить на пенсию — никого не останется:(
                    0
                    Ну как-то же им надо сообщения отправлять.
                    В принципе СМС в телегу пересылать несложно. Есть готовые решения и под это.
                      0
                      Это все началось еще до момента, когда Телегу «разблокировали».
                      Она изначально не рассматривалась как основной канал.
                        0
                        Разблокировали уже. Хотя и до этого блокировка никому особо не мешала.
                        Пришло время выкинуть велосипед и пересесть на стандартные технологии.
                          0
                          Проблема сокрытия контактных данных, заложенная в описании проблемы, остается.
                          Я бы рад — договорись все люди пользоваться чем-то одним, но утопии бывают только в книжках и все мы знаем чем они там заканчивают.
                            0
                            Телега не палит никаких данных и поддерживает множество профилей. Все прямо из коробки.

                            Так пусть не пользуются. Им же хуже. Вот официальный канал. Кто не читает — сам виноват. За неделю все подтянутся.
                              0
                              Увы, но нет:
                              1) Преподавателей текущая ситуация и шпионские игры вполне устраивают. Пишут себе записочки на кафедральных досках, а то что ты час ехал на пару, которой не будет — не их проблемы;
                              2) Когда я впервые пришел с предложением сделать Аду к научному руководителю, он мне на пальцах перечислил — у кого ЕСТЬ смартфоны. Хватило одних лишь рук.

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

                                В чатиках сейчас все. Корпорации управляются через чатики, страны управляются через них же. А вы о переносе пар беспокоитесь.
                                  0
                                  Уровня влияния «студент» не хватит, чтоб такое пролоббировать:(
                                    0

                                    Уровня влияния «студент» не хватит в любом случае, преподаватели так и продолжат писать записочки на кафедральных досках что бы вы ни делали.

                    0

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

                      +1
                      А никак. Сами зайдут. Не надо играть в шпионов и защищать информацию об отмене лекций или переносе занятий. Просто каналы с доступом по ссылке. И хватит безопасности.

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


                        Это совсем не вопрос выбора, но режима. Им может самим не нравится.
                          0
                          По этой же причине пришлось делать регистрацию для студентов через учебную почту.
                          И каналы в Telegram не подходят.
                            0
                            Вы же понимаете что студенты все это передают друг другу в тех же Телеграм чатиках? Это в лучшем случае, а в худшем в Вайбере каком тоже самое пересылают.
                            И защита этих чатиков от левых людей на уровне неуловимого Джо.
                              0
                              Бюрократическая машина этого не понимает. И никогда не поймет.
                              На бумаге все должно быть идеально. Или никак.

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

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