AWSのコスト削減を考えるシリーズ、第3弾です。
今回は、キャッシュサービスについてお話しします。
Re:Linkの匿名チャットでは、Redisを利用しています。
Redisというと、セッション情報や頻繁に参照するデータを一時的に保存し、データベースへのアクセスを減らす「キャッシュ」としての利用を思い浮かべる方が多いかもしれません。
しかし、Reにおける主な用途は、一般的なデータキャッシュとは少し異なります。
キャッシュは何に使っているか?
匿名チャットでは、複数のECSコンテナにまたがってWebSocket接続を扱います。接続しているユーザーが異なるコンテナへ振り分けられていても、同じチャットルーム内で問題なくメッセージをやり取りできなければなりません。
そこでRedisのPub/Sub機能を利用し、各コンテナ間でチャットメッセージを中継しています。必要な接続情報も共有することで、どのコンテナに接続しているユーザー同士でも、同じチャットルームで会話できる構成にしていました。
複数のコンテナが稼働する環境では、Redisは重要な役割を果たします。
では、サービスを公開したばかりで、利用者がまだ数人しかいない場合はどうでしょうか。
アクセスが少ないためオートスケーリングは動かず、ECSコンテナは常に1つだけ。すべてのWebSocket接続が、同じコンテナ内で完結しています。
その状態でも、Redisは本当に必要なのでしょうか。
利用されていなくても、Redisの稼働費用は発生します。
将来必要になるからという理由だけで、サービス開始時点から固定費を払い続けるべきなのか。あるいは、必要になるまでは別の方法で代替し、利用者が増えた段階でRedisを導入すべきなのか。
今回も、当たり前だと思っていた構成を一度疑うところから始まりました。
Redisの自動起動を考えてみる
「コンテナが1つの間はRedisを使わず、2つ以上になったときだけ自動的に起動すればよいのでは?」
そんな方法も考えました。
しかし、自動で切り替えるとなると、考えなければならないことが一気に増えます。
何をきっかけにRedisを起動するのか
- Redisが利用可能になるまで、ECSのスケールアウトをどう待たせるのか
- Redisの接続先を、各コンテナへどう反映するのか
- すべてのコンテナを、どのタイミングでRedis利用へ切り替えるのか
- Redisの起動に失敗した場合、どう切り戻すのか
- コンテナが1つに戻ったら、いつRedisを削除するのか
- ローリングデプロイで一時的にコンテナが増えた場合はどう扱うのか
- 接続中のWebSocketや配信途中のメッセージをどう守るのか
これらを一つずつ制御すれば、自動切り替え自体は実現できるかもしれません。
しかし、Redisの固定費を抑えるために、別の複雑な仕組みを作り込むことになります。開発やテスト、障害対応まで含めて考えると、サービス開始直後の私たちにとっては割に合いません。
ここまで考えたところで、自動切り替えはやめることにしました。
コンテナの最大起動数で静的に判断
自動切り替えは複雑になるため、Terraformの設定をもとに静的に判断することにしました。
具体的には、ECSの最大起動数が1つの場合はRedisを作成せず、コンテナ内でWebSocket接続とメッセージを管理します。
最大起動数を2つ以上に変更する場合は、TerraformでRedisもあわせて作成します。同時に、ECSへ渡す環境変数を変更し、メッセージの共有先をコンテナ内のメモリからRedisへ切り替えます。
最大起動数が1
→ Redisを作成しない
→ コンテナ内で処理する
最大起動数が2以上
→ Redisを作成する
→ 環境変数へRedisの接続先を設定する
これなら、実行中のアプリケーションがコンテナ数を監視して動的に切り替える必要はありません。
利用者が増えてスケールアウトが必要になったときに、Terraformの設定を変更してデプロイする。Redisの準備が完了したあとで、複数コンテナの構成へ移行できます。
完全な自動化ではありませんが、構成が単純で、切り替えのタイミングも明確です。
サービス開始直後は、将来のための固定費を払い続けるのではなく、必要になったときに構成ごと切り替える。
私たちにとっては、この方法がコストと運用負荷のバランスを取りやすい選択でした。
現在、チャット中の接続は?
もう一つ考えなければならないのが、構成を切り替える時点ですでに接続している利用者です。
TerraformでRedisを作成し、ECSの環境変数を変更すると、新しい設定を反映したコンテナへ順次入れ替わります。
しかし、古いコンテナではメッセージをコンテナ内で管理し、新しいコンテナではRedisを利用します。この2つが同時に動いている間は、同じチャットルームの利用者が別々のコンテナへ接続すると、メッセージを共有できない可能性があります。
では、切り替え時に接続しているWebSocketをどうするのか。
- 古い接続がすべて終了するまで待つのか
- 一時的に新規接続を止めるのか
- 接続中の利用者を切断し、再接続してもらうのか
- 新旧両方の方式へメッセージを配信する期間を設けるのか
- メンテナンス時間を設けて一斉に切り替えるのか
- 無停止で切り替えようとすれば、ここでも仕組みは複雑になります。
Re:Linkの場合、Redisを導入する段階でも利用者はまだ多くないと考えています。そのため、切り替え時には短いメンテナンス時間を設け、接続中のWebSocketを終了させたうえで、Redisを利用する構成へ一斉に切り替える方法が現実的です。
切り替え後は、利用者にWebSocketを再接続してもらいます。
将来、利用者が増えてサービスを止められなくなった場合には、接続のドレイニングや新旧方式を併用する移行処理が必要になるかもしれません。しかし、サービス開始直後から、そのための複雑な仕組みまで用意する必要はないと判断しました。
まとめ
Redisは、複数のコンテナ間でWebSocketのメッセージを共有するために欠かせない存在です。
しかし、ECSコンテナが1つしか起動しないサービス開始直後であれば、すべての接続をコンテナ内で管理できます。その段階から、将来のスケールアウトに備えてRedisの固定費を払い続ける必要があるのか、私たちは一度立ち止まって考えました。
コンテナ数に応じてRedisを自動的に起動する方法も考えましたが、起動完了の待機や接続先の切り替え、エラー時の対応など、仕組みが複雑になってしまいます。
そこで、Terraformに設定したECSの最大起動数を基準に、構成を静的に切り替えることにしました。
最大起動数が1つであればRedisを作成せず、コンテナ内で処理を完結させる。最大起動数を2つ以上に変更するときは、Redisを作成して接続先を環境変数へ設定する。
もちろん、最初からRedisを用意しておけば、将来のスケールアウトは容易になります。しかし、まだ利用者が少ない段階では、その「いつか」のための固定費も無視できません。
将来必要になる構成を、最初からすべて用意する必要はない。
必要になる条件をあらかじめ決め、そのタイミングで安全に切り替えられる設計を残しておく。これも、スタートアップ時のAWSコストを抑えるための一つの考え方だと思います。


