Serverless な キャッシュサービスである Momento を本番環境で使ってみた

# Momento いいぞ

Momento いいぞ。
何がいいって、サーバレスの定義が本当に好き。完全に同意。

https://jp.gomomento.com/blog/fighting-off-fake-serverless-bandits-with-the-true-definition-of-serverless/

特に自分でわざわざ Serverless って名乗っているサービスには偽サーバレス賊が多い気がしますねぇ笑

## スライドを元に簡単に説明を残しておきます。

このお話はこれまで勉強会で3回ほどお話させていただいているので、そのスライドを元にブログとしても残しておきたいと思います。

初演は [ServerlessDays Tokyo 2023](https://serverless.connpass.com/event/290640/) の CfP に出して通ったものでした。

その後、[JAWS-UG 札幌支部](https://jawsug-sapporo.doorkeeper.jp/events/162275) や [もめんと Meet-up](https://momentocommunity.connpass.com/event/301064/)でもお話させていただいたものです。

また、Momento さんにも事例として掲載していただいております。

https://jp.gomomento.com/case-studies/htb/

初演の ServerlessDays のものをベースに、もろもろ補足しながら進めていければと思います。


<div style="width: 50%">
<script async class="speakerdeck-embed" data-id="34087a16bb5545b38c9714912f7fc253" data-ratio="1.4" src="//speakerdeck.com/assets/embed.js"></script>
</div>

### 動画配信サービスのお話です。

動画配信サービス「hod」において、スパイクが予測されたので、それを回避するために頑張ったお話です。
 **AppSync** と **DynamoDB** からデータをとってくる構成になっているのですが、これを安易に拡張し続けたので、AppSyncへのスパイクリクエストが問題となりそう。というところから始まります。

 動画配信サービスですけど、動画周りのお話はほとんど出てこず、フロントから AppSync にきたスパイクを捌きましょう。という、お話です。

### 前提として、覚えておいてほしいAWSのquota

[![thumbnail](https://files.speakerdeck.com/presentations/34087a16bb5545b38c9714912f7fc253/slide_3.jpg?27125351)](https://speakerdeck.com/miu_crescent/dong-hua-pei-xin-sabisuno-ren-qi-fan-zu-pei-xin-nosupaikuakusesuni-sabaresukiyatusiyudeli-tixiang-kau?slide=4)

- AppSync
  - Rate of request tokens 2,000/s
- DynamoDB Hot Partition
  - 3,000 read/s
  - 1,000 wright/s

AppSync はトークンという単位が出てくるので、なかなか事前の設計が難しいです。実際に消費されたトークン数は **AppSync** を叩いた時のレスポンスのなかに、`X-Amzn-Appsync-Tokensconsumed:`として入ってくるので、それで確認することが可能です。

https://docs.aws.amazon.com/general/latest/gr/appsync.html

:::note info
**2024 年 1 月 10 日に AWS AppSync はリクエストトークンのデフォルトクォータを 1 秒あたり 2,000 から 10,000 へ引き上げる予定です。**
というメッセージが届いていたため、変更される予定があるようです。時期が来たらまた確認してみます。
:::

DynamoDBのホットパーテーションは、最初からちゃんと設計しておくと回避が可能なのですが、それを知らずに設計していたので、他の方法で回避する必要がありました。

https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-partition-key-design.html

### 全体のアーキテクチャ

すごいざっくりですが、全体のアーキテクチャです。

[![thumbnail](https://files.speakerdeck.com/presentations/34087a16bb5545b38c9714912f7fc253/slide_10.jpg?27125358)](https://speakerdeck.com/miu_crescent/dong-hua-pei-xin-sabisuno-ren-qi-fan-zu-pei-xin-nosupaikuakusesuni-sabaresukiyatusiyudeli-tixiang-kau?slide=11)

* 提供サービス
  * hod
    * 動画配信サービス
    * 2022年3月にフルスクラッチでリニューアル
  * HTBオンラインショップ
    * 2022年4月にフルスクラッチでリニューアル
* フロント
  * **React** + **TypeScript** 
  * **Amplify Hosting** に置いてます
  * 決済は **Stripe**
  * 認証認可は **Auth0**
* バックエンド
  * 両方のサービスの共通BFFとして **AppSync** と **DynamoDB**
  * ビジネスロジックは基本的に **Step Functions**
  * まれに **API Gateway** + **Lambda** もいます
    * Lambda のランタイム は 8割くらいが node.js
    * 一部、python もあります。
    * これは実装のしやすさで変えたので特に深い意味はないです
  * CMSとして **kintone** を使っています
    * JavaScript だけでカスタマイズができるというのが採用理由
    * ほぼ「Headless CMS」のような構成
    * DynamoDBとkintoneのデータの2重管理が辛い

### どうでしょうの新作がやってきた

2023年8月から「水曜どうでしょう」という番組の新作が放送されました。それに伴い放送直後からこの「hod」でも配信するということで、
放送直後のトラフィックがスパイクすることが予想されました。というか、いままで全部スパイクしてまして、

[![thumbnail](https://files.speakerdeck.com/presentations/34087a16bb5545b38c9714912f7fc253/slide_14.jpg?27125362)](https://speakerdeck.com/miu_crescent/dong-hua-pei-xin-sabisuno-ren-qi-fan-zu-pei-xin-nosupaikuakusesuni-sabaresukiyatusiyudeli-tixiang-kau?slide=14)

リニューアルする前はオンプレサーだったので、増強するにも限度があり、なかなか対応が難しかったのですが、
オンデマンドサービスを謳っている以上、好きな時にいつでも見れるようにサービスを提供したい、という強い思いもありまして
このスパイクを乗りこなす方向に舵を切ります

過去のデータはあまり残っていなかったのですが、いろいろ紐解いて **5,000TPS** はさばけるようにしよう。という目標を立てました。

[![thumbnail](https://files.speakerdeck.com/presentations/34087a16bb5545b38c9714912f7fc253/slide_17.jpg?27125362)](https://speakerdeck.com/miu_crescent/dong-hua-pei-xin-sabisuno-ren-qi-fan-zu-pei-xin-nosupaikuakusesuni-sabaresukiyatusiyudeli-tixiang-kau?slide=17)

### AppSync のトークン消費量がすごく多い

Amplify CLI で DynamoDB は全て定義しているのですが、`@connection ディレクティブ`というテーブル同士のリレーションをつけてデータをまとめて引っ張ってくることを可能にする手法があるのですが、機能追加のたびにこの方法でどんどんテーブルを増やしておりました。

:::note info
`@connection` は GraphQL Transformer v1 の記述方法です。
v2 で代わりに `@hasOne`や`belongsTo`など、もう少し細かい記述に変わりました。
https://docs.amplify.aws/javascript/tools/cli/migration/transformer-migration/
:::
フロントエンドでは `recoil` という状態管理ライブラリを使用しています。
現状最初のロードで必要な番組情報などのデータを全てロードして、recoilに入れ、その後のロードをできるだけする無くするという方法をとっています。
これは Core Web Vitals な観点からすると逆効果ですが、**画面遷移時にAPIに対してリクエストを行わない** という形を作ることで、スパイク対策を施す部分を限定的にすることができるため、有効な方法だったと考えます。

[![thumbnail](https://files.speakerdeck.com/presentations/34087a16bb5545b38c9714912f7fc253/slide_21.jpg?27125362)](https://speakerdeck.com/miu_crescent/dong-hua-pei-xin-sabisuno-ren-qi-fan-zu-pei-xin-nosupaikuakusesuni-sabaresukiyatusiyudeli-tixiang-kau?slide=21)

ということで、最初のロードで番組データ(10MBくらい)を一回でロードしてくるのですが、この時AppSyncのリゾルバーがDynamoDBからデータを取得してくれます。

* 1クエリで 3000以上のリゾルバーが解決されている
* これにより、多くのAppSyncのリソースが使われるため、トークンが200前後消費されている
  * 通常の場合1クエリで1トークンなので、通常の200倍のトークンを使っている
  * また、デフォルトでquota が2000なので10人アクセスしたら溢れちゃうことに。
* DynamoDB も各エピソードの情報が入っているテーブルにアクセスが集中するの
  * ここがホットパーテーション的にはボトルネックになりそう。

AppSyncのトークンはある程度の負荷をかけると、裏側がスケールアウトするのか、トークン消費量がへるという傾向があったので、いろいろ試しまし方、100トークンくらいまでしか下がりませんでした。

これだと全然足りません。

### AWS のキャッシュサービスを考える

我々はサーバレスサービスしか使わないという誓約と制約でやっているので、採用できるものがなかったです。

* ElastiCache はVPCが出てくるので不採用
  * （最近出たElastiCache Serverless も定義からズレるので不採用）
* DAXも同様の理由で不採用
* AppSync の Cache も不採用
  * 一応試したけど、トークン消費量の低減には影響がなかった
  * DynamoDBへの負荷をオフロードすることだけ可能なのはわかった
  * 時間課金はNotサーバレス

### ホントのサーバレスMomento

* Momento の中の人が来日している時に何回か一緒に飲んでたのでサービスは知ってた
* いつも相談している Serverless Operations のキムさんにあれこれ相談
* ちょうどGAされたAppSyncの **Merged API** を活用して 既存のものに影響を与えないように枝分かれを増やすことに
* 後は AppSync のリゾルバーとして Lambda を動かして、そこから Momento CaChe とやりとりをするという形
* 大体2週間くらいでできた

[![thumbnail](https://files.speakerdeck.com/presentations/34087a16bb5545b38c9714912f7fc253/slide_45.jpg?27125362)](https://speakerdeck.com/miu_crescent/dong-hua-pei-xin-sabisuno-ren-qi-fan-zu-pei-xin-nosupaikuakusesuni-sabaresukiyatusiyudeli-tixiang-kau?slide=45)

### 工夫したところ

#### Lambda　のペイロード制限

今回扱うデータは10MB前後あるんですが、Lambdaは同期呼び出しで6MBまでという制限があるため、データを返すことができないので、なんとかする必要があります。
また、Lambdaでレスポンスストリーミングという方法もありますが、AppSyncのリゾルバーとしては使えませんでした。

これを解決するために、圧縮とチャンク分割を行なって、Momento にキャッシュさせる方法を採用しました。

[![thumbnail](https://files.speakerdeck.com/presentations/34087a16bb5545b38c9714912f7fc253/slide_50.jpg?27125362)](https://speakerdeck.com/miu_crescent/dong-hua-pei-xin-sabisuno-ren-qi-fan-zu-pei-xin-nosupaikuakusesuni-sabaresukiyatusiyudeli-tixiang-kau?slide=50)

#### Lambda のバースト

Lambdaは全体の同時実行数によるリミットだけではなく、バースト耐性というものも存在するため、上限緩和申請が通ったとしてもそこまで一気に増えるわけでhなくスロットルが発生します。

この辺りはAWS 下川さんの投稿に詳しくありますのでご一読ください

https://aws.amazon.com/jp/builders-flash/202212/lambda-burst-resistance/?awsf.filter-name=*all

このブログにある通り、Provisioned Concurrency で対応することにしました。
（時間課金は辛い、、）


:::note info
1分間で500同時実行のスケーリングでしたが、re:Invent 2023 が終わってから、アップデートがあり、だいぶ早くなったようです。これも後で検証してみたいですね
https://aws.amazon.com/jp/about-aws/whats-new/2023/12/aws-lambda-functions-scale-up/
:::

#### Momento の上限緩和

今回関係する部分では、以下のテーブルにあるリミットに対して上限緩和をする必要がありました。

|Momento キャッシュの上限| 値|
|:-|:-|
|キャッシュあたりのAPIレート（データプレーン）|100 rps|
| キャッシュあたりのスループット|1MB/s|
|アイテムの最大サイズ|1MB|

ドキュメントにもある通り、リミットは基本的には変更可能なソフトリミットです

https://docs.momentohq.com/ja/cache/manage/limits

キムさんがいろいろと間を取り持っていただいて、無事に上限緩和の対応をしていただけました。
本当に感謝です。

[![thumbnail](https://files.speakerdeck.com/presentations/34087a16bb5545b38c9714912f7fc253/slide_54.jpg?27125362)](https://speakerdeck.com/miu_crescent/dong-hua-pei-xin-sabisuno-ren-qi-fan-zu-pei-xin-nosupaikuakusesuni-sabaresukiyatusiyudeli-tixiang-kau?slide=54)

### モニタリングと負荷試験

AWS が配布している「Distributed Load Testing」を使ってGraphQLに対して負荷試験を行いました

https://aws.amazon.com/jp/solutions/implementations/distributed-load-testing-on-aws/

Momento もプランをあげることで、CloudWatch に直接メトリクスを流し込むことが可能になるので、こちらも合わせて監視項目としました。

Momento にデータの取得をオフロードできたおかげでAppSyncのトークン消費量も大きく減らすことができ、結果的に無事に目標であった 5,000 TPS でも無事に動くことを確認することができました。

このとき驚きだったのが、負荷をかけるとレスポンスが早くなるMomentoです。実に頼もしい。

[![thumbnail](https://files.speakerdeck.com/presentations/34087a16bb5545b38c9714912f7fc253/slide_64.jpg?27125362)](https://speakerdeck.com/miu_crescent/dong-hua-pei-xin-sabisuno-ren-qi-fan-zu-pei-xin-nosupaikuakusesuni-sabaresukiyatusiyudeli-tixiang-kau?slide=64)

最終的な対応としては

* AppSync の Rate of request tokens は 20,000 Token/s までは上げてもらいました
  * Momento の実装をしながらもしものためにへ移行して対応。
  * AWSの川原さん布村さん本当にありがとうございました。
* AppSyncのトークン消費量は200から1まで減少
  * Momentoにオフロードしたおかげ
* この時Lambdaも多くの同時実行数が必要になる
  * Concurrent executions は 20,000 まで段階的に上限緩和
  * バーストによるスロットリングを回避するために Provisioned Concurrency を設定
* DynamoDB の ホットパーテーションは Momento がいるので大丈夫
  * データの更新時だけDynamoのデータを読みにいくようにしたので、DynamoDBはほとんど叩かれない。

これで、準備万端です。後は当日を迎えるだけ

### 結果

無事に陥落せず！
しかし、データをよく見てみると

[![thumbnail](https://files.speakerdeck.com/presentations/34087a16bb5545b38c9714912f7fc253/slide_72.jpg?27125362)](https://speakerdeck.com/miu_crescent/dong-hua-pei-xin-sabisuno-ren-qi-fan-zu-pei-xin-nosupaikuakusesuni-sabaresukiyatusiyudeli-tixiang-kau?slide=72)

どうやら秒と分が違っていたようで、60倍以上も構えていてしまったようでした。笑

今すぐ見たい！というユーザーの方にサービスを提供できたことがなにより嬉しかったです。

## まとめ

* サーバレスキャッシュがやってきた！
  * 全体のキャッシュ戦略をしっかり考えて再設計したい
  * Momento の使い方を改めて考えていきたい
  * キムさん本当に何から何までありがとうございます。
* NoSQL の設計を今一度
  * `@connection`を多用して大変なことになったので、再設計したい
  * Amplify CLI 縛りで続けるか
  * もしくはリゾルバー自分で書いちゃうか、、
* 上限緩和は大変
  * 実際の負荷試験を繰り返し実施する必要があったのでどうしても時間がかかってしまう
  * AWSの川原さん布村さんに本当に感謝です


みんなもMomento使ってみてね！

[![thumbnail](https://files.speakerdeck.com/presentations/34087a16bb5545b38c9714912f7fc253/slide_67.jpg?27125362)](https://speakerdeck.com/miu_crescent/dong-hua-pei-xin-sabisuno-ren-qi-fan-zu-pei-xin-nosupaikuakusesuni-sabaresukiyatusiyudeli-tixiang-kau?slide=67)