Hasura. Hochleistungs-GraphQL-zu-SQL-Server-Architektur

Hallo Habr! Ich präsentiere Ihnen die Übersetzung des Artikels „Architektur einer Hochleistungs-GraphQL-zu-SQL-Engine“ .

Dies ist eine Übersetzung eines Artikels darüber, wie es intern strukturiert ist und welche Optimierungen und Architekturlösungen Hasura bietet - ein leistungsstarker, leichter GraphQL-Server, der als Schicht zwischen Ihrer Webanwendung und der PostgreSQL-Datenbank fungiert.

Sie können ein GraphQL-Schema basierend auf einer vorhandenen Datenbank generieren oder eine neue erstellen. Es unterstützt GraphQL-Abonnements aus der Box basierend auf Postgres-Triggern, dynamischer Zugriffskontrolle, automatischer Generierung von Joins, löst das Problem von N + 1-Anforderungen (Batching) und vieles mehr.


Sie können Fremdschlüsseleinschränkungen in PostgreSQL verwenden, um hierarchische Daten in einer einzigen Abfrage abzurufen. Sie können diese Abfrage beispielsweise ausführen, um die Alben und die entsprechenden Titel abzurufen (wenn in der Tabelle "Titel" ein Fremdschlüssel erstellt wurde, der auf die Tabelle "Album" verweist).

{ album (where: {year: {_eq: 2018}}) { title tracks { id title } } } 

Wie Sie vielleicht erraten haben, können Sie Daten beliebiger Tiefe anfordern. Diese API in Kombination mit der Zugriffskontrolle ermöglicht es Webanwendungen, Daten von PostgreSQL abzufragen, ohne ein eigenes Backend zu schreiben. Es wurde entwickelt, um Anfragen so schnell wie möglich zu beantworten, eine hohe Bandbreite zu haben und gleichzeitig Prozessorzeit und Speicherverbrauch auf dem Server zu sparen. Wir werden über architektonische Lösungen sprechen, mit denen wir dies erreichen konnten.

Lebenszyklus anfordern


Eine an Hasura gesendete Anfrage durchläuft die folgenden Phasen:

  1. Empfangen von Sitzungen : Die Anforderung fällt in das Gateway, das den Schlüssel (falls vorhanden) überprüft und verschiedene Header hinzufügt, z. B. die Kennung und die Benutzerrolle.
  2. Analyse von Abfragen: Hasura empfängt eine Abfrage, analysiert Header, um Benutzerinformationen abzurufen, und erstellt GraphQL AST basierend auf dem Anforderungshauptteil.
  3. Validierung von Anforderungen : Es wird geprüft, ob die Anforderung semantisch korrekt ist, und dann werden die Zugriffsrechte angewendet, die der Rolle des Benutzers entsprechen.
  4. Abfrageausführung: Die Abfrage wird in SQL konvertiert und an Postgres gesendet.
  5. Antwortgenerierung : Das Ergebnis der SQL-Abfrage wird verarbeitet und an den Client gesendet (das Gateway kann bei Bedarf gzip verwenden ).

Ziele


Die Anforderungen sind ungefähr wie folgt:

  1. Der HTTP-Stack sollte einen minimalen Overhead hinzufügen und in der Lage sein, viele gleichzeitige Anforderungen für einen hohen Durchsatz zu verarbeiten.
  2. Schnelle SQL-Generierung aus GraphQL-Abfrage.
  3. Die generierte SQL-Abfrage sollte für Postgres effizient sein.
  4. Das Ergebnis der SQL-Abfrage sollte effektiv von Postgres zurückgegeben werden.

GraphQL-Abfrageverarbeitung


Es gibt verschiedene Ansätze, um die für eine GraphQL-Abfrage erforderlichen Daten zu erhalten:

Herkömmliche Resolver


Das Ausführen von GraphQL-Abfragen umfasst normalerweise das Aufrufen des Resolvers für jedes Feld.
Im Abfragebeispiel erhalten wir die Alben, die 2018 veröffentlicht wurden, und fordern dann für jedes Album die entsprechenden Titel an - ein klassisches Problem von N + 1-Abfragen. Die Anzahl der Abfragen wächst exponentiell mit zunehmender Abfragetiefe.

