Cloud Intelligence™Cloud Intelligence™

Cloud Intelligence™

Firestoreスケーリング:500/50/5ルールと検証方法

By Matthias BaetensOct 28, 20246 min read

このページはEnglishDeutschEspañolFrançaisItalianoPortuguêsでもご覧いただけます。

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をご覧ください。クラウドの可能性を最大限に引き出すお手伝いをします。