Smart Cache Service basierend auf ZeroMQ und Tarantool

Ruslan Aromatov, Hauptentwickler, ICD



Hallo Habr! Ich arbeite als Backend-Entwickler bei der Moscow Credit Bank und habe während meiner Arbeit einige Erfahrungen gesammelt, die ich gerne mit der Community teilen möchte. Heute werde ich Ihnen erzählen, wie wir mit der mobilen Anwendung MKB Online unseren eigenen Cache-Service für die Frontserver unserer Kunden geschrieben haben. Dieser Artikel kann für diejenigen nützlich sein, die am Service-Design beteiligt sind und mit der Microservice-Architektur, der Tarantool-In-Memory-Datenbank und der ZeroMQ-Bibliothek vertraut sind. In dem Artikel wird es praktisch keine Beispiele für Code und Erklärungen der Grundlagen geben, sondern nur eine Beschreibung der Logik der Dienste und ihrer Interaktion mit einem bestimmten Beispiel, das seit mehr als zwei Jahren an unserem Kampf arbeitet.

Wie alles begann


Vor ungefähr 6 Jahren war das Schema einfach. Als Vermächtnis des Outsourcing-Unternehmens haben wir zwei Mobile-Banking-Clients für iOS und Android sowie einen Frontserver für sie. Der Server selbst wurde in Java geschrieben, ging auf verschiedene Arten (hauptsächlich Seife) in sein Backend und kommunizierte mit den Clients, indem er XML über https übertrug.

Client-Anwendungen konnten sich irgendwie authentifizieren, eine Liste von Produkten anzeigen und ... sie schienen in der Lage zu sein, einige Überweisungen und Zahlungen vorzunehmen, aber tatsächlich machten sie es nicht sehr gut und nicht immer. Daher war auf dem Frontserver weder eine große Anzahl von Benutzern noch eine ernsthafte Belastung zu verzeichnen (was jedoch nicht verhinderte, dass er etwa alle zwei Tage einmal herunterfiel).

Es ist klar, dass wir (und zu dieser Zeit bestand unser Team aus vier Personen) als Verantwortliche für die mobile Bank nicht zu dieser Situation passten, und zunächst haben wir die aktuellen Anwendungen in Ordnung gebracht, aber der Frontserver erwies sich als sehr schlecht, also musste es so sein Schreiben Sie das Ganze schnell neu, ersetzen Sie gleichzeitig XML durch JSON und wechseln Sie zum WildFly- Anwendungsserver. Über ein paar Jahre verteilt wird das Refactoring nicht auf einen separaten Beitrag zurückgeführt, da alles hauptsächlich getan wurde, um sicherzustellen, dass das System nur stabil funktioniert.

Allmählich entwickelten sich die Anwendungen und der Server stabiler und ihre Funktionalität wurde ständig erweitert, was sich auszahlt - es gab immer mehr Benutzer.

Gleichzeitig traten Probleme wie Fehlertoleranz, Redundanz, Replikation und - beängstigend zu denken - Hochlast auf.

Eine schnelle Lösung für das Problem bestand darin, einen zweiten WildFly-Server hinzuzufügen, und die Anwendungen lernten, zwischen ihnen zu wechseln. Das Problem der gleichzeitigen Arbeit mit Client-Sitzungen wurde durch das in WildFly integrierte Infinispan- Modul gelöst.

Wie zuvor

Es schien, dass das Leben besser wurde ...

So kannst du nicht leben


