Rails6.1でセッションをキャッシュとは別のmemcachedへ保存しようとした時、色々調べたことをメモに残します。
目次
- 環境
- デフォルトのセッションストレージ
- キャッシュの保存先を変更するための準備
- デフォルトポートの memcached へキャッシュを保存
- キャッシュをキャッシュ用memcachedへと移動
- セッションストレージをデフォルトのmemcachedへ移動
- セッションストレージをsession用のmemcachedへ移動
- キャッシュとセッションを同じ memcached へ保存する
- ソースコード
環境
デフォルトのセッションストレージ
まずは、デフォルトのセッションストレージ CookieStore
を確認してみます。
2.3 セッションストレージ | Rails セキュリティガイド - Railsガイド
Railsアプリの作成
Railsアプリをゼロから作成してきます。今回は rails_dalli_sample
とします。
% rails new rails_dalli_sample
今回の動作確認ではwebpacker使いません。そのため、Gemfileでコメントアウトしておきます。
source 'https://rubygems.org' ... # Transpile app-like JavaScript. Read more: https://github.com/rails/webpacker #gem 'webpacker', '~> 5.0' # コメントアウト
また、Railsでの memcached クライアントは Dalli gem のため、Gemfileに追加します。
petergoldstein/dalli: High performance memcached client for Ruby
gem 'dalli'
改めて bundle install します。
% bundle install
続いて、ControllerとViewを生成します。
今回は home
Controller に index
メソッドをもたせます。
% bin/rails g controller home index --helper=false --assets=false Running via Spring preloader in process 21510 create app/controllers/home_controller.rb route get 'home/index' invoke erb exist app/views/home create app/views/home/index.html.erb invoke test_unit create test/controllers/home_controller_test.rb
作成したControllerの index メソッドで、セッションに値を入れます。
5.1 セッションにアクセスする | Action Controller の概要 - Railsガイド
今回は key を foo
、値を bar
とします。
class HomeController < ApplicationController def index session[:foo] = 'bar' end end
Viewは自動生成のものを流用します。
ただ、今回はwebpackerを使わないため、layoutファイル (app/views/layouts/application.html.erb
) から javascript_pack_tag
タグの部分をコメントアウトしておきます。
<%#= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
また、これは任意ですが、手元のRailsが複数ある場合はRailsの起動ポートを変えておきます。
rails s 時のデフォルトのポート番号を変更する - Qiita
# config/puma.rb port ENV.fetch("PORT") { 3700 }
動作確認
準備ができたので、Railsを起動します。
% bin/rails s => Booting Puma => Rails 6.1.3.2 application starting in development => Run `bin/rails server --help` for more startup options Puma starting in single mode... * Puma version: 5.3.2 (ruby 3.0.1-p64) ("Sweetnighter") * Min threads: 5 * Max threads: 5 * Environment: development * PID: 21698 * Listening on http://127.0.0.1:3700 * Listening on http://[::1]:3700 Use Ctrl-C to stop
ブラウザで http://localhost:3700/home/index
へアクセスすると、Cookieに暗号化された値が保存されています。
キャッシュの保存先を変更するための準備
セッションの保存先を変える前に、まずはRailsのキャッシュの保存先をmemcachedへと移動してみます。
フラグメントキャッシュを使うために erb を修正
今回はフラグメントキャッシュを有効化してみます。
- Rails のキャッシュ機構 - Railsガイド
- Ruby on Rails のフラグメントキャッシュのキャッシュキーはどのように決まるか | by TAGAWA Takao | traveloco-tech | Medium
フラグメントキャッシュとして保存するよう、 View ( app/views/home/index.html.erb
) に追記します。
<% cache 'my_cache' do %> <h1>Home#index</h1> <p>Find me in app/views/home/index.html.erb</p> <% end %>
環境ごとのDB設定を追加
今回は色々なパターンを確認することから、いくつもの環境設定を用意します。
DBは使わないものの、環境に応じた設定がされていないとエラーになることから、以下の設定を config/database.yml
の末尾へ追加します。
cache: <<: *default database: db/development.sqlite3 cache_port: <<: *default database: db/development.sqlite3 memd_session: <<: *default database: db/development.sqlite3 memd_session_port: <<: *default database: db/development.sqlite3 session_cache_store: <<: *default database: db/development.sqlite3
docker composeによる memcached を用意
次に、memcachedを用意します。
今回は docker compose を使って memcached を3つたてます。
service名 | mac上のポート | 用途 |
---|---|---|
default | 11211 | デフォルトポートで起動するmemcached |
cache | 17001 | Cache用memcached |
session | 17002 | Session用memcached |
docker-compose.ymlはこんな感じです。
version: "3" services: default: image: memcached ports: - 11211:11211 cache: image: memcached ports: - 17001:11211 session: image: memcached ports: - 17002:11211
docker compose で memcached を起動しておきます。
Compose CLI Tech Preview | Docker Documentation
% docker compose up -d
memcached の中身を確認するPythonスクリプトを作成
memcachedの中身の確認ですが、RubyMineではさくっとできなかったため、別の方法を探してみました。
Pythonのライブラリを探したところ
- https://github.com/dlrust/python-memcached-stats
- キー一覧の取り出し
- https://github.com/pinterest/pymemcache
- 取り出したキーを元に値を取得する
を組み合わせれば良さそうでした。
そこで、コマンドライン引数としてポートを渡せば中身を確認できるようなスクリプト
import sys from pymemcache.client.base import Client from memcached_stats import MemcachedStats if __name__ == '__main__': port = sys.argv[1] mem = MemcachedStats('localhost', port) client = Client(f'localhost:{port}') for key in mem.keys(): print(client.gets(key))
を作りました。
これで準備は完了です。
デフォルトポートの memcached へキャッシュを保存
環境ファイルの作成
まずは、docker compose 上にある、デフォルトポートの memcached へキャッシュを保存してみます。
今回の記事では色々なパターンを試すことから、 config/environments
ディレクトリの中に、各パターンの環境を作成します。
3.20 Rails環境を作成する | Rails アプリケーションを設定する - Railsガイド
今回は development.rb
をコピーし、各パターンの環境を作成します。
ここでは、 cache.rb
を作成し、デフォルトポートの memcached へキャッシュを保存する設定を行います。
まずは既存の設定
# Enable/disable caching. By default caching is disabled. # Run rails dev:cache to toggle caching. if Rails.root.join('tmp', 'caching-dev.txt').exist? config.action_controller.perform_caching = true config.action_controller.enable_fragment_cache_logging = true config.cache_store = :memory_store config.public_file_server.headers = { 'Cache-Control' => "public, max-age=#{2.days.to_i}" } else config.action_controller.perform_caching = false config.cache_store = :null_store end
を削除します。
次に、デフォルトポートの memcached を使うよう設定します。
2.5 ActiveSupport::Cache::MemCacheStore | Rails のキャッシュ機構 - Railsガイド
config.cache_store = :mem_cache_store
動作確認
準備ができたので、 環境 cache を指定して起動します。
1.2 rails server | Rails のコマンドラインツール - Railsガイド
% bin/rails s -e cache
ブラウザで http://localhost:3700/home/index
へアクセスした後、 memcached の状態をPythonスクリプトで確認します。
ポート 11211 のmemcached のみ、データが格納されていました。
% python display_memcached.py 11211 (b'\x04\x08o: ActiveSupport::Cache::Entry\t:\x0b@valueI"I<h1>Home#index</h1>\n<p>Find me in app/views/home/index.html.erb</p>\n\x06:\x06ET:\r@versionI"\x00\x06;\x07F:\x10@created_atf\x171622552901.4518201:\x10@expires_in0', b'1') ...
次の確認を行う前に、すべての memcached を再起動し、Pythonスクリプトでデータがないことを確認しておきます。
なお、以降の確認後も同じ作業を実施するものとします。
キャッシュをキャッシュ用memcachedへと移動
続いて、キャッシュをキャッシュ用memcachedへと移動します。
環境ファイルの作成
ホストとポートを指定するには、 config.cache_store
の設定を追加すれば良さそうです。
キャッシュの初期化時には、クラスタ内の全memcachedサーバーのアドレスを指定する必要があります。指定がない場合、memcachedがローカルのデフォルトポートで動作していると仮定して起動しますが、この設定は大規模サイトには向いていません。
そのため、上記で作成した cache.rb
をコピーした cache_port.rb
ファイルをenvironments の中に用意し、以下の設定に書き換えます。
config.cache_store = :mem_cache_store, 'localhost:17001'
動作確認
環境を指定して起動します。
% bin/rails s -e cache_port
ブラウザで http://localhost:3700/home/index
へアクセスした後、 memcached の状態をPythonスクリプトで確認すると、ポート 17001 のmemcachedにキャッシュが保存されていました。
# 空 % python display_memcached.py 11211 # 指定したポートの memcached にキャッシュが保存 % python display_memcached.py 17001 (b'\x04\x08o: ActiveSupport::Cache::Entry\t:\x0b@valueI"I<h1>Home#index</h1>\n<p>Find me in app/views/home/index.html.erb</p>\n\x06:\x06ET:\r@versionI"\x00\x06;\x07F:\x10@created_atf\x161622554358.104258:\x10@expires_in0', b'3') (b'\x04\x08o: ActiveSupport::Cache::Entry\t:\x0b@valueI"I<h1>Home#index</h1>\n<p>Find me in app/views/home/index.html.erb</p>\n\x06:\x06ET:\r@versionI"\x00\x06;\x07F:\x10@created_atf\x161622554358.104258:\x10@expires_in0', b'3') (b'\x04\x08o: ActiveSupport::Cache::Entry\t:\x0b@valueI"I<h1>Home#index</h1>\n<p>Find me in app/views/home/index.html.erb</p>\n\x06:\x06ET:\r@versionI"\x00\x06;\x07F:\x10@created_atf\x161622554358.104258:\x10@expires_in0', b'3') (b'\x04\x08o: ActiveSupport::Cache::Entry\t:\x0b@valueI"I<h1>Home#index</h1>\n<p>Find me in app/views/home/index.html.erb</p>\n\x06:\x06ET:\r@versionI"\x00\x06;\x07F:\x10@created_atf\x161622554358.104258:\x10@expires_in0', b'3') # こちらも空 % python display_memcached.py 17002
セッションストレージをデフォルトのmemcachedへ移動
Rails6.1では dalli_store を指定しても動作しない
キャッシュの移動ができたため、次はセッションストレージを移動します。
設定方法については、DalliのWikiに記載がありました。
Caching with Rails · petergoldstein/dalli Wiki
そこで、memd_session.rb という環境ファイルを用意し
config.session_store = :dalli_store, 'localhost:11211'
と設定します。
続いて、
% bin/rails s -e memd_session
とした後、ブラウザでアクセスしてみましたが、セッションストレージはCookie Storeのままでした。
調べてみたところ、Rails 5.2でワーニングが出るようになり、:dalli_store
から :mem_cache_store
へと変更されたようです。
- :mem_cache_store vs. :dalli_store in Rails? · Issue #557 · petergoldstein/dalli
- Upgrade rails from 5.1 to 5.2 - yuanjiang.space
- Cache Invalidation Complexity: Rails 5.2 and Dalli Cache Store
Rails6.1では mem_cache_store を指定する
memd_session.rb
を修正します。
# 指定したポートにある memcached へCacheを保存 config.cache_store = :mem_cache_store, 'localhost:17001' # セッションストアを memcached へ変更 config.session_store :mem_cache_store # << 変更箇所
動作確認
再度 bin/rails s -e memd_session
で起動し、ブラウザで http://localhost:3700/home/index へアクセスします。
Cookieのキー _rails_dalli_sample_session
の代わりに _session_id
がありました。また、値もセッションIDだけになっています。
続いて、memcachedの値を確認します。
フラグメントキャッシュは、指定通り 17001 ポートにありました。
% python display_memcached.py 17001 (b'\x04\x08o: ActiveSupport::Cache::Entry\t:\x0b@valueI"I<h1>Home#index</h1>\n<p>Find me in app/views/home/index.html.erb</p>\n\x06:\x06ET:\r@versionI"\x00\x06;\x07F:\x10@created_atf\x161622554618.821114:\x10@expires_in0', b'5') ...
一方、セッションの値はデフォルトポートのmemcachedに入っていました。
\x07I"\x08foo\x06:\x06EFI"\x08bar\x06;
のようにして、 foo=bar
なセッションの値が確認できました。
% python display_memcached.py 11211 (b'\x04\x08{\x07I"\x08foo\x06:\x06EFI"\x08bar\x06;\x00TI"\x10_csrf_token\x06;\x00FI"1D7iFliPuRIrsP1D1GfYM0lr8WhM3xKdaPWDRod9Uhs0=\x06;\x00F', b'6') ...
セッションストレージをsession用のmemcachedへ移動
コードを読んで、設定方法を調査
ポートを指定する方法がRailsガイドでは分かりませんでした。
そこで、Dalliの公式Wikiにあったように、
config.session_store :mem_cache_store, 'localhost:17002'
と、第2引数にホストとポートを指定して起動してみたところ
`session_store': wrong number of arguments (given 2, expected 0..1) (ArgumentError)
というエラーになりました。
そこでRailsガイドのセッションストレージの記載
ActionDispatch::Session::MemCacheStore
:データをmemcachedクラスタに保存する (この実装は古いのでCacheStoreを検討すべき)
より、 ActionDispatch::Session::MemCacheStore
の実装を見ると
# https://github.com/rails/rails/blob/v6.1.3.2/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb#L17 class MemCacheStore < Rack::Session::Dalli # ...
となっていました。
次に Rack::Session::Dalli
の実装を見てみると
# https://github.com/petergoldstein/dalli/blob/v2.7.11/lib/rack/session/dalli.rb#L10 module Rack module Session class Dalli < defined?(Abstract::Persisted) ? Abstract::Persisted : Abstract::ID attr_reader :pool, :mutex DEFAULT_DALLI_OPTIONS = { :namespace => 'rack:session', :memcache_server => 'localhost:11211' } # ...
と、オプション :memcache_server
としてセッション用のmemcachedを渡せそうでした。
設定
調査結果をもとに、環境ファイル memd_session_port.rb
に
# 指定したポートにある memcached へCacheを保存 config.cache_store = :mem_cache_store, 'localhost:17001' # セッションストアを memcached へ変更し、ポートも指定する config.session_store :mem_cache_store, memcache_server: 'localhost:17002'
と設定しました。
動作確認
上記で作成した環境ファイルを指定して
% bin/rails s -e memd_session_port
と起動すると
に保存されていました。
# 無い % python display_memcached.py 11211 # Cache用 % python display_memcached.py 17001 (b'\x04\x08o: ActiveSupport::Cache::Entry\t:\x0b@valueI"I<h1>Home#index</h1>\n<p>Find me in app/views/home/index.html.erb</p>\n\x06:\x06ET:\r@versionI"\x00\x06;\x07F:\x10@created_atf\x151622644680.48053:\x10@expires_in0', b'1') ... # セッション用 % python display_memcached.py 17002 (b'\x04\x08{\x07I"\x08foo\x06:\x06EFI"\x08bar\x06;\x00TI"\x10_csrf_token\x06;\x00FI"1IseQM7XUAmDJwXYDllu0OwpS8DpoLCTRjATi8kIzOGE=\x06;\x00F', b'2') ...
想定通りの設定ができたようです。
キャッシュとセッションを同じ memcached へ保存する
当初の目的 セッションをキャッシュとは別のmemcachedへ保存する
は達成したものの、先ほど見たRailsガイド
ActionDispatch::Session::MemCacheStore
:データをmemcachedクラスタに保存する (この実装は古いのでCacheStoreを検討すべき)
の この実装は古いのでCacheStoreを検討すべき
が気になりました。
また、「Action Controller の概要」にも
ユーザーセッションに重要なデータが含まれていない場合、またはユーザーセッションを長期間保存する必要がない場合 (flashメッセージで使いたいだけの場合など) は、ActionDispatch::Session::CacheStoreを検討してください。この方式では、Webアプリケーションに設定されているキャッシュ実装を利用してセッションを保存します。この方法のよい点は、既存のキャッシュインフラをそのまま利用してセッションを保存できることと、管理用の設定を追加する必要がないことです。この方法の欠点はセッションが短命になり、セッションがいつでも消える可能性がある点です。
とありました。
そこで CacheStore
も試してみます。
設定
Railsガイドによると、
config.session_store: セッションの保存に使うクラスを指定します。指定できる値は:cookie_store(デフォルト)、:mem_cache_store、:disabledです。:disabledを指定すると、Railsでセッションが扱われなくなります。デフォルトでは、アプリケーション名と同じ名前のcookieストアがセッションキーとして使われます。カスタムセッションストアを指定することもできます。
config.session_store :my_custom_store
カスタムストアはActionDispatch::Session::MyCustomStoreとして定義する必要があります。
とありました。
ActionDispatch::Session::CacheStore
はすでに存在していることから、Railsガイドに従って環境ファイル session_cache_store.rb
に
# 指定したポートにある memcached へCacheを保存 config.cache_store = :mem_cache_store, 'localhost:17001' # セッションストアを "ActionDispatch::Session::CacheStore" にする config.session_store :cache_store # ちなみに、以下の書き方でも動作した # config.session_store ActionDispatch::Session::CacheStore
に、キャメルケースをスネークケースに変換したシンボル :cache_store
で指定します。
動作確認
上記で作成した環境ファイルを指定して
% bin/rails s -e session_cache_store
と起動してアクセス後に確認したところ、
# 無い % python display_memcached.py 11211 # キャッシュとセッションが同居 % python display_memcached.py 17001 (b'\x04\x08o: ActiveSupport::Cache::Entry\t:\x0b@value{\x07I"\x08foo\x06:\x06EFI"\x08bar\x06;\x07TI"\x10_csrf_token\x06;\x07FI"1-z6lVxxcPeSrD6sBI0LsfxUWlmsIVr7ybExtJix2uX8=\x06;\x07F:\r@version0:\x10@created_atf\x161622879435.064419:\x10@expires_in0', b'2') ... (b'\x04\x08o: ActiveSupport::Cache::Entry\t:\x0b@valueI"I<h1>Home#index</h1>\n<p>Find me in app/views/home/index.html.erb</p>\n\x06:\x06ET:\r@versionI"\x00\x06;\x07F:\x10@created_atf\x161622879435.008329:\x10@expires_in0', b'1') ... # 無い % python display_memcached.py 17002
と、キャッシュとセッションが同居していました。
ちなみに、
config.cache_store = :file_store, Rails.root.join('tmp', 'cache', 'files') config.session_store :cache_store
とすると、キャッシュとはファイルが別だったものの、セッションもファイルストレージに保存されていました。
こんな感じです。
^D^Ho: ActiveSupport::Cache::Entry :^K@value{^GI"^Hfoo^F:^FEFI"^Hbar^F;^GTI"^P_csrf_token^F;^GFI"1Va3rVZwaQqxekgbKJHpN6dyA5JEyFtrbrO9kWqmOdfs=^F;^GF:^M@version0:^P@created_atf^W1622870283.7916899:^P@expires_in0
ソースコード
Githubに上げました。
thinkAmi-sandbox/rails_session_of_memcached-sample