L'évolution du traitement des webhooks Facebook: de zéro à 25 000 par seconde

Très probablement, personne n'a besoin de dire ce que sont les webhooks. Mais juste au cas où: les webhooks sont un mécanisme pour signaler des événements dans un système externe. Par exemple, sur l'achat dans une boutique en ligne via une caisse en ligne, l'envoi d'un code à un référentiel GitHub ou les actions des utilisateurs dans les chats. Dans une API typique, vous devez constamment interroger le serveur si l'utilisateur a écrit quelque chose dans le chat. En utilisant le mécanisme de webhook, vous pouvez vous "abonner" aux notifications, et le serveur lui-même enverra une requête HTTP lorsqu'un événement se produit. C'est plus pratique et plus rapide que de demander constamment de nouvelles données sur le serveur.



ManyChat est une plateforme qui aide les entreprises à communiquer avec leurs clients via le chat dans les messageries instantanées. Les webhooks sont l'une des parties importantes de ManyChat, car c'est à travers eux que l'entreprise communique avec les clients. Et ils communiquent beaucoup - par exemple, via un système, les entreprises envoient des milliards de messages par mois à leurs clients.

La plupart des messages sont envoyés via Facebook Messenger. Il a une fonctionnalité - une API lente. Lorsqu'un client écrit un message pour commander une pizza, Facebook envoie un webhook à ManyChat. La plateforme la traite, renvoie la demande et l'utilisateur reçoit un message. En raison de la lenteur de l'API, certaines requêtes durent quelques secondes. Mais lorsque la plate-forme ne répond pas pendant longtemps, l'entreprise perd le client et Facebook peut déconnecter l'application des webhooks.

Par conséquent, le traitement des webhooks est l'une des principales tâches d'ingénierie de la plateforme. Pour résoudre le problème, ManyChat a changé son architecture de traitement à plusieurs reprises au cours de trois ans, passant d'un simple contrôleur dans Yii à un système distribué avec Galaxies. En savoir plus à ce sujet sous la coupe Dmitry Kushnikov (cancellarius)

Dmitry Kushnikov dirige le développement chez ManyChat et programme professionnellement en PHP depuis 2001. Dmitry vous expliquera comment l'architecture a changé avec la croissance du service et de la charge, quelles solutions et technologies ont été appliquées à différentes étapes, comment le traitement des webhooks a évolué et comment la plate-forme parvient à faire face à une charge énorme en utilisant des ressources modestes en PHP.

Remarque. L'article est basé sur le rapport de Dmitry «L'évolution du traitement des webhooks Facebook: de zéro à 12500 par seconde» dans PHP Russie 2019 . Mais pendant qu'il se préparait, les indicateurs ont atteint 25 000.


Qu'est-ce que ManyChat


Tout d'abord, je vais vous présenter le contexte de nos tâches. ManyChat est un service qui aide les entreprises à utiliser les messageries instantanées pour le marketing, les ventes et le support. Le produit principal est la plateforme de Messenger Marketing sur Facebook Messenger . Pendant trois ans, plus d'un million d'entreprises de 100 pays du monde ont utilisé le service pour communiquer avec 700 millions de leurs clients.

Côté client, ça ressemble à ça.


Boutons, images et galeries dans les boîtes de dialogue de Facebook Messenger.

Il s'agit de l'interface Facebook Messenger. En plus des messages texte, vous pouvez y envoyer des éléments interactifs pour interagir avec les clients, engager un dialogue, augmenter l'intérêt pour vos produits et vendre.

Du côté des entreprisestout a l'air différent. Il s'agit de l'interface de notre application Web où, à l'aide d'une interface visuelle, les représentants commerciaux créent et programment des scripts de dialogue. L'image est un exemple de scénario.


Le cœur de notre système est le composant Flow Builder.

L'ensemble de scripts et de règles d'automatisation que nous appelons un bot . Par conséquent, pour simplifier, nous pouvons dire que ManyChat est un concepteur de bot.


Un exemple de bot.

Le client de l'entreprise qui participe au dialogue est appelé l' abonné , car pour l'interaction, le client souscrit au bot .

Pourquoi Facebook


Pourquoi Facebook Messenger, nous sommes le pays du télégramme survivant? Il y a des raisons pour cela.

  • Telegram , №1 Facebook. 1,5 , Telegram 200-300 .
  • Facebook , . , Facebook - .
  • Facebook F8 - 300 . Facebook Messenger. 20 . ManyChat 40%.

Facebook


L'interaction avec Facebook est organisée comme suit:



