Cloud Run SQLite Persistence: GCS Mount vs. Litestream Performance Comparison (Part 1)
Cloud Run の Google Cloud Storage マウントと Litestream のパフォーマンス比較(1回目)
結論
Cloud Run の Google Cloud Storage マウントと、Litestream のどちらの手法も、130 req/s 以上を受付可能で、SQLite3 データベースも永続化できた。データベースへの更新を伴うようなリクエストでも、たった1台で 130 req/s 程度は捌くことができる。
ただし、Google Cloud Storage マウントの場合は、コンテナ起動中に新リビジョンを作成すると、データ損失の恐れがある。
3秒のテストでは短すぎるため、再実験が必要そうである。また、レイテンシを下げるため、Compute Engine VM を使って再実行する予定である。
今回の実験では、Litestream を第1世代のインスタンスで実行した場合に、900並列のリクエストを 293.4 req/s で捌くことができた。
今回実験した範囲では、手法間・世代間で倍以上パフォーマンスが違うということにはならなそうだ。
本記事の構成
- 結論
- 本記事の構成
- 参考文献
- はじめに
- パフォーマンス比較
- 仕様
- Cloud Run の Google Cloud Storage マウント
- Litestream
- 設定概要
- Cloud Run の Google Cloud Storage マウント
- Litestream
参考文献
- Cloud Run コンテナ ランタイムの契約https://cloud.google.com/run/docs/container-contract?hl=ja
- Litestream How-To Guideshttps://litestream.io/guides/
- litestream-rubyhttps://github.com/fractaledmind/litestream-ruby
- HerokuからCloud Run + Litestreamへ移行したhttps://memo.yammer.jp/posts/cloud-run-litestream
- 北陸三県.rb Lightning Talks in Kanazawa に参加してきたhttps://muryoimpl.com/blog/2025-01-20/participated-in-hokuriku-3-ken-lightning-talks/
- Litestream で思ったよりお金が掛かっているとき、もしかしたら昔のデータが消えていないかもよhttps://zenn.dev/karno/articles/eb580d347d7d86
- Rails 8はSQLiteで大幅に強化された「個人が扱えるフレームワーク」(翻訳)https://note.com/yasslab/n/n89d6850e296d
はじめに
Rails 8 では、SQLite のサポートが手厚くなり、「本番環境も SQLite で動かして良いよ」という状態である。
Cloud Run は、Google Cloud の Serverless Computing Platform であり、使用していないときにはコンテナが0にスケールダウンし、リクエストが来たときにコンテナが起動する仕組みが実現できる。アプリケーション サーバーには「状態」を持たないのが理想だが、何らかのデータベースが必要なサービスが多い。どのように永続化するかはなかなか面白い問題である。Cloud SQL のような RDBMS を使うのが一般的だが、こちらは「起動」し続けて使うことが多く、DB がゼロ スケールしないのである。Firestore のような Serverless なデータベースを用いる戦略もなくはない。
一定注目されているのが、コンテナ数を最大1に制限し、SQLite をバックエンド データベースとして使用しつつ、Cloud Storage に永続化するという戦略である。コンテナが1台しかないのであれば、競合の問題は発生しない。コンテナが起動するタイミングで SQLite のデータベースを Cloud Storage から読み取り、コンテナが終了するタイミングで Cloud Storage に書き戻しができていれば、問題なく動作しそうだ…というわけである。
Cloud Run は、Google Cloud Storage を(GCS Fuse で)マウントすることができる。また、Litestream という、SQLite の DB 及び WAL を(streaming replication で)Cloud Storage に永続化するライブラリがある。本記事では、どちらの方がパフォーマンスに優れるか、注意するべき点は何かを検証する。
- Cloud Run の Google Cloud Storage マウント
- Litestream
パフォーマンス比較
仕様
計測される側の Ruby on Rails の動作は以下である。
id, count
の2カラムを持つcounters
テーブルを作成seeds.rb
でid = 'count_up', count = 0
のレコードを1件作成/count_up
への POST リクエスト毎に、id = 'count_up'
のレコードのcount
を1増やす(update_all("count = count + 1")
)- レスポンスには、
count
の値を{"count":4856}
の形式で返す /count_up
への POST リクエストを多数回実行し、Request per second (RPS) を計測
パフォーマンス計測には k6 を用い、以下のような単純なシナリオを実行させた。
import http from 'k6/http';
import { check } from 'k6';
export const options = {
vus: 900,
duration: '3s',
};
export default function() {
const res = http.post('https:// URL .asia-northeast1.run.app/count_up');
check(res, { "status is 200": (res) => res.status === 200 });
}
コンテナの同時リクエスト受け付け数は 900 とした。
Cloud Run のコンテナには、1 vCPU と 512 MB のメモリを割り当てた。
Cloud Run の Google Cloud Storage マウント
Google Cloud Storage マウントを行う場合、第2世代が必須となる。
- 150並列
vus: 150
とし、計測した結果以下の結果を得た(抜粋)。
checks_total.......................: 649 179.608057/s
checks_succeeded...................: 100.00% 649 out of 649
checks_failed......................: 0.00% 0 out of 649
HTTP
http_req_duration.......................................................: avg=656.82ms min=19.87ms med=730.44ms max=1.71s p(90)=1.48s p(95)=1.53s
{ expected_response:true }............................................: avg=656.82ms min=19.87ms med=730.44ms max=1.71s p(90)=1.48s p(95)=1.53s
http_req_failed.........................................................: 0.00% 0 out of 649
http_reqs...............................................................: 649 179.608057/s
running (03.6s), 000/150 VUs, 649 complete and 0 interrupted iterations
179.6 req/s 受付できたと言って良いだろう。
- 900並列
vus: 900
とし、計測した結果以下の結果を得た(抜粋)。
checks_total.......................: 1441 132.552259/s
checks_succeeded...................: 100.00% 1441 out of 1441
checks_failed......................: 0.00% 0 out of 1441
HTTP
http_req_duration.......................................................: avg=4.14s min=21.17ms med=3.24s max=9.39s p(90)=8.85s p(95)=9.14s
{ expected_response:true }............................................: avg=4.14s min=21.17ms med=3.24s max=9.39s p(90)=8.85s p(95)=9.14s
http_req_failed.........................................................: 0.00% 0 out of 1441
http_reqs...............................................................: 1441 132.552259/s
EXECUTION
vus.....................................................................: 110 min=110 max=900
vus_max.................................................................: 900 min=900 max=900
running (10.9s), 000/900 VUs, 1441 complete and 0 interrupted iterations
default ✓ [======================================] 900 VUs 3s
パフォーマンスは伸びなかった…というか悪化した。
132.6 req/s 受付できたと言って良いだろう。
Litestream
Litestream の動作には特別な要件がないため、デフォルトの世代(2025-05 時点では第1世代)と、第2世代を指定したもの、両方について確認した。
第1世代
- 150並列
vus: 150
とし、計測した結果以下の結果を得た(抜粋)。
checks_total.......................: 633 135.508697/s
checks_succeeded...................: 100.00% 633 out of 633
checks_failed......................: 0.00% 0 out of 633
HTTP
http_req_duration.......................................................: avg=957.61ms min=21.95ms med=647.31ms max=3.34s p(90)=2.89s p(95)=3.1s
{ expected_response:true }............................................: avg=957.61ms min=21.95ms med=647.31ms max=3.34s p(90)=2.89s p(95)=3.1s
http_req_failed.........................................................: 0.00% 0 out of 633
http_reqs...............................................................: 633 135.508697/s
running (04.7s), 000/150 VUs, 633 complete and 0 interrupted iterations
default ✓ [======================================] 150 VUs 3s
135.5 req/s 受付できたと言って良いだろう。
- 900並列
vus: 900
とし、計測した結果以下の結果を得た(抜粋)。
checks_total.......................: 1637 293.350468/s
checks_succeeded...................: 100.00% 1637 out of 1637
checks_failed......................: 0.00% 0 out of 1637
HTTP
http_req_duration.......................................................: avg=1.62s min=21.43ms med=2.18s max=3.22s p(90)=2.97s p(95)=3.06s
{ expected_response:true }............................................: avg=1.62s min=21.43ms med=2.18s max=3.22s p(90)=2.97s p(95)=3.06s
http_req_failed.........................................................: 0.00% 0 out of 1637
http_reqs...............................................................: 1637 293.350468/s
EXECUTION
vus.....................................................................: 213 min=213 max=900
vus_max.................................................................: 900 min=900 max=900
running (05.6s), 000/900 VUs, 1637 complete and 0 interrupted iterations
default ✓ [======================================] 900 VUs 3s
293.4 req/s だった。倍以上のパフォーマンスが出ているようだ。
第2世代
- 150並列
vus: 150
とし、計測した結果以下の結果を得た(抜粋)。
checks_total.......................: 557 148.273847/s
checks_succeeded...................: 100.00% 557 out of 557
checks_failed......................: 0.00% 0 out of 557
HTTP
http_req_duration.......................................................: avg=855.4ms min=21.62ms med=955.69ms max=2.02s p(90)=1.61s p(95)=1.83s
{ expected_response:true }............................................: avg=855.4ms min=21.62ms med=955.69ms max=2.02s p(90)=1.61s p(95)=1.83s
http_req_failed.........................................................: 0.00% 0 out of 557
http_reqs...............................................................: 557 148.273847/s
running (03.8s), 000/150 VUs, 557 complete and 0 interrupted iterations
default ✓ [======================================] 150 VUs 3s
148.3 req/s 受付できたと言って良いだろう。若干パフォーマンスが上がっているようだが、計測誤差程度と言えなくもない。
- 900並列
vus: 900
とし、計測した結果以下の結果を得た(抜粋)。
checks_total.......................: 1347 178.225271/s
checks_succeeded...................: 100.00% 1347 out of 1347
checks_failed......................: 0.00% 0 out of 1347
HTTP
http_req_duration.......................................................: avg=2.97s min=22.08ms med=3.46s max=5.87s p(90)=5.34s p(95)=5.52s
{ expected_response:true }............................................: avg=2.97s min=22.08ms med=3.46s max=5.87s p(90)=5.34s p(95)=5.52s
http_req_failed.........................................................: 0.00% 0 out of 1347
http_reqs...............................................................: 1347 178.225271/s
EXECUTION
vus.....................................................................: 123 min=123 max=900
vus_max.................................................................: 900 min=900 max=900
running (07.6s), 000/900 VUs, 1347 complete and 0 interrupted iterations
default ✓ [======================================] 900 VUs 3s
178.2 req/s 受付できたと言って良いだろう。150並列よりは良いようだ。
第1世代より値が悪かったので、再度実行してみた。
checks_total.......................: 1361 185.216461/s
checks_succeeded...................: 100.00% 1361 out of 1361
checks_failed......................: 0.00% 0 out of 1361
HTTP
http_req_duration.......................................................: avg=2.78s min=21.93ms med=3.21s max=5.69s p(90)=5.32s p(95)=5.45s
{ expected_response:true }............................................: avg=2.78s min=21.93ms med=3.21s max=5.69s p(90)=5.32s p(95)=5.45s
http_req_failed.........................................................: 0.00% 0 out of 1361
http_reqs...............................................................: 1361 185.216461/s
EXECUTION
vus.....................................................................: 79 min=79 max=900
vus_max.................................................................: 900 min=900 max=900
running (07.3s), 000/900 VUs, 1361 complete and 0 interrupted iterations
default ✓ [======================================] 900 VUs 3s
185.2 req/s であり、ひとつ前のテストとはあまり変化がなかった。
設定概要
Cloud Run の Google Cloud Storage マウント
Google Cloud Storage マウントは、Cloud Run の実行環境のうち、The second generation のみで利用できる Cloud Run コンテナ ランタイムの契約。
先に注意事項を書いておくと、「未保存のデータがある状態で、新リビジョンの作成等で新しいインスタンスをロードして、1時的に2台以上のインスタンスが存在してしまうと、データの一部(最新の WAL 分)が失われる」ということである。データを失わないために、アクセスが全く来ておらず、インスタンスが0の状態で、Cloud Run の新しいリビジョンを作成するのが良い。前のインスタンスに /count_up
を行っている段階で、Cloud Storage に WAL が保存されているのも確認できたが、新しいインスタンスがそれをロードしないのか、良く分からなかった。一定量の保存を行ったときと、インスタンスの終了時には、WAL から DB に書き戻す処理が行われるが、そうして .sqlite3
ファイルになっていた状態であれば、データが失われることはないようだった。
細かいところは、GitHub にアップロードしているソースコード db-cloud-storage https://github.com/takotakot/rails8-google-cloud-example/tree/main/db-cloud-storage を参照してほしい。要点は以下のように、バケット名 (id) とマウント先のパス (mount_path) を指定するだけである。
# ...
{
volume_mounts {
name = "sqlite-bucket"
# See Dockerfile WORKDIR
mount_path = "/rails/storage"
}
}
# ...
volumes {
name = "sqlite-bucket"
gcs {
bucket = google_storage_bucket.sqlite_bucket.name
read_only = false
}
}
細かいコミット履歴が追いたければ、Pull Request #5 の2コミット目以降が良いだろう。
Litestream
Litestream は、SQLite の WAL を Cloud Storage に永続化するライブラリである。世代 (generation) の管理機能と、復元 (restore) 機能も持つ。WAL を見張り、バックグラウンドで転送してくれる。litestream-ruby は、Ruby で Litestream を簡単に使えるようにするための Gem である。Gem をインストールし、litestream.yml
を作成するだけで、SQLite の DB と WAL を Cloud Storage に永続化できる。
Litestream を使用すると、コンテナ起動時に、最新のデータを Cloud Storage からコピーし、更新が書き込まれる度に、差分を Cloud Storage にストリーミングで書き出す…ということが実現できる。
ストリーミング保存であるため、Cloud Storage バケットを直接マウントする方法と異なり、新リビジョンの作成等で新しいインスタンスをロードして、1時的に2台以上のインスタンスが存在してしまっても、データ損失は発生しなかった。
全体については別記事に譲るとして、主要な設定は以下である。Cloud Run で実行する場合、自動的にサービスアカウントを読み取って認証された状態で実行してくれるようだった。
if [ "${LITESTREAM_REPLICA_BUCKET}" ]; then
# Restore databases
for db in ".sqlite3" "_cable.sqlite3" "_cache.sqlite3" "_queue.sqlite3"; do
./bin/rails litestream:restore -- --database="storage/${RAILS_ENV}${db}" --if-replica-exists
done
dbs:
- path: storage/production.sqlite3
replicas:
- type: gcs
bucket: $LITESTREAM_REPLICA_BUCKET
path: production.sqlite3
- path: storage/production_cache.sqlite3
replicas:
- type: gcs
bucket: $LITESTREAM_REPLICA_BUCKET
path: production_cache.sqlite3
- path: storage/production_queue.sqlite3
replicas:
- type: gcs
bucket: $LITESTREAM_REPLICA_BUCKET
path: production_queue.sqlite3
- path: storage/production_cable.sqlite3
replicas:
- type: gcs
bucket: $LITESTREAM_REPLICA_BUCKET
path: production_cable.sqlite3
ディスカッション
コメント一覧
まだ、コメントがありません