Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

SQLite on Railsシリーズ(05)SQLiteのコンパイルオプションを最適化する(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

参考: Rails 8はSQLiteで大幅に強化された「個人が扱えるフレームワーク」(翻訳)|YassLab 株式会社

日本語タイトルは内容に即したものにしました。

SQLite on Railsシリーズ(05)SQLiteのコンパイルオプションを最適化する(翻訳)

本記事は、Ruby on RailsアプリケーションをSQLiteで強化する方法を紹介するシリーズ記事の第5弾です。

今回は、production環境のWebアプリケーションをより適切にサポートするために、SQLiteをコンパイル時に調整する方法について詳しく解説します。これは、シリーズの過去記事でSQLiteデータベースの実行時設定を最適化したときの方法と密接に関連しています。


しかし本題に入る前に、ちょっとした話があります。
私がこの記事を書いているという事実は、自分の作品を公開することの力と、Rubyオープンソースコミュニティの素晴らしさの証です。

私がSQLite設定の最適化に関する記事を公開したとき、Nate Hopkinsが以下の返信でSQLiteのコンパイルをDockerfileで最適化する方法を共有してくれました。

正直に言うと、SQLiteのインストールを最適化することでSQLiteデータベースを最適化できるかどうかまでは考えていませんでした。私は個人的にRailsプロジェクトでDockerを使っていないので、NateのDockerfileは使いどころがありませんでした。

しかし、Julia EvansがSQLiteのコンパイルは簡単だという記事を書いていたのを思い出したので、プロジェクトに合わせてSQLiteをインストール・コンパイルするBashスクリプトを作成できるかもしれないと考えました。NateのDockerfileをガイドとして使いつつSQLiteドキュメントを参考にやってみると、実際にはそれほど難しくありませんでした。私が使ったスクリプトは、このGistで参照できます。

sqlite3-rubygemで、SQLiteインストールをカスタムコンパイルしてから、システムにインストールされているSQLiteではなくこのSQLiteを使うように指示する方法を調べてみましたが、いくつか試してみたもののうまくいきませんでした。

sparklemotion/sqlite3-ruby - GitHub

そこで、行き詰まったときの定番としてGitHubリポジトリに新しいディスカッションをオープンしました。プロジェクトの主要なメンテナーの1人であるMike Dalessioがすぐに応答してくれました。私たちはやり取りを重ね、彼は私のマシンでデバッグするためにペアプログラミングの通話に参加しないかと提案しました。そこでの短い通話と後日のチャットの結果、Mikeは私が話を複雑にしすぎていることを指摘してくれました。

私はsqlite3-ruby gemをSQLiteのカスタムインストールに何とかしてバインドしようとしていましたが、本当に欲しかったのはSQLiteのインストール時にコンパイル時のフラグを設定可能にすることだけだったのです。Mikeは根本的な問題に気付いて(#400)、すぐに新しいプルリク(#402)をオープンし、sqlite3-ruby gemがSQLiteのインストールとコンパイル時に利用するコンパイル時のフラグをユーザーが設定できるようにしました。その結果、ユーザーがコンパイル時のオプションを渡せるようになったsqlite3-ruby gemの新しいリリースv1.6.5が誕生しました。

本記事でこの複雑なストーリーをすべて語りたかった理由は、何もかもが素晴らしいと思ったからです。これこそRuby/Railsコミュニティの真の力です。NateがDockerfileを共有してくれたことから、Mikeが私の真意を理解して実現のためにあらゆる作業を行ったことまで、私たちは共同で新しいものを作り上げたのです。開発者がSQLiteのコンパイル時オプションでRailsアプリを微調整できる方法、しかもこれほどシンプルでわかりやすい方法を見つけたことに、私は心から興奮しています。


では、これが可能になったことは何を意味するのでしょうか。つまり、Railsアプリに最適化した形でSQLiteデータベースを完全に制御することが可能になったということです。

コンパイル時オプションと実行時オプションを両方とも最適化することで、はじめてSQLiteデータベースを真の意味で微調整できるようになります。
それに、コンパイル時オプションを調整できることは、Railsアプリにとっても大きなメリットです。

SQLiteのデフォルト設定は、下位互換性への取り組みや、より一般的な組み込みシステムでのユースケースが優先されているので、そのままではどちらも最新のWebアプリケーションには特に役立ちません。しかもSQLiteのドキュメントには、デフォルトのコンパイル設定はほとんどの実用目的に適していないとさえ書かれており、SQLiteで使われるCPUサイクル数とメモリのバイト数を最小限に抑えるために設定することが推奨されている、12個(訳注: 現在は13個)のコンパイル時フラグの概要がその下に記載されています。

前回の記事と同様に、ここではRailsアプリケーション用に設定を推奨するフラグを簡単に紹介します。しかしその前に、sqlite3-ruby gem のこの新しい機能をどのように活用すればよいのでしょうか。

最初に重要な点は、sqlite3-rubyのバージョンは1.6.5以上を使う必要があります。以下のようにGemfilegem "sqlite3", "~> 1.6.5"と書き、次にforce_ruby_platform: trueオプションを追加して、BundlerがSQLiteをソースからコンパイルしてその"ruby"プラットフォームgemを使うように指示する必要があります1。 つまり、Gemfileの完全なsqlite3エントリは以下のようになります。

gem "sqlite3", "~> 1.6.5", force_ruby_platform: true

これで、適切なバージョンのsqlite3-ruby gemが確実に使われるようになり、gemがSQLiteをコンパイルするときに、ビルド済みのバイナリを使わないようになります。

次に、コンパイル時フラグの Bundler設定オプションを設定する必要があります。Nokogiriのコンパイル時フラグを調整したことがある方なら見覚えがあるはずです。これは、以下のようにbundler CLI で設定できます。

bundle config set build.sqlite3 \
"--with-sqlite-cflags='-DSQLITE_DEFAULT_CACHE_SIZE=9999 -DSQLITE_DEFAULT_PAGE_SIZE=4444'"

: 上の値はデモンストレーション目的専用です。これをコピーしてプロジェクトで実行しないでください。この後すぐに適切なCFLAGSセットを提供します。

このコマンドを実行すると、プロジェクトの.bundler/configファイルが作成(または更新)され、以下のようなオプションが含まれます。

BUNDLE_BUILD__SQLITE3: "--with-sqlite-cflags='-DSQLITE_DEFAULT_CACHE_SIZE=9999 -DSQLITE_DEFAULT_PAGE_SIZE=4444'"

: 上のように二重引用符""で囲まれた文字列内で一重引用符''を使うことで、コンパイラフラグ間のスペースが正しく解釈されます。

これで完了です。必要な変更はこの2つだけです。
このように、Gemfile.bundler/configファイルを更新するだけで、プロジェクトに合わせて微調整されたSQLiteインストールが実現します。これらの手順と、sqlite3-ruby gemのより高度な利用法に関する追加手順はsqlite3-ruby gemのインストールドキュメントに記載されています。


いよいよ本題に入りましょう。どのコンパイル時フラグをどう使えばよいのでしょうか。短い答えとしては、SQLiteで推奨されているフラグを使います。ただし、Webアプリケーションで無意味なフラグは除きます。SQLiteドキュメントでは12個のフラグが推奨されています。個別のフラグの機能についての説明はここでは繰り返さないので、詳しくはドキュメントをお読みください。

SQLITE_DQS=0
SQLITE_THREADSAFE=0
SQLITE_DEFAULT_MEMSTATUS=0
SQLITE_DEFAULT_WAL_SYNCHRONOUS=1
SQLITE_LIKE_DOESNT_MATCH_BLOBS
SQLITE_MAX_EXPR_DEPTH=0
SQLITE_OMIT_DECLTYPE
SQLITE_OMIT_DEPRECATED
SQLITE_OMIT_PROGRESS_CALLBACK
SQLITE_OMIT_SHARED_CACHE
SQLITE_USE_ALLOCA
SQLITE_OMIT_AUTOINIT

: SQLiteのドキュメント自体には、この推奨コンパイル時オプションのセットを使っても改善は5%程度にとどまると記載されています。

上記の推奨コンパイル時オプションをすべて使用すると、SQLiteライブラリは約3%小さくなり、CPUサイクルの使用量は約5%少なくなります。つまり、これらのオプションはさほど大きな違いを生みません。ただし設計状況によっては少しでも役立つことがあります。

これらのオプションのうちSQLITE_OMIT_DEPRECATEDSQLITE_OMIT_DECLTYPEの2つは、sqlite3-ruby gem では無効です。gem が機能するにはSQLiteのこれらの機能が必要なので、これらを削除する必要があります 2

また、SQLITE_OMIT_AUTOINITオプションも削除する必要があります。このオプションは、アプリケーションがSQLiteのinitializeメソッドを適切なタイミングで呼び出すことを要求するためです。そのレベルの制御を保証できずinitialize を適切に呼び出せない場合は、セグメント違反が発生します。

SQLiteの全文検索機能を追加するSQLITE_ENABLE_FTS5オプションをビルドに追加することも可能です。これは、データベースがどのように使われるかで異なりますが、現在ElasticSearchMeilisearchを利用している場合は、それらの依存関係をSQLiteに置き換えることを検討できるでしょう。

以上の削除(と追加の可能性)を反映したフラグセットは以下のようになります。これらはSQLのパフォーマンスを強化する9つのフラグです。

SQLITE_DQS=0
SQLITE_THREADSAFE=0
SQLITE_DEFAULT_MEMSTATUS=0
SQLITE_DEFAULT_WAL_SYNCHRONOUS=1
SQLITE_LIKE_DOESNT_MATCH_BLOBS
SQLITE_MAX_EXPR_DEPTH=0
SQLITE_OMIT_PROGRESS_CALLBACK
SQLITE_OMIT_SHARED_CACHE
SQLITE_USE_ALLOCA
SQLITE_ENABLE_FTS5

上のフラグは、以下のCLIコマンドでBundlerコンフィグに変換できます。

bundle config set build.sqlite3 \
"--with-sqlite-cflags='-DSQLITE_DQS=0 -DSQLITE_THREADSAFE=0 -DSQLITE_DEFAULT_MEMSTATUS=0 -DSQLITE_DEFAULT_WAL_SYNCHRONOUS=1 -DSQLITE_LIKE_DOESNT_MATCH_BLOBS -DSQLITE_MAX_EXPR_DEPTH=0 -DSQLITE_OMIT_PROGRESS_CALLBACK -DSQLITE_OMIT_SHARED_CACHE -DSQLITE_USE_ALLOCA -DSQLITE_ENABLE_FTS5'"

または、プロジェクトの.bundler/configファイルを以下の内容で直接更新してもよいでしょう。

BUNDLE_BUILD__SQLITE3: "--with-sqlite-cflags='-DSQLITE_DQS=0 -DSQLITE_DEFAULT_MEMSTATUS=0 -DSQLITE_DEFAULT_WAL_SYNCHRONOUS=1 -DSQLITE_LIKE_DOESNT_MATCH_BLOBS -DSQLITE_MAX_EXPR_DEPTH=0 -DSQLITE_OMIT_PROGRESS_CALLBACK -DSQLITE_OMIT_SHARED_CACHE -DSQLITE_USE_ALLOCA -DSQLITE_ENABLE_FTS5'"

これで、bundle installを実行すれば完了です。

今後の記事では、すべての微調整項目がどのように組み合わされるか、およびパフォーマンスプロファイルの比較について解説します。現時点では、これらのコンパイル時オプションを前述の実行時設定を用いて調整するだけで、RailsアプリのSQLiteエクスペリエンスが著しく向上する、とだけ言っておきます。

これで、SQLiteが提供するさまざまなつまみを調整して、振る舞いやパフォーマンス特性を微調整可能になりました。これはひとえにRubyコミュニティの素晴らしさのおかげです。私はそこが大好きです。

関連記事

SQLite on Railsシリーズ(01)Gitブランチごとにデータベースを切り替える(翻訳)

SQLite on Railsシリーズ(02)SQLiteをチューニングで強化する(翻訳)

SQLite on Railsシリーズ(03)SQLite拡張機能を読み込む(翻訳)

SQLite on Railsシリーズ(04)LitestreamでSQLiteをバックアップしよう(翻訳)


  1. force_ruby_platform: trueオプションはBundler2.3.18 以上でないと利用できない点にご注意ください。Bundler 2.1以降(2.3.18まで)では、bundle config set force_ruby_platform trueを実行する必要がありますが、この方法はオプションがGemfileに対してグローバルに設定されてしまうという残念な副作用があります😕。Bundler 2.0以前はbundle config force_ruby_platform trueを実行する必要がありますが、これにも同じ副作用があります。 
  2. Webアプリは複数のスレッドを使うので、SQLITE_THREADSAFE=0オプションも削除する必要があるのではと思われるかもしれませんが、その必要はありません。sqlite3-ruby gem はレスポンスの待機中にGVLを解放しないため、並列処理(parallelism)は不可能です。つまり、sqlite3 APIの呼び出しはRubyプロセスで発生する他の作業とパラレルに実行できません。RailsアプリのActive Record自身は既にスレッド安全です。つまり、Active Recordがスレッド安全で、かつsqlite3-ruby gemはパラレル化不可のため、SQLite自身に独自のスレッド安全レイヤを追加する必要はありません。 

CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。