http://habrahabr.ru/company/mailru/blog/232981/
«Ну а здесь, знаешь ли, приходится бежать со всех ног, чтобы только остаться на том же месте, а чтобы попасть в другое место, нужно бежать вдвое быстрее»
Льюис Кэрролл, «Алиса в Зазеркалье»
Недавно мы в Таргете 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.