Эволюция обработки вебхуков Facebook: с нуля до 25 000 в секунду

Скорее всего, рассказывать, что такое вебхуки (webhooks) — никому не нужно. Но на всякий случай: вебхуки — это механизм оповещения о событиях во внешней системе. Например, о покупке в интернет-магазине через онлайн-кассу, отправке кода в GitHub-репозиторий или действиях пользователей в чатах. В типичном API нужно постоянно опрашивать сервер, написал ли пользователь что-нибудь в чате. С помощью механизма вебхуков можно «подписаться» на оповещения, и сервер сам отправит HTTP-запрос, когда произойдет событие. Это удобнее и быстрее, чем постоянно запрашивать новые данные на сервере.

t8xp4lbyixxglmd2wlzdjkzgrbk.jpeg

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 миллионов своих клиентов.

Со стороны клиента это выглядит так.

car88depa1ktozsq9z_pnfgkwoe.png
Кнопки, картинки и галереи в диалогах в Facebook Messenger.

Это интерфейс Facebook Messenger. Кроме текстовых сообщений, в нем можно отправлять интерактивные элементы, чтобы взаимодействовать с клиентами, вовлекать в диалог, повышать интерес к своим продуктам и продавать.

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

4fn5qvo7pcwuqmojh3zsyzjru4q.png
Сердце нашей системы — компонент Flow Builder.

Совокупность сценариев и правил автоматизации мы называем ботом. Поэтому, если упростить, то можно сказать, что ManyChat — это конструктор ботов.

ixi23mwbcq56ue2zcoyhyqjcy-e.png
Пример бота.

Клиент бизнеса, который участвует в диалоге, называется подписчиком, потому что для взаимодействия клиент подписывается на бота.

Почему Facebook


Почему Facebook Messenger, мы же страна выжившего Telegram? На это есть причины.

  • Telegram популярен в России, но в Европе и Америке мессенджер №1 это Facebook. В нем 1,5 миллиарда пользователей, а в Telegram всего 200–300 миллионов.
  • На Facebook гораздо больше бизнесов, которые хотят общаться со своими клиентами. Не все из них перешли в мессенджер, но они пользуются Facebook и через какое-то время будут готовы это сделать.
  • По утверждению Facebook на конференции F8 в Сан-Хосе на платформе работает 300 тысяч разработчиков. По всему миру они создают ботов для Facebook Messenger. Каждый месяц через платформу между бизнесом и пользователями отправляется 20 миллиардов сообщений. ManyChat занимает долю в 40%.


Взаимодействие с Facebook


Взаимодействие с Facebook устроено примерно так:

y9_mgwxr-h-8dm1hzmnobmnvmsk.png

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

Технологический стек


Все это мы делаем на скромном стеке. В основе, конечно, PHP. Веб-сервером работает Nginx, основная база данных — PostgreSQL, а еще есть Redis и Elasticsearch. Все это крутится в облаках Amazon Wev Services.

Обработка Facebook Webhook


Примерно так выглядит вебкух Facebook«а: это запрос с payload в формате JSON.

{
    "object":"page",
    "entry":[
        }
            "id":"",
            "time":1458692752478,
            "messaging":[
                {
                    "sender":{
                        "id":""
                    },
                    "recipient":{
                        "id":""
                    },

                    ...
                }
            ]  
        }
    ]
}


Вебхуки — это всего 10% нашей нагрузки, но важнейшая часть системы. Через них бизнес общается с пользователями. Если сообщения тормозят или не отправляются, то пользователь отказывается взаимодействовать с ботом, а бизнес теряет клиента.

Давайте посмотрим на эволюцию нашей архитектуры с момента запуска продукта.

Май 2016 года. Мы только запустили наш сервис: 20 ботов, из которых 10 тестовые, и 20 подписчиков. Нагрузка составляла 0 RPS.

Схема взаимодействия выглядела так:

o9g04hhven61_yy9lgtam45cosk.png

  • Запрос идет в nginx.
  • Nginx обращается к PHP-FPM.
  • PHP-FPM поднимает приложение на Yii.
  • Вебхук-контроллер обрабатывает логику и в соответствии с ней отправляет запросы в Facebook.

Связка Nginx и PHP-FPM


Июнь 2016. Через месяцмы анонсировали ManyChat на ProductHunt и количество ботов выросло до 2 тысяч. Число подписчиков увеличилось до 7 тысяч.

