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

Solid Cache README: DBベースのキャッシュストア(翻訳)

概要

MITライセンスに基づいて翻訳・公開いたします。

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

  • 2024/09/27: 更新

rails/solid_cache - GitHub

Solid Cache README: DBベースのキャッシュストア(翻訳)

Solid Cacheは、データベースを保存場所として利用するActive Supportキャッシュストアで、従来のRedisやMemcachedストアのようなメモリのみで用いられることが多いキャッシュストアよりもずっと大きなキャッシュを保持できます。最新のSSDドライブは高速なので、ほとんどのキャッシュ目的では、ディスクとRAMの違いによるアクセス時間のペナルティは重要ではありません。簡単に言えば、メモリ内に小さなキャッシュを配置するよりも、ディスク上に巨大なキャッシュを保持する方が通常は効果的です。

🔗 インストール方法

Rails 8の新規アプリケーションでは、デフォルトでSolid Cacheが設定されます。ただし、それより前のバージョンのRailsを実行している場合は、以下の手順に沿うことで手動で追加できます。

  1. bundle add solid_cache
  2. bin/rails solid_cache:install

上のコマンドを実行することで、Solid Cacheがproduction向けのキャッシュストアとして設定され、config/cache.ymldb/cache_schema.rbが作成されます。

次に、config/database.ymlファイルにキュー用のデータベース設定を追加する必要があります。SQLiteを使っている場合は、以下のような設定になります。

production:
  primary:
    <<: *default
    database: storage/production.sqlite3
  cache:
    <<: *default
    database: storage/production_cache.sqlite3
    migrations_paths: db/cache_migrate

MySQL/PostgreSQL/Trilogyを使っている場合は、以下のような設定になります。

production:
  primary: &primary_production
    <<: *default
    database: app_production
    username: app
    password: <%= ENV["APP_DATABASE_PASSWORD"] %>
  cache:
    <<: *primary_production
    database: app_production_cache
    migrations_paths: db/cache_migrate

設定が終わったら、db:prepareを実行して、データベースの作成とスキーマ読み込みが行われるようにしてください。

🔗 設定方法

config/cache.yml設定ファイルまたはconfig/solid_cache.yml設定ファイルが読み込まれます。このファイルの置き場所はSOLID_CACHE_CONFIG環境変数で変更できます。

設定ファイルのフォーマットは以下のとおりです。

default:
  store_options: &default_store_options
    max_age: <%= 60.days.to_i %>
    namespace: <%= Rails.env %>
  size_estimate_samples: 1000

development: &development
  database: development_cache
  store_options:
    <<: *default_store_options
    max_size: <%= 256.gigabytes %>

production: &production
  databases: [production_cache1, production_cache2]
  store_options:
    <<: *default_store_options
    max_entries: <%= 256.gigabytes %>

store_optionsの完全なリストについては、後述のキャッシュの設定を参照してください。キャッシュ探索に渡したオプションは、この設定ファイルで指定したオプションを上書きします。

🔗 コネクションの設定方法

この設定ファイルには、databasedatabasesconnects_toのいずれか1つを設定できます。これらは、SolidCache::Record#connects_toでキャッシュデータベースを設定するのに使われます。

databasecache_dbを指定すると、以下のように設定されます。

SolidCache::Record.connects_to database: { writing: :cache_db }

databases[cache_db, cache_db2]を指定することは、以下と同等です。

SolidCache::Record.connects_to shards: { cache_db1: { writing: :cache_db1 },  cache_db2: { writing: :cache_db2 } }

connects_toが設定されている場合は、直接渡されます。

上記のどの項目も設定されていない場合、Solid CacheはActiveRecord::Baseコネクションプールを利用します。この設定は、キャッシュの読み出しと書き込みが、データベーストランサクションの一部としてラップされることを意味します。

🔗 エンジンの設定方法

エンジンには以下の5つのオプションを設定できます。

executor
RailsのExecutorを利用して非同期操作をラップします。デフォルトはアプリのExecutorです。
connects_to
Active RecordのSolidCache::Record抽象モデルで利用するカスタム接続先の値です。メインアプリでシャーディングや別のキャッシュデータベースを利用する場合に必要です。このオプションを指定することでconfig/solid_cache.ymlファイルの値が上書きされます。
size_estimate_samples
キャッシュにmax_sizeが設定されている場合、サイズの推定に利用するサンプル数を指定します。
encrypted
キャッシュ値を暗号化すべきかどうかを指定します(詳しくは暗号化を有効にするを参照)。
encryption_context_properties
暗号化コンテキストのカスタムプロパティです。

これらのオプションは、以下のようにRailsの設定ファイルで指定できます。

