NoSQLクラウドデータベースの中でも、Firestoreはモバイル、Web、サーバー開発に対応する柔軟でスケーラブルな選択肢として際立っています。ただし、その豊富な機能をもってしても「Firestoreならどんな負荷でも難なくさばける」というのはよくある誤解です。理論上はそのとおりでも、実際はもう少し複雑です。たとえば目玉機能をリリース予定で、過去にトラフィックの急増を経験している場合。あるいはユーザーの利用傾向を把握していて、特定の時間帯に大幅な負荷増が見込まれる場合。本記事では、Firestoreで起こりがちなスケーリングの問題を取り上げ、それを検証して回避するための実践的な方法を紹介します。
500/50/5ルール:Firestoreスケーリングの基本
Firestoreはスケールを前提に設計されていますが、他の伸縮自在な分散システムと同じく、増え続ける負荷に追従するには時間が必要です。そこで登場するのが500/50/5ルールです。
新しいコレクションへのアクセスは毎秒500オペレーションを上限に開始し、5分ごとにトラフィックを50%ずつ増やしていく。
このガイドラインを守ることで、Firestore内部のスケーリング機構が成長に追いつけるようになり、高レイテンシやDEADLINE_EXCEEDEDエラーといったよくある問題を防げます。
k6登場:頼れる負荷テストツール
500/50/5ルールの重要性を示すために、オープンソースの負荷テストツールk6を使ったスクリプトを用意しました。k6を選ぶ理由はいくつかあります。
- JavaScriptベースのスクリプト言語で、扱いがシンプル
- リアルタイムのパフォーマンス指標と詳細な分析情報が得られる
- スケーラビリティが高く、単一インスタンスから毎秒10万〜30万リクエストを生成できる
スクリプトの内容
スクリプトはこちらで公開しています。動作の概要は次のとおりです。
初期負荷とターゲット負荷:
- 毎秒500リクエスト(RPS)からスタート
- 1500 RPSを目標に設定(もちろん自由に変更可能)
ランプアップ戦略:
- 各負荷レベルを5分間(300秒)維持
- 1分かけて負荷を50%ずつ引き上げる
- 目標RPSに達するまでこのパターンを繰り返す
ステージの動的生成:
- 必要なステージ数を自動で計算
- 「安定」と「ランプアップ」を交互に挟むステージ群を生成
- 各ステージのターゲットRPSと所要時間をログに出力して可視化
ドキュメントの選択:
- ファイル(「orders.txt」)からドキュメントIDを読み込み
- リクエストごとにIDをランダムに選択
- 本記事ではダミーデータを使用しているため、実際のユースケースでは自分でドキュメントIDを用意する必要があります
リクエストの実行:
- Firestore REST APIに対してGETリクエストを送信
- ベアラートークンによる認証を実装
- トークン取得用のスクリプトもあわせて用意
パフォーマンスのモニタリング:
- 成功した読み取り件数とエラーを計測
- 200以外のステータスコードは詳細とともにログに記録
k6をインストール(Macならbrewなどが便利)したうえで、k6 run warm-up.jsを実行すればスクリプトが動きます。スクリプトに必要なトークンはgenerate-firebase-token.pyで取得できます。どちらのスクリプトにも書き換えるべき変数があるので、エディタの「検索」機能でINSERTを探して置き換えてください。
検証実験:成功と失敗
500/50/5ルールに従った場合と無視した場合で結果がどう変わるか、2つの実験で比較しました。
実験1:失敗パターン
このシナリオでは500/50/5ルールを完全に無視し、毎秒2000リクエスト(RPS)でいきなりスタートして、5分間で2500 RPSまで引き上げました。
```js
// Warmup parameters
const initialRPS = 2000;
const targetRPS = 2500;
const stablePeriodSeconds = 300; // 5 minutes
const rampPeriodSeconds = 0;
const stageCount = Math.ceil(Math.log(targetRPS / initialRPS) / Math.log(1.5));
```
Ran between 1/1/10 between 0110 and 0115 CEST
Results:
```bash
INFO[0335] Warmup Stages: source=console
INFO[0335] Stage 1: Target RPS: 2500, Duration: 300s source=console
✗ status is 200
↳ 4% — ✓ 6408 / ✗ 123474
checks.........................: 4.93% ✓ 6408 ✗ 123474
data_received..................: 23 MB 70 kB/s
data_sent......................: 4.6 MB 14 kB/s
dropped_iterations.............: 1 0.003028/s
errors.........................: 123474 373.866077/s
http_req_blocked...............: avg=473.49ms min=0s med=0s max=59.9s p(90)=0s p(95)=0s
http_req_connecting............: avg=287.6ms min=0s med=0s max=38.39s p(90)=0s p(95)=0s
http_req_duration..............: avg=806.08ms min=0s med=0s max=1m3s p(90)=0s p(95)=2.01s
{ expected_response:true }...: avg=12.62s min=311.44ms med=9.48s max=1m0s p(90)=30.92s p(95)=36.56s
http_req_failed................: 95.06% ✓ 123474 ✗ 6409
http_req_receiving.............: avg=82.85ms min=0s med=0s max=59.4s p(90)=0s p(95)=30µs
http_req_sending...............: avg=651.35µs min=0s med=0s max=8.82s p(90)=0s p(95)=92µs
http_req_tls_handshaking.......: avg=261.72ms min=0s med=0s max=57.6s p(90)=0s p(95)=0s
http_req_waiting...............: avg=722.58ms min=0s med=0s max=1m2s p(90)=0s p(95)=1.89s
http_reqs......................: 129883 393.271845/s
iteration_duration.............: avg=32.65s min=2.58µs med=33.98s max=1m12s p(90)=48.47s p(95)=51.64s
iterations.....................: 129883 393.271845/s
successful_reads...............: 4.93% ✓ 6408 ✗ 123474
vus............................: 47 min=0 max=25000
vus_max........................: 25000 min=4179 max=25000
running (5m30.3s), 00000/25000 VUs, 129882 complete and 21 interrupted iterations
firestore_warmup ✓ [======================================] 00021/25000 VUs 5m0s 2105.47 iters/s
```
Key Visualiserで見るとこんな様子です。

