Slide 1

Slide 1 text

STORES 株式会社 @hogelog (Sunao Komuro) 2022年10月 十年ものアプリのセッションストレージを クッキーからRedisに移行するときに気に したこと、それでも起きてしまったこと

Slide 2

Slide 2 text

自己紹介 2 @hogelog (Sunao Komuro) - STORES 株式会社 CTO室 - 2021年6月入社 - サービス開発、基盤領域、マネージャーなど を経て、現在はいちバックエンドエンジニア

Slide 3

Slide 3 text

目次 3 背景 計画と実装 リリースと失敗 修正と再挑戦 まとめ 01 02 03 04 05

Slide 4

Slide 4 text

Railsのセッションストア 背景

Slide 5

Slide 5 text

Railsのセッションストア 5 - `rails new myapp` しただけでセッションが利用可能 - デフォルトセッションストアはCookieStore 1 │ class CountController < ApplicationController 2 │ def show 3 │ session[:count] = session[:count].to_i + 1 4 │ render plain: "hello: #{session[:count]}" 5 │ end 6 │ end

Slide 6

Slide 6 text

CookieStoreの嬉しいところ 6 - 追加インフラ不要 - 追加コードも一切不要 - ユーザが増えてもセッションがあふれない - STORESのRailsもCookieStoreでした

Slide 7

Slide 7 text

CookieStoreの嬉しくないところ 7 - セッションの実体がクライアントにある - セッション無効化などの制御も難しい - セキュリティ的にもサーバサイドに置くことがベストプラクティス - > However the client can edit cookies that are stored in the web browser so expiring sessions on the server is safer. Rails Guides - > The best practice is to use a database based session OWASP Cheet Sheet Series: Ruby on Rails Cheet Sheet - セッションをサーバサイドに切り替えよう

Slide 8

Slide 8 text

セッションストレージをサーバサイドに切り替える 計画と実装

Slide 9

Slide 9 text

要件 9 - 対象サービス: STORES(ネットショップ) - 2012年8月リリース - ほぼモノリシックRailsアプリケーション - セッション数は1000万オーダー - セッション期限 - ログインユーザ: 2週間 - 非ログインユーザ: セッションクッキー(すぐ揮発) - セッションのリセットはしたくない

Slide 10

Slide 10 text

技術選定 10 - ストレージ: Amazon MemoryDB for Redis - Redis プロトコルを喋る Aurora 的な存在 - セッションストア: redis-rails (redis-actionpack) gem - 技術選定の理由は STORES Product Blog にて - https://product.st.inc/entry/2022/07/07/142350 - https://product.st.inc/entry/2022/07/07/142350

Slide 11

Slide 11 text

移行戦略 11 - セッションは維持したまま - ダブルライトによる段階的な移行 1. クッキーのみ 2. ダブルライト (Write: クッキー&Redis, Read: クッキー) 3. ダブルライト (Write: クッキー&Redis, Read: Redis) 4. Redisのみ

Slide 12

Slide 12 text

必要な部品 12 - ダブルライト (Write: クッキー&Redis, Read: クッキー) - ほぼ CookieStore だが書き込みは RedisStore にも実行 - ダブルライト (Write: クッキー&Redis, Read: Redis) - ほぼ RedisStore だが書き込みは CookieStore にも実行

Slide 13

Slide 13 text

ダブルライト (Write: クッキー & Redis, Read: クッキー) 13 - `class CookieToRedis < ActionDispatch::Session::CookieStore` - delete_session, commit_session でダブルライト 1 │ class CookieToRedis < ActionDispatch::Session::CookieStore 2 │ def initialize(app, options = {}) 3 │ super(app, options[:cookie_store]) 4 │ @redis_store = ActionDispatch::Session::RedisStore.new(app, options[:redis_store]) 5 │ end 6 │ 7 │ def delete_session(req, session_id, options) 8 │ super 9 │ @redis_store = delete_session(req, session_id, options) 10 │ end 11 │ 12 │ def commit_session(req, res) 13 │ super 14 │ @redis_store.commit_session(req, res) 15 │ end 16 │ end 17 │ 18 │ Rails.application.config.session_store CookieToRedis, { 19 │ cookie_store: { key: "cookie_id" }, 20 │ redis_store: { key: "redis_id", servers: ..., expire_after: 2.weeks }, 21 │ }

Slide 14

Slide 14 text

ダブルライト (Write: クッキー & Redis, Read: Redis) 14 - `class RedisToCookie < ActionDispatch::Session::RedisStore` - delete_session, commit_session でダブルライト 1 │ class RedisToCookie < ActionDispatch::Session::RedisStore 2 │ def initialize(app, options = {}) 3 │ super(app, options[:redis_store]) 4 │ @cookie_store = ActionDispatch::Session::CookieStore.new(app, options[:cookie_store]) 5 │ end 6 │ 7 │ def delete_session(req, session_id, options) 8 │ super 9 │ @cookie_store = delete_session(req, session_id, options) 10 │ end 11 │ 12 │ def commit_session(req, res) 13 │ super 14 │ @cookie_store.commit_session(req, res) 15 │ end 16 │ end 17 │ 18 │ Rails.application.config.session_store RedisToCookie, { 19 │ cookie_store: { key: "cookie_id" }, 20 │ redis_store: { key: "redis_id", servers: ..., expire_after: 2.weeks }, 21 │ }