Business utilise une application Web pour configurer la logique du bot. Lorsqu'un client interagit avec le bot via le téléphone, Facebook reçoit des informations à ce sujet et nous envoie un webhook. ManyChat le traite en fonction de la logique programmée par l'entreprise et renvoie la demande. Facebook remet ensuite le message sur le téléphone de l'utilisateur.

Pile technologique


Nous faisons tout cela sur une pile modeste. Au cœur, bien sûr, se trouve PHP. Le serveur Web exécute Nginx, la base de données principale est PostgreSQL, et il existe également Redis et Elasticsearch. Tout tourne dans les nuages ​​d'Amazon Web Services.

Gestion du Webhook Facebook


Voici à quoi ressemble la webcam de Facebook: il s'agit d'une demande avec une charge utile au format JSON.

{
    "object":"page",
    "entry":[
        }
            "id":"<PAGE_ID>",
            "time":1458692752478,
            "messaging":[
                {
                    "sender":{
                        "id":"<PSID>"
                    },
                    "recipient":{
                        "id":"<PAGE_ID>"
                    },

                    ...
                }
            ]  
        }
    ]
}

Les webhooks ne représentent que 10% de notre charge, mais la partie la plus importante du système. Grâce à eux, l'entreprise communique avec les utilisateurs. Si les messages ralentissent ou ne sont pas envoyés, l'utilisateur refuse d'interagir avec le bot et l'entreprise perd le client.

Jetons un œil à l'évolution de notre architecture depuis le lancement du produit.

Mai 2016 . Nous venons de lancer notre service: 20 bots, dont 10 tests et 20 abonnés. La charge était de 0 RPS.

Le schéma d'interaction ressemblait à ceci:



  • La demande est envoyée à nginx.
  • Nginx accède à PHP-FPM.
  • PHP-FPM prend l'application jusqu'à Yii.
  • Le contrôleur de webhook traite la logique et envoie des demandes à Facebook conformément à celle-ci.


Un tas de Nginx et PHP-FPM


Juin 2016. Un mois plus tard, nous avons annoncé ManyChat sur ProductHunt et le nombre de bots est passé à 2 000. Le nombre d'abonnés est passé à 7 mille.

À ce moment, le premier problème est apparu dans le système. L'API Facebook n'est pas très rapide: certaines requêtes peuvent prendre plusieurs secondes, et plusieurs requêtes peuvent prendre des dizaines de secondes. Mais le serveur webhook veut que nous répondions rapidement. En raison de la lenteur de l'API, nous ne répondons pas longtemps: le serveur jure d'abord, puis il peut complètement déconnecter l'application des webhooks.

Il y a peu d'utilisateurs, nous développons toujours l'application, nous recherchons notre marché, notre audience et le problème de charge est déjà apparu. Mais nous avons été sauvés par une solution simple: au moment où le contrôleur démarre, nous interrompons l'accès à Facebook. Nous disons à Facebook que tout va bien, mais en arrière-plan, nous traitons les demandes et le webhook.



Files d'attente sur PostgreSQL


Décembre 2016. Le service a augmenté de 5 à 10 fois: 10 000 bots et 700 000 abonnés.

Parallèlement, nous avons travaillé sur de nouvelles tâches: affichage des statistiques, remise des messages, conversion des impressions et des transitions. Également implémenté le chat en direct. En plus d'automatiser les interactions, il donne aux entreprises la possibilité d'écrire des messages directement à leur abonné.

La résolution de ces problèmes a multiplié par 4 le nombre de crochets suivis. Pour chaque message que nous avons envoyé, nous avons reçu 3 webhooks supplémentaires. Le système de traitement devait encore être amélioré. Nous sommes une petite plate-forme, seules deux personnes ont travaillé sur le backend, nous avons donc choisi la solution la plus simple - les files d'attente sur PostgreSQL.

Nous ne souhaitons pas encore implémenter de systèmes complexes, nous partageons donc simplement les flux de traitement. Les webhooks qui doivent être traités rapidement pour que l'utilisateur reçoive une réponse sont traités de manière synchrone. Tous les autres sont envoyés dans des files d'attente pour les demandes asynchrones.



Files d'attente chez Redis


Juin 2017. Le service se développe: 75 000 bots, 7 millions d'abonnés.

Nous mettons en œuvre une autre nouvelle fonctionnalité. Tous les webhooks que nous avons traités concernaient uniquement les communications dans le messager. Mais maintenant, nous avons décidé de donner aux entreprises la possibilité de communiquer avec les abonnés des pages commerciales et avons commencé à traiter de nouveaux types de webhooks - ceux qui se rapportent au flux de la page elle-même.

