Redis Cache-Synchronisierung für Go-Dienst


Einleitung


Bei der Verfeinerung eines Projekts wurde es notwendig, häufig angeforderte Daten zwischenzuspeichern. Die Implementierung von Caching ist auf verschiedene Arten möglich, aber ich wollte es mit minimalen Änderungen am ursprünglichen Projekt implementieren. Das Ergebnis, seine Vor- und Nachteile werden im Folgenden beschrieben.


Wie war alles


Zu Beginn wurde für jede Abfrage, die den Bezeichner des angeforderten Objekts enthielt, eine Abfrage in der PostgreSQL-Datenbank (DB) ausgeführt. Genauer gesagt mehrere Abfragen, da zur Bildung einer vollständigen Antwort mehrere Datenbanktabellen angewendet werden mussten. Infolge der Verarbeitung von Anforderungen wurde ein ziemlich komplexes Objekt gebildet, von dem einige Felder durch Schnittstellen dargestellt werden. Im Speicher belegt dieses Objekt ca. 250 kB.


Die Leistung mit dieser Implementierung war nicht großartig, nicht mehr als 3500 RPS (Anfrage pro Sekunde), wenn dieselben Daten mit 1000 konkurrierenden Threads angefordert wurden.


Es stellte sich sofort die Frage, wie man den RPS erhöht: Router wechseln, Datenbank optimieren, Daten zwischenspeichern? Der Router wurde recht gut genutzt ( github.com/julienschmidt/httprouter ), und das Ersetzen des Routers in einem großen Projekt wird viel Zeit in Anspruch nehmen und es besteht ein hohes Risiko, dass etwas kaputt geht. Um die Arbeit mit der Datenbank zu optimieren, müssen Sie auch einen wesentlichen Teil des Codes neu schreiben (jetzt wird github.com/jmoiron/sqlx verwendet). Offensichtlich ist das Zwischenspeichern der beste Weg, um den RPS zu erhöhen.


Einfache Lösung


Das Einfachste, was mir in den Sinn kommt, ist die Verwendung eines In-Memory-Cache. Bei Verwendung eines solchen Cache wurden ungefähr 20.000 RPS erhalten. Die Leistung des speicherinternen Caches ist ausgezeichnet, Sie können einen solchen Cache jedoch nicht mit vielen Dienstinstanzen verwenden. Sie wissen nie, zu welcher Instanz des Dienstes eine Anfrage fliegen wird, und es kann Anfragen nicht nur zum Empfangen von Daten, sondern auch zum Löschen / Aktualisieren geben.


Die mit dem In-Memory-Cache erzielte Leistung wurde bei der weiteren Suche nach einer Lösung als Standard herangezogen.


Idee, schlechte Idee


Ist es möglich, das Abfrageergebnis so abzulegen, wie es in der NoSQL Redis-Datenbank ist? Dies ist eine typische Lösung für das Zwischenspeichern von Antwortanforderungen. Daten werden im Speicher gespeichert. Wenn Sie mehrere Instanzen des Dienstes verwenden, können alle einen gemeinsamen Cache verwenden. Diese Lösung wurde schnell umgesetzt. Und die Tests zeigten ... Und die Tests zeigten, dass die Leistung nicht viel stieg.
Weitere Untersuchungen ergaben, dass die Hauptleistungsverluste mit dem Marshalling und dem Unmarshaling verbunden sind. Das Konvertieren einer Struktur in JSON und umgekehrt erfordert die Verwendung von Reflection, was in der Leistung extrem teuer ist. Es ist unmöglich, das Marshalling / Unmarshalling abzulehnen, da ein vollwertiges Objekt aus dem Cache abgerufen werden muss, mit dem Methoden von Strukturen aufgerufen werden können, und nicht nur die Werte einzelner Felder. Die Verwendung verschiedener Bibliotheken mit der Optimierung von Marshalling / Unmarshaling sparte ebenfalls nicht, es gab Wachstum, aber der speicherinterne Cache war sehr weit entfernt. Daher wurde beschlossen, sich nicht mit dem "Igel und der Schlange" anzufreunden und einen Hybrid-Cache zu erstellen.


Hybrid "Schlange und Igel"


Sie können es nicht als vollwertigen Hybrid bezeichnen (siehe Abb.). Tatsächlich stellte sich heraus, dass es sich um einen speicherinternen Cache handelte, der jedoch über Redis synchronisiert wurde (die Bibliothek github.com/go-redis/redis wurde verwendet ). In Redis wird nur die eindeutige Kennung des von der Datenbank angeforderten Objekts (ID-Objekt) gespeichert. Es wird Redis während der Verarbeitung einer Anforderung zum Erstellen eines Objekts oder einer Anforderung zum Abrufen eines vorhandenen Objekts aus der Datenbank hinzugefügt. Die Objekt-ID dient als Schlüssel für den Wert in Redis und der Wert ist die generierte UUID (Universal Unique Identifier, Universal Unique Identifier). Die UUID wird nur generiert, wenn das Objekt zu Redis hinzugefügt wird. Warum diese UUID benötigt wird, wird später beschrieben.



