Die Entwicklung der Facebook-Webhook-Verarbeitung: von null auf 25.000 pro Sekunde

Höchstwahrscheinlich muss niemand sagen, was Webhooks sind. Aber nur für den Fall: Webhooks sind ein Mechanismus zum Melden von Ereignissen in einem externen System. Zum Beispiel über den Kauf in einem Online-Shop über eine Online-Kasse, das Senden eines Codes an ein GitHub-Repository oder Benutzeraktionen in Chats. In einer typischen API müssen Sie den Server ständig abfragen, wenn der Benutzer etwas im Chat geschrieben hat. Mithilfe des Webhook-Mechanismus können Sie Benachrichtigungen "abonnieren", und der Server selbst sendet eine HTTP-Anforderung, wenn ein Ereignis auftritt. Dies ist bequemer und schneller als das ständige Anfordern neuer Daten auf dem Server.



ManyChat ist eine Plattform, mit der Unternehmen über Chat in Instant Messenger mit ihren Kunden kommunizieren können. Webhooks sind einer der wichtigen Bestandteile von ManyChat, da das Unternehmen über sie mit den Kunden kommuniziert. Und sie kommunizieren viel - zum Beispiel senden Unternehmen über ein System monatlich Milliarden von Nachrichten an ihre Kunden.

Die meisten Nachrichten werden über Facebook Messenger gesendet. Es hat eine Funktion - eine langsame API. Wenn ein Kunde eine Nachricht schreibt, um Pizza zu bestellen, sendet Facebook einen Webhook an ManyChat. Die Plattform verarbeitet es, sendet die Anfrage zurück und der Benutzer erhält eine Nachricht. Aufgrund der langsamen API dauern einige Anforderungen einige Sekunden. Wenn die Plattform jedoch längere Zeit nicht reagiert, verliert das Unternehmen den Client und Facebook kann die Anwendung von Webhooks trennen.

Daher ist die Verarbeitung von Webhooks eine der wichtigsten technischen Aufgaben der Plattform. Um das Problem zu lösen, hat ManyChat seine Verarbeitungsarchitektur im Laufe von drei Jahren mehrmals von einem einfachen Controller in Yii auf ein verteiltes System mit Galaxien geändert. Lesen Sie mehr darüber unter dem Schnitt Dmitry Kushnikov (Storno)

Dmitry Kushnikov leitet die Entwicklung bei ManyChat und programmiert seit 2001 professionell in PHP. Dmitry wird Ihnen erzählen, wie sich die Architektur zusammen mit dem Wachstum von Service und Last verändert hat, welche Lösungen und Technologien in verschiedenen Phasen angewendet wurden, wie sich die Webhook-Verarbeitung entwickelt hat und wie die Plattform mit bescheidenen Ressourcen in PHP mit einer enormen Last fertig wird.

Hinweis. Der Artikel basiert auf Dmitrys Bericht „Die Entwicklung der Facebook-Webhook-Verarbeitung: von null auf 12.500 pro Sekunde“ in PHP Russia 2019 . Aber während er sich vorbereitete, stiegen die Indikatoren auf 25.000.


Was ist ManyChat?


Zunächst werde ich Sie in den Kontext unserer Aufgaben einführen. ManyChat ist ein Service, mit dem Unternehmen Instant Messenger für Marketing, Vertrieb und Support einsetzen können. Das Hauptprodukt ist die Plattform für Messenger Marketing auf Facebook Messenger . Drei Jahre lang nutzten mehr als 1 Million Unternehmen aus 100 Ländern der Welt den Service, um mit 700 Millionen ihrer Kunden zu kommunizieren.

Auf der Client-Seite sieht es so aus.


Schaltflächen, Bilder und Galerien in den Dialogen im Facebook Messenger.

Dies ist die Facebook Messenger-Oberfläche. Zusätzlich zu Textnachrichten können Sie darin interaktive Elemente senden, um mit Kunden zu interagieren, in einen Dialog zu treten, das Interesse an Ihren Produkten zu steigern und zu verkaufen.