Diese Möglichkeit, mit Sitzungen zu arbeiten, war jedoch nicht ohne Nachteile. Ich werde diejenigen erwähnen, die nicht zu uns passten.

  1. Sitzungsverlust. Das wichtigste Minus. Beispielsweise sendet eine Anwendung zwei Anforderungen an Server-1: Die erste Anforderung ist die Authentifizierung und die zweite ist eine Anforderung für eine Liste von Konten. Die Authentifizierung ist erfolgreich. Auf Server-1 wird eine Sitzung erstellt. Zu diesem Zeitpunkt wird die zweite Clientanforderung aufgrund einer schlechten Kommunikation plötzlich abgebrochen, und die Anwendung wechselt zu Server-2, wodurch die Weiterleitung der zweiten Anforderung erneut gesendet wird. Bei einer bestimmten Arbeitslast hat Infinispan möglicherweise keine Zeit, Daten zwischen Knoten zu synchronisieren. Infolgedessen kann Server-2 die Client-Sitzung nicht überprüfen, sendet eine verärgerte Antwort an den Client, der Client ist traurig und beendet seine Sitzung. Der Benutzer muss sich erneut anmelden. Traurig
  2. Ein Neustart des Servers kann auch zum Verlust von Sitzungen führen. Zum Beispiel nach einem Update (und das passiert ziemlich oft). Wenn Server-2 gestartet wird, kann es nicht funktionieren, bis die Daten mit Server-1 synchronisiert sind. Es scheint, dass der Server gestartet wurde, aber tatsächlich keine Anfragen annehmen sollte. Dies ist unpraktisch.
  3. Dies ist ein integriertes WildFly-Modul, das verhindert, dass wir von diesem Anwendungsserver zu Microservices wechseln.

Von hier aus wurde eine Liste von dem, was wir möchten, irgendwie von selbst erstellt.

  1. Wir möchten Client-Sitzungen so speichern, dass jeder Server (egal wie viele es sind) unmittelbar nach dem Start Zugriff auf sie hat.
  2. Wir möchten alle Kundendaten zwischen Anfragen speichern (zum Beispiel Zahlungsparameter und all das).
  3. Wir möchten im Allgemeinen beliebige Daten auf einem beliebigen Schlüssel speichern.
  4. Außerdem möchten wir Kundendaten empfangen, bevor die Authentifizierung erfolgreich ist. Zum Beispiel ist der Benutzer authentifiziert und alle seine Produkte sind genau dort, frisch und warm.
  5. Und wir wollen entsprechend der Last skalieren.
  6. Führen Sie das Docker aus, schreiben Sie Protokolle auf einen einzelnen Stapel und zählen Sie Metriken usw.
  7. Oh ja, und damit alles schnell geht.

Mehl der Wahl


Bisher haben wir keine Microservice-Architektur implementiert, daher haben wir uns zunächst hingesetzt, um verschiedene Optionen zu lesen, anzusehen und auszuprobieren. Es war sofort klar, dass wir ein schnelles Repository und eine Art Add-On darüber benötigen, das sich mit Geschäftslogik befasst und die Zugriffsschnittstelle zum Repository darstellt. Darüber hinaus wäre es schön, einen schnellen Transport zwischen den Diensten zu gewährleisten.

Sie wählten lange, stritten sich viel und experimentierten. Ich werde jetzt nicht die Vor- und Nachteile aller Kandidaten beschreiben, dies gilt nicht für das Thema dieses Artikels. Ich sage nur, dass der Speicher tarantool sein wird , wir werden unseren Service in Java schreiben und ZeroMQ wird als Transport funktionieren. Ich werde nicht einmal argumentieren, dass die Auswahl sehr zweideutig ist, aber sie wurde weitgehend durch die Tatsache beeinflusst, dass wir keine unterschiedlichen großen und schweren Frameworks (wegen ihres Gewichts und ihrer Langsamkeit), Boxed-Lösungen (wegen ihrer Vielseitigkeit und mangelnden Anpassung) mögen, sondern gleichzeitig Wir lieben es, alle Teile unseres Systems so gut wie möglich zu kontrollieren. Um die Arbeit der Dienste zu steuern, haben wir den Prometheus- Metrik-Erfassungsserver mit seinen praktischen Agenten ausgewählt, die in fast jeden Code integriert werden können. Die Protokolle von all dem werden auf den ELK-Stapel gelegt.

Nun, es scheint mir, dass es bereits zu viel Theorie gab.

Start und Ziel


Das Entwurfsergebnis war ungefähr ein solches Schema.

Wie wollen wir

Lagerung

Es sollte so dumm wie möglich sein, nur Daten und deren aktuellen Status zu speichern, aber immer ohne Neustart zu funktionieren. Entwickelt, um verschiedene Versionen von Frontservern zu bedienen. Wir behalten alle Daten im Speicher, Wiederherstellung im Falle eines Neustarts durch .snap- und .xlog-Dateien.

