Cloudflare WorkersでGraphQLリクエストをキャッシュして30msで返すようにした話

公開日: 2019年12月10日GitHub

この記事は、GraphQL Advent Calendar 2019の 5 日目の記事です。 GraphQL API のキャッシュ層を Cloudflare Workers で作成する方法を解説してみます。

サマリ

GraphQL では RESTful API における GET 系のものであっても POST で送信するという規約があります。そのため、URL だけを見てそのクエリの取得内容を知ることはできません。結果として、RESTful API のように GET である特定の URL、特定のクエリパラメータのときはキャッシュされたデータを返す、というようなことを実装することが難しい、という問題があります。

今回は、この問題を解決するためにCloudflare Workersを利用して CDN 上に GraphQL API サーバーのキャッシュレイヤを作成してみたので、その方法を紹介したいと思います。

作成したコードは GitHub に公開しているので、こちらもご参照ください。

https://github.com/wawoon/graphql-cache-worker

Cloudflare Workers とは?

Cloudflare Workers は、Cloudflare が提供している CDN のエッジサーバー上で実行される FaaS です。AWS が提供している Lambda や、GCP が提供している Cloud Functions の、エッジサーバーで実行されるバージョンだと思ってください。

何がすごいの?

  • wranglerという CLI ツールを使うことで、一瞬で FaaS アプリケーションのローカルでの開発、デプロイをすることができます。
  • javascript, webpack, wasm に対応しており、特に webpack を利用した開発の場合では、npm packages を import することができます。
    • 今回、GraphQL のクエリをパースするために、graphql-tag という npm ライブラリを使いましたが、普通に npm i をして使うことができました。
    • とても便利。
  • cloudflare を DNS に利用している場合、Cloudflare 経由で割当しているドメインへのリクエストを、すべてのこの Cloudflare Worker で intercept することができます。なので、リバースプロキシの役割をしたり、今回のようにキャッシュレイヤを作成することもできます。
  • cloudflare worker にはデフォルトで global な URL が付与されます。なので、cloudflare worker を単独で API サーバーのように活用することもできます。
  • KVS もついており、かなりスケールするのでキャッシュの保存にも使うことができ、とても使い勝手がよいです

作ったもの

https://github.com/wawoon/graphql-cache-worker

任意の GraphQL のエンドポイントに来た GraphQL のリクエストに対して、query, variables が一定であれば cache を返す worker を作成しました。 今回, https://github.com/lucasbento/graphql-pokemon で公開されている GraphQL API の前段にたてています。

実際にデプロイされている worker が以下です https://graphql-cache-worker.wawoon.workers.dev

全く同じクエリを

https://graphql-cache-worker.wawoon.workers.devhttps://graphql-pokemon.now.sh/

に投げると、480ms から 30ms まで高速化しました

元々のバージョン 480ms

キャッシュを噛ませたバージョン 30ms

何をしているのか

ここに、上記の graphql-pokemon で有効なクエリを投げると、初回のみ GraphQL API サーバーにリクエストを投げ、2 回目以降はキャッシュされたレスポンスを返します。

どうやって作るのか

https://github.com/wawoon/graphql-cache-worker に完成版のコードがあります。

ざっくりと開発の流れを説明します。

wrangler を使う

wrangler は cloudflare workers で開発をするときに使うボイラープレート作成, デプロイ, cloudflare の各種 API を叩いてくれるツールです。イメージとしては、react 開発における create-react-app です。

https://developers.cloudflare.com/workers/tooling/wrangler/commands/

インストール

1npm i @cloudflare/wrangler
2

API Key の登録

1wrangler config
2

プロジェクト作成

1wrangler generate プロジェクト名 --type="webpack"
2

また、ローカル環境ではホットリローディングを書けながら、ローカル開発環境を立ち上げることができます。

1wrangler preview --watch
2

これをすると以下の

が表示されます。 Testing タブを選ぶと、Postman のような HTTP リクエストを任意に投げることもできる。

以下のようなコードが index.js に作成されているので、これを書き換えつつ実装を進めます。

1addEventListener("fetch", (event) => {
2 event.respondWith(handleRequest(event.request));
3});
4/**
5 * Respond with hello worker text
6 * @param {Request} request
7 */
8async function handleRequest(request) {
9 return new Response("Hello worker!", {
10 headers: { "content-type": "text/plain" },
11 });
12}
13

KV を利用可能にする

Cloudflare Worker には Key-value store が付属しています。(※月 5\$からのプランに加入する必要がある)

