スティッキーセッションをやめ可用性・弾力性を高める
こんにちは。エンジニアの佐々木です。
私たちが運営する「北欧、暮らしの道具店」のサーバーサイドで使用している言語はPHPで、フレームワークにはLaravelを採用しています。また、サーバー自体はECS on Fargateで稼働させています。
フロントエンドではJavaScriptのフレームワークにVue.js、CSSでSass(SCSS)を採用しており、これらを元にビルドした成果物はDocker(ECSタスク)内に格納していました。
先日、これらの成果物はDocker内には格納せず、AWSのS3に配置しCloudFrontから配信するよう変更を加えました。CloudFrontとS3は普段から使用しており実装コストが低かったため採用しています。
目的はスティッキーセッションをやめることで可用性・弾力性を高めることです。
この記事では、見直しを行うにいたった背景や移行の進め方、その他の改善点や変化についてご紹介します。
そもそもなぜDocker内に置いていたか
私が開発に参加した時点では元々CloudFront + S3の構成で配信を行っており、セッションの管理はElastiCache(Redis)で行っていました。しかし、ある時デプロイしてもフロントの変更が上手く反映されてないことに気づきます。
反映されないことがあった当時、ビルドツールとしてはLaravel Mixを採用しており、キャッシュバスティングはクエリストリングで行われる仕組みとなっていました。
この時、S3はまだ「強い一貫性」がサポートされておらず、「結果整合性」でオブジェクトを参照する状態でした。
この「結果整合性」の影響によって、以下の状態に陥っていました。
S3オブジェクトの更新を行ってもCloudFrontが参照するタイミングによっては古いまま
そのままCloudFrontは古いオブジェクトをキャッシュ
そのため仮にクエリストリングを変更しても変更が反映されない
新規のオブジェクトであれば「結果整合性」の影響を回避できるため、クエリストリングではなくファイル名にバージョニングハッシュを組み込みたいと思っていました。しかしLaravel Mixの方針としてはそれは行わないということでした。
ビルドツールの変更やプラグインの自作等で対応することも一瞬考えましたが、早く対応したい中で工数やメンテナンスし続けることを考えた結果、以下のように対応しました。
CloudFront + S3の構成をやめサーバー内にアセットを保持
スティッキーセッションを有効にする
スティッキーセッションとは、簡単に説明すると「サーバーが複数台ある場合に、セッションが有効な間、クライアントとサーバーを常に一対一で接続させるロードバランサーの機能」となります。
クラシコムではローリングデプロイを採用しており、デプロイ中はリクエストによって新旧のコードが混在してしまう可能性がありました。スティッキーセッションを有効にすることで、セッションを特定のサーバーに固定し問題を回避するようにしたのです。
見直しに至った背景
サーバー内に成果物を格納しスティッキーセッションを有効にすることで、変更が反映されない問題は解消されました。しかし今度は違う問題が発生することとなります。
クライアントとサーバーを常に一対一で接続させるということは、仮に特定のクライアントからリクエストが大量にくれば、特定のサーバーのみ負荷が高くなってしまうことがあるということです。
通常時からかなり余裕を持ったキャパシティでシステムを動かしておき、大抵のリクエストが捌ける状態になっていれば問題にならない可能性もあります。が、さすがにこれは非効率です。
特定クライアントからの集中リクエストの防御策としては、WAFのレートリミット設定などもあります。ただし、レートリミットの評価を行うためのカウントの間隔より短い時間で大量にリクエストされると、リクエストの遮断を実施する前にサーバ負荷が高くなり、レイテンシが上がってしまい他のクライアントが巻き添えを食ってしまうことがありえます。
ちなみにAWS WAFの場合だと30 秒ごとにリクエストのレートをチェックし毎回直近 5 分間のリクエストをカウントしているので、それより短時間で集中してリクエストがくると厳しいです。
このような状態では、オートスケーリングの設定をしていても可用性が保てているとは言い難いですし、弾力性を失った状態とも言えます。大きな問題となったことは幸いありませんが、いつかは直さねばと修正タイミングを図っていました。
今回修正対応を実施したきっかけは、少し前にLaravelのバージョンを9に、Vue.jsを3へとバージョンアップするに伴い、ビルドツールをLaravel MixからViteに切り替えたことでした。Viteにしたことでビルド時にバージョニングハッシュがデフォルトでファイル名に組み込まれるようになっていたからです。
移行の進め方
移行自体は難しくありません。大まかに、以下のような流れで実施しています。
CloudFrontとS3を準備
CIでビルドした成果物をS3に配置
参照先がコンテナ内からCloudFrontになるよう環境変数を変更
ECSタスクに成果物を配置しないようDocker側を対応
3の環境変数の変更に関してですが、Laravel 9以降からLaravelが正式にVIteに対応しておりプラグインも提供されています。
これによって、ASSET_URLという環境変数をセットすることでアセット類の参照URLを切り替えることが可能となっています。
今回はこの機能を利用するため、コンテナ内のアセット類にアクセスするURLとCloudFrontのURLは異なるようにしました。結果、参照先の切り替え時には環境変数の値を一つ変更すれば良いだけという、非常に切り替えが行いやすい状態にできたと思います。
一つ注意点として、CIでビルドする時にもASSET_URLという環境変数をセットしておく必要があります。この環境変数がセットされていないと、SCSSのurl()関数を使用した箇所のURLの置換が行われないまま成果物内に今までと同じURLが書き込まれます。画像ファイルなどを同一階層で管理してurl()でロードしている場合などは気をつけましょう。
その他改善点、変化
スティッキーセッションをやめたことで可用性・弾力性が改善されました。今回、その他にも副作用としていくつか改善点、変化がありました。
カナリアリリース時のオペレーション考慮事項の削減
ALBの加重ルーティングによるカナリアリリース終了時、リクエストの流量を0に設定しても一定時間はスティッキーセッションがそのまま継続される
そのため、0に設定された側のサーバーをそのまま落とすと、一部ユーザーにとって障害となってしまうという危険性があった
ALBのターゲットレスポンスタイムが上昇
アセット類のレスポンスで平均が押し下げられていた
実質的に悪化したわけではない
ALB, ECSへのリクエスト量が減った
ログ調査時、サーバーのアクセスログのスキャン量が減少
ログは基本的にCloudWatch Logs, S3に置いており、アセット類のアクセスログを確認することは多くないため地味に嬉しいポイント
また、配信方法を変えたことが直接的に影響しているわけではありませんが、以下のような改善も加えることができました。
CloudFrontで圧縮形式にBrotliを採用
gzipよりも圧縮率が高いため、ファイルサイズが小さくなりアプリケーションのパフォーマンス改善に寄与
圧縮してくれるファイルタイプは特定のContent-Typeだけでimage/jpegやimage/pngなどは対象外
ref: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/ServingCompressedFiles.html
Dockerイメージのビルド時間短縮
これまではNGINXのDockerイメージビルド時にアセット類のビルドを実行
そしてmanifest.jsonをPHP側のイメージに渡すという依存関係があった
CIでアセットビルドを行うことで依存関係が解消され、並列でDockerイメージのビルドもできるようになりCIの実行時間が短縮した
まとめ
以上をまとめます。
アセット類の配信をサーバーからCDNに変更し、スティッキーセッションをやめることで、サービスの可用性・弾力性を高めた
Laravel + Viteの組み合わせであれば環境変数でURLのスイッチが容易なので、実際の切り替え作業もロールバックも容易
同時に運用面でいくつかのメリットを享受できるようになった
お客様の体験を妨げないことを目的に対応しましたが、結果的に開発者にも嬉しい対応となりました。
クラシコムではほとんどのエンジニアがフルサイクルで開発を行っています。領域にとらわれず、良いサービスを作ることに興味のあるエンジニアを募集しています。