Blockdiagramm der Komponenteninteraktion für die Cache-Synchronisation über Redis


Der In-Memory-Cache basiert auf sync.Map. Bei Hybrid-Cache-Elementen wird TTL (Time to Live, Lifetime) festgelegt. Wenn Redis "Foul" -Elemente bereinigt, wird der speicherinterne Cache durch den Timer (time.AfterFunc) bereinigt. Es durchläuft alle Elemente des Caches und prüft, ob das Element "verrottet" ist. Wenn auf ein Cache-Element zugegriffen wird, verlängert sich seine Lebensdauer, und ein ähnlicher Vorgang wird mit Schlüsseln in Redis ausgeführt.


Also jetzt nach dem Algorithmus. Wenn eine Anfrage eingeht und wir das Objekt abrufen müssen, wird die folgende Abfolge von Aktionen ausgeführt:


  1. Wir prüfen, ob es in Redis ein Objekt mit einem bestimmten ID-Objekt gibt. Wenn ja, können wir den Service-Instanz-Cache aus dem In-Memory entnehmen:
    1. Befindet sich das Objekt nicht im speicherinternen Cache, wird es aus der Datenbank entnommen und der Cache mit der UUID von Redis zum speicherinternen Cache hinzugefügt und die TTL des Schlüssels in Redis aktualisiert.
    2. Befindet sich das Objekt im speicherinternen Cache, wird es aus dem Cache entnommen. Überprüfen Sie, ob die UUID im Cache und in Redis übereinstimmt, und aktualisieren Sie in diesem Fall die TTL im Cache und in Redis. Wenn die UUID nicht übereinstimmt, löschen Sie das Objekt aus dem speicherinternen Cache, nehmen Sie es aus der Datenbank und fügen Sie den Cache mit der UUID von Redis zum speicherinternen Cache hinzu.
  2. Befindet sich das Objekt nicht in Redis, entfernen Sie es aus dem Cache, wenn sich das Objekt im Cache befindet. Nehmen Sie ein Objekt aus der Datenbank und fügen Sie es dem Cache und Redis hinzu. Fügen Sie dem Cache ein Objekt mit einer UUID von Null hinzu, um die Situation zu beseitigen, in der das Aktualisieren / Löschen eines Eintrags schneller ist als das Hinzufügen zum Cache ( andreyverbin comment ). Beim ersten Zugriff auf den Cache wird dann der Unterschied zwischen UUID und Redis aufgedeckt, und Daten aus der Datenbank werden erneut angefordert.

Wenn eine Anforderung zum Löschen eines Objekts eintrifft, wird es sofort aus der Datenbank gelöscht und anschließend zwischengespeichert:


  1. Löschen Sie das Objekt in Redis.
  2. Löschen Sie das Objekt im speicherinternen Cache.

Wenn nun eine ähnliche Anforderung in einer anderen Instanz des Dienstes eintrifft, wird das Objekt nicht verwendet, obwohl es sich noch im speicherinternen Cache befinden kann.


Objektaktualisierung nach Aktualisierung in der Datenbank:


  1. Löschen Sie das Objekt in Redis.
  2. Löschen Sie das Objekt im speicherinternen Cache.

Wenn Sie ein Objekt in einer anderen Instanz des Dienstes anfordern, wird angezeigt, dass es sich nicht in Redis befindet. Sie müssen es daher aus der Datenbank abrufen. Wenn es eine andere Instanz des Dienstes gibt und die Anforderung nach dem Aktualisieren des Objekts und dem Hinzufügen des Objekts durch die zweite Instanz in Redis zu ihr geflogen ist, wird bei der Überprüfung der UUID ein Unterschied festgestellt, und die dritte Instanz des Dienstes entnimmt das Objekt ebenfalls der Datenbank.


Das heißt Tatsächlich glauben wir in jeder unverständlichen Situation, dass unser Cache nicht korrekt ist, und wir müssen Daten aus der Datenbank entnehmen.


Fazit


Die entwickelte Lösung hat sowohl Vor- als auch Nachteile.


Vorteile


  • Das entwickelte Caching-Schema ermöglichte die Erreichung von etwa 19000 RPS, was fast den Tests mit In-Memory-Cache entspricht.
  • Der ursprüngliche Projektcode weist eine minimale Anzahl von Änderungen auf.

Nachteile


  • Wenn Redis abstürzt, sinkt die Leistung des Dienstes drastisch und die Arbeit mit der Datenbank wird fortgesetzt.
  • Jede Instanz des Dienstes benötigt mehr Speicher, da sie über einen eigenen speicherinternen Cache verfügt.

Da hohe Leistung wichtiger war, halte ich die Minuspunkte nicht für kritisch. In Zukunft gibt es die Idee, eine Bibliothek zu schreiben, um die Implementierung des Hybrid-Caches zu vereinfachen, da in anderen Projekten ein ähnliches Caching verwendet werden muss.

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


All Articles