Компания
900,03
рейтинг
12 августа 2014 в 12:42

Разработка → Push-уведомления в REST API на примере системы Таргет Mail.Ru



«Ну а здесь, знаешь ли, приходится бежать со всех ног, чтобы только остаться на том же месте, а чтобы попасть в другое место, нужно бежать вдвое быстрее»
Льюис Кэрролл, «Алиса в Зазеркалье»

Недавно мы в Таргете Mail.Ru реализовали систему push-уведомлений. Грамотное использование очередей задач позволяет реализовать быструю систему доставки уведомлений. В этом посте я расскажу о применении и реализации этой модели в нашем сервисе.

Модели взаимодействия пользователя API с сервисом

Можно выделить две основные модели взаимодействия пользователя с сервисом: pull и push. Рассмотрим их применительно к задаче организации уведомлений.

Pull-модель – способ взаимодействия пользователя с сервисом, при котором пользователь инициирует запрос к сервису и синхронно получает на него ответ. Если результат не готов, пользователь вынужден периодически опрашивать сервис, пока не получит интересующие его результаты. Преимущество этого способа – это простота реализации как клиента, так и сервера за счет синхронности выполняемого алгоритма. А недостаток – создание излишней нагрузки как на стороне сервиса, так и на стороне клиента. Чтобы сохранить актуальность состояния объектов в своей системе, клиент вынужден с высокой частотой делать запросы к сервису.

Push-модель – способ взаимодействия пользователя с сервисом, при котором сам сервис доставляет данные пользователю на заранее заданный адрес. Преимущество: отсутствие паразитной нагрузки – запрос отправляется тогда и только тогда, когда в системе произошли какие-то изменения. Недостаток: большая, по сравнению с pull-моделью, сложность реализации как следствие асинхронности процессов.

Применение push-модели в сервисе Таргет Mail.ru

Таргет Mail.Ru – система показа таргетированной рекламы на проектах Mail.Ru Group. Его API позволяет работать с рекламными кампаниями и объявлениями, получать различную статистику. В подобных системах push-уведомления находят все более широкое применение.

Пример №1
Пользователь добавляет новое рекламное объявление. Чтобы начать показываться, объявление должно пройти предварительную модерацию. При использовании традиционной pull-модели пользователь был бы вынужден периодически опрашивать сервис. С push-моделью пользователь автоматически получает уведомление об изменении статуса объявления.

Пример №2
Часто API используется агентствами, автоматизирующими работу сразу нескольких (часто десятков и сотен) клиентов. Наличие системы push-уведомлений позволяет контролировать все манипуляции, производимые с данными их аккаунтов. Также на одно событие может быть зарегистрировано сразу несколько подписчиков.

Реализация push-модели в сервисе Таргет Mail.Ru

В API Таргета Mail.Ru клиенты имеют возможность подписываться на изменения объектов системы – кампаний и рекламных объявлений. Наше API следует идеологии REST. Подписки на push-уведомления отлично в нее вписываются: «подписка» является ресурсом, который можно создать, получить и удалить.

Все подписки текущего пользователя:

GET /api/v2/subscriptions.json HTTP/1.1
Host: target.mail.ru

Создание новой подписки:

POST /api/v2/subscriptions.json HTTP/1.1
Host: target.mail.ru

{
    "callback_url": "http://target-api-client.org/subscription_callback.json",
    "resource": "CAMPAIGN",
    "resource_id": 123
}

При подписке указывается URL, на который при любом изменении ресурса будет отправлен POST-запрос с уведомлением:

{
    "created": "2014-06-02 18:23:29.797499",
    "diff": {
        "name": {
            "+++": "Новое название",
            "---": "Старое название"
        },
        "updated": {
            "+++": "2014-06-02 18:23:29",
            "---": "2014-06-02 18:21:58"
        }
    },
    "id": "07c0810ac51c47c98e001b1e91c94ba4",
    "resource": "CAMPAIGN",
    "resource_id": 86461
}

Удаление подписки:

DELETE /api/v2/subscriptions/<int:subscription_id>.json HTTP/1.1
Host: target.mail.ru

Детали реализации

После фиксации изменения данных в БД формируется diff между старым состоянием и новым.

Создается новая задача, содержащая diff, и добавляется в очередь. Мы используем Tarantool Queue — очередь задач поверх NoSQL хранилища Tarantool. Разбором очереди занимается отдельный демон, написанный на python с использованием библиотеки Gevent. В его обязанности входит забор задач из очереди и непосредственная отправка уведомлений. Использование гринлетов позволяет одновременно отсылать большое количество уведомлений даже при высоких значениях таймаутов. Демон использует пул гринлетов. На каждой итерации бесконечного цикла он смотрит на количество свободных мест в пуле, берет задачи из очереди, запускает обработчики задач и добавляет их в пул.

Для синхронизации основного потока исполнения и обработчиков используется еще одна внутренняя очередь — gevent.queue. Каждый обработчик принимает задачу, взятую из очереди, извлекает из нее url, отправляет на него уведомление, помещает задачу во внутреннюю очередь и завершается. После раздачи новых задач демон разбирает внутреннюю очередь с выполненными задачами, отправляет подтверждения их выполнения в Tarantool Queue и погружается в непродолжительный сон.