Tabelle (Speicherplatz) für Client-Sitzungen:

  • Sitzungs-ID
  • Kunden-ID;
  • Version (Service)
  • Aktualisierungszeit (Zeitstempel);
  • Lebenszeit (ttl);
  • serialisierte Sitzungsdaten.

Hier ist alles einfach: Der Client wird authentifiziert, der Frontserver erstellt eine Sitzung und speichert sie im Speicher, wobei er sich an die Zeit erinnert. Bei jeder Datenanforderung wird die Zeit aktualisiert, sodass die Sitzung am Leben bleibt. Wenn sich herausstellt, dass die Daten auf Anfrage veraltet sind (oder überhaupt keine vorhanden sind), geben wir einen speziellen Rückkehrcode zurück, nach dem der Client seine Sitzung beendet.

Einfache Cache-Tabelle (für alle Sitzungsdaten):

  • Schlüssel;
  • Sitzungs-ID
  • Art der gespeicherten Daten (beliebige Anzahl);
  • Aktualisierungszeit (Zeitstempel);
  • Lebenszeit (ttl);
  • serialisierte Daten.

Tabelle der Kundendaten, die vor der Anmeldung aufgewärmt werden müssen:
  • Kunden-ID;
  • Sitzungs-ID
  • Version (Service)
  • Art der gespeicherten Daten (beliebige Anzahl);
  • Aktualisierungszeit (Zeitstempel);
  • Zustand;
  • serialisierte Daten.

Ein wichtiges Feld ist hier der Zustand. Tatsächlich gibt es nur zwei davon - Leerlauf und Aktualisierung. Sie werden von einem darüber liegenden Dienst platziert, der für Clientdaten an das Backend geht, sodass eine andere Instanz dieses Dienstes nicht dieselbe (bereits nutzlose) Arbeit ausführt und das Backend nicht lädt.

Gerätetabelle:

  • Kunden-ID;
  • Geräte-ID
  • Aktualisierungszeit (Zeitstempel);

Die Gerätetabelle ist erforderlich, damit der Client bereits vor der Authentifizierung im System seine ID ermitteln und mit dem Empfang seiner Produkte beginnen kann (Aufwärmen des Caches). Die Logik lautet wie folgt: Der erste Eingang ist immer kalt, da wir vor der Authentifizierung nicht wissen, welche Art von Client von einem unbekannten Gerät stammt (mobile Clients übertragen bei allen Anforderungen immer Geräte-IDs). Alle nachfolgenden Einträge von diesem Gerät werden von einem Aufwärmcache für den damit verbundenen Client begleitet.

Die Arbeit mit Daten wird durch Serverprozeduren vom Java-Dienst isoliert. Ja, ich musste Lua lernen, aber es dauerte nicht lange. Neben der Datenverwaltung selbst sind Lua-Prozeduren auch für die Rückgabe aktueller Zustände, die Indexauswahl, das Bereinigen veralteter Datensätze in Hintergrundprozessen (Fasern) und den Betrieb des integrierten Webservers verantwortlich, über den der direkte Dienstzugriff auf Daten ausgeführt wird. Hier ist es - die Schönheit, alles mit den Händen zu schreiben - die Möglichkeit der unbegrenzten Kontrolle. Aber das Minus ist das gleiche - Sie müssen alles selbst schreiben.

Tarantool selbst arbeitet in einem Docker-Container. Alle erforderlichen Lua-Dateien werden dort in der Phase der Image-Assemblierung abgelegt. Die gesamte Assembly durch Gradle-Skripte.

Master-Slave-Replikation. Auf dem anderen Host wird genau derselbe Container ausgeführt wie das Replikat des Hauptspeichers. Es wird im Falle eines Notfallabsturzes des Masters benötigt - dann wechseln die Java-Dienste zum Slave und es wird zum Master. Für alle Fälle gibt es einen dritten Sklaven. Selbst ein vollständiger Datenverlust ist in unserem Fall traurig, aber nicht tödlich. Im schlimmsten Fall müssen sich Benutzer anmelden und alle Daten abrufen, die erneut in den Cache gelangen.