結果は成功率5%未満。散々な数字です。
実験2:成功パターン
こちらでは500/50/5ルールに忠実に従い、500 RPSからスタートして約20分かけて1500 RPSまで段階的に引き上げました。
```js
// Warmup parameters
const initialRPS = 500;
const targetRPS = 1500;
const stablePeriodSeconds = 300; // 5 minutes
const rampPeriodSeconds = 60; // 1 minute
const stageCount = Math.ceil(Math.log(targetRPS / initialRPS) / Math.log(1.5));
```
Ran between 1/1/10 between 0140 and 0158 CEST
Results:
```bash
INFO[1111] Warmup Stages: source=console
INFO[1111] Stage 1: Target RPS: 500, Duration: 300s source=console
INFO[1111] Stage 2: Target RPS: 750, Duration: 60s source=console
INFO[1111] Stage 3: Target RPS: 750, Duration: 300s source=console
INFO[1111] Stage 4: Target RPS: 1125, Duration: 60s source=console
INFO[1111] Stage 5: Target RPS: 1125, Duration: 300s source=console
INFO[1111] Stage 6: Target RPS: 1500, Duration: 60s source=console
✗ status is 200
↳ 99% — ✓ 863739 / ✗ 231
checks.........................: 99.97% ✓ 863739 ✗ 231
data_received..................: 1.5 GB 1.4 MB/s
data_sent......................: 173 MB 156 kB/s
dropped_iterations.............: 20999 18.915827/s
errors.........................: 231 0.208084/s
http_req_blocked...............: avg=50.75ms min=0s med=0s max=41.09s p(90)=1µs p(95)=1µs
http_req_connecting............: avg=35.83ms min=0s med=0s max=29.93s p(90)=0s p(95)=0s
http_req_duration..............: avg=554.84ms min=0s med=334.66ms max=1m0s p(90)=728.85ms p(95)=1.29s
{ expected_response:true }...: avg=554.07ms min=304.44ms med=334.66ms max=59.46s p(90)=728.84ms p(95)=1.29s
http_req_failed................: 0.02% ✓ 231 ✗ 863739
http_req_receiving.............: avg=68.05ms min=0s med=6.92ms max=59.42s p(90)=21.82ms p(95)=160.12ms
http_req_sending...............: avg=288.4µs min=0s med=32µs max=12.11s p(90)=89µs p(95)=150µs
http_req_tls_handshaking.......: avg=16.93ms min=0s med=0s max=46.68s p(90)=0s p(95)=0s
http_req_waiting...............: avg=486.5ms min=0s med=327.66ms max=1m0s p(90)=615.6ms p(95)=943.67ms
http_reqs......................: 863970 778.261194/s
iteration_duration.............: avg=609.81ms min=2.2µs med=334.94ms max=1m0s p(90)=748.42ms p(95)=1.35s
iterations.....................: 863970 778.261194/s
successful_reads...............: 99.97% ✓ 863739 ✗ 231
vus............................: 14 min=14 max=5720
vus_max........................: 5849 min=1000 max=5849
running (18m30.1s), 00000/05849 VUs, 863970 complete and 14 interrupted iterations
firestore_warmup ✓ [======================================] 00014/05849 VUs 18m0s 1499.93 iters/s
```
Key Visualiserで見るとこんな様子です。

スケーリング開始時

スケーリング終了時
結果は成功率99.97%。圧倒的な数字です。
自分でテストを実行する
スクリプトはk6を使ってローカルで実行できます。セットアップが手軽でコストもかからないというメリットがある一方で、ローカルマシンのリソースに上限がある、インスタンスが1台しかない、ネットワーク環境に左右されるといった制約も避けられません。より精度の高い結果を得たい場合は、(Google Cloudの)VM上で実行するのがおすすめです。
500/50/5ルールは単なる推奨事項ではなく、Firestoreをスムーズかつ効率的にスケールさせるための欠かせないガイドラインです。このルールに沿ってk6のようなツールでスケーリング戦略を検証しておけば、パフォーマンス上の落とし穴を避け、サービスの成長に合わせてアプリケーションを安定して稼働させ続けられます。
データベースのスケーリングは、急がば回れ。よいスケーリングを!

— -
Firestoreのスケーリングをさらに深掘りしたい方や、クラウドインフラの最適化でお困りの方は、doit.comをご覧ください。クラウドの可能性を最大限に引き出すお手伝いをします。