Rails.application.configure do
  config.solid_cache.size_estimate_samples = 1000
end
🔗 キャッシュの設定方法

Solid Cacheでは、標準のActiveSupport::Cache::Storeにあるオプションに加えて、以下のオプションもサポートしています。

error_handler
発生したすべてのActiveRecord::ActiveRecordErrorを処理するために呼び出すProcを指定します(デフォルト: エラーを"warning"としてログ出力する)。
expiry_batch_size
古いレコードを削除するときのバッチサイズ(デフォルト: 100
expiry_method
期限を失効させる方法を:threadまたは:jobで指定します(デフォルト: :thread)。
expiry_queue
失効ジョブをどのキューに追加するかを指定します(デフォルト: :default
max_age
キャッシュ内でエントリが失効せずに持続する最大期間(デフォルト: 2.weeks.to_i)。nilに設定することも可能ですが、次のmax_entriesでキャッシュサイズの上限を設定しない限り利用は推奨されません。
max_entries
キャッシュに保存できるエントリの最大数を指定します(デフォルト: nil -- 上限なしを意味する)。
max_size
キャッシュエントリの最大サイズを指定します(デフォルト: nil -- 上限なしを意味する)。
cluster非推奨化
キャッシュデータベースのクラスタに渡すオプションをハッシュで指定します(例: { shards: [:database1, :database2, :database3] })。
clusters非推奨化
複数のキャッシュクラスタに渡すオプションをハッシュの配列で指定します(:clusterオプションが設定済みの場合は無視されます)。
shards
データベースの配列またはハッシュを指定します。
active_record_instrumentation
キャッシュのクエリをinstrumentation(計測)の対象にするかどうかを指定します(デフォルト: true)。
clear_with
キャッシュを:truncate:deleteのどちらでクリアするかを指定します(デフォルトは:truncateですが、Rails.env.test?の場合は:deleteが使われます)。
max_key_bytesize
正規化されたキーの最大サイズをバイト単位で指定します(デフォルト: 1024)。

キャッシュクラスタについて詳しくは、キャッシュをシャーディングするを参照してください。

🔗 キャッシュの失効

Solid Cacheは、キャッシュへの書き込みをトラッキングします。書き込みごとにカウンタが1ずつ増加し、カウンタがexpiry_batch_sizeの50%に達すると、バックグラウンドスレッドで実行するタスクを追加します。このタスクでは以下を行います。

  1. max_entriesまたはmax_sizeが設定済みの場合は、これらの値を超えたかどうかをチェックする
    現在のエントリは、SolidCache::Entryテーブルに登録されている最大のIDから最小のIDを減算することで推測します。
    現在のサイズは、エントリのbyte_sizeカラムをサンプリングすることで推測します。

  2. 値を超えた場合は、expiry_batch_sizeの個数分のエントリを削除する

  3. 超えていない場合は、max_ageより長い期間存在しているエントリを最大expiry_batch_sizeの個数分削除する

バッチサイズの50%の個数に達したら失効処理を行うようになっています。これにより、キャッシュサイズを削減する必要が生じたときに、キャッシュの書き込みよりも早期のタイミングでレコード上のキャッシュを失効させることが可能になります。

失効処理をトリガーするタイミングを書き込み時のみに限定しているので、キャッシュがアイドリング状態であればバックグラウンドのスレッドもアイドリング状態になります。

キャッシュの失効処理をスレッドではなくバックグラウンドで行いたい場合は、 expiry_method:jobを指定します。これにより、SolidCache::ExpiryJobがエンキューされるようになります。

🔗 キャッシュをシャーディングする

Solid Cacheでは、複数データベースにわたるキャッシュのシャーディング(sharding)で、Maglevの一貫したハッシュスキームを利用しています。

シャーディングを行う手順は次のとおりです。

  1. database.ymlファイルにデータベースシャーディング用の設定を追加する
  2. config.solid_cache.connects_toでシャーディングを設定する
  3. キャッシュのシャードをクラスタオプション経由で渡す

例:

# config/database.yml
production:
  cache_shard1:
    database: cache1_production
    host: cache1-db
  cache_shard2:
    database: cache2_production
    host: cache2-db
  cache_shard3:
    database: cache3_production
    host: cache3-db
# config/cache.yml
production:
  databases: [cache_shard1, cache_shard2, cache_shard3]

追記(2024/10/21)
以下は削除されました。

🔗 セカンダリのキャッシュクラスタ

セカンダリのキャッシュクラスタを追加可能です。読み取りはプライマリクラスタ(=リストの冒頭にあるクラスタ)だけに送信されます。

書き込みはすべてのクラスタに対して行われます。プライマリクラスタへの書き込みは同期的に行われますが、セカンダリクラスタへの書き込みは非同期的に行われます。

以下の操作を実行することで、複数のクラスタを指定できます。

# config/solid_cache.yml
production:
  databases: [cache_primary_shard1, cache_primary_shard2, cache_secondary_shard1, cache_secondary_shard2]
  store_options:
    clusters:
      - shards: [cache_primary_shard1, cache_primary_shard2]
      - shards: [cache_secondary_shard1, cache_secondary_shard2]

🔗 シャードに名前を付ける

シャーディングで利用するノードキーには、デフォルトでdatabase.ymlファイルに記載されているデータベースの名前が使われます。

クラスター設定内で以下のようにシャーディング用の名前を追加できます。これにより、一貫性のあるハッシュを壊さずに、シャードをシャッフルしたり削除したりできるようになります。

production:
  databases: [cache_primary_shard1, cache_primary_shard2, cache_secondary_shard1, cache_secondary_shard2]
  store_options:
    clusters:
    shards:
      cache_primary_shard1: node1
      cache_primary_shard2: node2

🔗 暗号化を有効にする

以下のようにencryptプロパティを追加することでキャッシュ値を暗号化できます。

# config/cache.yml
production:
  encrypt: true

または

# application.rb
config.solid_cache.encrypt = true

アプリケーションでActive Recordの暗号化機能を設定しておく必要があります。

Solid Cacheは、デフォルトで、それ用に最適化されたカスタムencryptorとメッセージシリアライザーを利用します。

まず、encryptorのActiveRecord::Encryption::Encryptor.new(compress: false)で圧縮を無効にします(キャッシュデータは既に圧縮されているため)。

次に、シリアライザとしてActiveRecord::Encryption::MessagePackMessageSerializer.newを利用します。このシリアライザはバイナリカラムでのみ利用可能ですが、標準のシリアライザよりも約40%多くのデータを保存できます。

必要に応じて、以下のように独自のコンテキストプロパティを選択することも可能です。

# application.rb
config.solid_cache.encryption_context_properties = {
  encryptor: ActiveRecord::Encryption::Encryptor.new,
  message_serializer: ActiveRecord::Encryption::MessageSerializer.new
}

🔗 インデックスの上限サイズ

Solid Cacheのマイグレーションでは、1024バイトのエントリを持つインデックスの作成を試行します。このサイズがデータベースに対して大きすぎる場合は、以下を行う必要があります。

  1. マイグレーションファイルでインデックスのサイズを編集する
  2. キャッシュのmax_key_bytesizeに新しい値を設定する

🔗 開発

bin/rake testでテストを実行します。デフォルトではSQLiteに対して実行されます。

MySQLやPostgreSQLに対してテストを実行することも可能です。

最初にデータベースを起動します。

$ docker compose up -d

次に、以下を実行してデータベーススキーマをセットアップします。

$ TARGET_DB=mysql bin/rails db:setup
$ TARGET_DB=postgres bin/rails db:setup

次に、対象データベースに対してテストを実行します。

$ TARGET_DB=mysql bin/rake test
$ TARGET_DB=postgres bin/rake test

🔗 複数バージョンのRailsでテストする

Solid Cacheでは、複数バージョンのRailsをテストするためにappraisalに依存しています。

thoughtbot/appraisal - GitHub

Railsのバージョンを指定してテストを実行するには、以下のようにします。

bundle exec appraisal rails-7-1 bin/rake test

Gemfile内の依存関係を更新したときは、必ず以下を実行してください。

$ bundle
$ appraisal update

これにより、すべてのRailsバージョンで依存関係が確実に更新されます。

🔗 Solid Cacheの実装について

Solid CacheはFIFO(first in, first out: 先入れ先出し)方式のキャッシュです。FIFOはLRU(least recently used: 直近で最も使われていないものから削除する)方式ほど効率は高くありませんが、キャッシュの寿命を長くすることで効率を補っています。

FIFOキャッシュは以下の理由で管理がずっと簡単です。

  1. 項目がいつ読み出されたかをトラッキングする必要がない。

  2. 最大IDと最小IDを比較することでキャッシュサイズの推定と制御が行える。

  3. テーブルの一方の端から削除し、反対側の端に追加することで断片化を回避できる(少なくともMySQLでは)。

🔗 アップグレード

Solid Cache v0.3.0以前からアップグレードする場合は、「v0.4.0以上にアップグレードする場合」ドキュメントを参照してください。

🔗 ライセンス

Solid Cache is licensed under MIT.

関連記事

Solid Queue README -- DBベースのActive Jobバックエンド(翻訳)


CONTACT

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