Anfragen von Postgres sind:

 SELECT id,title FROM album WHERE year = 2018; 

Diese Anfrage sendet alle Alben an uns zurück. Angenommen, die Anzahl der von der Anforderung zurückgegebenen Alben ist gleich N. Dann würden wir für jedes Album die folgende Anforderung ausführen:

 SELECT id,title FROM tracks WHERE album_id = <album-id> 

Insgesamt erhalten Sie N + 1 Abfragen, um alle erforderlichen Daten zu erhalten.

Stapelanfragen


Tools wie dataloader wurden entwickelt, um das Problem von N + 1-Anforderungen mithilfe von Batching zu lösen. Die Anzahl der SQL-Abfragen für die eingebetteten Daten hängt nicht mehr von der Größe des ersten Beispiels ab, da Jetzt wirkt es sich auf die Anzahl der Knoten in der GraphQL-Abfrage aus. In diesem Fall sind 2 Anfragen an Postgres erforderlich, um die erforderlichen Daten zu erhalten:

Wir bekommen Alben:

 SELECT id,title FROM album WHERE year = 2018 

Wir erhalten die Titel für die Alben, die wir in der vorherigen Anfrage erhalten haben:

 SELECT id, title FROM tracks WHERE album_id IN {the list of album ids} 

Insgesamt gehen 2 Anfragen ein. Wir haben vermieden, SQL-Abfragen für die Tracks für jedes einzelne Album auszuführen. Stattdessen haben wir den WHERE-Operator verwendet, um alle erforderlichen Tracks gleichzeitig in einer Abfrage abzurufen.

Tritt bei


Dataloader wurde für die Arbeit mit verschiedenen Datenquellen entwickelt und ermöglicht nicht die Nutzung der Funktionen einer bestimmten Datenquelle. In unserem Fall ist Postgres die einzige Datenquelle und bietet wie alle relationalen Datenbanken die Möglichkeit, Daten aus mehreren Tabellen mit einer einzigen Abfrage mithilfe des JOIN-Operators zu erfassen. Wir können alle für eine GraphQL-Abfrage erforderlichen Tabellen ermitteln und mithilfe von JOINs eine einzelne SQL-Abfrage generieren, um alle Daten abzurufen. Es stellt sich heraus, dass die für jede GraphQL-Abfrage erforderlichen Daten mit einer einzigen SQL-Abfrage abgerufen werden können. Diese Daten werden konvertiert, bevor sie an den Client gesendet werden.

Eine solche Anfrage:

 SELECT album.id as album_id, album.title as album_title, track.id as track_id, track.title as track_title FROM album LEFT OUTER JOIN track ON (album.id = track.album_id) WHERE album.year = 2018 

Wird uns solche Daten zurückgeben:

 album_id, album_title, track_id, track_title 1, Album1, 1, track1 1, Album1, 2, track2 2, Album2, NULL, NULL 

Dann wird es in JSON konvertiert und an den Client gesendet:

 [ { "title" : "Album1", "tracks": [ {"id" : 1, "title": "track1"}, {"id" : 2, "title": "track2"} ] }, { "title" : "Album2", "tracks" : [] } ] 

Optimierung der Antwortgenerierung


Wir haben festgestellt, dass die meiste Zeit in der Abfrageverarbeitung für die Konvertierung des Ergebnisses einer SQL-Abfrage in JSON aufgewendet wird.

Nach mehreren Versuchen, diese Funktion auf verschiedene Weise zu optimieren, haben wir beschlossen, sie auf Postgres zu übertragen. Postgres 9.4 ( veröffentlicht um die Zeit von Hasuras erstem Release ) hat eine Funktion für die JSON-Aggregation hinzugefügt, die uns bei unserer Arbeit geholfen hat. Nach dieser Optimierung sahen SQL-Abfragen folgendermaßen aus:

 SELECT json_agg(r.*) FROM ( SELECT album.title as title, json_agg(track.*) as tracks FROM album LEFT OUTER JOIN track ON (album.id = track.album_id) WHERE album.year = 2018 GROUP BY album.id ) r 