Von der Geschäftsseitealles sieht anders aus. Dies ist die Oberfläche unserer Webanwendung, über die Unternehmensvertreter mithilfe einer visuellen Oberfläche Dialogskripte erstellen und programmieren. Das Bild ist ein Beispiel für ein Szenario.


Das Herzstück unseres Systems ist die Flow Builder-Komponente.

Die Skripte und Automatisierungsregeln nennen wir einen Bot . Zur Vereinfachung können wir daher sagen, dass ManyChat ein Bot-Designer ist.


Ein Beispiel für einen Bot.

Der Client des Unternehmens, das am Dialog teilnimmt, wird als Abonnent bezeichnet , da der Client für die Interaktion den Bot abonniert .

Warum Facebook


Warum Facebook Messenger, wir sind das Land des überlebenden Telegramms? Dafür gibt es Gründe.

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

Facebook


Die Interaktion mit Facebook ist folgendermaßen organisiert:



Business verwendet eine Webanwendung, um die Logik des Bots zu konfigurieren. Wenn ein Kunde über das Telefon mit dem Bot interagiert, erhält Facebook Informationen dazu und sendet uns einen Webhook. ManyChat verarbeitet es abhängig von der vom Unternehmen programmierten Logik und sendet die Anforderung zurück. Dann liefert Facebook die Nachricht an das Telefon des Benutzers.

Technologischer Stapel


Wir machen das alles auf einem bescheidenen Stapel. Im Zentrum steht natürlich PHP. Auf dem Webserver wird Nginx ausgeführt, die Hauptdatenbank ist PostgreSQL, und es gibt auch Redis und Elasticsearch. Alles dreht sich in den Amazon Web Services-Clouds.

Umgang mit Facebook Webhook


So sieht die Webcam von Facebook aus: Dies ist eine Anfrage mit Nutzdaten im JSON-Format.

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

                    ...
                }
            ]  
        }
    ]
}

Webhooks machen nur 10% unserer Last aus, sind aber der wichtigste Teil des Systems. Über sie kommuniziert das Unternehmen mit den Benutzern. Wenn Nachrichten langsamer werden oder nicht gesendet werden, weigert sich der Benutzer, mit dem Bot zu interagieren, und das Unternehmen verliert den Client.

Werfen wir einen Blick auf die Entwicklung unserer Architektur seit der Einführung des Produkts.

Mai 2016 . Wir haben gerade unseren Service gestartet: 20 Bots, von denen 10 Testbots sind, und 20 Abonnenten. Die Last betrug 0 RPS.

Das Interaktionsschema sah folgendermaßen aus:



  • Die Anfrage geht an nginx.
  • Nginx greift auf PHP-FPM zu.
  • PHP-FPM bringt die Anwendung auf Yii.
  • Der Webhook-Controller verarbeitet die Logik und sendet entsprechend Anfragen an Facebook.


Eine Menge Nginx und PHP-FPM


Juni 2016. Einen Monat später kündigten wir ManyChat auf ProductHunt an und die Anzahl der Bots stieg auf 2.000. Die Anzahl der Abonnenten ist auf 7 Tausend gestiegen.

In diesem Moment trat das erste Problem im System auf. Die Facebook-API ist nicht sehr schnell: Einige Anfragen können mehrere Sekunden dauern, und mehrere Anfragen können mehrere zehn Sekunden dauern. Der Webhook-Server möchte jedoch, dass wir schnell reagieren. Aufgrund der langsamen API reagieren wir nicht lange: Der Server schwört zuerst und kann dann die Anwendung vollständig von Webhooks trennen.

Es gibt nur wenige Benutzer, wir entwickeln die Anwendung noch, wir suchen unseren Markt, unser Publikum und das Ladeproblem ist bereits aufgetreten. Wir wurden jedoch durch eine einfache Lösung gerettet: In dem Moment, in dem der Controller startet, unterbrechen wir den Zugriff auf Facebook. Wir sagen Facebook, dass alles in Ordnung ist, aber im Hintergrund bearbeiten wir Anfragen und Webhook.



Warteschlangen auf PostgreSQL


