Cloudflare WorkersでGraphQLリクエストをキャッシュして30msで返すようにした話
この記事は、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.dev と https://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/wrangler2
API Key の登録
1wrangler config2
プロジェクト作成
1wrangler generate プロジェクト名 --type="webpack"2
また、ローカル環境ではホットリローディングを書けながら、ローカル開発環境を立ち上げることができます。
1wrangler preview --watch2
これをすると以下の
が表示されます。 Testing タブを選ぶと、Postman のような HTTP リクエストを任意に投げることもできる。
以下のようなコードが index.js に作成されているので、これを書き換えつつ実装を進めます。
1addEventListener("fetch", (event) => {2 event.respondWith(handleRequest(event.request));3});4/**5 * Respond with hello worker text6 * @param {Request} request7 */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");34// 書き込み5await COLLECTION_NAME.put("key_name", value);67// 書き込み(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'])56 const baseStr =7 JSON.stringify(parsedBody["query"]) +8 JSON.stringify(parsedBody["variables"]);910 const cacheKey = md5(baseStr);11 return cacheKey;12}13
今回は、query, variables のみでキャッシュキーを作成しましたが、graphql-tagのような AST 化を行うツールを使うことで、特定条件のときのみキャッシュさせる、などの処理も行うことが可能です。
実際に作成したコード
1const gql = require("graphql-tag");2const md5 = require("md5");34addEventListener("fetch", (event) => {5 event.respondWith(handleRequest(event.request));6});78// https://github.com/lucasbento/graphql-pokemon をサンプル用のAPIとして利用する9const graphqlEndpointURL = "https://graphql-pokemon.now.sh/";10const cacheExpireSeconds = 60;1112/**13 * Respond with hello worker text14 * @param {Request} request15 */1617async function handleRequest(request) {18 console.log("Got request", request);19 const newRequest = request.clone();2021 if (request.method === "POST") {22 const body = await request.json();23 const cacheKey = calcCacheKey(body);24 console.log("cacheKey:", cacheKey);2526 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 }3334 const response = await fetch(new Request(graphqlEndpointURL, newRequest));3536 // APIリクエストが成功したときのみcacheに保存する37 if (response.status < 400) {38 console.log("request successed, trying to write cache");39 const resBody = await response.text();4041 // https://developers.cloudflare.com/workers/reference/storage/expiring-keys/42 await GRAPHQL_CACHE.put(cacheKey, JSON.stringify(resBody), {43 expirationTtl: cacheExpireSeconds,44 });4546 console.log("succeeded to write cache");47 return new Response(JSON.stringify(resBody), response);48 }4950 console.log("error occurred", response);51 return response;52 }5354 const response = await fetch(request);55 return response;56}5758// cache用のkeyを作成する59function calcCacheKey(parsedBody) {60 // 今回はqueryの値とvariableの値のみを利用するが、graphql-tagでastが取得できるのでクエリに応じて好きに分岐できる61 // const ast = gql(parsedBody['query'])6263 const baseStr =64 JSON.stringify(parsedBody["query"]) +65 JSON.stringify(parsedBody["variables"]);6667 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/