限界を超えろ!Rails × SQLite3


SQLite3 in production シリーズ第四弾。最終回です。

前回までの記事で、「同時アクセスに弱い」というデメリットゆえに Web アプリケーションと SQLite3 の相性が悪いこと、それを克服する WAL と busy_handler という2つの機能を説明しました。

Rails 8 は busy_handler を活用して、SQLite3 の Concurrency を最大限引き出しました。最終回ではその手法を見ていきましょう!

前回のおさらい

Rails は単純に busy_handler を使うだけでは、書き込み同士が衝突した時のエラーを解消することができませんでした。その理由は2つ。

  1. トランザクション開始時に共有ロックを取るために、書き込みが並行するだけでデッドロックが発生する
  2. busy_handler による待機中も GVL を握り続けてしまう

施策① IMMEDIATE モード

1 については、シンプルな解決策を SQLite3 がオプションとして提供しています。それが IMMEDIATE ↗ というトランザクションモードです。

このモードを使うと、トランザクション開始時に書き込み用のロック(RESERVEDロック)を取るため、トランザクションが並行することを防げます。 デメリットとして、読み取り専用のトランザクションでも書き込み枠を占有してしまいます。しかし Rails の ActiveRecord は、実際に書き込みが発生する瞬間まで BEGIN を遅らせる(Lazy Transaction)ため、このデメリットを巧みに回避しています。

Rails の開発チームは、トランザクション発行時のデフォルトクエリを BEGIN IMMEDIATE TRANSACTION に変更しました。それが次の PR です。

Ensure SQLite transaction default to IMMEDIATE mode ↗

施策② Ruby の sleep メソッドを活用する

続く GVL を解放できない問題に対しては、「busy_handler の中で Ruby の sleep メソッドを呼び出す」という解決策が採用されました。

C の sleep も Ruby の Kernel.#sleep も、最終的には OS のシステムコールを呼び出します。しかし、Ruby の sleep は GVL を一時的に解放してからシステムコールを呼ぶという点が重要です。 これにより、Ruby は他のスレッドに実行権限を渡すことができます。

この変更は sqlite-ruby に専用のインタフェース busy_handler_timeout を追加し、それを Rails で利用することで完結しました。

これにより、書き込みが衝突した際に「他方のトランザクション完了を待つ」という挙動を、Ruby VM 全体を止めずに(並行性を維持したまま) 実現できるようになったのです。

ついにゴール!

これら3つの変更(PR) によって、Rails は SQLite3 のベストパフォーマンスを引き出せるようになりました。 WAL モードによって「読み書きの衝突」が、busy_handler によって「書き込み同士の衝突」が解決され、SQLite3 はもはや「同時アクセスに弱い」データベースではなくなったのです。

これらの改善により、production 環境においても SQLite3 を採用できる!という話になりました。

Supercharge the One Person Framework with SQLite: Rails World 2024 ↗ から力強い一節を引用しましょう。

So, yes, it is actually possible to run full-featured Rails applications in production, with no compromises on features or performance, all on a single machine.

意訳:機能やパフォーマンスを犠牲にすることなく、フル機能の Rails アプリケーションをすべて 1 台のマシンで本番環境で実行することが実際に可能になります。

水平スケーリングは?

第一回では SQLite3 について、「同時アクセスに弱い」の他に、「水平スケーリングが困難」という弱点を取り上げました。

この点について、Rails は特段新しい解決策を提示していません。しかし、「同時アクセスに弱い」という構造的問題の解消によって、垂直スケーリングという選択肢が現実味を帯びます。

要は、ハイスペックなサーバーの持つ大量の CPU / Memory なら、大量リクエストを十分さばけるでしょ、という話です。

この理屈については、筆者も若干懐疑的な印象を抱いています。IPO を狙ったり、社会のインフラとなるような大規模なサービスは、シングルサーバーでは厳しいのが現実ではないでしょうか?

しかし、 Supercharge the One Person Framework with SQLite: Rails World 2024 ↗ では、SQLite3 を本番環境で採用しつつ、ビジネス的な成功を納めた実例が複数紹介されています。 「SQLite3 は本番環境では使えない」と、無条件に断ずる時代は終わりました。

まとめ

SQLite3 を本番環境で使えるようにするため、Rails の開発チームは奮闘しました。月並みですが、我々はここから多くのことを学ぶべきです。

  • 常識にとらわれずに、理想的な状況とはなにか?を追求する
  • ツールの特性を深く理解し、ハックできるポイントを探る

筆者は、「Minimum Rails」 を愛する開発者として、Rails × SQLite3 × シングルサーバーというアーキテクチャの採用事例が増え、またそれらがビジネスとして成功することを心から望んでいます。

フィードバックを送る

  • • お送りいただいた内容は、筆者が全て目を通し、今後の励みとさせていただきます
  • プライバシー: お名前やメールアドレスの収集は行っておりません
  • 返信: 全てへの返信はお約束できかねますが、必要な場合は本文内に連絡先を添えてください
  • 公開の可能性: 個人を特定できない形で記事内で紹介させていただく場合があります