ハスラ。 高性能GraphQL to SQL Serverアーキテクチャ

こんにちは、Habr! 記事「高性能GraphQLからSQLエンジンへのアーキテクチャ」の翻訳を紹介します



これは、徹底的に動作する方法と、Hasuraがどのような最適化とアーキテクチャソリューションを実行するかについての記事の翻訳です。これは、WebアプリケーションとPostgreSQLデータベース間のレイヤーとして機能する高性能で軽量なGraphQLサーバーです。



既存のデータベースに基づいてGraphQLスキーマを生成したり、新しいデータベースを作成したりできます。 Postgresトリガー、動的アクセス制御、結合の自動生成に基づくボックスからのGraphQLサブスクリプションをサポートし、N + 1リクエスト(バッチ処理)などの問題を解決します。









PostgreSQLの外部キー制約を使用して、単一のクエリで階層データを取得できます。 たとえば、アルバムとそれに対応するトラックを取得するためにこのクエリを実行できます(「album」テーブルを指す「track」テーブルに外部キーが作成されている場合)



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





ご想像のとおり、任意の深さのデータを要求できます。 このAPIとアクセス制御を組み合わせることで、Webアプリケーションは独自のバックエンドを作成せずにPostgreSQLからデータをクエリできます。 サーバーでのプロセッサ時間とメモリ消費を節約しながら、クエリを可能な限り迅速に処理し、高いスループットを実現するように設計されています。 これを実現できるアーキテクチャソリューションについて説明します。



ライフサイクルをリクエストする



Hasuraに送信されるリクエストは、次の段階を通過します。



  1. セッションの受信 :リクエストはゲートウェイに送られ、ゲートウェイはキー(存在する場合)をチェックし、識別子やユーザーロールなどのさまざまなヘッダーを追加します。
  2. クエリ解析:Hasuraはクエリを受信し、ヘッダーを解析してユーザー情報を取得し、リクエスト本文に基づいてGraphQL ASTを作成します。
  3. リクエストの検証 :リクエストが意味的に正しいかどうかをチェックし、ユーザーのロールに対応するアクセス権が適用されます。
  4. クエリ実行 :クエリはSQLに変換され、Postgresに送信されます。
  5. 応答の生成 :SQLクエリの結果が処理され、クライアントに送信されます( ゲートウェイは必要に応じてgzipを使用できます )。


目標



要件はおよそ次のとおりです。



  1. HTTPスタックは最小限のオーバーヘッドを追加し、多くの同時リクエストを処理して高いスループットを実現できる必要があります。
  2. GraphQLクエリからの高速SQL生成。
  3. 生成されたSQLクエリはPostgresにとって効率的です。
  4. SQLクエリの結果は、Postgresから効果的に返される必要があります。


GraphQLクエリ処理



GraphQLクエリに必要なデータを取得するには、いくつかのアプローチがあります。



従来のレゾルバ



GraphQLクエリの実行には、通常、各フィールドのリゾルバー呼び出しが含まれます。

リクエストの例では、2018年にリリースされたアルバムを取得し、それぞれに対応するトラックをリクエストします-N + 1リクエストの古典的な問題。 要求の数は、要求の深さが増すにつれて指数関数的に増加します。



Postgresによるリクエストは次のとおりです。



 SELECT id,title FROM album WHERE year = 2018;
      
      





このクエリは、すべてのアルバムを返します。 リクエストから返されたアルバムの数がNに等しいと仮定します。その後、各アルバムに対して次のクエリを実行します。



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





合計で、N + 1個のクエリを取得して、必要なすべてのデータを取得します。



バッチリクエスト



データローダーなどのツールは、バッチ処理を使用してN + 1リクエストの問題を解決するように設計されています。 埋め込みデータのSQLクエリの数は、初期選択のサイズに依存しなくなりました。 現在、GraphQLクエリのノード数に影響します。 この場合、必要なデータを取得するには、Postgresへの2つのリクエストが必要です。



アルバムを取得します:



 SELECT id,title FROM album WHERE year = 2018
      
      





前のリクエストで受け取ったアルバムのトラックを取得します。



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





合計2つのクエリが受信されます。 個々のアルバムのトラックでSQLクエリを実行することは避け、代わりにWHERE演算子を使用して、1つのクエリで必要なすべてのトラックを一度に取得しました。



参加する



Dataloaderはさまざまなデータソースで動作するように設計されており、特定のデータソースの機能を活用することはできません。 私たちの場合、Postgresは唯一のデータソースであり、すべてのリレーショナルデータベースと同様に、JOIN演算子を使用して単一のクエリで複数のテーブルからデータを収集する機能を提供します。 GraphQLクエリに必要なすべてのテーブルを決定し、JOINを使用してすべてのデータを取得する単一のSQLクエリを生成できます。 GraphQLクエリに必要なデータは、単一のSQLクエリを使用して取得できることがわかりました。 このデータは、クライアントに送信される前に変換されます。



そのような要求:



 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
      
      





そのようなデータが返されます:



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





次に、JSONに変換され、クライアントに送信されます。



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





応答生成の最適化



クエリ処理のほとんどの時間は、SQLクエリの結果をJSONに変換する機能に費やされることがわかりました。



この機能をさまざまな方法で最適化しようと何度か試みた後、Postgresに転送することにしました。 Postgres 9.4( Hasuraの最初のリリースの頃にリリースされた)は、JSON集約関数を追加して、私たちが望むことをするのを助けました。 この最適化の後、SQLクエリは次のようになり始めました。



 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
      
      





このクエリの結果には1つの列と1つの行があり、この値はそれ以上の変換なしでクライアントに送信されます。 テストによると、このアプローチはHaskell変換関数よりも約3〜6倍高速です。



準備されたステートメント



生成されたSQLクエリは、クエリのネストのレベルと使用条件に応じて、非常に大きく複雑になる場合があります。 通常、Webアプリケーションには、さまざまなパラメーターで繰り返し実行されるクエリのセットがあります。 たとえば、2018年ではなく2017年に前のクエリを実行する必要があります。準備済みステートメントは、パラメータのみが変更される複雑なSQLクエリの繰り返しがある場合に最適です。



このクエリが初めて実行されたとしましょう:



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





SQLクエリを実行する代わりに、準備されたステートメントを作成します。



 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.
      
      





その後すぐに実行します:



 EXECUTE prep_1('2018');
      
      





2017年にGraphQLクエリを実行する必要がある場合は、異なる引数を使用して同じ準備済みステートメントを呼び出すだけです。



 EXECUTE prep_1('2017');
      
      





これにより、GraphQLクエリの複雑さに応じて、約10〜20%の速度が向上します。



ハスケル



Haskellはいくつかの理由でうまく機能します。





最後に



上記のすべての最適化により、パフォーマンスが大幅に向上します。







実際、PostgreSQLへの直接呼び出しに比べてメモリ消費量が少なく、待ち時間がわずかであるため、ほとんどの場合、バックエンドのORMをGraphQL API呼び出しに置き換えることができます。



ベンチマーク:



テストスタンド:



  1. 8GB RAMおよびi7を搭載したラップトップ
  2. 同じコンピューターで実行されているPostgres
  3. wrkは比較ツールとして使用され、さまざまなタイプのリクエストに対してrpを「最大化」しようとしました
  4. Hasura GraphQLエンジンの1つのインスタンス
  5. 接続プールのサイズ:50
  6. データセットチヌーク




リクエスト1:tracks_media_some



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







リクエスト2:tracks_media_all



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







リクエスト3:album_tracks_genre_some



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







リクエスト4:album_tracks_genre_all



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








All Articles