Rails6.1で、セッションをキャッシュとは別のmemcachedへ保存する

Rails6.1でセッションをキャッシュとは別の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に暗号化された値が保存されています。

f:id:thinkAmi:20210605143809p:plain

 

キャッシュの保存先を変更するための準備

セッションの保存先を変える前に、まずは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のライブラリを探したところ

を組み合わせれば良さそうでした。

そこで、コマンドライン引数としてポートを渡せば中身を確認できるようなスクリプト

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がローカルのデフォルトポートで動作していると仮定して起動しますが、この設定は大規模サイトには向いていません。

Rails のキャッシュ機構 - Railsガイド

 
そのため、上記で作成した 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 へと変更されたようです。

 

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を検討すべき)

5 セッション | Action Controller の概要 - Railsガイド

より、 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

と起動すると

  • キャッシュはキャッシュ用memcached (17001ポート)
  • セッションはセッション用memcached (17002ポート)

に保存されていました。

# 無い
% 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を検討すべき)

5 セッション | Action Controller の概要 - Railsガイド

この実装は古いのでCacheStoreを検討すべき が気になりました。

また、「Action Controller の概要」にも

ユーザーセッションに重要なデータが含まれていない場合、またはユーザーセッションを長期間保存する必要がない場合 (flashメッセージで使いたいだけの場合など) は、ActionDispatch::Session::CacheStoreを検討してください。この方式では、Webアプリケーションに設定されているキャッシュ実装を利用してセッションを保存します。この方法のよい点は、既存のキャッシュインフラをそのまま利用してセッションを保存できることと、管理用の設定を追加する必要がないことです。この方法の欠点はセッションが短命になり、セッションがいつでも消える可能性がある点です。

5 セッション | Action Controller の概要 - Railsガイド

とありました。

そこで CacheStore も試してみます。

 

設定

Railsガイドによると、

config.session_store: セッションの保存に使うクラスを指定します。指定できる値は:cookie_store(デフォルト)、:mem_cache_store、:disabledです。:disabledを指定すると、Railsでセッションが扱われなくなります。デフォルトでは、アプリケーション名と同じ名前のcookieストアがセッションキーとして使われます。カスタムセッションストアを指定することもできます。

config.session_store :my_custom_store

カスタムストアはActionDispatch::Session::MyCustomStoreとして定義する必要があります。

3.1 Rails全般の設定 | Rails アプリケーションを設定する - Railsガイド

とありました。

 
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