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

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

概要

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

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

rails/solid_cache - GitHub

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

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

Solid Cacheは、データベースを利用したActive Supportキャッシュストアの実装です。

SSD上のSQLデータベースを利用することで、従来のRedisやMemcadhedのようなメモリ上のみのキャッシュよりもずっと大容量かつ安価なキャッシュを実現します。

🔗 利用法

Solid CacheをRailsのキャッシュとして設定するには、Railsアプリの環境コンフィグで以下を追加する必要があります。

config.cache_store = :solid_cache_store

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

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

  1. 項目がいつ読み出されたかをトラッキングする必要がない。
  2. 最大IDと最小IDを比較することでキャッシュサイズの推定と制御が行える。
  3. テーブルの一方の端から削除し、反対側の端に追加することで断片化を回避できる(少なくともMySQLでは)。

🔗 インストール方法

アプリケーションのGemfileに以下の行を追加します。

gem "solid_cache"

続いて以下を実行します。

$ bundle

または、以下を実行して手動でインストールすることも可能です。

$ gem install solid_cache

続いて以下のマイグレーションを実行します。

$ bin/rails solid_cache:install:migrations

続いて以下を実行します。

$ bin/rails db:migrate

🔗 設定方法

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コネクションプールを利用します。この設定は、キャッシュの読み出しと書き込みが、データベーストランサクションの一部としてラップされることを意味します。

🔗 エンジンの設定方法

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

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

これらのオプションは、以下のように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オプションが設定済みの場合は無視されます)。
active_record_instrumentation
キャッシュのクエリをinstrumentation(計測)の対象にするかどうかを指定します(デフォルト: true)。
clear_with
キャッシュを:truncate:deleteのどちらでクリアするかを指定します(デフォルトは:truncateですが、Rails.env.test?の場合は:deleteが使われます)。
max_key_bytesize
正規化されたキーの最大サイズをバイト単位で指定します(デフォルト: 1024)。

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

🔗 キャッシュの失効

  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がエンキューされるようになります。

🔗 キャッシュ専用データベースを利用する場合

以下のようなデータベース設定をdatabase.ymlファイルに追加します。

development:
  cache:
    database: cache_development
    host: 127.0.0.1
    migrations_paths: "db/cache/migrate"

以下を実行してデータベースを作成します。

$ bin/rails db:create

以下を実行してマイグレーションをインストールします。

$ bin/rails solid_cache:install:migrations

キャッシュ用のマイグレーションファイルをカスタムのマイグレーションフォルダに移動します。

$ mkdir -p db/cache/migrate
$ mv db/migrate/*.solid_cache.rb db/cache/migrate

以下のように、エンジンの設定ファイルで新しいデータベースを指すように設定します。

# config/solid_cache.yml
production:
  database: cache

マイグレーションを実行します。

$ bin/rails db:migrate

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

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/solid_cache.yml
production:
  databases: [cache_shard1, cache_shard2, cache_shard3]

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

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

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

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

# 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
      - shards:
          cache_secondary_shard1: node3
          cache_secondary_shard2: node4

🔗 暗号化を有効にする

イニシャライザに以下を追加します。

ActiveSupport.on_load(:solid_cache_entry) do
  encrypts :value
end

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

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 is licensed under MIT.

関連記事

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


CONTACT

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