Slide 15

Slide 15 text

セッションストア切り替えの実施と発生した問題 リリースと失敗

Slide 16

Slide 16 text

ダブルライトの有効化(一回目) 16 - Redis (MemoryDB) へのダブルライトは順調に進む - Redisにも正しいセッションが書き込まれている - しかし、キーの増加が止まらずメモリが溢れてしまう💣💥

Slide 17

Slide 17 text

なぜキーの増加が止まらなかったのか(その1) 17 - maxmemory-policy デフォルト値 - ElastiCache Redis: volatile-lru - MemoryDB: noeviction - 適切な maxmemory-policy が設定されていなかった

Slide 18

Slide 18 text

MemoryDB 設定の調整 18 - maxmemory-policy の設定 - MemoryDB のキーが揮発しメモリに余裕が - しかし、しばらくすると揮発しなくなる - キーが増え続けメモリが溢れる💣💥

Slide 19

Slide 19 text

なぜキーの増加が止まらなかったのか(その2) 19 - 調査 - TTLなしキー(揮発しないキー)でいっぱいになっていた - Redis monitor コマンドで調査 - ダブルライト実装、rack、redis-rails の精査 - 原因 - CookieToRedisダブルライト実装の不備 - STORES のセッション期限仕様 - redis-rails gem の仕様(不具合?)

Slide 20

Slide 20 text

CookieToRedisダブルライト実装の不備 20 - CookieToRedisはRedisStoreに対しcookie_store設定値で書き込 んでいた 373 │ def commit_session(req, res) 374 │ session = req.get_header RACK_SESSION 375 │ options = session.options … 388 │ if not data = write_session(req, session_id, session_data, options) rack-2.2.3.1/lib/rack/session/abstract/id.rb

Slide 21

Slide 21 text

STORES のセッション期限仕様 21 - セッション期限 - ログインユーザ: 2週間 - 非ログインユーザ: セッションクッキー(すぐ揮発) - CookieStoreではログインセッション期限は2週間に伸ばしている がデフォルトは`expire_after: nil` Rails.application.config.session_store CookieToRedis, { cookie_store: { key: "cookie_id" }, redis_store: { key: "redis_id", servers: ..., expire_after: 2.weeks }, }

Slide 22

Slide 22 text

CookieStore における `expire_after: nil` の挙動 22 - セッション内容はセッションクッキーに書き込まれる - ブラウザ終了などによりすぐに揮発する

Slide 23

Slide 23 text

RedisStore における `expire_after: nil` の挙動 23 - セッションキーはセッションクッキーに書き込まれる - ブラウザ終了などによりすぐに揮発する - セッション内容はRedisにTTLなしで書き込まれる - TTLなしのキーは揮発しない - どこからも読まれない揮発しないRedisキーが生まれる

Slide 24

Slide 24 text

非ログインユーザの揮発しないセッション内容がMemoryDBに残り続けていた 24 - 非ログインユーザのセッションを `expire_after: nil` で書き込み - セッションキーはセッションクッキーなのですぐ揮発 - セッション内容はTTLなしでMemoryDBに蓄積される - TTLなしの揮発しないキーでメモリが溢れる💣💥

Slide 25

Slide 25 text

問題の修正と切り替えの再挑戦 修正と再挑戦

Slide 26

Slide 26 text

ダブルライト実装の修正 26 - CookieStore も `expire_after: 2.weeks` に統一 - CookieStore非ログインユーザのセッション期限も2週間に Rails.application.config.session_store CookieToRedis, { cookie_store: { key: "cookie_id", expire_after: 2.weeks }, redis_store: { key: "redis_id", servers: ..., expire_after: 2.weeks }, }

Slide 27

Slide 27 text

モニタリングの整備 27 - Redisキーも一度全て破棄 - モニタリングなど周辺整備

Slide 28

Slide 28 text

セッションストア切り替え再実施 28 - TTL なしのキーが生まれていない - MemoryDBメモリ使用量も2週間程度で高止まり - その後も順調に進み、無事 Redis 移行完了

Slide 29

Slide 29 text

なにをすべきだったか、これからなにをすべきか ふりかえり

Slide 30

Slide 30 text

CookieStoreを使うべきだったのか 30 - 2012年のリリース時点では2022年の状況はわからない - 一旦CookieStoreで走っていくのは悪くない(かもしれない) - セッションストアの切り替えは大変 - 別の選択肢を考えても良いかも知れない

Slide 31

Slide 31 text

セッションのリセットは許容できなかったのか 31 - セッションのリセットを許容していればダブルライトは不要 - リセット許容した方がコストも少なかったかもしれない - ステークホルダーとのコミュニケーションを避けて技術的解決 に走っていたかもしれない

Slide 32

Slide 32 text

redis-rails gem 32 - 人気の gem だが開発は活発ではない - `expire_after: nil` 仕様(不具合?)のIssueも反応なし - STORES 及び世界中で広く使われているgem - Issue を投げるだけではない、より積極的なコントリビュートの必 要性 - @hogelog の今後の活躍にご期待ください

Slide 33

Slide 33 text

33