В этот момент появилась первая проблема в системе. API Facebook не очень быстрый: некоторые запросы могут длиться несколько секунд, а несколько запросов десятки секунд. Но сервер вебхуков хочет, чтобы мы отвечали быстро. Из-за медленного API мы долго не отвечаем: сервер сначала ругается, а потом может вообще отключить приложение от вебхуков.

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

uawwkuo6kpyu7_4cxib5btfvasq.png

Очереди на PostgreSQL


Декабрь 2016. Сервис вырос в 5–10 раз: 10 тысяч ботов и 700 тысяч подписчиков.

Параллельно мы работали над новыми задачами: отображение статистики, доставляемость сообщений, конверсии показов и переходов. Также реализовали Live Chat. Кроме автоматизации взаимодействий он дает бизнесу возможность писать сообщения своему подписчику напрямую.

Решение этих задач увеличило количество отслеживаемых хуков в 4 раза. На каждое отправляемое сообщение мы получали 3 дополнительных вебхука. Систему обработки снова требовалось улучшать. Мы маленькая платформа, на бэкенде работало всего два человека, поэтому выбрали самое простое решение — очереди на PostgreSQL.

Мы пока еще не хотим реализовывать сложные системы, поэтому просто разделяем потоки обработки. Вебхуки, которые нужно обрабатывать быстро, чтобы пользователь получил ответ, обрабатываем синхронно. Все остальные отправляем в очереди для асинхронных запросов.

05xoc80prmektoowus0ogzrxqks.png

Очереди на Redis


Июнь 2017. Сервис растет: 75 тысяч ботов, 7 миллионов подписчиков.

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

Ленты бизнес-страниц обновляются не так редко. Маркетологи часто что-то постят, потом следят за каждый лайком и считают их. Огромного трафика на бизнеса-страницах нет. Но бывают обратные ситуации, например,»день Кэти Перри».

Кэти Перри — знаменитая американская певица с огромным количеством фанатов по всему миру. Только в ее группе на Facebook 64 миллиона подписчиков. В какой-то момент маркетологи певицы решили сделать бота на Facebook Messenger и выбрали нашу платформу. В тот момент, когда они опубликовали сообщение с призывом подписаться на бот, наша нагрузка выросла в 3–4 раза.

Эта ситуация помогла нам понять, что без нормальной реализации очередей мы ничего не можем сделать. Как решение выбрали Redis.

Выбрать Redis для очередей — фантастически удачное решение.

Он помог решить огромное количество задач. Сейчас каждую секунду через наш Redis-кластер проходит 1 миллион разных запросов. Мы используем его не только для всех каскадных очередей, но и для других задач, например, мониторинга.

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

573ukqmfkwheeseoxvkdadulp54.png

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

fkvcfdcbeisa-ew7hcvusczdags.png

Кажется, что это редкий случай, но тестирование на наших нагрузках показало, что это будет происходить часто.

Мы начали искать другое решение. Здесь на помощь пришла простота и мощь Redis — мы решили сделать очередь на каждого бота.

jhzy7fcdcsjc4x7yatu6pbde3xw.jpeg

Как это работает? Сообщения, которые касаются каждого бота, складываем в очередь. Чтобы не поднимать обработчик на каждую очередь, сделали контрольную очередь. Она работает так. Каждый раз, когда приходит запрос от бота, в Redis публикуются два сообщения: одно в очередь бота, второе в контрольную. Обработчик следит за контрольной и каждый раз запускает демона, когда там есть задача обработать бота. Демон разгребает очередь соответствующего бота.

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

Кроме того, очереди виртуальные. Это всего лишь ячейки в памяти Redis. Когда в очереди ничего нет, её не существует, она не занимает ничего.

ReactPHP


Январь 2018 года. Мы достигли отметки в 1 миллиард сообщений в месяц.

Нагрузка составляла 5 тысяч RPS на систему. Это не пиковая нагрузка, а стандартная. Когда появляются боты знаменитых певиц все растет в несколько раз уже от этой цифры. Но это не проблема. Проблема в PHP-FPM: он уже не выдерживает нагрузку в 5 тысяч RPS.

Все в то время говорили о модном асинхронном процессинге. Мы к нему присмотрелись, увидели ReactPHP, провели быстрые тесты, заменили им PHP-FPM и мгновенно получили прирост в 4 раза.

qs5rikje32_otggcyz--ow8fj4g.png

