サーバサイドにおけるGraphQLの実行管理
https://cdn-images-1.medium.com/max/2000/1*SbqIRU9go4b6vWoMzh96Wg.png
手法一覧
下記記事の要約を行う。
REST では、そのパラメータによって各 API のエンドポイントがどの程度のコストがあるがわかるが、1 つの GraphQL クエリーは数千のデータベース操作を生み出す可能性がある。スケーラブルな GraphQL API を設計するためには、バックエンドに負荷をかけるような API の実行を制限する必要がある。
Persisted queries
これは、クライアントサイドで実行されるクエリがあらかじめ分かっている場合に、そのクエリを毎回サーバに送信するのではなく、クエリに応じた識別子をクエリの組を事前にサーバに教えておき、クライアントが実際にクエリを発行する際にはその識別子のみを送信するだけでよくなる、という手法のことで、以下の利点がある。
事前に分かっているクエリのみを実行するので、攻撃者によって重たいクエリを発行させることがない
クライアント/サーバ間の帯域を節約できる
これは、サーバサイドもクライアントサイドも独自に開発している場合に有用な方法 であり、また、サーバサイドとクライアントサイドが密結合するので、リリースのサイクルを合わせるのが大変という問題がある。
Request size limit and request timeout
Request size の制限は、GraphQL クエリにおいてはパフォーマンスの面から見てもあまり有用ではない。クエリ内の文字列を制限したところで、クエリ自体の複雑度が高ければパフォーマンスは悪くなってしまう。
Request timeout の制限は、フロントエンドにタイムアウトエラーを伝えることができるが、バックエンドの実装によっては、一度走ってしまった GraphQL クエリのリゾルバの実行を止められない可能性がある。
これらは必要な設定ではあるが、コストの高いクエリの実行を制限するためには十分ではない。
Query complexity analysis
クエリの AST から複雑度、例えば木の深さ、を計算する方法。しかし、GraphQL の良さは "深い" 入れ子のクエリを実行できることだったはずで、これを制限することは SQL クエリの "join" 数を制限するのに等しい。
さらには、ここでいう「複雑なクエリ」が必ずしも多量のバックエンド操作を必要とするとは限らない。これは、リゾルバやキャッシュの実装に依存する。
Cost analysis
コストベースの考え方はかなり先進的で、フィールドごとにコストを設定し、AST を分析することで GraphQL クエリ全体のコストを見積もる。
クエリが可変数の値を返す場合は、ページ毎に利用可能な最大数を設定することで見積もりを行える。コスト単位は、生成される SQL クエリー数や、実行される REST API コール数、リゾルバーの平均実行時間等が利用できる。
ただし、効果的なコスト分析を実装するためには、設計/開発に多大な労力が必要となる。が、その実装を加速させるツールが GraphQL エコシステムに出てきつつある。
Resolver function timeout
GraphQLサーバーは API プロキシだと見做すことができる。クエリーは、GraphQL サーバーによって実行される API 関数(リゾルバー) のツリーとなる。1つのリゾルバーの実行時間が、GraphQL クエリー全体のパフォーマンスに大きく影響する。
データベースやデータストア、リモートプロシージャ等を含んだビジネスロジックが実行されるのは大抵リゾルバーなので、そのリゾルバー単位で制限をかけることも考えられる。
実用的に考えれば、リゾルバーによって呼び出される操作には短いタイムアウトを設定しておき、タイムアウト発生時にはバックエンドの操作をキャンセルし、リゾルバーは例外を投げ、対応するフィールドには null を設定し、GraphQL クエリ結果にエラーを設定する。
Resolver function operations count
リゾルバーの呼び出し回数を数える。ただし、一回のリゾルバー呼び出しが複数のバックエンド操作を呼び出すのであれば、その分回数をインクリメントする。そして、リゾルバーが実行の最大回数に到達したら、他の関連するリゾルバーの実行も停止させる。GraphQL クエリーはその時点で完了するが、いくつかのフィールドは null であり、GraphQL レスポンスにエラーが追加される。
コストベースの分析には及ばないが、有用ではある。
GraphQL query timeout
graphql-js や express-graphql ライブラリにはタイムアウトのオプションは存在しない。ので、自前で実装するしかない。
code:js
request.incrementResolverCount = function () {
var runTime = Date.now() - startTime;
if (runTime > config.graphql.queryTimeout) {
if (request.logTimeoutError) {
logger('ERROR', 'Request ' + request.uuid + ' query execution timeout');
}
request.logTimeoutError = false;
throw 'Query execution has timeout. Field resolution aborted';
}
this.resolverCount++;
};
関連ツール
コスト計算したい場合
RateLimit かけたい場合
事例
GitHub の事例
Node limit と Rate limit の2つの制限をかけている。
Node limit
first もしくは last は必須
上記の値は 1-100 の間
1 度のコールでの node の上限は 500000
Rate limit
クエリ毎にポイントを計算する
5000 ポイント/h