Centrifugo v2 - die Zukunft des Server-Echtzeitnachrichtensystems und eine Bibliothek für Go

Published on July 12, 2018

Centrifugo v2 - die Zukunft des Server-Echtzeitnachrichtensystems und eine Bibliothek für Go

    Vielleicht haben einige Leser bereits von Centrifugo gehört . In diesem Artikel wird die Entwicklung der zweiten Version des Servers und der neuen Echtzeitbibliothek für die Sprache Go beschrieben, die ihre Grundlage bildet.


    Ich heiße Alexander Emelin. Im letzten Sommer bin ich dem Avito-Team beigetreten, wo ich jetzt an der Entwicklung des Avito Messenger-Backends mitarbeite. Neue Arbeiten, die sich direkt auf die schnelle Übermittlung von Nachrichten an Benutzer bezogen, und neue Kollegen haben mich dazu inspiriert, weiter am Open-Source-Projekt Centrifugo zu arbeiten.



    Kurz gesagt, dies ist ein Server, der die Aufgabe übernimmt, persistente Verbindungen von Benutzern Ihrer Anwendung aufrechtzuerhalten. Websocket oder SockJS Polyfill wird als Transport verwendet , der, wenn es nicht möglich ist, eine Websocket-Verbindung herzustellen, Eventource, XHR-Streaming, Long-Polling und andere HTTP-basierte Transporte verwenden kann. Kunden abonnieren Kanäle, in denen das Backend über die Centrifuge-API neue Nachrichten veröffentlicht, sobald sie entstehen. Danach werden Nachrichten an Benutzer gesendet, die den Kanal abonniert haben. Mit anderen Worten - dies ist ein PUB / SUB-Server.



    Derzeit wird der Server in einer relativ großen Anzahl von Projekten verwendet. Darunter befinden sich zum Beispiel einige Mail.Ru-Projekte (Intranets, Technopark / Technosphere-Trainingsplattformen, Zertifizierungscenter usw.). Mit der Hilfe von Centrifugo gibt es ein schönes Dashboard an der Rezeption im Badoo-Büro in Moskau und 350.000 Benutzer sind am spot.im-Dienst angeschlossen zur Zentrifuge.


    Einige Verweise auf frühere Artikel auf dem Server und seiner Anwendung für diejenigen, die zuerst über das Projekt erfahren:



    Ich habe im Dezember letzten Jahres angefangen, an der zweiten Version zu arbeiten, und fahre bis heute fort. Mal sehen was passiert. Ich schreibe diesen Artikel nicht nur, um das Projekt irgendwie populär zu machen, sondern auch, um vor der Veröffentlichung von Centrifugo v2 etwas konstruktiveres Feedback zu erhalten - jetzt gibt es Spielraum für Manöver und rückwärts inkompatible Änderungen.


    Echtzeitbibliothek für Go


    In der Go-Community stellt sich von Zeit zu Zeit die Frage - gibt es Alternativen zu socket.io on Go? Manchmal fiel mir auf, dass den Entwicklern empfohlen wird, in Richtung Centrifugo zu schauen. Centrifugo ist jedoch ein selbstgehosteter Server, keine Bibliothek - der Vergleich ist nicht fair. Ich wurde auch mehrmals gefragt, ob es möglich sei, Centrifugo-Code wiederzuverwenden, um Echtzeitanwendungen in der Sprache Go zu erstellen. Und die Antwort war: theoretisch möglich, aber auf eigenes Risiko und Risiko - ich konnte die Abwärtskompatibilität der internen Paket-API nicht garantieren. Es ist klar, dass es keinen besonderen Grund gibt, jemanden zu riskieren, und Gabelungen sind auch eine Option für sich. Außerdem würde ich nicht sagen, dass die API für interne Pakete für diese Verwendung überhaupt vorbereitet wurde.


    Daher ist es eine der ehrgeizigen Aufgaben, die ich im Zuge der Arbeit an der zweiten Version des Servers lösen wollte, den Server-Core in einer separaten Bibliothek auf Go zuzuordnen. Ich halte es für sinnvoll, wenn man bedenkt, wie viele Funktionen die Zentrifuge aufweist, um an die Produktion angepasst zu werden. Es sind viele Features im Lieferumfang enthalten, die dazu beitragen, skalierbare Echtzeitanwendungen zu erstellen, sodass der Entwickler keine eigene Lösung schreiben muss. Ich habe früher über diese Funktionen geschrieben und werde im Folgenden einige davon erläutern.


    Ich werde versuchen, ein weiteres Plus der Existenz einer solchen Bibliothek zu rechtfertigen. Die meisten Benutzer von Centrifugo sind Entwickler, die Backends in Sprachen / Frameworks mit schlechter Parallelität schreiben (z. B. Django / Flask / Laravel / ...): Sie arbeiten mit einer großen Anzahl persistenter Verbindungen, wenn dies möglich ist, dann auf eine nicht offensichtliche oder ineffiziente Weise. Dementsprechend können, um bei der Entwicklung des Servers zu helfen, in Go geschrieben werden, nicht alle Benutzer (aufgrund der mangelnden Sprachkenntnisse). Daher kann selbst eine sehr kleine Community von Go-Entwicklern rund um die Bibliothek bei der Entwicklung des Centrifugo-Servers mithelfen.


    Das Ergebnis war die Zentrifugenbibliothek . Dies ist immer noch WIP, aber absolut sind alle in der Github-Beschreibung genannten Funktionen implementiert und funktionieren. Da die Bibliothek vor der Gewährleistung der Abwärtskompatibilität eine relativ umfangreiche API bietet, würde ich gerne einige erfolgreiche Beispiele für den Einsatz in echten Go-Projekten erfahren. Das gibt es nicht. Ebenso erfolglos :). Es gibt keine.


    Ich verstehe, dass ich, wenn ich eine Bibliothek fast wie einen Server benenne, immer mit Verwirrung umgehen muss. Aber ich denke, dass dies die richtige Wahl ist, da Kunden (wie Zentrifuge-js, Centrifuge-Go) sowohl mit der Centrifugo-Bibliothek als auch mit dem Centrifugo-Server arbeiten. Außerdem ist der Name in den Köpfen der Benutzer bereits fest verankert, und ich möchte diese Assoziationen nicht verlieren. Und für etwas mehr Klarheit möchte ich noch einmal klarstellen:


    • Zentrifuge - die Go-Sprachbibliothek,
    • Centrifugo ist eine schlüsselfertige Lösung, ein separater Dienst, der in Version 2 auf der Centrifuge-Bibliothek aufgebaut wird.

    Aufgrund des Designs (eines separaten Dienstes, der nichts über Ihr Backend weiß) geht Centrifugo davon aus, dass der Nachrichtenfluss über Echtzeittransport vom Server zum Client geleitet wird. Was meinst du Wenn ein Benutzer beispielsweise eine Nachricht in einen Chat schreibt, müssen Sie diese Nachricht zunächst an das Backend der Anwendung senden (z. B. AJAX im Browser), diese auf der Backend-Seite überprüfen, ggf. in der Datenbank speichern und dann an die Centrifuge-API senden. Die Bibliothek beseitigt diese Einschränkung, sodass Sie den bidirektionalen Austausch asynchroner Nachrichten zwischen Server und Client sowie RPC-Aufrufe organisieren können.



    Schauen wir uns ein einfaches Beispiel an: Wir implementieren einen kleinen Server unter Verwendung der Centrifuge-Bibliothek. Der Server empfängt Nachrichten von Browser-Clients über Websocket. Der Client verfügt über ein Textfeld, in das Sie eine Nachricht eingeben können. Drücken Sie die Eingabetaste. Die Nachricht wird an alle Benutzer gesendet, die den Channel abonniert haben. Dies ist die am meisten vereinfachte Version des Chats. Es schien mir, dass es am bequemsten wäre, es in Form eines Kerns zu platzieren .


    Sie können wie gewohnt laufen:


    git clone https://gist.github.com/2f1a38ae2dcb21e2c5937328253c29bf.git
    cd 2f1a38ae2dcb21e2c5937328253c29bf
    go get -u github.com/centrifugal/centrifuge
    go run main.go

    Gehen Sie dann zu http: // localhost: 8000 und öffnen Sie mehrere Browser-Registerkarten.


    Wie Sie sehen, tritt der Einstiegspunkt in die Geschäftslogik der Anwendung auf, wenn die On().Connect()Rückruffunktion eingestellt ist:


    node.On().Connect(func(ctx context.Context, client *centrifuge.Client, e centrifuge.ConnectEvent) centrifuge.ConnectReply {
        client.On().Disconnect(func(e centrifuge.DisconnectEvent) centrifuge.DisconnectReply {
            log.Printf("client disconnected")
            return centrifuge.DisconnectReply{}
        })
        log.Printf("client connected via %s", client.Transport().Name())
        return centrifuge.ConnectReply{}
    })

    Der auf Callback-Funktionen basierende Ansatz schien mir am besten für die Interaktion mit der Bibliothek zu sein. Ein ähnlicher, nur schwach typisierter Ansatz wird bei der Implementierung des socket-io-Servers auf Go verwendet . Wenn Sie plötzlich Gedanken darüber haben, wie die API idiomatischer gestaltet werden könnte, würde ich mich freuen zu hören.


    Dies ist ein sehr einfaches Beispiel, das nicht alle Funktionen einer Bibliothek demonstriert. Einige stellen möglicherweise fest, dass es für solche Zwecke einfacher ist, eine Bibliothek für die Arbeit mit Websocket zu verwenden. Zum Beispiel Gorilla Websocket. Das ist eigentlich der Fall. In diesem Fall müssen Sie jedoch einen anständigen Servercode aus dem Beispiel im Gorilla Websocket-Repository kopieren. Was wäre wenn:


    • Sie müssen die Anwendung auf mehrere Maschinen skalieren.
    • oder Sie benötigen mehr als einen gemeinsamen Kanal, aber mehrere - und Benutzer können sie dynamisch abonnieren und abbestellen, während Sie durch Ihre Anwendung navigieren.
    • oder Sie müssen arbeiten, wenn eine Websocket-Verbindung nicht hergestellt werden konnte (der Browser des Clients wird nicht unterstützt, eine Browsererweiterung wird installiert, ein Proxy befindet sich im Pfad zwischen dem Client und dem Server, der die Verbindung unterbricht).
    • oder Sie müssen Nachrichten wiederherstellen, die der Client in kurzen Unterbrechungen der Internetverbindung verpasst hat, ohne die Hauptdatenbank zu laden.
    • oder Sie müssen die Berechtigung des Benutzers im Kanal steuern,
    • oder Sie müssen eine permanente Verbindung von Benutzern deaktivieren, die in der Anwendung deaktiviert wurden.
    • oder Informationen darüber benötigen, wer gerade in dem Kanal anwesend ist, oder Ereignisse, die jemand von dem Kanal abonniert / abbestellt hat,
    • oder benötigen Sie Metriken und Monitoring?

    Die Zentrifugenbibliothek kann Ihnen dabei helfen - in der Tat hat sie alle wichtigen Funktionen übernommen, die bisher in Centrifugo verfügbar waren. Weitere Beispiele, die die oben genannten Punkte zeigen, sind auf Github zu finden .


    Das starke Erbe von Centrifugo kann nachteilig sein, da die Bibliothek alle Servermechaniken übernommen hat, was sehr originell ist und möglicherweise jemandem unklar erscheint oder mit überflüssigen Funktionen überlastet ist. Ich habe versucht, den Code so zu organisieren, dass nicht verwendete Funktionen die Gesamtleistung nicht beeinträchtigen.


    Es gibt einige Optimierungen in der Bibliothek, die eine effizientere Ressourcennutzung ermöglichen. Auf diese Weise werden mehrere Nachrichten in einem Websocket-Frame zusammengefasst, um Systemaufrufe bei Write zu speichern oder beispielsweise Gogoprotobuf für die Serialisierung von Protobuf-Nachrichten und andere zu verwenden. Apropos Protobuf.


    Binäres Protobuf-Protokoll


    Ich wollte unbedingt, dass Centrifugo mit binären Daten arbeitet ( und nicht nur mit mir ). Daher wollte ich in der neuen Version neben dem auf JSON basierenden Protokoll ein binäres Protokoll hinzufügen. Nun wird das gesamte Protokoll in Form eines Protobuf-Schemas beschrieben . Dies erlaubte es, es strukturierter zu gestalten und einige nicht naheliegende Lösungen im Protokoll der ersten Version zu überdenken.


    Ich denke, dass Sie nicht lange sagen müssen, welche Vorteile Protobuf gegenüber JSON bietet - Kompaktheit, Serialisierungsgeschwindigkeit und Schaltungsstärke. Es gibt auch keine Unlesbarkeit, aber jetzt haben Benutzer die Möglichkeit zu entscheiden, was für sie in einer bestimmten Situation wichtiger ist.


    Im Allgemeinen sollte der durch das Centrifugo-Protokoll generierte Verkehr bei Verwendung von Protobuf anstelle von JSON um das ~ 2-fache abnehmen (ohne Anwendungsdaten). Gleichzeitig sank der CPU-Verbrauch in meinen synthetischen Lasttests im Vergleich zu JSON. Tatsächlich sagen diese Zahlen wenig aus, in der Praxis hängt alles vom Lastprofil einer bestimmten Anwendung ab.


    Um Interesse zu wecken, habe ich auf einem Rechner mit Debian 9.4 und 32 Intel® Xeon® Platinum 8168 CPU bei 2.70 GHz einen vCPU-Benchmark eingeführt, mit dem der Durchsatz der Client-Server-Interaktion bei Verwendung des JSON-Protokolls und des Protobuf-Protokolls verglichen werden konnte. Es gab 1000 Abonnenten für einen Kanal. Nachrichten wurden in 4 Streams an diesen Kanal gesendet und an alle Abonnenten gesendet. Die Größe jeder Nachricht betrug 128 Byte.


    Ergebnisse für JSON:


    $ go run main.go -s ws://localhost:8000/connection/websocket -n 1000 -ns 1000 -np 4 channel
    Starting benchmark [msgs=1000, msgsize=128, pubs=4, subs=1000]
    Centrifuge Pub/Sub stats: 265,900 msgs/sec ~ 32.46 MB/sec
     Pub stats: 278 msgs/sec ~ 34.85 KB/sec
      [1] 73 msgs/sec ~ 9.22 KB/sec (250 msgs)
      [2] 71 msgs/sec ~ 9.00 KB/sec (250 msgs)
      [3] 71 msgs/sec ~ 8.90 KB/sec (250 msgs)
      [4] 69 msgs/sec ~ 8.71 KB/sec (250 msgs)
      min 69 | avg 71 | max 73 | stddev 1 msgs
     Sub stats: 265,635 msgs/sec ~ 32.43 MB/sec
      [1] 273 msgs/sec ~ 34.16 KB/sec (1000 msgs)
      ...
      [1000] 277 msgs/sec ~ 34.67 KB/sec (1000 msgs)
      min 265 | avg 275 | max 278 | stddev 2 msgs

    Ergebnisse für den Protobuf-Fall:


    $ go run main.go -s ws://localhost:8000/connection/websocket?format=protobuf -n 100000 -ns 1000 -np 4 channel
    Starting benchmark [msgs=100000, msgsize=128, pubs=4, subs=1000]
    Centrifuge Pub/Sub stats: 681,212 msgs/sec ~ 83.16 MB/sec
     Pub stats: 685 msgs/sec ~ 85.69 KB/sec
      [1] 172 msgs/sec ~ 21.57 KB/sec (25000 msgs)
      [2] 171 msgs/sec ~ 21.47 KB/sec (25000 msgs)
      [3] 171 msgs/sec ~ 21.42 KB/sec (25000 msgs)
      [4] 171 msgs/sec ~ 21.42 KB/sec (25000 msgs)
      min 171 | avg 171 | max 172 | stddev 0 msgs
     Sub stats: 680,531 msgs/sec ~ 83.07 MB/sec
      [1] 681 msgs/sec ~ 85.14 KB/sec (100000 msgs)
      ...
      [1000] 681 msgs/sec ~ 85.13 KB/sec (100000 msgs)
      min 680 | avg 680 | max 685 | stddev 1 msgs
    

    Möglicherweise stellen Sie fest, dass die Bandbreite einer solchen Installation bei Protobuf mehr als doppelt so groß ist. Das Client-Skript finden Sie hier - dies ist das Nats-Benchmark-Skript, das an die Realität der Zentrifuge angepasst ist .


    Es ist auch erwähnenswert, dass die JSON-Serialisierungsleistung auf dem Server mit demselben Ansatz wie in gogoprotobuf - dem Pufferpool und der Codegenerierung - „herausgepumpt“ werden kann. In diesem Moment wird JSON durch das Paket aus der standardmäßigen Go-Library serialisiert, die auf reflect basiert. In Centrifugo wird die erste JSON-Version beispielsweise manuell mit einer Bibliothek serialisiert , die einen Pufferpool bereitstellt . Ähnliches kann in der zweiten Version in Zukunft gemacht werden.


    Es sollte hervorgehoben werden, dass protobuf auch verwendet werden kann, wenn mit dem Server über einen Browser kommuniziert wird. Der Javascript-Client verwendet dazu die Bibliothek protobuf.js. Da die protobufjs-Bibliothek ziemlich umfangreich ist und die Anzahl der Benutzer des binären Formats gering ist, generieren wir mithilfe des Webpacks und des Tree-Shake-Algorithmus zwei Versionen des Clients - eine mit JSON-Protokollunterstützung und die andere mit Unterstützung für JSON und Protobuf. In anderen Umgebungen, in denen die Größe der Ressourcen keine so wichtige Rolle spielt, machen sich die Kunden möglicherweise keine Gedanken um diese Trennung.


    JSON-Web Token (JWT)


    Ein Problem bei der Verwendung eines Standalone-Servers wie Centrifugo besteht darin, dass er nichts über Ihre Benutzer und deren Authentifizierungsmethode weiß und welchen Sitzungsmechanismus Ihr Backend verwendet. Und Sie müssen Verbindungen irgendwie authentifizieren.


    In der Centrifuge wurde hierfür die SHA-256-HMAC-Signatur verwendet, die auf einem geheimen Schlüssel basiert, der nur dem Backend und der Centrifuge bekannt ist. Dadurch wurde sichergestellt, dass die vom Client übermittelte Benutzer-ID wirklich zu ihm gehört.


    Vielleicht war die korrekte Übertragung von Verbindungsparametern und die Erzeugung von Token eine der Hauptschwierigkeiten bei der Integration von Centrifugo in das Projekt.


    Als die Zentrifuge erschien, war der JWT-Standard noch nicht so beliebt. Nun, einige Jahre später, gibt es Bibliotheken zur Generierung von JWT für die gängigsten Sprachen . Die Hauptidee von JWT ist genau das, was die Zentrifuge benötigt: Bestätigung der Authentizität der übertragenen Daten. In der zweiten Version von HMAC wurde die manuell generierte Signatur durch die Verwendung von JWT ersetzt. Dadurch war es nicht mehr erforderlich, Hilfsfunktionen für die korrekte Erzeugung eines Token in Bibliotheken für verschiedene Sprachen zu unterstützen.


    In Python kann das Token zum Verbinden mit Centrifugo beispielsweise folgendermaßen generiert werden:


    import jwt
    import time
    token = jwt.encode({"user": "42", "exp": int(time.time()) + 10*60}, "secret").decode()
    print(token)

    Es ist wichtig zu wissen, dass Sie den Benutzer bei Verwendung der Centrifuge-Bibliothek auf eine für Go - native Art und Weise - innerhalb der Middleware - authentifizieren können. Beispiele sind im Repository.


    GRPC


    Während des Entwicklungsprozesses habe ich GRPC-bidirektionales Streaming als Transport für die Kommunikation zwischen Client und Server (zusätzlich zu Websocket und HTTP-basierten SockJS-Foldbacks) ausprobiert. Was kann ich sagen Er hat gearbeitet. Ich habe jedoch kein einziges Szenario gefunden, in dem das bidirektionale GRPC-Streaming besser wäre als Websocket. Ich habe mir hauptsächlich die Servermetriken angesehen: den erzeugten Verkehr über die Netzwerkschnittstelle, den CPU-Verbrauch des Servers bei einer großen Anzahl eingehender Verbindungen, den Speicherverbrauch pro Verbindung.


    GRPC abgetretener Websocket in jeder Hinsicht:


    • In ähnlichen Szenarien generiert GRPC 20% mehr Verkehr.
    • GRPC benötigt 2-3 mal mehr CPU (je nach Konfiguration der Verbindungen - alle sind auf verschiedenen Kanälen abonniert oder alle auf einem Kanal.)
    • GRPC benötigt viermal mehr RAM pro Verbindung. Bei 10k-Verbindungen verfügt der Websocket-Server beispielsweise über 500 MB Speicher und GRPC-2 GB.

    Die Ergebnisse waren genug ... erwartet. Im Allgemeinen hatte GRPC als Client-Transport keinen Sinn - und löschte den Code mit gutem Gewissen bis vielleicht zu besseren Zeiten.


    GRPC ist jedoch gut für das, wofür es hauptsächlich entwickelt wurde - um Code zu generieren, mit dem RPC-Aufrufe zwischen Diensten nach einem vorgegebenen Muster durchgeführt werden können. Daher wird neben der HTTP-API in der Centrifuge jetzt auch die GRPC-basierte API unterstützt, z. B. für die Veröffentlichung neuer Nachrichten an den Kanal und andere verfügbare Server-API-Methoden.


    Kundenprobleme


    Durch die in der zweiten Version vorgenommenen Änderungen wurde die obligatorische Bibliotheksunterstützung für die Server-API entfernt. Die Integration auf der Serverseite wurde einfacher, jedoch hat sich das Clientprotokoll im Projekt geändert und verfügt über eine ausreichende Anzahl von Funktionen. Dies macht die Implementierung von Clients ziemlich schwierig. Für die zweite Version haben wir nun einen Client für Javascript , der in Browsern funktioniert, mit NodeJS und React-Native arbeiten sollte. Es gibt einen Kunden auf Go und baut auf seiner Basis und auf der Grundlage des Gomobile-Projekts Bindungen für iOS und Android auf .


    Für vollkommenes Glück gibt es nicht genug native Bibliotheken für iOS und Android. Für die erste Version von Centrifugo kamen sie aus der Open-Source-Community. Ich möchte glauben, dass so etwas jetzt passieren wird.


    Ich habe vor kurzem mein Glück versucht, indem ich einen Antrag auf MOSS-Förderung von Mozilla gestellt hatte , um Geld in die Entwicklung von Kunden zu investieren, was aber abgelehnt wurde. Der Grund ist nicht genug aktive Community auf Github. Leider stimmt das, aber ich sehe einige Schritte, um die Situation zu verbessern.



    Fazit


    Ich habe nicht alle Funktionen zum Ausdruck gebracht, die in Centrifugo v2 erscheinen werden. Weitere Informationen finden Sie in der Ausgabe von Github . Die Serverfreigabe hat noch nicht stattgefunden, wird aber bald geschehen. Es gibt noch nicht abgeschlossene Momente, einschließlich der Notwendigkeit, Dokumentation hinzuzufügen. Prototypendokumentation kann unter dem Link eingesehen werden . Wenn Sie ein Centrifugo-Benutzer sind, ist jetzt der richtige Zeitpunkt, um die zweite Version des Servers zu beeinflussen. Eine Zeit, in der es nicht so gruselig ist, etwas zu zerbrechen, um es später besser zu machen. Für Interessierte: Die Entwicklung konzentriert sich in Zweig c2 .


    Es fällt mir schwer zu beurteilen, wie sehr die Zentrifugenbibliothek, die im Herzen von Centrifugo v2 liegt, gefragt ist. Momentan bin ich zufrieden, dass ich es auf den aktuellen Stand bringen konnte. Der wichtigste Indikator für mich ist jetzt die Antwort auf die Frage "hätte ich diese Bibliothek in einem persönlichen Projekt selbst verwendet?". Meine Antwort lautet ja. Bei der Arbeit? Ja Daher glaube ich, dass andere Entwickler dies schätzen werden.


    PS Ich möchte mich bei den Jungs bedanken, die bei Geschäft und Rat geholfen haben - Dmitry Korolkov, Artemy Ryabinkov, Oleg Kuzmin. Ohne dich wäre es eng.