Dezember 2016. Der Service wuchs um das 5- bis 10-fache: 10.000 Bots und 700.000 Abonnenten.

Gleichzeitig haben wir an neuen Aufgaben gearbeitet: Anzeigen von Statistiken, Nachrichtenübermittlung, Konvertieren von Impressionen und Übergängen. Auch Live Chat implementiert. Neben der Automatisierung von Interaktionen können Unternehmen Nachrichten direkt an ihren Abonnenten schreiben.

Die Lösung dieser Probleme erhöhte die Anzahl der Kettenhaken um das Vierfache. Für jede gesendete Nachricht haben wir 3 zusätzliche Webhooks erhalten. Das Verarbeitungssystem musste erneut verbessert werden. Wir sind eine kleine Plattform, nur zwei Personen haben am Backend gearbeitet, daher haben wir die einfachste Lösung gewählt - Warteschlangen unter PostgreSQL.

Wir wollen noch keine komplexen Systeme implementieren, also teilen wir einfach die Verarbeitungsabläufe. Webhooks, die schnell verarbeitet werden müssen, damit der Benutzer eine Antwort erhält, werden synchron verarbeitet. Der Rest wird in Warteschlangen für asynchrone Anforderungen gesendet.



Warteschlangen bei Redis


Juni 2017. Der Service wächst: 75.000 Bots, 7 Millionen Abonnenten.

Wir implementieren eine weitere neue Funktion. Alle von uns verarbeiteten Webhooks betrafen nur die Kommunikation im Messenger. Jetzt haben wir uns entschlossen, Unternehmen die Möglichkeit zu geben, mit Abonnenten von Unternehmensseiten zu kommunizieren, und neue Arten von Webhooks zu verarbeiten - solche, die sich auf den Feed der Seite selbst beziehen.

Geschäftsseiten-Feeds werden nicht selten aktualisiert. Vermarkter posten oft etwas, dann folgen sie jedem wie und zählen sie. Es gibt keinen großen Verkehr auf Geschäftsseiten. Aber es gibt umgekehrte Situationen, zum Beispiel Katy Perry Day .

Katy Perry ist eine berühmte amerikanische Sängerin mit einer großen Anzahl von Fans auf der ganzen Welt. Allein in ihrer Facebook-Gruppe gibt es 64 Millionen Abonnenten. Irgendwann entschieden sich die Vermarkter des Sängers, einen Bot auf Facebook Messenger zu erstellen und entschieden sich für unsere Plattform. In diesem Moment, als sie eine Nachricht veröffentlichten, in der sie aufgefordert wurden, den Bot zu abonnieren, stieg unsere Last um das 3-4-fache.

Diese Situation hat uns zu dem Verständnis verholfen, dass wir ohne die normale Implementierung der Warteschlangen nichts tun können. Als Lösung wählten sie Redis.
Die Wahl von Redis für Warteschlangen ist eine fantastisch gute Entscheidung.
Er half bei der Lösung einer Vielzahl von Problemen. Jetzt leitet jede Sekunde durch unseren Redis-Cluster 1 Million verschiedene Anfragen. Wir verwenden es nicht nur für alle kaskadierenden Warteschlangen, sondern auch für andere Aufgaben, zum Beispiel die Überwachung.

Warteschlangen auf Redis wurden beim ersten Versuch nicht implementiert. Als wir anfingen, Webhooks in Redis zu falten und in einem Prozess zu verarbeiten, haben wir den Trichter oben erweitert: Es wurden auch mehr eingehende Webhooks verarbeitet, aber der Prozess selbst dauerte noch einige Zeit. Diese erste Entscheidung war erfolglos.



Als sie versuchten, die Anzahl dieser Anfragen zu skalieren, gab es einen leichten Zusammenbruch. Die Warteschlange kann Anforderungen von verschiedenen Seiten sammeln, aber Anforderungen von einer Seite können hintereinander gestellt werden. Wenn ein Handler langsam ist, werden Anforderungen von einem Abonnenten und von einem Bot in der falschen Reihenfolge verarbeitet. Der Benutzer sendet Nachrichten, führt einige Aktionen mit dem Bot aus, erhält jedoch zufällig eine Antwort.



