Как мы в hh.ru отправляем пользователям миллиард уведомлений в месяц
В hh.ru много пользователей, а уведомлений мы отправляем еще больше: о регистрации, о восстановлении пароля, об изменении статуса услуг, о новых сообщениях и т.д. Одних только email-уведомлений мы отправляем около 900 миллионов в месяц, а ведь есть еще пуши и смс.
Меня зовут Кирилл, я — тимлид команды Bonjour в hh.ru. Сегодня я расскажу как у нас устроены рассылки.
Исторически так сложилось, что в hh.ru своя система для отправки уведомлений. Мы думали о том, чтобы перейти на подрядчиков или использование внешних сервисов, но учитывая наши объемы, это было бы либо технически сложно для них, либо экономически невыгодно для нас.
Email-уведомления
В hh.ru сервисная архитектура, и во время работы многие сервисы отправляют свои email-уведомления. Нам хотелось отправлять все уведомления из одного конкретного места: чтобы в нем была унифицированная логика и чтобы нам было проще добавлять туда новую, общую функциональность.
За короткий период времени сервисы могут создать большое количество сообщений. Для распределения этой нагрузки мы используем Rabbit-очереди. В этих очередях подчас лежит по несколько миллионов сообщений, и это вполне нормальная для нас ситуация. Когда сервису нужно отправить письмо, он добавляет сообщение в эту очередь, используя переменные, которые специфичны для каждого конкретного письма.
Некоторые важные письма, вроде регистрации, восстановления пароля и документов, пользователи хотят получать мгновенно. Но если мы будем складывать эти сообщения в общую очередь, время доставки таких писем может достигать нескольких часов. Чтобы решить эту проблему мы используем несколько очередей с различными типами, которые обрабатываются параллельно и независимо друг от друга.
Эти очереди читают инстансы сервиса, который знает: как собрать письмо по сообщению из Rabbit, как получить его текст, верстку, отправителей etc.
Помимо этого он добавляет туда дополнительную техническую информацию, например, заголовки, которые в дальнейшем помогают разбирать инциденты пользователей. Еще пример дополнительной технической информации — это добавление пикселя в каждое письмо, с помощью которого мы в дальнейшем можем отследить каждое открытие письма.
Также мы бы хотели иметь статистику по отправкам писем, но писать каждую отправку письма в базу было бы довольно накладно, поэтому на этом этапе мы просто записываем все отправки в лог.
Итак, наше письмо готово. Нам осталось только отправить его пользователям. Для этого нам надо открыть SMTP соединение, передать данные, получить ответ, правильно его распарсить и переотправить письмо в случае неудачи. Все эти типовые задачи помогает решать софт, который называется MTA — Mail Transfer Agent.
Раньше мы использовали Exim, но в дальнейшем перешли на Postfix, поскольку он больше соответствует нашим требованиям к производительности. Далее мы получаем логи Postfix и заливаем их в Hadoop вместе с логами из предыдущего этапа, данными по отправкам и открытию пикселя. На основе всех этих данных мы в дальнейшем строим аналитику, а также выводим информацию для техподдержки по статусам отправок и открытий.
Итоговая схема выглядит приблизительно так:
Пуш-уведомления
Схема отправки пушей практически идентична со схемой отправки email-ов, за исключением того, что в качестве брокера сообщений мы используем Kafka. И здесь мы аналогично используем отдельную очередь для отправки важных уведомлений. Эту очередь слушает сервис, который хранит в себе информацию об устройствах пользователей, ключи отправки и непосредственно отправляет уведомления в нужные сервисы (Android/ios/web etc).
В какой-то момент мы столкнулись с проблемой дублирования уведомлений. Например, при каждом открытии резюме пользователю отправлялся пуш. Чтобы решить эту проблему, мы стали хранить историю последних отправок и проверять, было ли недавно отправлено подобное уведомление. Это помогло нам избежать дублей.
Получается, что нам необходимо хранить историю уведомлений, при этом довольно ограниченное количество времени, а также уметь быстро её читать. В качестве такого хранилища идеально подходит память, но поскольку у нас есть несколько инстансов одного и того же сервиса, им нужна общая память. И в качестве такого хранилища мы используем Redis.
SMS-ки
SMS-уведомлений у нас гораздо меньше, чем email-ов и пушей. У нас есть специальный сервис, куда делают запрос по HTTP все сервисы, которые хотят отправить SMS, а внутри него уже у нас живет очередь на базе.
В какой-то момент мы столкнулись с проблемой — hh.ru представлен в разных странах, и отправка, например, из России в Казахстан стоит в несколько раз дороже, чем просто по России. Мы решили её так: заключаем договоры с локальными провайдерами, и в момент отправки по профилю пользователя определяем, кого из них нам лучше использовать.
Некоторые из проблем, с которыми мы сталкиваемся
Борьба провайдеров со спамом
Есть проблема, с которой вы можете столкнуться при самостоятельной отправке email без использования каких-то внешних сервисов — это борьба крупных провайдеров со спамом. Они могут закидывать ваши письма в спам, искусственно завышать время ответа (тем самым замедляя вашу очередь отправки), а могут и просто возвращать ошибки при отправках. Не всегда понятно, почему алгоритм решил, что вот конкретно это письмо являются спамом. Существуют разные причины, по которым это может произойти, и одна из них, с которой мы сталкиваемся довольно часто — это резкое увеличение количества отправок с нашей стороны.
Нам приходится следить за тем, чтобы наши сервера отправляли определенное количество писем с определенной скоростью и мы не можем добавить новый сервер с новым ip и рассылать по нему с такой же скоростью, поэтому нам приходится увеличивать скорость отправок постепенно. Для крупных сервисов, которые любят внезапно увеличивать время ответа, мы выделяем отдельные инстансы Postfix и благодаря этому вся очередь не замедляется.
Проблемы на стороне получателя
Помимо траблов с крупными почтовыми провайдерами мы также сталкиваемся с проблемами отправок писем на корпоративных пользователей. Например, это может быть неправильно настроенный сервер на стороне получателя. В выявлении этих проблем нам помогает анализ логов, про которые я рассказывал ранее.
Публичные спам-листы
Многие компании используют публичные почтовые спам-листы и поэтому нам приходится самостоятельно управлять репутацией наших почтовых серверов. Мы мониторим наши попадания в них и стараемся своевременно из них выходить. В этом нам помогает в том числе бот, которого мы написали. Он вовремя уведомляет нас в Slack о том, что мы попали в новый спам-лист.
Многое может пойти не так. От момента, когда мы решили отправить письмо пользователю и до момента, когда он его действительно получил. Поэтому мы стараемся мониторить как можно больше, чтобы быстро определять проблемы. Мы мониторим:
Все наши сервисы на стандартные параметры — RPS, потребление памяти, CPU, количество тредов etc.
Размеры очередей. Если в очереди слишком много сообщений, это значит, что сервисы перестали справляться с нагрузкой и мы выпустили какое-то направление, которое их замедлило.
Каждый крон, который генерирует письма и разбиваем этот мониторинг на этапы, чтобы понимать, на каком из них произошла проблема.
Специфичные ответы от почтовых сервисов и время их ответа, чтобы понять, что нас не замедляют.
Время от момента, когда письмо было создано, до момента, когда это письмо было отправлено
Количество отправок в целом. Если отправок слишком мало, значит по какой-то причине письма перестали отправляться и это может стать проблемой. Если отправок слишком много — тоже беда. Как-то раз у нас в kafka-клиенте случился баг, из-за которого одно и то же письмо отправлялось бесконечное количество раз, а наши пользователи получили десятки и сотни тысяч одинаковых писем. Не надо так.
Заключение
Как видите, организовать и поддерживать свой сервис уведомлений — довольно сложная задача. Поэтому, если у вас есть возможность использовать готовый сервис, то я рекомендую вам не заморачиваться лишний раз. Но если нет, я надеюсь что наш опыт вам поможет.
Делитесь в комментах вашим опытом по управлению рассылками и уведомлениями, нам интересно!