Les flux de pages d'entreprise ne sont pas rarement mis à jour. Les spécialistes du marketing publient souvent quelque chose, puis ils se suivent et les comptent. Il n'y a pas de trafic énorme sur les pages professionnelles. Mais il y a des situations inverses, par exemple, Katy Perry Day .

Katy Perry est une célèbre chanteuse américaine avec un grand nombre de fans à travers le monde. Il y a 64 millions d'abonnés dans son seul groupe Facebook. À un moment donné, les spécialistes du marketing du chanteur ont décidé de faire un bot sur Facebook Messenger et ont choisi notre plate-forme. À ce moment, lorsqu'ils ont publié un message appelant à s'abonner au bot, notre charge a augmenté de 3 à 4 fois.

Cette situation nous a permis de comprendre que sans l'implémentation normale des files d'attente, nous ne pouvons rien faire. Comme solution, ils ont choisi Redis.
Choisir Redis pour les files d'attente est une très bonne décision.
Il a aidé à résoudre un grand nombre de problèmes. Désormais, chaque seconde via notre cluster Redis transmet 1 million de demandes différentes. Nous l'utilisons non seulement pour toutes les files d'attente en cascade, mais également pour d'autres tâches, par exemple la surveillance.

Les files d'attente sur Redis n'ont pas été implémentées du premier coup. Lorsque nous avons commencé à plier les webhooks dans Redis et à les traiter en un seul processus, nous avons élargi l'entonnoir en haut: il y avait également plus de webhooks entrants traités, mais le processus lui-même prenait encore un certain temps. Cette première décision n'a pas abouti.



Lorsqu'ils ont essayé d'augmenter le nombre de ces demandes, il y a eu un léger effondrement. La file d'attente peut accumuler des demandes de différentes pages, mais les demandes d'une page peuvent aller de suite. Si un gestionnaire est lent, les demandes d'un abonné et d'un bot seront traitées dans le mauvais ordre. L'utilisateur envoie des messages, effectue certaines actions avec le bot, mais reçoit une réponse au hasard.



Cela semble être un cas rare, mais des tests sur nos charges de travail ont montré que cela se produira fréquemment.

Nous avons commencé à chercher une autre solution. Ici, la simplicité et la puissance de Redis sont venues à la rescousse - nous avons décidé de faire une file d'attente pour chaque bot .



Comment ça fonctionne? Les messages relatifs à chaque bot sont ajoutés à la file d'attente. Afin de ne pas augmenter le gestionnaire à chaque file d'attente, nous avons créé une file d'attente de contrôle . Elle travaille comme ça. Chaque fois qu'une demande provient d'un bot, deux messages sont publiés dans Redis: un dans la file d'attente du bot, le second dans le contrôle. Le gestionnaire surveille le contrôle et chaque fois qu'il démarre le démon lorsqu'il y a une tâche pour traiter le bot. Le démon ratisse la file d'attente du bot correspondant.

En plus de la tâche principale, nous avons résolu le problème des «voisins bruyants». C'est à ce moment-là qu'un bot a généré une énorme masse de webhooks et cela ralentit le système, car d'autres pages sont en attente de traitement. Pour résoudre le problème, il suffit d' évoluer : lorsque la file d'attente de contrôle est pleine, nous ajoutons de nouveaux gestionnaires.

De plus, les files d'attente sont virtuelles . Ce ne sont que des cellules de la mémoire Redis. Quand il n'y a rien dans la file d'attente, ça n'existe pas, ça n'occupe rien.

ReactPHP


Janvier 2018 . Nous avons atteint 1 milliard de publications par mois.

La charge était de 5 000 RPS par système. Ce n'est pas une charge de pointe, mais standard. Lorsque des bots de chanteurs célèbres apparaissent, tout croît déjà plusieurs fois à partir de cette figure. Mais ce n'est pas un problème. Le problème est en PHP-FPM: il ne peut plus supporter la charge de 5 000 RPS.

Tout le monde à l'époque parlait de traitement asynchrone à la mode. Nous l'avons regardé de plus près, avons vu ReactPHP, effectué des tests rapides, l'avons remplacé par PHP-FPM et avons instantanément obtenu une augmentation de 4 fois.



Nous n'avons pas réécrit le traitement de notre traitement - ReactPHP a augmenté le cadre Yii. Tout d'abord, nous avons levé 4 services ReactPHP, et plus tard nous en avons atteint 30. Pendant longtemps, nous avons vécu sur eux, et le framework a fait face à la charge.