Das Ergebnis dieser Abfrage besteht aus einer Spalte und einer Zeile, und dieser Wert wird ohne weitere Konvertierungen an den Client gesendet. Nach unseren Tests ist dieser Ansatz etwa drei- bis sechsmal schneller als die Haskell-Konvertierungsfunktion.

Vorbereitete Aussagen


Die generierten SQL-Abfragen können abhängig von der Verschachtelungsebene der Abfrage und den Verwendungsbedingungen sehr groß und komplex sein. In der Regel verfügen Webanwendungen über eine Reihe von Abfragen, die wiederholt mit unterschiedlichen Parametern ausgeführt werden. Beispielsweise muss die vorherige Abfrage für 2017 anstelle von 2018 ausgeführt werden. Vorbereitete Anweisungen eignen sich am besten für Fälle, in denen sich eine komplexe SQL-Abfrage wiederholt, in der nur Parameter geändert werden.

Angenommen, diese Abfrage wird zum ersten Mal ausgeführt:

 { album (where: {year: {_eq: 2018}}) { title tracks { id title } } } 

Wir erstellen eine vorbereitete Anweisung für die SQL-Abfrage, anstatt sie auszuführen:

 PREPARE prep_1 AS SELECT json_agg(r.*) FROM ( SELECT album.title as title, json_agg(track.*) as tracks FROM album LEFT OUTER JOIN track ON (album.id = track.album_id) WHERE album.year = $1 GROUP BY album. 

Danach führen wir es sofort aus:

 EXECUTE prep_1('2018'); 

Wenn Sie die GraphQL-Abfrage für 2017 ausführen müssen, rufen wir einfach dieselbe vorbereitete Anweisung mit einem anderen Argument auf:

 EXECUTE prep_1('2017'); 

Dies führt zu einer Geschwindigkeitssteigerung von etwa 10 bis 20%, abhängig von der Komplexität der GraphQL-Abfrage.

Haskell


Haskell funktioniert aus mehreren Gründen gut:


Zusammenfassend


Alle oben genannten Optimierungen führen zu gravierenden Leistungsvorteilen:



In der Tat ermöglichen ein geringer Speicherverbrauch und unbedeutende Verzögerungen im Vergleich zu direkten Aufrufen von PostgreSQL in den meisten Fällen, ORMs in Ihrem Backend durch GraphQL-API-Aufrufe zu ersetzen.

Benchmarks:

Prüfstand:

  1. Laptop mit 8 GB RAM und i7
  2. Postgres läuft auf demselben Computer
  3. wrk wurde als Vergleichstool verwendet und für verschiedene Arten von Anfragen haben wir versucht, die RPS zu "maximieren"
  4. Eine Instanz der Hasura GraphQL Engine
  5. Größe des Verbindungspools: 50
  6. Datensatz : Chinook


Anfrage 1: track_media_some

 query tracks_media_some { tracks (where: {composer: {_eq: "Kurt Cobain"}}){ id name album { id title } media_type { name } }} 

  • Anfragen pro Sekunde: 1375 req / s
  • Verzögerung: 17,5 ms
  • CPU: ~ 30%
  • RAM: ~ 30 MB (Hasura) + 90 MB (Postgres)

Anfrage 2: track_media_all

 query tracks_media_all { tracks { id name media_type { name } }} 

  • Anfragen pro Sekunde: 410 req / s
  • Verzögerung: 59 ms
  • CPU: ~ 100%
  • RAM: ~ 30 MB (Hasura) + 130 MB (Postgres)

Anfrage 3: album_tracks_genre_some

 query albums_tracks_genre_some { albums (where: {artist_id: {_eq: 127}}) { id title tracks { id name genre { name } } }} 

  • Anfragen pro Sekunde: 1029 req / s
  • Verzögerung: 24ms
  • CPU: ~ 30%
  • RAM: ~ 30 MB (Hasura) + 90 MB (Postgres)

Anfrage 4: album_tracks_genre_all

 query albums_tracks_genre_all { albums { id title tracks { id name genre { name } } } 

  • Anfragen pro Sekunde: 328 req / s
  • Verzögerung: 73 ms
  • CPU: 100%
  • RAM: ~ 30 MB (Hasura) + 130 MB (Postgres)

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


All Articles