駅メモ!チームエンジニアの id:yumlonne です。
この記事では駅メモ!で使っていた Memcached を廃止し Redis に統合した経緯や流れを紹介します。
記事内で提供するサンプルコードは、駅メモ!の実装に合わせ Perl となってます。 簡単なコードなので Perl に詳しく無い方でも十分理解できると思います。
KVS 統合の背景
駅メモ!は AWS を使ってサービスを提供しています。
統合前は Amazon ElastiCache で Memcached と Redis の両方を運用していました。 Memcached はプライマリノードのみ、Redis はプライマリノードとレプリカノードそれぞれ 1 台の構成でした。 それとほとんど同じ構成が他に 2 セットあるため、全体を見ると Memcached は 3 ノード存在していました。
Memcached は m6g.large
を利用していたため、リザーブドノード料金で年間 3000 ドル以上のコスト削減が期待できました。
Memcached と Redis の使い分け
駅メモ!では Memcached と Redis を以下のように使い分けていました。
- Memcached
- セッションデータ
- 揮発して良いデータ
- Redis
- ランキングデータ(sorted sets)
- 揮発してはいけないデータ
- ほとんどのデータは RDS で永続化しています
- ここではパフォーマンス面で揮発を許容できないことを指しています
統合先として Redis を選択した理由はいくつかありますが、決め手は低負荷かつ高パフォーマンスなランキング処理の実現に Redis が得意とする sorted sets 型を利用していたためです。
セッションデータの移行
ユーザに再ログインする手間をかけさせたくないため、Memcached のセッションデータだけは Redis に移行することとしました。 以下はセッションデータの移行の流れです。これによりメンテナンスなどを実施することなくセッションデータの移行を進められました。
- アクセス時にセッションデータを Memcached から Redis に移行するモジュールを作成
- (箇条書きの下にコードのサンプルを示します)
- 本番反映してからセッションの有効期限分の時間が経つまで様子を見る
- セッションの有効期限分の期間を空けることでアクセスのあるユーザのセッションデータは 1 で作成したモジュールによって移行されます
- アクセスのないユーザのセッションデータは移行されませんが、どちらにせよセッションデータの有効期限が過ぎているのでユーザには影響ありません
- Redis のセッションデータのみを参照するモジュールに差し替える
- 1 で作成したモジュールは Memcached も参照するので最終的には差し替える必要があります
以下は 1 で作成したモジュールのコードのサンプルです。
# セッションの登録 # 新規の登録は全てRedisに向ける sub set_session { my ($key, $value) = @_; # 有効期限を設定しシリアライズしてRedisに保存 $redis->set($key, $value, 'EX', $session_expire); } # セッションの取得 # Redisから取得できればそれを返し、そうでなければMemcachedから取得したものをRedisに登録して返す sub get_session { my ($key) = @_; my $session; $session = $redis->get($key); if (defined $session) { return $session; } $session = $memcached->get($key); if (defined $session) { set_session($key, $session); } return $session; }
Memcached を Redis に統合
Memcached を操作するコード全てを Redis に差し替えることを検討しましたが、差分が膨大になることでエンバグのリスク増え、動作確認およびレビュー範囲が広がってしまいます。そこで既存の Memcached を使うキャッシュモジュールと同じインターフェースを持つ Redis 実装のキャッシュモジュールを作成して差し替えることにしました。
Memcached を使うキャッシュモジュールで呼ばれていた関数を調査し、必要最低限の機能を実装しました。以下は Redis 実装のキャッシュモジュールの関数の一覧です。
- set, set_multi, add, replace
- get, get_multi
- incr, decr
- delete, flush_all(Redis では flushdb に相当)
これらの関数の実装では以下の苦労がありました。
Memcached と Redis のモジュールの振る舞いの違い
Memcached のキャッシュモジュールは Cache::Memcached::Fast::Safe で、Redis のキャッシュモジュールは Redis をベースに作成しました。
それぞれのモジュールで同じインターフェースを提供することを考えていましたが、一部のコマンドの振る舞いにクセがあり、同じインターフェースにできなかった部分もあります。
例えば Memcached は decr で 10 から 9 のように桁が減ったとき、値を取得し直すと"9"
ではなく"9 "
のように 2 文字返してきます。
use Cache::Memcached::Fast::Safe; # ローカルのMemcachedに接続するインスタンスを作成 my $memcached = Cache::Memcached::Fast::Safe->new({ servers => [ { address => "localhost:11211"} ]}); $memcached->set("key", 10); $memcached->decr("key", 1); warn sprintf('"%s"', $memcached->get("key")); # => "9 "
Perl では上記のような状態でも、数値として扱う分には問題にならないため、振る舞いの違いは許容できました。
Perl オブジェクトを素直に set できない
Memcached にはフラグ機能があり、データとは別にメタ情報を保存することができます。一方で Redis にはこのような仕組みはありません。
数値や文字列を単純に保存する場合には問題になりませんが、構造化されたデータを保存する場合には少し困ります。取り出したデータがただのバイナリデータなのか、構造化されたデータをシリアライズしたものなのかを判断できないためです。
駅メモ!では数値や文字列の扱いが多いものの、一部の機能で Perl オブジェクトを保存していました。
上記を踏まえ、今回は以下の対応としました。
- set する値が Perl のオブジェクト(ref した結果が true)なら Storable#nfeeze を使ってシリアライズする
- get した値を Storable#thaw でデシリアライズしてみて、エラーになったら thaw する前の値を返す
他の手法として、必ず一定のフォーマットのオブジェクトにしてからシリアライズしてセットするやりかたがあります。
この手法では Memcached のフラグ機能と同等の恩恵を得られますがデメリットもあります。ただの数値や文字列もシリアライズして保存すると、その値に対して incr などの直接の操作ができなくなり、redis-cli などで直接データを確認することも難しくなります。
上に書いた通り、駅メモ!では単純な数値や文字列を保存しているケースが多いため、この手法は採用しませんでした。
まとめとその後の展開
今回は駅メモ!における Memcached の廃止と Redis への統合についての手法を紹介しました。 費用面のコスト削減もさることながら、管理すべきミドルウェアが減ったことで運用面のコストも下がって一石二鳥だったと思います。
その後の展開として「Memcached と Redis の使い分け」 で触れたランキング処理について、最もデータ容量の大きいランキングを RDS に処理させるように改善し、Redis のスペックも下げることができました。こちらもいずれ記事にしたいと考えていますのでお楽しみに!
2023/01/29追記: MySQL でランキング処理を行うようにする仕組みの記事を公開しました!