Java-Dienst

Entwickelt als typischer zustandsloser Mikroservice. Es hat keine Konfiguration, alle erforderlichen Parameter (und es gibt 6 davon) werden beim Erstellen des Docker-Containers durch Umgebungsvariablen übergeben. Es funktioniert mit dem Frontserver über den ZeroMQ-Transport (org.zeromq.jzmq - die Java-Schnittstelle zur nativen libzmq.so.5.1.1, die wir selbst erstellt haben) unter Verwendung unseres eigenen Protokolls. Es funktioniert mit einer Vogelspinne über einen Java-Connector (org.tarantool.connector).

Die Service-Initialisierung ist ganz einfach:

  • Wir starten einen Logger (log4j2);
  • Aus den Umgebungsvariablen (wir befinden uns im Docker) lesen wir die für die Arbeit erforderlichen Parameter.
  • Wir starten den Server für Metriken (Steg);
  • Verbindung zur Vogelspinne herstellen (asynchron);
  • Wir starten die erforderliche Anzahl von Thread-Handlern (Arbeitern);
  • Wir starten einen Broker (zmq) - einen endlosen Nachrichtenverarbeitungszyklus.

Von alledem ist nur die Nachrichtenverarbeitungs-Engine interessant. Unten sehen Sie ein Diagramm des Microservices.

Message Broker-Logik

Beginnen wir mit dem Start des Brokers. Unser Broker ist eine Reihe von zmq-Sockets vom Typ ROUTER, die Verbindungen von verschiedenen Clients akzeptieren und für den Versand von Nachrichten verantwortlich sind, die von diesen kommen.

In unserem Fall haben wir einen Listening-Socket auf der externen Schnittstelle, der Nachrichten von Clients unter Verwendung des TCP-Protokolls empfängt, und den anderen, der Nachrichten von Worker-Threads unter Verwendung des Inproc-Protokolls empfängt (es ist viel schneller als TCP).