Dies scheint ein seltener Fall zu sein, aber Tests unserer Workloads haben gezeigt, dass dies häufig vorkommen wird.

Wir suchten nach einer anderen Lösung. Hier kam die Einfachheit und Kraft von Redis zur Rettung - wir beschlossen, für jeden Bot eine Warteschlange zu erstellen .



Wie es funktioniert? Nachrichten, die sich auf jeden Bot beziehen, werden der Warteschlange hinzugefügt. Um den Handler nicht für jede Warteschlange zu erhöhen, haben wir eine Steuerwarteschlange erstellt . Sie arbeitet so. Jedes Mal, wenn eine Anfrage von einem Bot kommt, werden zwei Nachrichten in Redis veröffentlicht: eine in der Warteschlange des Bots, die zweite in der Steuerung. Der Handler überwacht das Steuerelement und jedes Mal, wenn er den Dämon startet, wenn eine Aufgabe zur Verarbeitung des Bots vorliegt. Der Dämon stellt die Warteschlange des entsprechenden Bots auf.

Neben der Hauptaufgabe haben wir das Problem der „lauten Nachbarn“ gelöst. In diesem Fall hat ein Bot eine große Menge von Webhooks generiert und das System verlangsamt, da andere Seiten auf die Verarbeitung warten. Um das Problem zu lösen, reicht eine Skalierung aus : Wenn die Steuerwarteschlange voll ist, fügen wir neue Handler hinzu.

Darüber hinaus sind die Warteschlangen virtuell . Dies sind nur Zellen im Redis-Speicher. Wenn sich nichts in der Warteschlange befindet, existiert es nicht, es belegt nichts.

ReactPHP


Januar 2018 . Wir haben 1 Milliarde Beiträge pro Monat erreicht.

Die Last betrug 5.000 RPS pro System. Dies ist keine Spitzenlast, sondern Standard. Wenn Bots berühmter Sänger auftauchen, wächst alles schon mehrmals aus dieser Figur. Aber das ist kein Problem. Das Problem liegt in PHP-FPM: Es kann der Last von 5.000 RPS nicht mehr standhalten.

Alle sprachen damals von modischer asynchroner Verarbeitung. Wir haben es uns genauer angesehen, ReactPHP gesehen, Schnelltests durchgeführt, es durch PHP-FPM ersetzt und sofort eine 4-fache Steigerung erzielt.



Wir haben die Verarbeitung unserer Verarbeitung nicht umgeschrieben - ReactPHP hat das Yii-Framework erhöht. Zuerst haben wir 4 ReactPHP-Dienste aufgerufen, und später haben wir 30 erreicht. Wir haben lange davon gelebt und das Framework hat die Last bewältigt.

Sobald wir den Trichter erweitert hatten, kam es zu einem weiteren Zusammenbruch: Nach dem Starten des Trichters an der Rezeption begann die Verarbeitung erneut zu leiden. Um dieses Problem bereits zu lösen, haben wir uns entschlossen, die Verarbeitung in Cluster aufzuteilen.

Cluster


Sie nahmen Bots, verteilten sie in Cluster und bauten logische Ketten von Redis, Postgres und einem Handler.



Als Ergebnis haben wir das Konzept der „Galaxie“ entwickelt - eine logische physische Abstraktion über die Verarbeitung . Es besteht aus Instanzen: Redis, PostgreSQL und einer Reihe von PHP-Diensten. Jeder Bot gehört zu einem bestimmten Cluster, und ReactPHP weiß, in welchem ​​Cluster die Nachricht für diesen Bot platziert werden muss. Das obige Schema funktioniert weiter.


Das Universum erweitert sich, das Universum unserer Systeme auch, und wir fügen in diesem Fall eine neue „Galaxie“ hinzu.
Galaxien sind unsere Art zu skalieren.

Ersetzen von ReactPHP durch eine Reihe von Nginx und Lua


