У Яндекса есть сервис для добросовестных рассыльщиков писем — Почтовый офис. (Для недобросовестных у нас в Почте есть Антиспам и кнопка «Отписаться».) С его помощью они могут понимать, какое количество их писем пользователи Яндекс.Почты удаляют, сколько времени их читают, насколько дочитывают. Меня зовут Антон Холодков, и я занимался разработкой серверной части этой системы. В этом посте я расскажу о том, как именно мы ее разрабатывали и с какими трудностями столкнулись.
Для рассыльщика интерфейс Почтового офиса полностью прозрачен. Достаточно зарегистрировать в системе свой домен или email. Сервис собирает и анализирует данные по множеству параметров: имени и домену отправителя, времени, признаку спам/не спам, прочитано/не прочитано. Также реализована агрегация по полю list-id — специальному заголовку для идентификации рассылок. Источников данных у нас несколько.
Во-первых, система доставки писем в метабазы. Она генерирует события о том, что письмо поступило в систему. Во-вторых, веб-интерфейс почты, где пользователь меняет состояния письма: читает его, помечает как спам или удаляет. Все эти действия должны попасть в хранилище.
При сохранении и модификации информации нет жестких временных требований. Запись может добавляться или изменяться довольно долго. В этот момент статистика будет отражать предыдущее состояние системы. При больших объёмах данных это незаметно пользователю. Одно-два его действия, еще не положенных в базу, не смогут изменить статистику на сколько-нибудь большое значение.
В веб-интерфейсе скорость отклика очень важна — торможения выглядят некрасиво. Кроме того, важно не допустить «скачков» значений при обновлении окна браузера. Такая ситуация возникает, когда извлекаются данные то с одной головы, то с другой.
Пару слов хочется сказать о выборе платформы. Мы сразу поняли, что не каждое решение справится с таким потоком данных, который у нас есть. Изначально было три кандидата: MongoDB, Postgres, наша собственная связка Lucene+Zookeeper. От первых двух мы отказались из-за недостаточной производительности. Особенно большие проблемы были при вставке большого количества данных. В результате мы решили воспользоваться опытом коллег и использовали связку Lucene+Zookeper — такую же связку использует поиск по Яндекс.Почте.
Стандартом общения между компонентами внутри системы стал JSON. У Java и Javascript есть удобные средства для работы с ним. На C++ используем yajl и boost::property_tree. Все компоненты реализуют REST API.
Данные в системе хранятся в Apache Lucene. Как вы знаете, Lucene — это библиотека, разработанная Apache Foundation для полнотекстового поиска. Она позволяет хранить и извлекать любые предварительно проиндексированные данные. Мы превратили ее в сервер: научили хранить данные, добавлять в индекс и сжимать его. С помощью http запроса можно искать, добавлять, модифицировать. Есть и различные виды агрегации.
Чтобы каждая запись об изменении состояния была обработана во всех «головах» кластера, используется Zookeeper, ещё один продукт Apache Fondation. Мы доработали его и добавили возможность использования в качестве очереди.
Для извлечения и анализа данных из Lucene написан специальный демон. В нем сосредоточена вся логика работы. Вызовы веб-интерфейса превращаются в http-запросы к Lucene. Тут же реализована логика агрегации данных, сортировки и прочие обработки, которые нужны для показа данных в веб-интерфейсе.
Когда пользователи совершают действия в веб-интерфейсе, информация об этих действиях сохраняется через Zookeeper в Lucene. Каждое действие — например, нажатие кнопки «Спам» — изменяет состояние системы, и надо аккуратно модифицировать все данные, которые оно затрагивает. Это самая сложная часть системы, мы переписывали и отлаживали её дольше всего.
Первая попытка решить задачу была, что называется, «в лоб». Мы хотели складывать записи о состоянии письма в Lucene налету. Агрегировать данные предполагалось в реальном времени при извлечении. Это решение прекрасно работало на небольшом количестве записей. Суммирование сотен записей занимало микросекунды. Все выглядело отлично. Проблемы начинались при большом количестве записей. Например, тысячи уже обрабатывались секунды. Десятки тысяч — десятки секунд. Это раздражало пользователей и нас. Нужно было искать пути ускорения выдачи данных.
При этом агрегация обычных пользователей с десятками, сотнями и тысячами писем в день не была проблемой. Проблемой становились единичные рассыльщики, которые рассылали сотни тысяч писем за очень короткое время. Рассчитать для них данные в реальном времени было невозможно.
Решение было найдено после анализа запросов c веб-интерфейса. Видов запросов было мало, и все они сводились к суммированию данных или нахождению среднего значения серии данных. Мы добавили в базу записи агрегации и стали их модифицировать при добавлении или изменении записей о состоянии письма. Например, пришло письмо — прибавили к общему счетчику единицу. Пользователь удалил письмо — отняли единицу от общего счетчика и прибавили единицу к счетчику удаленных. Пользователь пометил письмо спамом — прибавили единицу к счетчику «спамовых писем». Число записей, которое надо обработать для выполнения запроса, стало меньше, и это сильно ускорило агрегацию. Zookeeper позволяет легко обеспечить целостность данных. На изменение агрегирующих записей требуется время, но мы можем позволить себе небольшое отставание данных.
Что получилось в итоге? Сейчас в системе четыре машины на Lucene, три на Zookeeper. Входные данные поступают с 10 машин и выдаются на шесть frontend машинах. В секунду система обрабатывает 4500 модификацирующих запросов и 1100 запросов на чтение. Объем хранения на сегодняшний день составляет 3.2 терабайт.
Система хранения на Lucene + Zookeeper зарекомендовала себя очень стабильно. Можно на лету отключить узел на Lucene, можно добавить узел. Zookeeper хранит историю и накатит нужное число событий на новую машину. Через некоторое время мы получим голову с актуальной информацией. Одна машина в кластере выделена под хранения бэкапов данных.
Несмотря на сжатые сроки разработки, система получилась надежная и быстрая. Архитектура позволяет легко масштабировать — как вертикально, так и горизонтально — и добавлять новые возможности анализа данных. Которые мы для вас обязательно скоро добавим.
Автор: antonkh