/** //   (   ,   ) ZContext zctx = new ZContext(); //    ZMQ.Socket clientServicePoint = zctx.createSocket(ZMQ.ROUTER); //    ZMQ.Socket workerServicePoint= zctx.createSocket(ZMQ.ROUTER); //     clientServicePoint.bind("tcp://*:" + Config.ZMQ_LISTEN_PORT); //     workerServicePoint.bind("inproc://worker-proc"); 

Nach dem Initialisieren der Sockets starten wir eine endlose Ereignisschleife.

 /** *      */ public int run() { int status;  try {   ZMQ.Poller poller = new ZMQ.Poller(2);    poller.register(workerServicePoint, ZMQ.Poller.POLLIN);    poller.register(clientServicePoint, ZMQ.Poller.POLLIN);    int rc;    while (true) {      //        rc = poller.poll(POLL_INTERVAL);      if (rc == -1) {        status = -1;        logger.errorInternal("Broker run error rc = -1");        break; //  -     }    //     ()    if (poller.pollin(0)) {       processBackendMessage(ZMsg.recvMsg(workerServicePoint));    }    //        if (poller.pollin(1)) {       processFrontendMessage(ZMsg.recvMsg(clientServicePoint));    }    processQueueForBackend(); }  } catch (Exception e) {    status = -1;  } finally {    clientServicePoint.close();    workerServicePoint.close();  }  return status; } 

Die Logik der Arbeit ist sehr einfach: Wir empfangen Nachrichten von verschiedenen Orten und machen etwas damit. Wenn etwas kritisch mit uns zusammengebrochen ist, verlassen wir die Schleife, was zum Absturz des Prozesses führt, der vom Docker-Daemon automatisch neu gestartet wird.

Die Hauptidee ist, dass der Broker sich nicht mit Geschäftslogik befasst, sondern nur den Nachrichtenkopf analysiert und Aufgaben an die Arbeitsthreads verteilt, die zuvor beim Start des Dienstes gestartet wurden. Dabei hilft ihm eine einzelne Nachrichtenwarteschlange mit Priorisierung einer festen Länge.

Lassen Sie uns den Algorithmus am Beispiel des obigen Schemas und Codes analysieren.

Nach dem Start werden die Thread-Mitarbeiter, die später als der Broker gestartet wurden, initialisiert und senden eine Bereitschaftsnachricht an den Broker. Der Broker akzeptiert sie, analysiert sie und fügt jeden Mitarbeiter der Liste hinzu.

Auf dem Client-Socket tritt ein Ereignis auf - wir haben die Nachricht1 erhalten. Der Broker ruft den Handler für eingehende Nachrichten auf, dessen Aufgabe ist:

  • Analyse des Nachrichtenkopfes;
  • Platzieren einer Nachricht in einem Halterobjekt mit einer bestimmten Priorität (basierend auf der Header-Analyse) und Lebensdauer;
  • Platzieren des Inhabers in der Nachrichtenwarteschlange;
  • Wenn die Warteschlange nicht voll ist, ist die Aufgabe des Handlers beendet.
  • Wenn die Warteschlange voll ist, rufen wir die Methode auf, um eine Fehlermeldung an den Client zu senden.

In derselben Iteration der Schleife rufen wir den Message Queue Handler auf:

  • Wir fordern die aktuellste Nachricht aus der Warteschlange an (die Warteschlange entscheidet dies selbst basierend auf der Priorität und Reihenfolge des Hinzufügens der Nachricht).
  • Überprüfen Sie die Lebensdauer der Nachricht (wenn sie abgelaufen ist, rufen Sie die Methode auf, um eine Fehlermeldung an den Client zu senden).
  • Wenn die Nachricht für die Verarbeitung relevant ist, versuchen Sie, den ersten freien Mitarbeiter arbeitsbereit zu machen.
  • Wenn es keine gibt, stellen Sie die Nachricht wieder in die Warteschlange (genauer gesagt, löschen Sie sie nicht von dort, sie bleibt dort hängen, bis ihre Lebensdauer abläuft).
  • Wenn wir einen Arbeiter zur Arbeit bereit haben, markieren wir ihn als beschäftigt und senden ihm eine Nachricht zur Bearbeitung.
  • Löschen Sie die Nachricht aus der Warteschlange.

Wir machen das gleiche mit allen nachfolgenden Nachrichten. Der Thread-Worker selbst ist wie ein Broker konzipiert - er hat denselben endlosen Nachrichtenverarbeitungszyklus. Aber wir brauchen keine sofortige Verarbeitung mehr, es ist für lange Aufgaben ausgelegt.

Nachdem der Mitarbeiter seine Aufgabe erledigt hat (z. B. zum Backend für die Produkte des Kunden oder in der Tarantel für die Sitzung), sendet er eine Nachricht an den Broker, die der Broker an den Client zurücksendet. Die Adresse des Kunden, an den die Antwort gesendet werden soll, wird ab dem Moment gespeichert, an dem die Nachricht vom Kunden im Inhaberobjekt eintrifft, das als Nachricht in einem etwas anderen Format an den Mitarbeiter gesendet wird und dann zurückkehrt.

Das Format der Nachrichten, die ich ständig erwähne, ist unsere eigene Produktion. ZeroMQ stellt uns standardmäßig die ZMsg-Klassen zur Verfügung - die Nachricht selbst und den ZFrame - Teil dieser Nachricht, im Wesentlichen nur ein Array von Bytes, die ich bei Bedarf verwenden kann. Unsere Nachricht besteht aus zwei Teilen (zwei ZFrames), von denen der erste ein binärer Header und der zweite Daten sind (der Anforderungshauptteil beispielsweise in Form einer JSON-Zeichenfolge, die durch ein Array von Bytes dargestellt wird). Der Nachrichtenkopf ist universell und wandert sowohl von Client zu Server als auch von Server zu Client.

Tatsächlich haben wir nicht das Konzept von "Anfrage" oder "Antwort", sondern nur Nachrichten. Der Header enthält: Protokollversion, Systemtyp (welches System angesprochen wird), Nachrichtentyp, Fehlercode auf Transportebene (wenn er nicht 0 ist, ist etwas in der Nachrichtenübertragungs-Engine passiert), Anforderungs-ID (Pass-Through-ID, die vom Client kommt - für die Ablaufverfolgung erforderlich), die Client-Sitzungs-ID (optional) sowie ein Zeichen für einen Fehler auf Datenebene (wenn beispielsweise die Backend-Antwort nicht analysiert werden konnte, setzen wir dieses Flag, damit der Parser auf der Clientseite die Antwort nicht deserialisiert, sondern Fehlerdaten empfängt auf andere Weise).

Dank eines einzigen Protokolls zwischen allen Microservices und einem solchen Header können wir die Komponenten unserer Services ganz einfach manipulieren. Sie können den Broker beispielsweise in einen separaten Prozess umwandeln und ihn zu einem einzelnen Nachrichtenbroker auf der Ebene des gesamten Microservice-Systems machen. Oder führen Sie beispielsweise Worker nicht in Form von Threads innerhalb des Prozesses aus, sondern als separate unabhängige Prozesse. Und während sich der Code in ihnen nicht ändert. Generell gibt es Raum für Kreativität.

Ein bisschen über Leistung und Ressourcen


Der Broker selbst ist schnell und die Gesamtbandbreite des Dienstes wird durch die Backend-Geschwindigkeit und die Anzahl der Worker begrenzt. Praktischerweise wird die gesamte erforderliche Speichermenge sofort zu Beginn des Dienstes zugewiesen, und alle Threads werden sofort gestartet. Die Warteschlangengröße ist ebenfalls festgelegt. Zur Laufzeit werden nur Nachrichten verarbeitet.

Beispiel: Zusätzlich zum Haupt-Thread startet unser aktueller Cache-Kampfdienst weitere 100 Worker-Threads, und die Warteschlangengröße ist auf dreitausend Nachrichten begrenzt. Im normalen Betrieb verarbeitet jede Instanz bis zu 200 Nachrichten pro Sekunde und verbraucht etwa 250 MB Speicher und etwa 2-3% der CPU. Bei Spitzenlasten springt es manchmal auf 7-8%. Es funktioniert alles auf einer Art virtuellem Dual-Core-Xeon.

Die reguläre Arbeit des Dienstes impliziert die gleichzeitige Beschäftigung von 3-5 Arbeitern (von 100) mit der Anzahl der Nachrichten in der Warteschlange 0 (dh sie werden sofort verarbeitet). Wenn sich das Backend verlangsamt, steigt die Anzahl der beschäftigten Mitarbeiter proportional zum Zeitpunkt seiner Reaktion. In Fällen, in denen ein Unfall auftritt und das Backend steigt, werden zuerst alle Mitarbeiter beendet. Danach beginnt die Nachrichtenwarteschlange zu verstopfen. Wenn es vollständig verstopft ist, reagieren wir auf Kunden mit Verweigerungen der Verarbeitung. Gleichzeitig verbrauchen wir keinen Speicher oder keine CPU-Ressourcen, geben keine stabilen Messdaten an und reagieren klar auf die Kunden, was gerade passiert.

Der erste Screenshot zeigt den regulären Betrieb des Dienstes.

Die regelmäßige Arbeit des Dienstes

Und beim zweiten ereignete sich ein Unfall - das Backend reagierte aus irgendeinem Grund nicht innerhalb von 30 Sekunden. Es ist zu sehen, dass zunächst alle Arbeiter ausgegangen sind, woraufhin die Nachrichtenwarteschlange zu verstopfen begann.

Unfall

Leistungstests


Die synthetischen Tests auf meiner Arbeitsmaschine (CentOS 7, Core i5, 16 GB RAM) zeigten Folgendes.

Arbeiten Sie mit dem Repository (Schreiben in die Vogelspinne und sofortiges Lesen dieses Datensatzes mit einer Größe von 100 Byte - Simulieren der Arbeit mit der Sitzung) - 12000 U / min.

Das gleiche, nur die Geschwindigkeit wurde nicht zwischen den Service-Tarantel-Punkten gemessen, sondern zwischen dem Kunden und dem Service. Natürlich musste ich selbst einen Kunden für Stresstests schreiben. Innerhalb einer Maschine konnten 7000 U / min erreicht werden. In einem lokalen Netzwerk (und wir haben viele verschiedene virtuelle Maschinen, deren physische Verbindung unklar ist) variieren die Ergebnisse, aber bis zu 5000 U / min für eine Instanz sind durchaus möglich. Gott weiß, welche Art von Leistung, aber sie deckt mehr als zehnmal unsere Spitzenlasten ab. Dies ist nur möglich, wenn eine Instanz des Dienstes ausgeführt wird, wir jedoch mehrere davon haben und Sie jederzeit so viele ausführen können, wie Sie benötigen. Wenn Dienste die Speichergeschwindigkeit blockieren, kann die Tarantel horizontal skaliert werden (Shard beispielsweise basierend auf der Client-ID).

Service Intelligence


Der aufmerksame Leser stellt wahrscheinlich bereits die Frage: Was ist die „Schlauheit“ dieses Dienstes, die im Titel erwähnt wird? Ich habe dies bereits beiläufig erwähnt, aber jetzt werde ich Ihnen mehr erzählen.

Eine der Hauptaufgaben des Dienstes bestand darin, die Zeit zu verkürzen, die für die Ausgabe ihrer Produkte an Benutzer erforderlich ist (Listen mit Konten, Karten, Einzahlungen, Darlehen, Servicepaketen usw.), und gleichzeitig die Belastung des Backends (Verringerung der Anzahl von Anforderungen in großen und schweren Oracle) aufgrund des Zwischenspeicherns in der Tarantel zu verringern.

Und er hat es ganz gut gemacht. Die Logik zum Aufwärmen des Client-Cache lautet wie folgt:

  • Der Benutzer startet die mobile Anwendung.
  • Eine AppStart-Anforderung mit der Geräte-ID wird an den Frontserver gesendet.
  • Der Frontserver sendet eine Nachricht mit dieser ID an den Cache-Dienst.
  • Der Dienst sucht in der Gerätetabelle nach der Client-ID für dieses Gerät.
  • Wenn es nicht da ist, passiert nichts (die Antwort wird nicht einmal gesendet, der Server wartet nicht darauf).
  • Wenn sich die Client-ID befindet, erstellt der Worker eine Reihe von Nachrichten zum Empfangen von Listen von Benutzerprodukten, die sofort vom Broker verarbeitet und im normalen Modus an die Worker verteilt werden.
  • Jeder Mitarbeiter sendet eine Anforderung für einen bestimmten Datentyp an den Benutzer, wobei der Status "Aktualisieren" in die Datenbank aufgenommen wird (dieser Status schützt das Backend davor, dieselben Anforderungen zu wiederholen, wenn sie von anderen Instanzen des Dienstes stammen).
  • Nach Erhalt der Daten werden diese in der Vogelspinne aufgezeichnet.
  • Der Benutzer meldet sich beim System an, und die Anwendung sendet Anforderungen zum Empfangen ihrer Produkte, und der Server sendet diese Anforderungen in Form von Nachrichten an den Cache-Dienst.
  • Wenn die Benutzerdaten bereits empfangen wurden, senden wir sie einfach aus dem Cache.
  • Wenn die Daten gerade empfangen werden (Status "Aktualisieren"), wird im Worker ein Datenwartezyklus gestartet (entspricht dem Anforderungszeitlimit für das Backend).
  • Sobald die Daten empfangen wurden (dh der Status dieses Datensatzes (Tupel) in der Tabelle lautet "Leerlauf"), gibt der Dienst diese an den Client weiter.
  • Wenn die Daten nicht innerhalb eines bestimmten Zeitintervalls empfangen werden, wird ein Fehler an den Client zurückgegeben.

In der Praxis konnten wir somit die durchschnittliche Zeit für den Empfang von Produkten für den Frontserver von 200 ms auf 20 ms, dh um das Zehnfache, und die Anzahl der Anforderungen an das Backend um das Vierfache reduzieren.

Die Probleme


Der Cache-Dienst arbeitet seit ungefähr zwei Jahren im Kampf und erfüllt derzeit unsere Anforderungen.

Natürlich gibt es immer noch ungelöste Probleme, manchmal treten Probleme auf. Java-Dienste in der Schlacht sind noch nicht gefallen. Die Vogelspinne fiel ein paar Mal auf SIGSEGV, aber es war eine alte Version, und nach dem Update kam es nicht wieder vor. Während des Stresstests fällt die Replikation ab, auf dem Master ist ein Rohrbruch aufgetreten, wonach der Slave abgefallen ist, obwohl der Master weiter gearbeitet hat. Es wurde durch Neustart des Sklaven entschieden.

Es gab einmal einen Unfall im Rechenzentrum, und es stellte sich heraus, dass das Betriebssystem (CentOS 7) keine Festplatten mehr sah. Das Dateisystem wurde schreibgeschützt. Das Überraschendste war, dass die Dienste weiterhin funktionierten, da wir alle Daten im Speicher behalten. Die Tarantel konnte keine .xlog-Dateien schreiben, niemand hat etwas protokolliert, aber irgendwie hat alles funktioniert. Der Neustartversuch war jedoch erfolglos - niemand konnte starten.

Es gibt ein großes ungelöstes Problem, und ich möchte die Meinung der Community zu diesem Thema hören. Wenn die Master-Tarantel abstürzt, können Java-Dienste zu Slave wechseln, der weiterhin als Master arbeitet. Dies geschieht jedoch nur, wenn der Master abstürzt und nicht funktionieren kann.

Ungelöstes Problem

Angenommen, wir haben drei Instanzen eines Dienstes, die mit Daten auf einer Master-Tarantel arbeiten. Die Dienste selbst fallen nicht aus, die Datenbankreplikation läuft, alles ist in Ordnung. Aber plötzlich fällt ein Netzwerk zwischen Knoten 1 und Knoten 4 auseinander, in dem der Assistent arbeitet. Service-1 entscheidet sich nach mehreren erfolglosen Versuchen, zur Sicherungsdatenbank zu wechseln, und beginnt dort mit dem Senden von Anforderungen.

Unmittelbar danach akzeptiert der Tarantel-Slave Datenänderungsanforderungen, wodurch die Replikation vom Master auseinanderfällt und wir inkonsistente Daten erhalten. Gleichzeitig arbeiten die Dienste 2 und 3 perfekt mit dem Master zusammen, und Dienst 1 kommuniziert gut mit dem ehemaligen Slave. Es ist klar, dass wir in diesem Fall anfangen, Client-Sitzungen und andere Daten zu verlieren, obwohl alles von der technischen Seite aus funktioniert. Wir haben ein solches potenzielles Problem noch nicht gelöst. Glücklicherweise ist dies seit 2 Jahren nicht mehr geschehen, aber die Situation ist ziemlich real. Jetzt kennt jeder Dienst die Nummer des Geschäfts, in das er geht, und wir haben eine Warnung für diese Metrik, die beim Wechsel vom Master zum Slave funktioniert. Und Sie müssen alles mit Ihren Händen reparieren. Wie lösen Sie solche Probleme?

Pläne


Wir planen, an dem oben beschriebenen Problem zu arbeiten, indem wir die Anzahl der Mitarbeiter begrenzen, die gleichzeitig mit einer Art von Anfrage beschäftigt sind, den Dienst sicher (ohne aktuelle Anfragen zu verlieren) stoppen und weiter polieren.

Fazit


Das ist vielleicht alles, obwohl ich das Thema eher oberflächlich durchgearbeitet habe, aber die allgemeine Logik der Arbeit sollte klar sein. Daher bin ich, wenn möglich, bereit, in den Kommentaren zu antworten. Ich habe kurz beschrieben, wie ein kleines Hilfssubsystem der Frontserver der Bank für die Bedienung mobiler Clients funktioniert.

Wenn das Thema für die Community von Interesse ist, kann ich Ihnen einige unserer Lösungen vorstellen, die zur Verbesserung der Qualität des Kundenservice für die Bank beitragen.

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


All Articles