In den nächsten sechs Monaten sind wir weiter gewachsen: 200 Millionen Abonnenten und 3 Milliarden Nachrichten pro Monat. Stellen Sie sich eine Site für 200 Millionen registrierte Benutzer vor - dieselbe Last.

Ein neues Problem ist aufgetreten. Webhooks sind kleine Aufgaben des gleichen Typs, und PHP eignet sich nicht zum Lösen dieser Aufgaben. Sogar ReactPHP hat nicht mehr geholfen.

  • Er konnte die Last von 10 000 RPS nicht bewältigen - seit der Einführung von ReactPHP hat die Last zugenommen.
  • Darüber hinaus musste es auch bei Bereitstellungen nacheinander neu gestartet werden, da Sie die Verarbeitung eingehender Webhooks nicht unterbrechen können. Facebook deaktiviert die Anwendung, wenn es feststellt, dass es Probleme gibt. Für ManyChat ist dies eine Katastrophe - 650.000 aktiv tätige Unternehmen werden uns nicht vergeben.

Daher haben wir nach und nach andere Logik als ReactPHP abgebissen, an die Prozessoren übergeben und neue Warteschlangen isoliert. Dabei stellten sie fest, dass ReactPHP eine einfache Aufgabe ausführt: Es nimmt einen Webhook und stellt ihn in eine Warteschlange . Der Rest wird durch Verarbeitungen erledigt. Gibt es Analoga für eine so einfache Aufgabe?

Wir haben uns daran erinnert, dass Nginx Module hat und die OpenResty- Bibliothek bemerkt . Neben der Unterstützung der Programmiersprache Lua verfügte sie über ein Modul für die Arbeit mit Redis. Ein in 3 Stunden geschriebener Test zeigte, dass die gesamte Arbeit von 30 Diensten auf ReactPHP direkt auf der Nginx-Seite ausgeführt werden kann.



So stellte sich heraus: Wir verarbeiten eine Art Endpunkt, nehmen den Anforderungshauptteil auf und fügen ihn direkt zu Redis hinzu.

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 und Lua haben zur Steigerung des Durchsatzes beigetragen. Wir bewältigen weiterhin unsere Arbeitsbelastung, der Service lebt weiter, alle sind glücklich.

Verbesserung der Lösung auf Lua


Die letzte Phase ( Anmerkung: zum Zeitpunkt des Berichts ) ist der Februar 2019 . 500 Millionen Abonnenten senden und empfangen jeden Monat 7 Millionen Nachrichten von einer Million Bots.

Dies ist ein Schritt zur Verbesserung unserer Lösung für Lua. Biss nach und nach etwas Logik aus den Warteschlangen ab und übertrage die primäre Verarbeitung der Verteilung von Webhooks zwischen Systemen an Lua. Jetzt sind unsere Systeme produktiver und weniger abhängig.



Wir pflegen eine getrennte Verarbeitung und eine asynchrone Verarbeitung . Die Verarbeitung betrifft Statistiken und andere Dinge - jetzt ist es ein völlig anderes System.

Das System scheint einfach, ist es aber nicht. Unter der Haube gibt es 500 Dienste, die ihre Anfragen bearbeiten. Das gesamte System läuft auf 50 Amazon-Instanzen: Redis, PostgreSQL und die PHP-Handler selbst.

Evolution verarbeiten


Highload kann in PHP cool sein.

Erinnern Sie sich kurz daran, wie wir dies bei der Entwicklung des Systems getan haben.

  • Begonnen mit normalem Nginx und PHP-FPM.
  • Warteschlangen zu PostgreSQL und dann zu Redis hinzugefügt.
  • Clustering hinzugefügt.
  • ReactPHP implementiert.
  • Wir haben ReactPHP durch eine Reihe von Nginx und Lua ersetzt und später die Logik in die Reihe verschoben.



Aus eigener Erfahrung haben wir herausgefunden, dass es möglich ist, Architektur zu erweitern und aufzubauen, indem anfällige Teile nacheinander geändert werden, einfache bekannte Ansätze verwendet werden und der Stapel nicht erweitert wird.

, , 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