
はじめに
こんにちは。ITインフラ本部 SRE部のシュウです。
今回はDMMブックスで発生したスティッキーセッションに関連するパフォーマンス問題の調査と対応について紹介します。
急にレスポンスタイムが悪化した
New RelicのAPMでDMMブックスのレスポンスタイムを確認していると、急にレスポンスタイムが悪化していました。

しかし各種インフラのメトリクスは常にモニタリングしており、負荷が高くなっている様子も見られなかったため、原因の特定に難航しました。
該当時間帯は、CPU使用率、メモリ使用率、ともに低い水準で推移しており、インフラのリソース不足が原因ではないと判断しました。
CPU使用率

メモリ使用率

しかし、さらに調査を進めると、CPU使用率の平均値には問題がないものの、最大値が非常に高くなっていることがわかりました。

コンテナ間での負荷分散がうまくいっていない可能性があるため、New RelicのAPMで各コンテナのレスポンスタイムを確認してみると、
全体的にレスポンスタイムが悪化しているわけではなく、特定のコンテナのレスポンスタイムだけが非常に悪化していました。

さらに、各コンテナの1分あたりのリクエスト数を確認したところ、特定のコンテナにリクエストが集中していることがわかりました。
このコンテナのIDはレスポンスタイムが悪化しているコンテナのIDと一致していました。

リクエストが特定のコンテナに集中している原因は、ALBのスティッキーセッション以外には考えられなかったため、
ALBのスティッキーセッションを一時的に無効化して、リクエストの分散状況を確認しました。
スティッキーセッション無効化中、特定のコンテナにリクエストが集中することはなくなり、各コンテナに均等にリクエストが分散されるようになりました。

十分な数のコンテナが稼働しており、CPUやメモリの使用率も低いため、
スティッキーセッションによるアクセス数の偏りが多少あっても、問題はないと考えていました。
しかし、今回の問題でCookieを使い回すボットからの大量アクセスがあると、特定のコンテナに負荷が偏る場合もあるとわかりました。
スティッキーセッション有効時、Cookieを使い回すと全てのアクセスが同じコンテナに割り振られるため、
スクレイピングなど、ボットからの大量アクセスに対して非常に相性が悪いとわかりました。
問題のリクエストの特定を試みるが失敗した
スティッキーセッションはALBが発行するCookieを利用しており、
同じコンテナに割り振られるのは同じCookieを持つクライアントからのリクエストです。
IP別のリクエスト数も確認してみましたが、問題のIPを特定できず、
同じCookieを使い回しながら、複数のIPからリクエストを送信してきているようです。

DMMブックスはALBが発行するCookieを利用してスティッキーセッションを実現しており、AWSALB という名前のCookieがALBによって付与されています。
問題のリクエストを特定するために、Nginxのログフォーマットを調整して、AWSALB のCookieの値をアクセスログに出力するようにしました。
map "$http_cookie" "$awsalb_cookie" { default ""; "~*AWSALB=([^;]+)" "$1"; } log_format json escape=json '{"@timestamp": "$time_iso8601",' ' "remote_addr": "$real_ip",' ' "x-forwarded-for": "$http_x_forwarded_for",' ' "status": "$status",' ' "protocol": "$server_protocol",' ' "method": "$request_method",' ' "host": "$host",' ' "path": "$request_uri",' ' "size": "$body_bytes_sent",' ' "reqtime": "$request_time",' ' "apptime": "$upstream_response_time",' ' "user_agent": "$http_user_agent",' ' "forwarded_for": "$http_x_forwarded_for",' ' "forwarded_proto": "$http_x_forwarded_proto",' ' "referrer": "$http_referer",' ' "x-amzn-trace-id": "$http_x_amzn_trace_id",' ' "alb-stickiness": "$awsalb_cookie",' ' "cdp_id": "$cookie_cdp_id"}';
しかし実際にログを確認してみると、同じクライアントからの連続したリクエストであっても、AWSALB のCookieの値が頻繁に変わっていることがわかりました。
スティッキーセッションの仕組みについて調べると、どうやらALBはアクセスがあるたびにAWSALB のCookieの値を更新しているようです。
そのため、Cookieの値をそのままログに出力しても、同じクライアントからのリクエストを特定できませんでした。

スティッキーセッションを無効化するため、Blue/Greenデプロイメントへ移行する
DMMブックスでスティッキーセッションを利用している理由は、
ECSのローリングアップデート時に、古いバージョンと新しいバージョンのコンテナが混在する状態でアクセスされた場合に、リリース内容によってはエラーとなる可能性があるためです。
そのため、ECSのデプロイメント方式をローリングアップデートからBlue/Greenデプロイメントに変更して、古いバージョンと新しいバージョンのコンテナへのトラフィック切り替えを一斉に行うようにして、
スティッキーセッションを不要にしました。
ECSの組み込みBlue/GreenデプロイメントはDMMブックスと相性が良くない
2025年7月17日にAWSはECSの組み込みBlue/Greenデプロイメント機能をリリースしました。
これによりCodeDeployを利用せずにECSでBlue/Greenデプロイメントが可能になりました。
それに伴いCodeDeployを利用したECSのBlue/Greenデプロイメントは非推奨となりました。
そのため、DMMブックスでもECSの組み込みBlue/Greenデプロイメントを利用することにしましたが、以前のCodeDeployを利用したBlue/Greenデプロイメントのようにそのまま使えませんでした。
ALBのリスナールールが複雑な場合はトラフィック切り替えに独自のLambdaを実装する必要がある
以前のCodeDeployを利用したBlue/Greenデプロイメントでは、ALBのリスナーを指定して、デプロイ時に該当リスナー内のターゲットグループを切り替える仕組みでした。
しかし、新しいECSの組み込みBlue/Greenデプロイメントでは、ALBのリスナールールを指定して、デプロイ時に該当リスナールール内のターゲットグループを切り替える仕組みになっています。
本番リスナールールとして設定されたリスナールールはターゲットグループが切り替えられますが、それ以外のリスナールールは自動で切り替わらないため、トラフィックが古いターゲットグループに流れ続けてしまいます。
CodeDeployを利用したBlue/Greenデプロイメントのように、リスナー単位でターゲットグループが切り替わらないので、リスナールールの数が多い場合はデフォルトの仕組みでは対応できません。
AWSの方に問い合わせたところ、ECSの組み込みBlue/Greenデプロイメントでは、1つのALBに対して1つの本番リスナールールしかサポートされていないとのことです。 ただし、最大5つのALBを指定できるため、同じALBを5回繰り返し指定すれば、最大で5つのリスナールールを本番リスナールールとして指定できるとのことでした。 しかし、DMMブックスでは6つ以上のリスナールールが存在しており、この場合は対応できません。
そのため、Lambdaの関数をBlue/Greenデプロイメントのライフサイクルフック(PRODUCTION_TRAFFIC_SHIFT)で動かして、すべてのリスナールールのターゲットグループを切り替える仕組みを実装する必要があります。
まとめ
十分な数のコンテナを用意して、CPUやメモリの使用率も低い場合であっても、
スティッキーセッションを有効化していると、ボットからアクセスが集中して特定のコンテナに負荷が偏る場合もあります。
平均値ではなく、最大値のメトリクスも確認して、特定のコンテナに負荷が集中していないか注意する必要があります。