
TANPのセッション管理をElastiCache(Redis)に変えた話
こんばんは!株式会社GraciaのCTOの林(@takumin513)です。TechNoteを開設してから半年間ずっと放置してきましたが、最近調べたことを振り返りも兼ねて投稿しようと思います。
弊社では、「TANP」というギフトに特化したネットショップを運営しております。フレームワークはCakePHP(3系)を使っています。
今回、ユーザーのログイン状態やカートの情報を管理するためのセッションをElastiCache(Redis)で管理するように変更したのですが、変更した背景と流れについてまとめようと思います。
Redisに変更しようと思った背景
現状のTANPの構成について
現状、TANPのアプリケーションはAWSの上に乗っており、ロードバランサーの下にEC2(アプリサーバー)が2つぶら下がっています。環境構築にはElasticBeanstalkを使っています。
CakePHPのデフォルト設定では、セッション情報はEC2の/tmp/以下にファイルとして保存されます。ファイル名には各ユーザーのセッションIDが入っています。各ユーザーは、ブラウザのクッキーに保存されたセッションIDを元に、サーバーのファイルを照合してセッションを読み書きします。
起きた問題について
TANPを開発し始めた初期は全く気にならなかったのですが、最近になってセッションが切れるまでの期間が短くなったように感じました(最大7日)。セッションが短い期間で切れてしまうと、せっかくユーザーがログインしてくれても再度ログインしないといけないため、再訪ユーザーのUXを損ねてしまいます。
アプリの方ではセッションの有効期限は半年ほどに設定していたのでおかしいなと思っていたのですが、調べていくと問題の原因は主に二つで、
1) ロードバランサーのスティッキーセッションの有効期限が短い
2) デプロイ時のALBの挙動
というものでした。
1) ロードバランサーのスティッキーセッションの有効期限が短い
ロードバランサーは、アクセスしてきたユーザーを自分の配下の複数のEC2に上手く分散して流してくれます。しかし、セッションファイルは各EC2の内部にファイルとして保存されるため、一度「TANP1」の方にアクセスしてきてログインした(セッションが生成された)ユーザーが、再度アクセスしてきたときに「TANP2」に流されてしまうと、「TANP2」にはセッションファイルが生成されていないためログイン状態が失われたように見えます。
これを防ぐのがロードバランサーの「スティッキーセッション」という仕組みで、ユーザーのアクセスを1つのEC2に固定して流してくれる役割を持っています。これにより、ユーザーは複数回アクセスしても毎回「TANP1」に流されるため、セッションが失われることがありません。
しかし、このスティッキーセッションは有効期限は最大で604800秒(=7日)なので、7日経つと切れてしまいます。厳密にいうと、アクセスするたびにクッキーの有効期限は更新されるので、「最終アクセス」から7日後に切れます(7日以内ににアクセスし続ければ切れることはないです)。また、7日経っても奇跡的に前回のEC2と同じサーバーに振り分けられればセッションが維持されますが、あまりいい設計とは言えません。
2) デプロイ時にアクセスするとセッションが切れる
これも初期の頃は気にならなかったのですが、アプリケーションのデプロイを行うとたまにセッションが切れるのが気になり始めました。これは、デプロイ時のロードバランサの挙動によるものでした。
本番環境へのデプロイは「ローリングデプロイ」で行なっています。ローリングデプロイとは、1台ずつサーバーをロードバランサから切り離してデプロイし、デプロイが終わったらサーバーをロードバランサに戻すという手法です。これにより、理論上ダウンタイムなしでデプロイすることができます。
しかし、ロードバランサーはスティッキーセッションのCookieを含んだリクエストを受信した際、ルーティング先のインスタンスのステータスを確認し、Unhealthyの場合は別のインスタンスへルーティングを行います。そのため、デプロイを行なっているインスタンスが一時的にUnhealthyになったタイミングでリクエストを受信されると、本来ルーティングされるインスタンスにルーティングされずセッションが切れる(ログイン状態が外れる)ということが起こり得ます。
解決策について
今までは、ロードバランサーの配下にEC2を1つしか置いていなかったので、この問題は発生しませんでした。なので、以前のようにサーバーを2つから1つに減らすということも検討したのですが、負荷や可用性の観点からサーバー2つのまま外部(ElastiCache)でセッション管理をした方がいいと考えました。ここからは、具体的に行った移行手順を書いて行きます。
移行手順
ElastiCacheのRedisクラスターを作成
AWSのElastiCacheのコンソール画面からRedisクラスターを作成します。
以下の二つの記事を参考にさせていただきました。
https://www.prj-alpha.biz/entry/2018/05/11/204233
https://www.prj-alpha.biz/entry/2018/05/06/230232
セキュリティグループの修正
サーバーからRedisクラスターへのアクセスを許可してあげます。具体的には、RedisクラスターのセキュリティグループのインバウンドでEC2を許可してあげます。
アプリケーションの設定ファイルを修正
CakePHPのアプリケーションの設定ファイルを変更し、セッションの保存にRedisを使用するようにします。hostにはプライマリエンドポイントを記入します。
'Cache' => [
'session' => [
'className' => 'Redis',
'duration' => '+180 days',
'prefix' => 'cake_redis_',
'host' => 'xxxx.xxxx.xxxx.xxxx.cache.amazonaws.com',
'port' => xxxx,
'fallback' => 'default',
]
],
'Session' => [
'defaults' => 'cache',
'handler' => [
'config' => 'session'
],
// cookieの設定など
],
https://book.cakephp.org/3.0/ja/core-libraries/caching.html#redisengine
EC2にphp-redisをインストールするスクリプト作成
CakePHPからRedisを扱うためには、サーバーにphp-redis拡張をインストールする必要があります。php-redisはサーバーに直接sshしてインストールしてもいいですが、インスタンスが切り替わったりAutoScalingした時に対応できないので、.ebextensionsにデプロイ時に実行するコマンドを書いておくことにしました。
# .ebextensions/php-redis.config
commands:
01_redis_install:
cwd: /tmp
test: '[ ! -f /etc/php.d/redis.ini ] && echo "redis not installed"'
command: |
wget https://github.com/nicolasff/phpredis/zipball/master -O phpredis.zip \
&& unzip -o phpredis.zip \
&& cd phpredis-phpredis-* \
&& phpize \
&& ./configure \
&& make \
&& make install \
&& echo extension=redis.so > /etc/php.d/redis.ini
.ebextensions以下のファイルは毎デプロイ時に実行されるため、php-redis拡張が入っていれば何もせず、入っていなければインストールするというようにしています。
http://qpleple.com/install-phpredis-on-amazon-beanstalk/
本番環境にデプロイする
デプロイすると、先ほどのスクリプトによりphp-redisがインストールされ、セッションがRedisに保存されるようになります。
まとめ
こんな感じでインフラがわからない僕でも無事にセッションの保存先をRedisへ移行できました。AWS様に感謝です。
弊社ではエンジニアの採用を行っています!現在サービスとしても急速に成長しており、様々なチャレンジができるいい環境になっております。
少しでも興味を持っていただけた方は以下のリンクから応募もしくは僕のTwitterなどに気軽にご連絡ください!!