この記事はJX通信社Advent Calendar&GraphQL Advent Calendarの1日目です。
JX通信社でNewsDigestというアプリを開発しているyamitzkyです。
NewsDigest では、アプリから利用する API に GraphQL を利用 しています。本番での利用を始めてからちょうど1年を過ぎました。
JX 通信社ではプログラミング言語として Python が使われることが多く、この GraphQL API も Python で作ってサーバーレス環境(AWS Lambda)にデプロイ していました。しかし、Lambda では要件が合わなくなってしまったため、現在では Amazon ECS で作った Docker クラスタ内で動いています。また、非サーバーレス化に合わせて、パフォーマンス要件を満たすために Go でのリプレイスを行いました。
この マイグレーションに伴って最も困難だったところがパフォーマンスチューニング です。 今回の記事では、Go で作った GraphQL API をどのようにパフォーマンスチューニングしたのかを紹介します。
ライブラリ選定
GraphQL の API は、一般的な API とは異なり、リクエストのパースやレスポンスの構築が難しい 、という点が挙げられます。RESTful API であれば「JSON」や「URLに対して正規表現をかける」など、言語標準のライブラリだけで簡単に実現しやすいのですが、GraphQL の場合は GraphQL の専用のスキーマやクエリなどの仕様があります。そのため、 ライブラリ選定が重要 になってきます。
NewsDigest では、 99designs/gqlgen というライブラリを選定しました*1。gqlgen はスキーマファーストで、冗長なボイラープレートが少なく、type safe で、 検証したライブラリの中で最も良いパフォーマンス (ns/op)でした。個人的な意見ですが、Go で GraphQL をやるのであれば、 gqlgen 一択だと思います。
他に検討したライブラリは以下のとおりです。*2
- graphql-python/graphene:元々利用していたライブラリ。パフォーマンスが悪かったため不採用
- graphql-go/graphql:スキーマ定義を Go でやる必要があり冗長だったのと、型安全ではなかったため不採用
- graph-gophers/graphql-go:一個一個の resolver 定義が必要で冗長だったため不採用
- samsarahq/thunder:パフォーマンスは gqlgen よりも少し良かったが、Interface に対応しておらず、移行できなかったため不採用
- playlyfe/go-graphql:メンテが止まっていたため不採用
GraphQL API のパフォーマンスチューニング
ユーザー体験を最大化するためには、なるべく API のレスポンスにかかる時間を短くしたいです。
通常の API のチューニングであれば、「どのエンドポイントが遅いのか?」をまず探ると思うのですが、GraphQL API の場合は /graphql
という単一のエンドポイントしかありません。
そこで、GraphQL の API でボトルネックを探る際には、 「どのリソースが遅いのか」をトレーシング するための、専用のツールなどを入れる必要があります。
gqlgen のトレーシング
GraphQL のパフォーマンスのメトリクスを取る際は、一般的には Apollo Tracing などが使われることが多いと思います。Apollo Tracing を使うと、どのリソースの解決に時間がかかっているかなどがよくわかります。 (この例では、 book のフィールドに author というものがあります)
しかし、Apollo Tracing への対応は、ライブラリ依存です。2018年3月当時は、gqlgen が対応していなかったため、この方法でのトレーシングはできませんでした*3。そのかわり OpenTracing というトレーシングに当時から対応していたため、こちらで対応することにしました。
OpenTracing / 分散トレーシング
OpenTracing は分散トレーシング(Distributed Tracing)のための規格のようなものです。あくまで規格なので、OpenTracing 自体はプログラムや個別の OSS ではありません。Jaeger や Zipkin のような OSS や、DATADOG のようなウェブサービスが、OpenTracing の規格に対応しています。
分散トレーシングというのは、一般的にはマイクロサービスのためのトレーシングに使われます。マイクロサービスの計測では、「ある1つのリクエスト」が、複数のマイクロサービスへのリクエストになり、ボトルネックが探りにくい、という問題があります。まさに、GraphQL API の計測が抱えていた問題と一緒です。
そこで、どの通信にどれくらい時間がかかったかや、どこで失敗したかを探りたい、というのが分散トレーシングの目的となります。
Jaeger の選定
OpenTracing は規格でしかないので、gqlgen の計測をする OSS の選択肢はいくつかあります。その中でも、Jaeger という OSS を選定しました。今回は、分散トレーシング自体初めてでいろいろわからなかったというのもあり、DATADOG のようなサービスは選定から外していました。
- Go 製の OSS
- ストレージとして Elasticsearch と Cassandra に公式対応
- Go、Python、Java、Node、C/C++ などに対応
- HTTP だけでもメトリクスを集められる*4
- アイコンが可愛い
分散トレーシングは個別のリクエストのトレーシングが注目されることが多く、「全体的にはどのリソースが遅いのか?」という統計的なものを得られる OSS は少ないのではないかと思います。その点 Jaeger は Elasticsearch に対応しており、 Kibana 上で集計して見ることもできるので良かったです。
Jaeger と Elasticsearch によるボトルネックの確認
こちらは実際の Jaeger の画面です(一部加工済み)。こちらの画面を見ると、どこでエラーが起きていて、どこの処理に時間がかかっているのがわかります。この場合、Query_piyo
(piyo
というリソース)の redis
の処理が、ボトルネックとなっていることがわかります。
ただしここでわかるのは、あくまで個別のリクエストについてです。そこで、Kibana を使って、全体的なメトリクスを確認します(operationName で絞るのがポイントです)。こうしてみると、全体的には hoge
のリソースの取得遅いようです。
(一部加工済みです)
Jaeger を入れてみてわかったこと
Jaeger を試してみてわかったのは、分散トレーシングの仕組み自体は、「GraphQL API」や「マイクロサービス」に限って便利なわけではない 、ということです。GraphQL であれば「どのリソースがボトルネックか」を知りたく、マイクロサービスであれば「どのマイクロサービスがボトルネックか」を知りたいのと同様、一般的なモノリスな API であっても「DB がボトルネックか、Redis がボトルネックか、アプリケーションがボトルネックか...」というのは知りたい情報です。実際、NewsDigest での利用方法でも、Redis やデータベースのアクセスのタイミングでトレーシングを仕込んでおり、Redis がボトルネックであることに気づいたりもしました。
APM サービスは他にもあるので、分散トレーシングの仕組みをわざわざ入れなくてももっと賢い方法はあるかもしれませんが、トレーシングが規格化されているのは特定のウェブサービスに依存しなくて済むのでいいなと感じました。
余談
今年の ISUCON は、GraphQL API のチューニング・・・とまではいかないまでも、マイクロサービスがお題になったりしないかな、と予想していました(笑)
JX 通信社では GraphQL API をもっと速くしてくれるサーバーサイドエンジニアを募集中です。
*1:当時は、vektah/gqlgen でした
*2:2018年3月に検証したため、現在は異なる可能性があります
*3:検証していませんが、現在は対応済みのようです。ありがとうございます。 https://github.com/99designs/gqlgen/pull/404
*4:一般的には、 HTTP 通信でマイクロサービス用の計測をすると遅い(無駄)なので、 udp を使うことが多いと思います。JX のインフラは AWS の Application Load Balancer を使うことが多いので、 HTTP で集められるのは助かりました