Мы не стали переписывать обработку нашего процессинга — ReactPHP поднимал фреймворк Yii. Сначала мы подняли 4 ReactPHP-сервиса, а позже дошли до 30. Достаточно долго мы жили именно на них, а фреймворк справлялся с нагрузкой.

Как только мы расширили воронку, случился еще один коллапс: после запуска воронки на приёме опять начал страдать процессинг. Чтобы решить уже эту проблему, решили выделить процессинг в кластеры.

Кластеры


Взяли ботов, распределили их по кластерам и выстроили логические цепочки из Redis, Postgres и обработчика.

cjmwoc_unxshst1gpdu-gbyl_le.png

В итоге у нас сформировалось понятие «Галактика» — логически физическая абстракция над процессингом. Она состоит из инстансов: Redis, PostgreSQL и набора PHP-сервисов. Каждый бот принадлежит какому-то кластеру, и ReactPHP знает о том, в какой кластер нужно поместить сообщение для данного бота. Дальше работает схема выше.

mleyscad0juq-izrevpl_huwi8i.png
Вселенная расширяется, Вселенная наших систем тоже, и мы добавляем новую «Галактику» когда это происходит.

«Галактики» — это наш способ масштабирования.


Заменяем ReactPHP на связку Nginx и Lua


Следующие полгода мы продолжали расти: 200 миллионов подписчиков и 3 миллиарда сообщений в месяц. Представьте сайт на 200 миллионов зарегистрированных пользователей — те же нагрузки.

Возникла новая проблема. Вебхуки — это небольшие однотипные задачи, а PHP не подходит для их решения. Даже ReactPHP уже не помогал.

  • Он не справлялся с нагрузкой в 10 тысяч RPS — с момента внедрения ReactPHP нагрузка выросла.
  • Его требовалось перезагружать даже при деплоях, притом последовательно, потому что нельзя прервать обработку входящих вебхуков. Facebook отключает приложение, когда понимает, что у него проблемы. Для ManyChat это катастрофа — 650 тысяч активно работающих бизнесов нас не простят.


Поэтому мы постепенно откусывали от ReactPHP разную логику, передавали ее в процессинги, вычленяли новые очереди. В процессе заметили, что ReactPHP выполняет одну простую задачу — берет вебхук и складывает его в очередь. Все остальное выполняют уже процессинги. Существуют ли аналоги для такой простой задачи?

Мы вспомнили, что у Nginx есть модули и заметили библиотеку OpenResty. Кроме поддержки языка программирования Lua у нее был модуль работы с Redis. Написанный за 3 часа тест показал, что всю работу 30 сервисов на ReactPHP можно выполнить прямо на стороне nginx.

aycmyfs312-ozvcqs9fvdbt0mny.png

Так выглядит то, что у нас получилось: обрабатываем какой-то 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. Теперь наши системы производительнее и менее зависимы.

079uci4cp73vm1_ocbdyla05fc8.png

Мы сохраняем по отдельности процессинг и асинхронную обработку. Обработка касается статистики и прочего — теперь это совершенно другая система.

Система кажется простой, но это не так. Под капотом крутится 500 сервисов, которые обрабатывают свои запросы. Вся система работает на 50 инстансах Амазона: Redis, PostgreSQL и сами обработчики PHP.

Эволюция процессинга


Highload можно классно делать на PHP.


Кратко вспомним как мы это делали в процессе развития системы.

  • Стартовали с обычного Nginx и PHP-FPM.
  • Добавили очереди на PostgreSQL, а потом и на Redis.
  • Добавили кластеризацию.
  • Внедрили ReactPHP.
  • Заменили ReactPHP на связку Nginx и Lua, а позже перенесли на связку логику.


e4l4z_wwdlb9amca7hywk_exaha.png

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

В этом рассказе мы проследили за решением технической задачи, а как тем временем эволюционировали процессы в компании, Дмитрий Кушников расскажет уже 11 февраля на TeamLead Conf. Из доклада узнаем, когда эффективно внедрение LeSS, а когда от этой методологии лучше отказаться.

А в календаре на весну у нас PHP Russia и Saint HighLoad++, и для этих конференций мы еще только формируем программу. Если в вашем стеке PHP занимает достойное место, вы научились готовить с ним сложные проекты и готовы поделится рецептами — приходите выступать на PHP Russia 13 мая. А если у вас highload без PHP, то ждём на Saint HighLoad++ в апреле в Питере.

© Habrahabr.ru