1wrangler kv:namespace create "namespace名"
2

で KV の namespace を作成することができます。 これを実行すると、コード内でどの定数でこの namespace と binding するのかを設定するためのコードが出力されるので、wrangler.toml にコピペします。

そうすると、worker 内で以下のような API で KVS を利用することができます

1// 読み込み
2const value = await COLLECTION_NAME.get("key_name");
3
4// 書き込み
5await COLLECTION_NAME.put("key_name", value);
6
7// 書き込み(expireを指定)
8await COLLECTION_NAME.put("key_name", value, { expiration: secondsSinceEpoch });
9await COLLECTION_NAME.put("key_name", value, { expirationTtl: secondsFromNow });
10

GraphQL のリクエストから cache キーを作成する

Cloudflare Workers では POST リクエストから body を取得することができるので、POST で通常送信される GraphQL のリクエストの中身も受け取ることができます。この body から KVS に保存する際のキャッシュキーを作成します。

1// cache用のkeyを作成する
2function calcCacheKey(parsedBody) {
3 // 今回はqueryの値とvariableの値のみを利用するが、graphql-tagでastが取得できるのでクエリに応じて好きに分岐できる
4 // const ast = gql(parsedBody['query'])
5
6 const baseStr =
7 JSON.stringify(parsedBody["query"]) +
8 JSON.stringify(parsedBody["variables"]);
9
10 const cacheKey = md5(baseStr);
11 return cacheKey;
12}
13

今回は、query, variables のみでキャッシュキーを作成しましたが、graphql-tagのような AST 化を行うツールを使うことで、特定条件のときのみキャッシュさせる、などの処理も行うことが可能です。

実際に作成したコード

1const gql = require("graphql-tag");
2const md5 = require("md5");
3
4addEventListener("fetch", (event) => {
5 event.respondWith(handleRequest(event.request));
6});
7
8// https://github.com/lucasbento/graphql-pokemon をサンプル用のAPIとして利用する
9const graphqlEndpointURL = "https://graphql-pokemon.now.sh/";
10const cacheExpireSeconds = 60;
11
12/**
13 * Respond with hello worker text
14 * @param {Request} request
15 */
16
17async function handleRequest(request) {
18 console.log("Got request", request);
19 const newRequest = request.clone();
20
21 if (request.method === "POST") {
22 const body = await request.json();
23 const cacheKey = calcCacheKey(body);
24 console.log("cacheKey:", cacheKey);
25
26 const cachedValue = await GRAPHQL_CACHE.get(cacheKey);
27 if (cachedValue) {
28 console.log("there is a cached value:", cachedValue);
29 return new Response(JSON.parse(cachedValue), {
30 headers: { "content-type": "application/json" },
31 });
32 }
33
34 const response = await fetch(new Request(graphqlEndpointURL, newRequest));
35
36 // APIリクエストが成功したときのみcacheに保存する
37 if (response.status < 400) {
38 console.log("request successed, trying to write cache");
39 const resBody = await response.text();
40
41 // https://developers.cloudflare.com/workers/reference/storage/expiring-keys/
42 await GRAPHQL_CACHE.put(cacheKey, JSON.stringify(resBody), {
43 expirationTtl: cacheExpireSeconds,
44 });
45
46 console.log("succeeded to write cache");
47 return new Response(JSON.stringify(resBody), response);
48 }
49
50 console.log("error occurred", response);
51 return response;
52 }
53
54 const response = await fetch(request);
55 return response;
56}
57
58// cache用のkeyを作成する
59function calcCacheKey(parsedBody) {
60 // 今回はqueryの値とvariableの値のみを利用するが、graphql-tagでastが取得できるのでクエリに応じて好きに分岐できる
61 // const ast = gql(parsedBody['query'])
62
63 const baseStr =
64 JSON.stringify(parsedBody["query"]) +
65 JSON.stringify(parsedBody["variables"]);
66
67 const cacheKey = md5(baseStr);
68 return cacheKey;
69}
70

終わりに

GraphQL のリクエストがデータ取得系であっても POST であるため、キャッシュを作成することができない、というモチベーションから今回の記事を作成しました。 Cloudflare Workers を利用するとかなり自由にキャッシュ設計をすることができるので、同じような悩みがある方はぜひ試してみることをおすすめします。

参考

https://github.com/cloudflare/wrangler https://developers.cloudflare.com/workers/tutorials/build-an-application/

This site uses Google Analytics.
source code