Dès que nous avons élargi l'entonnoir, un autre effondrement s'est produit: après avoir démarré l'entonnoir à la réception, le traitement a recommencé à souffrir. Pour résoudre ce problème déjà, nous avons décidé de séparer le traitement en clusters.

Clusters


Ils ont pris des bots, les ont répartis en clusters et ont construit des chaînes logiques à partir de Redis, Postgres et d'un gestionnaire.



En conséquence, nous avons formé le concept de «Galaxie» - une abstraction physique logique sur le traitement . Il se compose d'instances: Redis, PostgreSQL et un ensemble de services PHP. Chaque bot appartient à un cluster particulier et ReactPHP sait dans quel cluster le message pour ce bot doit être placé. Le schéma ci-dessus fonctionne plus loin.


L'Univers est en expansion, l'Univers de nos systèmes aussi, et nous ajoutons un nouveau «Galaxy» lorsque cela se produit.
Les galaxies sont notre méthode de mise à l'échelle.

Remplacer ReactPHP par un tas de Nginx et Lua


Au cours des six prochains mois, nous avons poursuivi notre croissance: 200 millions d'abonnés et 3 milliards de messages par mois. Imaginez un site pour 200 millions d'utilisateurs enregistrés - la même charge.

Un nouveau problème est apparu. Les webhooks sont de petites tâches du même type et PHP n'est pas adapté pour les résoudre. Même ReactPHP n'a plus aidé.

  • Il ne pouvait pas supporter la charge de 10 000 RPS - depuis l'introduction de ReactPHP, la charge a augmenté.
  • Il était nécessaire de le redémarrer même avec des déploiements, d'ailleurs, séquentiellement, car vous ne pouvez pas interrompre le traitement des webhooks entrants. Facebook désactive l'application lorsqu'il se rend compte qu'il a des problèmes. Pour ManyChat, c'est un désastre - 650 000 entreprises actives ne nous pardonneront pas.

Par conséquent, nous avons progressivement supprimé une logique différente de ReactPHP, l'avons transmise aux processeurs et isolé de nouvelles files d'attente. Dans le processus, ils ont remarqué que ReactPHP effectue une tâche simple - il prend un webhook et le place dans une file d'attente . Tout le reste se fait par traitements. Existe-t-il des analogues pour une tâche aussi simple?

Nous nous sommes souvenus que Nginx avait des modules et avons remarqué la bibliothèque OpenResty . En plus de prendre en charge le langage de programmation Lua, elle avait un module pour travailler avec Redis. Un test écrit en 3 heures a montré que tout le travail de 30 services sur ReactPHP peut se faire directement du côté nginx.



Voici comment cela s'est avéré: nous traitons une sorte de point de terminaison, récupérons le corps de la demande et l'ajoutons directement à 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 et Lua ont contribué à augmenter le débit. Nous continuons à faire face à notre charge de travail, le service continue, tout le monde est heureux.

Améliorer la solution sur Lua


La dernière étape ( note: au moment du rapport ) est février 2019 . 500 millions d'abonnés envoient et reçoivent 7 millions de messages d'un million de bots chaque mois.

C'est une étape pour améliorer notre solution sur Lua. Supprimez progressivement une partie de la logique des files d'attente et transférez le traitement principal de la distribution des webhooks entre les systèmes à Lua. Maintenant, nos systèmes sont plus productifs et moins dépendants.



Nous maintenons un traitement séparé et un traitement asynchrone . Le traitement concerne les statistiques et d'autres choses - c'est maintenant un système complètement différent.

Le système semble simple, mais il ne l'est pas. Sous le capot, 500 services traitent leurs demandes. L'ensemble du système fonctionne sur 50 instances Amazon: Redis, PostgreSQL et les gestionnaires PHP eux-mêmes.

Evolution du traitement


La charge élevée peut être cool à faire en PHP.

Rappelez-vous brièvement comment nous l'avons fait dans le cadre du développement du système.

  • Commencé avec Nginx normal et PHP-FPM.
  • Ajout de files d'attente à PostgreSQL, puis à Redis.
  • Ajout de clustering.
  • Implémentation de ReactPHP.
  • Nous avons remplacé ReactPHP par un groupe de Nginx et Lua, puis déplacé la logique vers le groupe.



D'après notre expérience, nous avons découvert qu'il est possible de développer et de construire une architecture en changeant successivement les parties vulnérables, en utilisant des approches simples et bien connues et en même temps sans étendre la pile.

, , 11 TeamLead Conf. , LeSS, .

PHP Russia Saint HighLoad++, . PHP , — PHP Russia 13 . highload PHP, Saint HighLoad++ .

Source: https://habr.com/ru/post/undefined/


All Articles