Скорее всего, рассказывать, что такое вебхуки (webhooks) — никому не нужно. Но на всякий случай: вебхуки — это механизм оповещения о событиях во внешней системе. Например, о покупке в интернет-магазине через онлайн-кассу, отправке кода в GitHub-репозиторий или действиях пользователей в чатах. В типичном API нужно постоянно опрашивать сервер, написал ли пользователь что-нибудь в чате. С помощью механизма вебхуков можно «подписаться» на оповещения, и сервер сам отправит HTTP-запрос, когда произойдет событие. Это удобнее и быстрее, чем постоянно запрашивать новые данные на сервере.
ManyChat — это платформа, которая помогает бизнесу общаться со своими клиентами через чаты в мессенджерах. Вебхуки — одна из важных частей ManyChat, потому что именно через них бизнес общается с клиентами. А общаются они много — например, через систему бизнесы отправляют своим клиентам миллиарды сообщений в месяц.
Основная масса сообщений отправляется через Facebook Messenger. У него есть особенность — медленный API. Когда клиент пишет сообщение, чтобы заказать пиццу, Facebook отправляет в ManyChat вебхук. Платформа его обрабатывает, отправляет запрос обратно и пользователь получает сообщение. Из-за медленного API некоторые запросы идут несколько секунд. Но когда платформа долго не отвечает, бизнес теряет клиента, а Facebook может отключить приложение от вебхуков.
Поэтому обработка вебхуков — это одна из главных инженерных задач платформы. Чтобы решить проблему, в ManyChat за три года работы несколько раз меняли архитектуру обработки с простого контроллера в Yii до распределенной системы с «Галактиками». Подробнее об этом под катом расскажет Дмитрий Кушников (cancellarius).
Дмитрий Кушников руководит разработкой в ManyChat и профессионально программирует на PHP с 2001 года. Дмитрий расскажет, как менялась архитектура вместе с ростом сервиса и нагрузкой, какие решения и технологии применялись на разных этапах, как эволюционировала обработка вебхуков и как платформе удается справляться с огромной нагрузкой с помощью скромных ресурсов на PHP.
Примечание. Статья основана на докладе Дмитрия «Эволюция обработки вебхука Facebook: с нуля до 12 500 в секунду» на PHP Russia 2019. Но пока он готовился, показатели выросли до 25 000.
Что такое ManyChat
Для начала введу в контекст наших задач. ManyChat — это сервис, который помогает бизнесу использовать мессенджеры для маркетинга, продаж и поддержки. Основной продукт — это платформа для Messenger Marketing в Facebook Messenger. За три года сервисом воспользовалось больше 1 миллион бизнесов из 100 стран мира, чтобы пообщаться с 700 миллионов своих клиентов.
Со стороны клиента это выглядит так.
Кнопки, картинки и галереи в диалогах в Facebook Messenger.
Это интерфейс Facebook Messenger. Кроме текстовых сообщений, в нем можно отправлять интерактивные элементы, чтобы взаимодействовать с клиентами, вовлекать в диалог, повышать интерес к своим продуктам и продавать.
Со стороны бизнеса все выглядит иначе. Это интерфейс нашего веб-приложения, где с помощью визуального интерфейса представители бизнеса создают и программируют сценарии диалогов. На картинке один из примеров сценария.
Сердце нашей системы — компонент Flow Builder.
Совокупность сценариев и правил автоматизации мы называем ботом. Поэтому, если упростить, то можно сказать, что ManyChat — это конструктор ботов.
Пример бота.
Клиент бизнеса, который участвует в диалоге, называется подписчиком, потому что для взаимодействия клиент подписывается на бота.
Почему Facebook
Почему Facebook Messenger, мы же страна выжившего Telegram? На это есть причины.
- Telegram популярен в России, но в Европе и Америке мессенджер №1 это Facebook. В нем 1,5 миллиарда пользователей, а в Telegram всего 200-300 миллионов.
- На Facebook гораздо больше бизнесов, которые хотят общаться со своими клиентами. Не все из них перешли в мессенджер, но они пользуются Facebook и через какое-то время будут готовы это сделать.
- По утверждению Facebook на конференции F8 в Сан-Хосе на платформе работает 300 тысяч разработчиков. По всему миру они создают ботов для Facebook Messenger. Каждый месяц через платформу между бизнесом и пользователями отправляется 20 миллиардов сообщений. ManyChat занимает долю в 40%.
Взаимодействие с Facebook
Взаимодействие с Facebook устроено примерно так:
Бизнес использует веб-приложение для настройки логики бота. Когда клиент взаимодействует с ботом через телефон, Facebook получает об этом информацию и отправляет нам вебхук. ManyChat его обрабатывает, в зависимости от логики, которая запрограммирована бизнесом, и отправляет запрос обратно. Дальше Facebook доставляет сообщение в телефон пользователя.
Технологический стек
Все это мы делаем на скромном стеке. В основе, конечно, PHP. Веб-сервером работает Nginx, основная база данных — PostgreSQL, а еще есть Redis и Elasticsearch. Все это крутится в облаках Amazon Wev Services.
Обработка Facebook Webhook
Примерно так выглядит вебкух Facebook’а: это запрос с payload в формате JSON.
{
"object":"page",
"entry":[
}
"id":"<PAGE_ID>",
"time":1458692752478,
"messaging":[
{
"sender":{
"id":"<PSID>"
},
"recipient":{
"id":"<PAGE_ID>"
},
...
}
]
}
]
}
Вебхуки — это всего 10% нашей нагрузки, но важнейшая часть системы. Через них бизнес общается с пользователями. Если сообщения тормозят или не отправляются, то пользователь отказывается взаимодействовать с ботом, а бизнес теряет клиента.
Давайте посмотрим на эволюцию нашей архитектуры с момента запуска продукта.
Май 2016 года. Мы только запустили наш сервис: 20 ботов, из которых 10 тестовые, и 20 подписчиков. Нагрузка составляла 0 RPS.
Схема взаимодействия выглядела так:
- Запрос идет в nginx.
- Nginx обращается к PHP-FPM.
- PHP-FPM поднимает приложение на Yii.
- Вебхук-контроллер обрабатывает логику и в соответствии с ней отправляет запросы в Facebook.
Связка Nginx и PHP-FPM
Июнь 2016. Через месяцмы анонсировали ManyChat на ProductHunt и количество ботов выросло до 2 тысяч. Число подписчиков увеличилось до 7 тысяч.
В этот момент появилась первая проблема в системе. API Facebook не очень быстрый: некоторые запросы могут длиться несколько секунд, а несколько запросов десятки секунд. Но сервер вебхуков хочет, чтобы мы отвечали быстро. Из-за медленного API мы долго не отвечаем: сервер сначала ругается, а потом может вообще отключить приложение от вебхуков.
Пользователей мало, мы еще разрабатываем приложение, ищем наш рынок, аудиторию, а уже появилась проблема нагрузки. Но нас спасло простое решение: в тот момент, когда запускается контроллер, прерываем обращение к Facebook. Мы говорим Facebook, что все хорошо, а сами в фоне обрабатываем запросы и вебхук.
Очереди на PostgreSQL
Декабрь 2016. Сервис вырос в 5-10 раз: 10 тысяч ботов и 700 тысяч подписчиков.
Параллельно мы работали над новыми задачами: отображение статистики, доставляемость сообщений,, конверсии показов и переходов. Также реализовали Live Chat. Кроме автоматизации взаимодействий он дает бизнесу возможность писать сообщения своему подписчику напрямую.
Решение этих задач увеличило количество отслеживаемых хуков в 4 раза. На каждое отправляемое сообщение мы получали 3 дополнительных вебхука. Систему обработки снова требовалось улучшать. Мы маленькая платформа, на бэкенде работало всего два человека, поэтому выбрали самое простое решение — очереди на PostgreSQL.
Мы пока еще не хотим реализовывать сложные системы, поэтому просто разделяем потоки обработки. Вебхуки, которые нужно обрабатывать быстро, чтобы пользователь получил ответ, обрабатываем синхронно. Все остальные отправляем в очереди для асинхронных запросов.
Очереди на Redis
Июнь 2017. Сервис растет: 75 тысяч ботов, 7 миллионов подписчиков.
Мы реализуем еще одну новую функцию. Все вебхуки, которые мы обрабатывали, касались только коммуникаций в мессенджере. Но теперь мы решили дать бизнесам возможность общаться с подписчиками бизнес-страниц и начали обрабатывать новые типы вебхуков — те, что касаются ленты самой страницы.
Ленты бизнес-страниц обновляются не так редко. Маркетологи часто что-то постят, потом следят за каждый лайком и считают их. Огромного трафика на бизнеса-страницах нет. Но бывают обратные ситуации, например, «день Кэти Перри».
Кэти Перри — знаменитая американская певица с огромным количеством фанатов по всему миру. Только в ее группе на Facebook 64 миллиона подписчиков. В какой-то момент маркетологи певицы решили сделать бота на Facebook Messenger и выбрали нашу платформу. В тот момент, когда они опубликовали сообщение с призывом подписаться на бот, наша нагрузка выросла в 3-4 раза.
Эта ситуация помогла нам понять, что без нормальной реализации очередей мы ничего не можем сделать. Как решение выбрали Redis.
Выбрать Redis для очередей — фантастически удачное решение.
Он помог решить огромное количество задач. Сейчас каждую секунду через наш Redis-кластер проходит 1 миллион разных запросов. Мы используем его не только для всех каскадных очередей, но и для других задач, например, мониторинга.
Очереди на Redis реализовали не с первой попытки. Когда мы начали просто складывать вебхуки в Redis и обрабатывать их одним процессом, то расширили воронку наверху: входящих вебхуков стало больше, обработанных тоже, но сам процесс все равно занимал какое-то время. Это первое решение было неудачным.
Когда же попробовали масштабировать количество этих запросов, случился небольшой коллапс. В очереди могут скапливаться запросы от разных страниц, но могут идти подряд запросы от одной страницы. Если один обработчик медлит, то запросы от одного подписчика и от одного бота будут обработаны в неправильном порядке. Пользователь отправляет сообщения, выполняет какие-то действия с ботом, но получит ответ вразнобой.
Кажется, что это редкий случай, но тестирование на наших нагрузках показало, что это будет происходить часто.
Мы начали искать другое решение. Здесь на помощь пришла простота и мощь Redis — мы решили сделать очередь на каждого бота.
Как это работает? Сообщения, которые касаются каждого бота, складываем в очередь. Чтобы не поднимать обработчик на каждую очередь, сделали контрольную очередь. Она работает так. Каждый раз, когда приходит запрос от бота, в Redis публикуются два сообщения: одно в очередь бота, второе в контрольную. Обработчик следит за контрольной и каждый раз запускает демона, когда там есть задача обработать бота. Демон разгребает очередь соответствующего бота.
Дополнительно к основной задаче мы решили проблему «шумных соседей». Это когда один бот сгенерировал огромную массу вебхуков и она тормозит систему, потому что другие страницы ждут обработку. Для решения проблемы достаточно масштабироваться: когда контрольная очередь наполняется мы добавляем новых обработчиков.
Кроме того, очереди виртуальные. Это всего лишь ячейки в памяти Redis. Когда в очереди ничего нет, её не существует, она не занимает ничего.
ReactPHP
Январь 2018 года. Мы достигли отметки в 1 миллиард сообщений в месяц.
Нагрузка составляла 5 тысяч RPS на систему. Это не пиковая нагрузка, а стандартная. Когда появляются боты знаменитых певиц все растет в несколько раз уже от этой цифры. Но это не проблема. Проблема в PHP-FPM: он уже не выдерживает нагрузку в 5 тысяч RPS.
Все в то время говорили о модном асинхронном процессинге. Мы к нему присмотрелись, увидели ReactPHP, провели быстрые тесты, заменили им PHP-FPM и мгновенно получили прирост в 4 раза.
Мы не стали переписывать обработку нашего процессинга — ReactPHP поднимал фреймворк Yii. Сначала мы подняли 4 ReactPHP-сервиса, а позже дошли до 30. Достаточно долго мы жили именно на них, а фреймворк справлялся с нагрузкой.
Как только мы расширили воронку, случился еще один коллапс: после запуска воронки на приёме опять начал страдать процессинг. Чтобы решить уже эту проблему, решили выделить процессинг в кластеры.
Кластеры
Взяли ботов, распределили их по кластерам и выстроили логические цепочки из Redis, Postgres и обработчика.
В итоге у нас сформировалось понятие «Галактика» — логически физическая абстракция над процессингом. Она состоит из инстансов: Redis, PostgreSQL и набора PHP-сервисов. Каждый бот принадлежит какому-то кластеру, и ReactPHP знает о том, в какой кластер нужно поместить сообщение для данного бота. Дальше работает схема выше.
Вселенная расширяется, Вселенная наших систем тоже, и мы добавляем новую «Галактику» когда это происходит.
«Галактики» — это наш способ масштабирования.
Заменяем ReactPHP на связку Nginx и Lua
Следующие полгода мы продолжали расти: 200 миллионов подписчиков и 3 миллиарда сообщений в месяц. Представьте сайт на 200 миллионов зарегистрированных пользователей — те же нагрузки.
Возникла новая проблема. Вебхуки — это небольшие однотипные задачи, а PHP не подходит для их решения. Даже ReactPHP уже не помогал.
- Он не справлялся с нагрузкой в 10 тысяч RPS — с момента внедрения ReactPHP нагрузка выросла.
- Его требовалось перезагружать даже при деплоях, притом последовательно, потому что нельзя прервать обработку входящих вебхуков. Facebook отключает приложение, когда понимает, что у него проблемы. Для ManyChat это катастрофа — 650 тысяч активно работающих бизнесов нас не простят.
Поэтому мы постепенно откусывали от ReactPHP разную логику, передавали ее в процессинги, вычленяли новые очереди. В процессе заметили, что ReactPHP выполняет одну простую задачу — берет вебхук и складывает его в очередь. Все остальное выполняют уже процессинги. Существуют ли аналоги для такой простой задачи?
Мы вспомнили, что у Nginx есть модули и заметили библиотеку OpenResty. Кроме поддержки языка программирования Lua у нее был модуль работы с Redis. Написанный за 3 часа тест показал, что всю работу 30 сервисов на ReactPHP можно выполнить прямо на стороне nginx.
Так выглядит то, что у нас получилось: обрабатываем какой-то endpoint, забираем тело запроса и складываем его напрямую в Redis.
location / {
error_log /var/log/nginx/error.log;
resolver ###resolver###;
content_by_lua '
ngx.req.read_body()
local mybody = ngx.req.get_body_data()
if not mybody then
return ngx.exit(400)
end
local hash = ngx.crc32_long(mybody)
local cluster = hash % ###wh_inbound_shards### + 1
local redis = require "resty.redis";
local red = radis.new()
red:set_timeout(3000)
local ok, err = red:connect("###redisConnectionWh2.server.host###", 6379)
if not ok then
ngx.log(ngx.ERR, err, "Redis failed to connect")
return ngx.exit(403)
end
local ok, err = red:rpush("###wh_inbound_queue###" .. queuesuffix .. cluster, mybody)
if not ok then
ngx.log(ngx.ERR, err, "Failed to write data", mybody)
return ngx.exit(500)
end
local ok, err = red:set_keepalive(10000, 100)
ngn.say("ok")
';
}
OpenResty и Lua помогли увеличить пропускную способность. Мы продолжаем справляться с нашей нагрузкой, сервис живет, все счастливы.
Улучшаем решение на Lua
Последний этап (прим: на момент доклада) — февраль 2019. 500 миллионов подписчиков отправляют и получают от миллиона ботов 7 миллионов сообщений каждый месяц.
Это этап улучшения нашего решения на Lua. Постепенно откусываем некоторую логику из очередей, а первичный процессинг распределения вебхуков между системами переносим на Lua. Теперь наши системы производительнее и менее зависимы.
Мы сохраняем по отдельности процессинг и асинхронную обработку. Обработка касается статистики и прочего — теперь это совершенно другая система.
Система кажется простой, но это не так. Под капотом крутится 500 сервисов, которые обрабатывают свои запросы. Вся система работает на 50 инстансах Амазона: Redis, PostgreSQL и сами обработчики PHP.
Эволюция процессинга
Highload можно классно делать на PHP.
Кратко вспомним как мы это делали в процессе развития системы.
- Стартовали с обычного Nginx и PHP-FPM.
- Добавили очереди на PostgreSQL, а потом и на Redis.
- Добавили кластеризацию.
- Внедрили ReactPHP.
- Заменили ReactPHP на связку Nginx и Lua, а позже перенесли на связку логику.
На своем опыте мы выяснили, что можно расти и строить архитектуру последовательно меняя уязвимые части, применять простые известные подходы и при этом не расширять стек.
В этом рассказе мы проследили за решением технической задачи, а как тем временем эволюционировали процессы в компании, Дмитрий Кушников расскажет уже 11 февраля на TeamLead Conf. Из доклада узнаем, когда эффективно внедрение LeSS, а когда от этой методологии лучше отказаться.
А в календаре на весну у нас PHP Russia и Saint HighLoad++, и для этих конференций мы еще только формируем программу. Если в вашем стеке PHP занимает достойное место, вы научились готовить с ним сложные проекты и готовы поделится рецептами — приходите выступать на PHP Russia 13 мая. А если у вас highload без PHP, то ждём на Saint HighLoad++ в апреле в Питере.
Автор: Олег Бунин