Общая схема алгоритма выглядит так (python):


queue = Queue(...) # соединяемся с сервером очереди задач

tube = queue.tube(...) # получаем очередь

worker_pool = Pool(...) # создаем пул обработчиков

processed_task_queue = gevent_queue.Queue() # внутренняя очередь; складываем в нее обработанные задачи

while run_application: # запускаем бесконечный цикл обработки задач
    free_workers_count = worker_pool.free_count()

    for number in xrange(free_workers_count): # проверяем, можно ли запустить еще обработчиков
        task = tube.take(...) # берем задачу из очереди
        if task:
            worker = Greenlet(notification_worker, task, ...) # создаем обработчик
            worker_pool.add(worker) # добавляем в пул
            worker.start() # и запускаем. Он выполнится асинхронно по отношению в основному потоку выполнения
    done_with_processed_tasks(processed_task_queue) # закрываем выполненные задачи
    sleep(...) # заслуженный отдых

Особенности работы по push-модели с точки зрения пользователя

Любая система имеет свои особенности и ограничения. Особенности обработки приходящих push-уведомлений вытекают из особенностей работы с очередями задач.

Проблема: одно и то же сообщение может прийти более одного раза.

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

Проблема: порядок прихода сообщений не детерминирован, сообщения могут приходить не в том порядке, в котором были отправлены.

Возможное решение: каждое сообщение хранит метку времени своего создания. Можно сравнить ее со временем последней модификации объекта в своей системе и не применять изменения, если сообщение старее.

Заключение

Мы выбрали именно push-модель, потому что она позволяет значительно сократить время доставки изменений до систем пользователей нашего API. Также немаловажным фактором стало меньшее количество «паразитной» нагрузки на наш сервис. По результатам нашего опыта реализации этой модели мы решили поделиться теми how-to, которые нам показались наиболее полезными.

Расскажите, как вы используете push-модель в своей разработке?

Другие API, поддерживающие push-уведомления

https://developers.google.com/google-apps/calendar/v3/push — push-уведомления в Google Calendar.
https://developers.google.com/drive/web/push — push-уведомления в Google Drive.
Автор: @marrrvin

Комментарии (9)

  • +5
    Немного странное именование полей вы выбрали:
    "name": {
        "+++": "Новое название",
        "---": "Старое название"
    }

    Если я захочу маппить входящий JSON в объект без переименования полей, то ничего не получится, потому что код превращается в
    newValue = data->diff->name->+++

    Что в большинстве языков невалидно.
    • 0
      Насколько я могу судить, речь идет о php, когда json_decode возвращает объект класса stdClass.
      В таком случае можно использовать примерно следующий синтаксис:

      $str = '{"name": {"+++": "Новое название", "---": "Старое название"}}';
      
      $obj = json_decode($str);
      
      echo $obj->name->{'+++'};
      


      Либо вызвать json_decode со вторым параметром, равным true, получить array и работать с ним.
      • +1
        Это вроде как нигде не проблема, ключом ассоциативного массива обычно является строка, поэтому всегда можно получить доступ как:
        data->diff->name["+++"]
        Вопрос удобно ли использовать два стиля обращения к ячейке?
        • 0
          Можно работать с массивами:

          $data = json_decode($json_string, true);
          echo $data['diff']['name']['+++'];
          


          Можно с объектами:

          $data = json_decode($json_string);
          echo $data->diff->name->{'+++'};
          

  • 0
    В нашем проекте ключевым сервисом является сервер обмена сообщениями. Рассылкой уведомлений о новом сообщении занимается отдельный демон. Демон хранит состояние клиента и некоторые данные. У нас есть следующие типы клиентов: Мобильный IOs, Android & WEB. Пуш уведомления на мобильные клиенты рассылаются через API Apple Push Notification Service & Google Cloud Messaging. Уведомление WEB клиентов происходит через флеш. Флеш открывает постоянное соединение с демоном на 80 порту, и при появлении нового события получает порцию JSON, который парсится и передаётся в кэллбэк JS, а тот в свою очередь изменяет HTML.
    • 0
      Интересно. А на каких технологиях реализован сам демон? Си?
      • 0
        да, сам демон написан на сях…
        исторически так сложилось, что у нас вся разработка преимущественно на си, есть питон и перл.

        общение между бэкендом и демонами преимущественно осуществляется по мемкешовому протоколу. В этом свои плюсы — отладка, не нужно писать собственных клиентов…

        написал один типовой демон и потом теражируй его, вписывая новый функционал
      • 0
        Очереди мы используем, но уже внутри процеса. Ну, во первых у нас нагрузка не такая, как у вас и масштабироваться не нужно. Одни поток принимает данные и распихивает его по очередям. Другие потоки читают свои очереди и делают соответствующие пуши на службы гугла и эпла. Один поток слушает (принимает) эпл нотификации, а еще один отвечает за статистику и данные (пишет в БД). Практически тоже, что и у вас… только масштабы меньше :)
        В общем все очень интересно…

        нагрузка 3 млн сообщ в день
        • 0
          из них более трети на эпл, чуть меньше на андроид и где-то более трети на WEB

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

Самое читаемое Разработка