Rails8.1とSQLite3とSolid三兄弟とLitestream
PostgreSQLやMySQLとRedisという組み合わせが定番になっている理由がちょっとわかったかも。
当初の構成
Rails 8.1
極力idiomaticに作る
nodeに依存しない
importmap-rails
propshaft
tailwindcss-rails
ViewComponent
メインDB: SQLite3
メインDBはクリティカルではないが、カウンタ更新がある(カウンタの値も単なる目安なので重要ではない)
(Solid三兄弟はインストールはしていたけど使わず)
デプロイ先: fly.io(Dockerベースで独自のfly deployでデプロイする)
プロセスは1個(スレッド数はPumaコンフィグの仰せのままに)
メモリ512MB
これが後で災いになった可能性???
改修1
Rails.cacheを有効にし、productionはSolid Cacheにした
この頃まではスムーズに動いていたはず
改修2
AIに管理用ダッシュボードを作ってもらい
productionではダッシュボードにSolid Cableが使われていた
ダッシュボードの監視対象にSolid Cacheもあった
メインDBはクリティカルなものではないので、AIに作らせたスクリプトでローカルdev環境に手動でバックアップ
ちなみにこのあたりで初めてN+1問題を踏みました、しかもViewComponent越しに 🫠
改修3
litestream を導入し、Cloudflare R2ストレージのバケットにレプリケーションするようにした
このあたりから、productionで間欠的にレスポンスが遅れる現象が頻発
レスポンスが遅れている間はサーバーログに何も出ない
レスポンスが回復した途端に、たいていLitestreamのレプリケーションかコンパクションかWAL反映、もしくはSolid CacheやSolid Cableのログが出てくることが多かった
ログにはっきり出ないので何が原因なのか随分悩んだ(プロファイリングとかをちゃんとやればよかったと思うけど)
今さらだけど、SQLite3はあくまで「ファイル単位のロック」であり、行ロックというものはない。
書き込みより読み取りがずっと多ければ、WALが効いてかなりいい線まで行くようだけど、書き込みでファイルが長めにロックされるとレスポンス低下につながるらしい。
対応
litestreamの設定をさんざん変えても、間欠的なレスポンス低下を減らすことはできてもゼロにはならない
以下の記事に書いた、Pumaをasyncモードにするのは「かなり」効いた: でもゼロにはならなかった
https://techracho.bpsinc.jp/hachi8833/2026_03_09/157005
結局、その後以下の対応を順次行った
当初、メインのSQLite3のレプリケーションに加えて、Solid三兄弟のSQLite3ファイルも全部litestreamでレプリケーションしていた(無意味)のをやめた
productionのキャッシュをSolid Cacheからmemcachedに変更
productionのAction CableをSolid Cableから asyncに変更
litestreamをやめ、当初の素朴なローカルdev環境へのバックアップに変えた ←今ここ
【Gemini】
納得がいかない理由、非常によくわかります。「モダンなRails 8のスタンダード」を信じて組んだのに、なぜ初期段階でここまでパフォーマンスが崩れるのか。その正体は、SQLite3の性能不足ではなく、「512MBというメモリ制約」と「LitestreamによるI/Oの競合」の最悪な組み合わせにあります。
原因を分解すると、納得できる理由が見えてきます。
1. Litestreamは「常時監視」という負荷を上乗せする
Litestreamは背後で常にSQLiteのWALファイルを監視(ポーリング)し、変更があればS3などに転送します。
* Solid Cable: 100msごとにDBを叩く(書き込み+読み取り)。
* Litestream: その書き込みを検知して、ファイルをスキャンし転送準備をする。
* 結果: 512MBのメモリ環境では、OSのファイルキャッシュが効きにくいため、この「書き込み→監視→転送」の一連の流れがすべて物理ディスクI/Oになり、メインのRailsプロセスを待たせます。
2. 「512MB」というメモリの壁
Rails 8が想定している「SQLite3でサクサク動く本番環境」は、実は最低でも1GB、できれば2GB以上のRAMがある環境を暗黙の前提としています。
* OSキャッシュの不足: SQLite3の高速化の肝は「メモリ上にDBのページをキャッシュすること」です。512MBだとRailsプロセスだけで大半を使い切り、OSがDBファイルをメモリに載せておけません。
* I/Oの連鎖: キャッシュがないため、Solid CacheやCableが動くたびに「ガリガリ」とディスクを読みに行きます。そこへLitestreamの監視が加わると、ディスクの順番待ち(I/O Wait)が発生し、アプリのレスポンスが「カクつく」ことになります。
3. Solid Cacheの「書き込み量」の罠
Solid CacheはRedisと違い、キャッシュの保存先がDBファイルです。
* Railsのデフォルト設定だと、ページ片やデータが頻繁にDBに書き込まれます。
* そのたびにWALファイルが更新され、Litestreamが「おっ、仕事だ!」と動き出します。
* 「アプリの書き込み」と「Litestreamの読み取り」が、限られたI/O帯域を奪い合った結果が、レスポンス低下の正体です。
結論:何が悪かったのか?
あなたの実装やRails 8の設計が悪いのではなく、「非常にアクティブなDB利用(Cache/Cable/Litestream)」に対して「ハードウェア資源(メモリ/IO)」が物理的に足りていなかったのが真相です。
* Cableを async に戻す: DBへの無駄な空振りを防ぐ。
* Solid Cacheをやめる: 書き込み頻度を劇的に下げる。
* Litestreamをやめる: バックグラウンドの監視プロセスを消す。
これで改善したということは、やはりディスクI/Oの飽和が原因でした。
「Rails 8 + SQLite3」を快適に使うための教訓:
512MBクラスのVPSやPaaSで動かすなら、「全部入り」は贅沢すぎるということです。そのスペックなら、今の「DBはメインのみ、他はインメモリ」という構成が、最もインテリジェントで実用的な解だと思います。
今の構成で、バックアップはどうされていますか?Litestreamの代わりに、1日に数回ファイルをコピーするだけのシンプルな運用なら、今のメモリでも耐えられるはずです。
これをどうやって記事にまとめようか🤔
まだ悩み中😓
production環境でSQLite3を使っている人がまだまだ少ないのが実情だと思う(要調査)
fly.ioの課金をケチって512MBにしていたのが根本原因?🫠メモリ増やしたらどうなるかはまだ試してない
RailsでSQLite3を使う場合、SQLite3のデータが余裕を持ってメモリに乗るようにするのが無難らしい
そしてそれ以前に、現在のアプリの要件にlitestreamレプリケーションは完全にオーバーキルだった
それを承知で試した面はあるけど
そしてキャッシュにSolid Cacheを使ったり、Action CableにSolid Cableを使っていたのも、Pumaプロセスが1個の状態ではオーバーキルだった
Solid CacheやSolid Cableは、サーバープロセスが複数になったときに検討すればよいはず
あるいは順序が逆で、Solid三兄弟を使うならサーバープロセスを増やすことを検討すべき?
次に考慮すべき点は、SQLiteは単一のコンピュータ上で最適に動作するように構築されていることです。つまり、スケーリングが必要な場合は垂直スケーリングが最適だということです。必要に応じて単一マシンのスペックサイズを拡張することになります。
🛤 Rails 8はSQLiteで大幅に強化された「個人が扱えるフレームワーク」(翻訳)|YassLab 株式会社
たぶんこうまとめるのかな?
RailsでSQLite3を使うなら、少なくともいきなりのマルチプロセスは避けて単一プロセスを強くする方向(垂直スケーリング)にすべし
RailsでSQLite3を使うなら、production環境のメモリやディスクI/Oをケチらないこと
書き込み頻度の高いアプリには要注意
Litestreamが本当に必要かどうかと、必要ならどんな頻度でレプリケーションするかを検討すべし
やるなら別プロセスを立てる方がよいかも
Solid Cache + SQLite3やSolid Queue + SQLite3をいきなり使うよりも、最初はmemcachedやasyncから始めよう
それで足りなくなったときに検討すればよい
どんな場合に別プロセスを立てる方がよいか?
その場合でもRedisの方が安心度は高そう?(Redis使ったことない)
DHHが考える「個人のためのフレームワーク」としてのRailsのターゲットは、こんな感じ?
SQLite3を使うなら
テーブルは1〜3個程度
読み出しがほとんどで書き込み少ない
負荷が高くなりそうな一般向けよりも、個人利用や社内利用のような限定的で負荷が小さそうな用途
単一プロセスで済むが、メモリとディスクI/Oは潤沢
であれば、オールSQLite3でもいけるのかな🤔
スケールさせたくなったら、そのときこそ伝統と実績のRDBMS + Redis構成に移るべきか🤔
【Gemini】
むしろ、Rails経験が浅い人にこそ「オールSQLite3(Solidスタック)」は非常に強くおすすめできます。
これまでは「本番ならRedisとPostgreSQLを入れなきゃ…」と、アプリ以外のインフラ設定で躓くことが多かったのですが、Rails 8以降のこの構成は「シンプルさ」と「学習コストの低さ」で圧倒的なメリットがあります。
今回の「遅延」というハードルを乗り越えるための、初心者向けの考え方を整理しました。
なぜ「オールSQLite3」が良いのか?
1. インフラが1つで済む: RedisやMemcached、PostgreSQLのインストール・運用管理が不要。
2. バックアップが楽: storage/*.sqlite3 ファイルをコピーするだけで、DBもキャッシュもジョブも全部バックアップできます。
3. 開発と本番が同じ: 自分のPCで動いているものがそのまま本番で動く安心感があります。
構築時の「落とし穴」を避ける3つのポイント
初心者がハマりやすい「遅延」や「エラー」を防ぐための設定のコツです。
1. ファイルを必ず分ける(Rails 8のデフォルト)
* メイン、Cache、Queue(Cable)でファイルを分けることで、1つの処理がDBをロックしてアプリ全体が止まるのを防げます。
2. WALモードを有効にする
* database.yml で journal_mode: wal を指定します。これがないと、誰かが書き込んでいる間、他の人は読み取りすらできず、アプリが「非常に重く」感じます。
3. 環境に合わせたアダプタ選び(今回のケース)
* Cable: 今回のように単一プロセス・低メモリなら、無理にSolid Cable(DB)を使わず async(メモリ)で十分。
* Queue: ジョブ(メール送信など)が少なければ Solid Queue は非常に便利です。
結論
「オールSQLite3」はモダンで賢い選択です。
今回の async への変更のように、「動かしてみて、ボトルネック(重い部分)だけを柔軟に入れ替える」 という経験自体が、Railsエンジニアとしての大きなステップアップになります。
現在のアプリで、Solid Queue(非同期ジョブ)を導入する予定はありますか?導入すると、さらに「SQLite3の使いこなし」が楽しくなりますよ。
納得がいかないお気持ち、よくわかります。「Rails 8でSQLite3が本気を出した」という触れ込みなのに、なぜ基本構成で遅延するのか?という疑問ですよね。
実は、Rails 8の最適化(WALモードや各種PRAGMA設定)をもってしても抗えない、Solid Cable固有の「設計構造」と「Rubyの特性」の組み合わせに原因があります。
結論から言うと、原因はSQLite3の性能不足ではなく、「データベースをメッセージバス(Pub/Sub)として使う際のオーバーヘッド」にあります。
なぜWALモードでも「遅延」を感じたのか?
1. 「書き込み」ではなく「頻繁なポーリング」の罠
WALモードは「読み取りと書き込みの衝突」を劇的に改善しますが、Solid Cableはデフォルトで100msごとにSELECTクエリを発行し続けます。512MBメモリの環境では、この「絶え間ないクエリ発行」自体がCPUリソースを食いつぶし、メインのスレッド(HTTPレスポンスを返すスレッド)の実行時間を奪います。
2. GVL(Global VM Lock)のブロッキング
SQLite3アダプタ(C拡張)がクエリを実行している間、Rubyの GVL(巨大なロック) が解放されないタイミングがあります。
* async:メモリ内のRubyオブジェクトを触るだけ(一瞬)。
* Solid Cable:C言語レベルでファイルI/Oを伴うシステムコールを呼ぶ。
このわずかな「待ち」が、100msおきのポーリングと重なることで、Webレスポンスに「チリツモ」で数十ms〜数百msのジッター(揺らぎ)を生ませ、体感的な「遅延」となります。
3. SQLite3の「コネクション」の奪い合い
単一プロセスで動かしている場合、Webスレッド、Solid Cacheスレッド、Solid Cableスレッドがそれぞれコネクションを要求します。SQLite3はファイルへのアクセスをシリアライズするため、OSレベルでのコンテキストスイッチが頻発し、スループットが低下します。
「オールSQLite3」がダメなわけではない
Rails 8が提唱する「オールSQLite3」が輝くのは、実は 「ディスクI/Oが高速(NVMe SSDなど)」かつ「ある程度のCPU・メモリ余力」 がある環境です。
512MBという極小環境では、皮肉にも「DBを介さずメモリで完結する async」の方が、マシンのポテンシャルを最大限に引き出せたということになります。
まとめ
Rails 8の設定は「SQLite3を限界まで速くする」ものですが、「物理的なメモリ不足とCPUの奪い合い」までは解決してくれません。
* Solid Cable: 「DBを無理やりRedisのように使う」ため、オーバーヘッドが大きい。
* Solid Cache/Queue: これらは「必要な時だけDBを叩く」ので、SQLite3との相性が良い。
納得感は出てきましたでしょうか?次は、同じSQLite3を使っている Solid Cache 側で、似たような遅延が起きていないか切り分ける方法を見てみますか?