GraphQL でのパフォーマンス関連の問題について色々紹介する

こんにちわ。[Openlogi Advent Calendar](https://qiita.com/advent-calendar/2017/openlogi) 18日目です。

[前回](https://qiita.com/haradakunihiko/items/690add60ecb9507688b8)、[前々回](https://qiita.com/haradakunihiko/items/a91a66e35031212023e3)と、GraphQLの実装について書きました。引き続きGraphQLについてです。

GraphQL で一番語られがちなのは、REST APIではできない柔軟なクエリについてですが、実際にアプリケーションで利用するにはいくつか超えなければならないハードルがあります。

個人的に上げるとするならば、認証、認可、パフォーマンスといったところでしょうか。本記事ではこのうちパフォーマンス周りのアプローチについて紹介していこうと思います。

さて、RESTの場合、特定のエンドポイントについて、取得するデータ量や、どの程度の時間がかかるかを想定するのは特に困難ではありません。しかし、GraphQLではクライアントサイドで取得すべきデータを定義するため、非常に高負荷なクエリが実行される可能性があります。

例えば、前回の記事では以下のようなスキーマを例に挙げました。
ここでは、ユーザーと、そのユーザーの閲覧できるフィードが存在することを表しています。

```
type Query {
  viewer: User
}
type User {
  id: String
  name: String
  feeds: [Feed]
}
type Feed {
  id: String
  title: String
  body: String
  author: User
}
```

こんなクエリを書いて、トップページのタイムラインを表示することを想定してます。

```
query {
  viewer {
    id
    name
    feeds {
      title
      body
      author {
        id
        name
      }
    }
  }
}
```


しかし、一方このスキーマに対しては、このようなクエリを書くことも可能です。（他人のフィードの一覧が見れるべきかというのはここでは気にしないことにします。）

```
query {
  viewer {
    id
    name
    feeds {
      title
      body
      author {
        id
        name
        feeds {
          title
          body
          author {
            id
            name
            feeds {
              // 以下無限ループ
            }
          }
        }
      }
    }
  }
}
```

このように、GraphQLでは数千・数万のデータベースアクセスが簡単に発生させることができる危険をはらんでいます。ということで、GraphQLのパフォーマンスについてのアプローチをいくつか紹介します。

## クエリにバリデーションをかける

### リクエストサイズのバリデーション
まず低レイヤのアプローチとしては、リクエストに送られてくるクエリのサイズで制限をかける方法があります。単純にクエリのサイズで制御をかけましょう。

### クエリの構造に対するバリデーション

もう少し複雑に行うのであれば、クエリのネストの階層を見る方法もあります。GraphQLのクエリはASTに変換して表現できるため、一定以上の深さは許可しないという制御をかけることも有用ですし、特定の構造のクエリを許可しないようにしても良いかもしれません。

## タイムアウトを行う
各Resolverの処理で、タイムアウトをかけることも有用です。[GraphQL query timeout and complexity management](https://medium.com/workflowgen/graphql-query-timeout-and-complexity-management-fab4d7315d8d) にあるので詳細はこちらを。一定時間を超えた場合に処理を止め、以降のResolverが発火しないようにするアプローチです。


## 永続化クエリ
問題なのは、想定していないクエリが送られてくる可能性があるということです。なので、あらかじめ決められたクエリのみを許可しましょうというアプローチです。
これは、[persistgraphql](https://github.com/apollographql/persistgraphql)のようなライブラリを使うことで実現できますが、雑にまとめると、

1. クライアントサイドでビルド時に利用するクエリの一覧を取得する。
2. それぞれのクエリをハッシュ化し、実際のクエリとハッシュ値とのマッピングを作成する
3. クライアントサイドからはハッシュ値を送り、サーバーサイドでクエリを解決して実行する

といった事を行います。


## ネットワークパフォーマンス

GraphQLでは取得するデータの構造をクライアント側からリクストに含めるため、クエリのリクエストサイズが膨大になるいう問題が発生しがちです。これは上述の永続化クエリでハッシュ値を利用することでも対策できますが、後述の[Apollo Engineのauto-persisted-query](https://www.apollographql.com/docs/engine/auto-persisted-queries.html)というものもあります。
これは、上述の永続化クエリとは異なり、リクエストサイズの削減のみを目的としています。初回実行時にサーバーサイドでクエリとハッシュ値をキャッシュしておくことで、2回目以降はクライアントからハッシュ値のみを送ることでクエリの実行を行うことを可能にします。

## n + 1 問題
少し毛色の違う内容ですが、GraphQLでは基本的にEager loadingのような仕組みを利用することができないため、単純に実装すれば必ず **n + 1 問題**が発生します。Facebookの [dataloader](https://github.com/facebook/dataloader) はこの問題をシンプルに解決してくれます。js以外でも同じようなライブラリがあるため探してみてください。


# 計測について
いずれのアプローチをとるにしろ、パフォーマンス改善には計測が必要です。
GraphQLの計測は、[Apollo Engine](https://www.apollographql.com/engine)というサービスを利用して非常に簡単に行うことができるのでこれを紹介します。

## 何ができるか
Apollo Engineでは、以下のような情報を見ることができます。

### クエリの実行日時、処理時間の情報
<img width="1043" alt="スクリーンショット 2017-12-18 0.07.24.png" src="https://qiita-image-store.s3.amazonaws.com/0/26136/c9bbdcb6-35ed-e710-0315-6cdc36994baf.png">


### 各クエリの処理時間とその件数の統計
<img width="1048" alt="スクリーンショット 2017-12-17 23.14.59.png" src="https://qiita-image-store.s3.amazonaws.com/0/26136/464d9a59-f1f1-d7d4-6285-d272313dd170.png">

### 各ノードに関する処理時間の統計
<img width="1067" alt="スクリーンショット 2017-12-17 23.15.51.png" src="https://qiita-image-store.s3.amazonaws.com/0/26136/80fa2aa3-164d-07a1-7b11-ed792d84aad1.png">

### 特定のリクエストについて、各ノードの処理時間
<img width="985" alt="スクリーンショット 2017-12-17 23.15.17.png" src="https://qiita-image-store.s3.amazonaws.com/0/26136/d7db0839-72a7-51e4-6f38-366d8e7f0488.png">

そのほか、エラーの発生率などの情報なども確認することができます。ちなみに1M Request/Monthまでが無料となっています。開発環境だけでも、問題のあるクエリが簡単に発見できるので入れておいて損はないです。

nodejsであれば、こんな感じで埋め込むことができます。

```js

var { Engine } = require('apollo-engine');

const engine = new Engine({
  engineConfig: {
    apiKey: process.env.APOLLO_ENGINE_API_KEY,
    logging: {
      level: 'DEBUG'
    }
  },
  graphqlPort: PORT,
  endpoint: '/graphql',
  dumpTraffic: true
});
engine.start()

app.use(engine.koaMiddleware());
```

# 最後に
AWSの[App Sync](https://aws.amazon.com/jp/appsync/)でも[Apollo](https://www.apollographql.com/)を利用した、オフライン対応やリアルタイムアップデートの機能が発表されました。これを機にGraphQLのエコシステムがさらに成長してくれるといいですね。