company_banner

Система офлайн-уведомлений Badoo

    Для того чтобы пользователи, находясь офлайн, узнавали о событиях на сайте, мы создали специальную систему уведомлений. В её задачи входит аккумулировать события для пользователя и в нужный момент сообщать о них через доступные каналы связи, такие как электронная почта и push-уведомления на смартфоны.
    Как организовано хранение событий? О каких событиях приходят уведомления? В какой момент они отправляются и по какому принципу? Сегодня мы постараемся ответить на все эти и другие вопросы.

    Статья дает общее описание архитектуры системы с небольшими техническими подробностями и будет интересна тем, кто только собирается или уже каким-то способом уведомляет своих пользователей обо всём новом, что произошло за время их отсутствия на сайте (в приложении, сервисе и т.п.)


    Какие бывают события?


    Все события нашего сайта порождаются действиями одних пользователей по отношению к другим. Найдя симпатичного человека, вы захотите посмотреть его анкету подробнее, тем самым создав событие «Посещение профиля». Если его профиль показался вам интересным, вполне вероятно, что вы захотите написать ему, таким образом создав событие «Новое сообщение». Вы можете добавить его в избранное, а также изъявить желание встретиться с ним. Если он тоже захочет встретиться с вами, то для каждого из пользователей случится событие «Взаимная симпатия».

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

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



    Как хранятся события?


    Чтобы не слать слишком много уведомлений пользователям, мы отправляем уведомление не на каждое событие, а группируем их в течение определённого периода, к примеру, за первые полчаса отсутствия пользователя на сайте. После первой отправки мы опять ждём и, если появились новые события, осуществляем отправку. Однако, некоторые особо важные уведомления могут отправляться вне очереди.

    Для агрегации событий нами была разработана специальная СУБД ― демон на Си, который умеет:

    • Хранить массив событий для каждого пользователя. Каждое событие характеризуется идентификатором отправителя, числовым типом (1 ― сообщения, 2 ― посетители и т.п.) и произвольной строкой данных, в которую можно передать дополнительную информацию, например в виде сериализованного массива.
    • Следить за временем, периодами, тайм-аутами и по запросу отдавать данные, готовые к отправке. Как только подходит время, демон отдаёт все накопленные события для очередного пользователя.
    • Всё общение с СУБД осуществляется по RPC-like протоколу, который передает данные, запакованные с помощью Google Protocol Buffers.

    В каждом из двух наших дата-центров запущено по четыре таких демона, чтобы без проблем справляться с нагрузкой, которая в пиках достигает 25 тысяч запросов в секунду, а также минимизировать время простоя в случае, если требуется перезагрузить/обновить демон, или в случае падения. Нагрузка между ними распределяется по простому принципу:

    user_id % 4 = <номер демона>

    Таким образом, события для каждого пользователя всегда «живут» в одной и той же базе. Данные пользователей хранятся не только в памяти, но и сохраняются на диск, поэтому уведомления не теряются. Если потребуется изменить шардинг и добавить ещё несколько демонов, то демоны «гасятся», данные перемещаются нужным образом между хранилищами и при запуске загружаются в память. Это крайне редкая ситуация ― за всё время нам потребовалось делать такое всего лишь раз.

    По каким каналам происходит доставка?


    Для того чтобы оповещать пользователей о новых событиях, мы используем электронную почту, а также iOS и Android push-уведомления.

    Стоит немного сказать о том, что такое push-уведомления. Каждое приложение для iOS имеет возможность присылать вам уведомления, даже когда оно не запущено (конечно, если вы это не запретите). Доставка уведомлений в таком случае осуществляется через сервера Apple, а не напрямую. Когда вы устанавливаете приложение на свой iPhone, Apple сообщает разработчику идентификатор установленного приложения в своей системе, который мы используем как адрес для отправки сообщений. Такая же система существует и для Android-смартфонов, но доставка уже осуществляется через сервера Google.

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

    Как формируются уведомления?


    Для каждого из используемых нами каналов имеется собственный механизм формирования уведомлений.

    Общая логика такова, что есть уведомления, содержащие информацию только об одном типе событий (2 посетителя, 5 сообщений и т.п.), и есть групповые уведомления (2 посетителя и 1 сообщение; 2 сообщения и 1 взаимная симпатия и т.п.). Для электронных писем существует несколько отдельных шаблонов о новых сообщениях, посетителях и т.п. плюс шаблон группового письма, в котором каждое событие представлено отдельной строкой.

    Для push-уведомлений мы составили тексты на каждый отдельный тип событий в нескольких вариантах. Поскольку в уведомление нельзя вместить много информации, то для групп событий мы выбрали несколько базовых комбинаций (к примеру, посетители + сообщения, сообщения + взаимные симпатии), для которых и написали варианты текстов.

    • Пример «одиночного» уведомления в нескольких вариантах:
      • Вариант 1: У вас <число> новых сообщений от девушек!
      • Вариант 2: Девушки написали вам <число> новых сообщений!

    • Пример «группового» уведомления:
      • У вас <число> сообщений, а также новые люди хотят с вами встретиться! Узнайте, кто...


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

    Как устроена архитектура системы?


    С одной стороны, во время использования сайта пользователи генерируют события, которые сразу же добавляются в БД. На графике можно увидеть соотношение событий за 24 часа в одном из двух наших дата-центров (точные цифры на графиках попросили не публиковать):



    С другой стороны, на нескольких серверах непрерывно запущен php-скрипт, который постоянно опрашивает БД, есть ли уведомления, время отправки которых уже подошло. Ниже можно увидеть график количества пользователей, для которых готовы уведомления:



    Если нужно отправить уведомление на смартфон, то запись в БД помечается специальным флагом и откладывается на 10 минут. Если в часовом поясе пользователя сейчас ночь, то мы отправим уведомление с параметром «Без звука», чтобы не нарушать сон пользователя.

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

    При каждом запросе страницы от зарегистрированного пользователя мы посылаем команду в БД для очистки всех его событий. Бывает так, что пользователь попал на промо-страницу и, не увидев ничего нового, ушёл с сайта. На таких страницах, где видны не все счетчики новых событий, мы указываем, какие конкретно события нужно чистить.

    Когда приходит время очередной отправки, а пользователь всё ещё имеет статус «онлайн», мы помечаем это особым флагом и откладываем запись в БД на некоторое время, ожидая, что он либо продолжит активность на сайте, либо окажется в статусе «офлайн».

    Что значит «онлайн»?


    Как определить, что пользователь сейчас на сайте, недавно был или уже «офлайн»? Эти понятия достаточно субъективны и подобраны опытным путем.

    Пользователи могут проявлять активность на сайте и в наших мобильных приложениях. Для того чтобы хранить время последней активности, мы используем ещё одну СУБД собственной разработки ― Last Access. Этот демон «из коробки» вычисляет статус «офлайн» на основе всех данных активности пользователя: спустя 30 минут после последнего действия пользователь окончательно перестает быть «онлайн».

    Для того чтобы отправлять уведомления, используется более сложная логика определения онлайн-статуса. Сначала мы проверяем Last Access: если он сообщает, что статус пользователя сменился на «офлайн», то можно смело осуществлять отправку, т.к. уже прошло достаточно много времени. В противном случае для сайта и приложений используется следующий алгоритм: если человек пользуется сайтом, то для отправки должно пройти 15 минут. Если же используется мобильное приложение и прошло менее 15 минут, то мы просто проверяем, есть ли соединение с запущенным приложением, и если нет, то отсылаем уведомление.

    Описанная система удобно и ненавязчиво сообщает пользователям о том, что нового для них происходит на сайте. Мы постоянно дорабатываем и улучшаем её, чтобы она была действительно полезной и нужной нашим многочисленным пользователям.

    Александр Treg Трегер, разработчик.
    Badoo
    260,00
    Big Dating
    Поделиться публикацией

    Похожие публикации

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

      +1
      А как так вышло такое распределение?
      user_id % 4 = <номер демона>
      Когда fisher говорит:
      Остаток от деления, первая буква логина, есть всякие способы — все они не работают в саппорте. То есть они там простые и красивые, но все они в саппорте не работают. Потому что как только оказывается, что какая-то нода вылетает и нужно сразу же вместо десяти серверов вдруг использовать только восемь по каким-то причинам, две ноды нужно временно выключить, данные с них куда-то перенести, и тут появляется засада полная, потому что эта формула даёт сбой.

      Что будете делать, когда появится третий дата-центр?
        0
        В каждом из двух наших дата-центров запущено по четыре таких демона
        Это в пределах одного ДЦ в новом соответсвенно еще 4 поднимут
          +2
          fisher говорил о шардинге данных (юзеры/фотки/комментарии и тд) по серверам СУБД. Это там, в базах данных, бешеные терабайты, там критически важно уметь нормально переносить выход из строя каких-то нод. В данном случае Александр говорит про узкоспециализированный демон, занимающийся своей, конкретной, опять же довольно узкоспециализированной задачей.
          +2
          Что будете делать, когда появится третий дата-центр?

          А когда он появится? такие штуки происходят совсем нечасто, мы все-таки пока еще не гугл :)
            +4
            WANTYOU — ok =))
              +2
              Можете рассказать поподробнее:
              1. Для агрегации событий нами была разработана специальная СУБД
                Если я правильно понимаю, то для пользовательских данных вы используете реляционную базу данных, а для событий собственную. В этом случае, каким образом вы гарантируете консистентность данных между ними? Two-phase commit, idempotence или какой-нибудь другой механизм?
              2. Данные пользователей хранятся не только в памяти, но и сохраняются на диск, поэтому уведомления не теряются [при перезапуске демона]. Каким образом вы обеспечиваете durability и высокую производительность [25kreq/s]? append-only + fsync на несколько событий или иным способом? Возможна ли все-таки потеря данных при падении демона?
              3. Каким образом вы отмечаете отправленные события? Что будет, если электронное письмо или push-уведомление уже отправлено, событие еще не помечено, как отправленное, а демон упал? Будет ли одно и то же событие продублировано при перезапуске демона (т.е. получит ли пользователь два одинаковых сообщения)?

              Спасибо.
                +1
                1. Для пользовательских данных используется MySQL. События в демоне, агрегирующем уведомления, хранятся по user_id получателя.

                • Если пользователь удаляется — прошел коммит в MySQL, то в демон отправляется команда — удалить все по user_id. Если команда не прошла, то консистентность может быть нарушена.
                • Перед каждой отправкой мы проверяем, что получатель не удален.
                • Каждого пользователя, который отправил вам уведомления мы также проверяем, что он не удален. Таким образом невозможна ситуация, когда вам придет уведомление, в котором фигурируют удаленные пользователи (на момент отправки, конечно).


                2. 25kreq/s — это на 4 демона, то есть на каждый по 6 в пике. (подробнее могут ответить наши системные программисты)

                3. Событие не будет продублировано. Отданные скрипту уведомления демон внутри себя откладывает в специальную очередь и ждет, когда ему подтвердят отправку специальной командой — cancel(timeout, flags) или confirm. По умолчанию он по таймауту очистит эту уведомления командой confirm. Второй раз он может отдать то же уведомление, если мы его отложили командой cancel на некоторое время с какими-то флагами, которые означают причину. К примеру, причина — пользователь еще онлайн.
                  +2
                  Данные пользователей хранятся не только в памяти, но и сохраняются на диск, поэтому уведомления не теряются [при перезапуске демона]. Каким образом вы обеспечиваете durability и высокую производительность [25kreq/s]? append-only + fsync на несколько событий или иным способом? Возможна ли все-таки потеря данных при падении демона?

                  Раз в n минут (для этого демона сделано вроде раз в 5-10 минут, если память не изменяет) демон форкается и потомок сохраняет новые данные в очередной файл изменений. Раз в k минут (1 или 2 часа, вроде) демон форкается и потомок сохраняет полный снепшот данных на диск. При загрузке, соответственно, сначала грузится последний снепшот, а затем накатываются изменения.

                  Таким образом, при падении демона, мы можем потерять новые события за максимум n минут + время на поднятие демона. Демон не настолько критичный, т.к. сами данные не теряются (они в реляционной СУБД), а событиями за небольшой промежуток времени можно пренебречь (пользователь все равно увидит новые события зайдя на сайт). Тем более что падения демона довольно редкое событие.

                  Временами n и k можно управлять, конечно. Основные затраты — время на системный вызов fork(). В случае если демон сжирает значительное кол-во памяти (скажем 60 GiB и больше), системный вызов fork() может выполняться 1-2 секунды, что недопустимо. Демон о котором здесь идет речь занимает в памяти примерно 4-5 GiB.

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

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