PLAY DEVELOPERS BLOG

HuluやTVerなどの日本最大級の動画配信を支える株式会社PLAYが運営するテックブログです。

HuluやTVerなどの日本最大級の動画配信を支える株式会社PLAYが運営するテックブログです。

ALBの「アイドルタイムアウト」とステータスコード502,504の話

ソリューション技術部の杉嵜です。4月から部署名が変わってますが、過去にFinchの記事を書いた杉嵜と同じ人です。ちなみに当時はFinch v0.3.0でしたが、現在はv0.5.0までアップデートされています。

developers.play.jp

今回はAWS Application Load Balancer(ALB)の属性「アイドルタイムアウト」の話です。アイドルタイムアウトは秒数を設定するパラメータで、デフォルトは60秒です。そのまま使う人もいるかもしれませんが、ターゲットサーバの動作や設定値によっては、意図しない「504 Gateway Timeout」や「502 Bad Gateway」を返す原因になりかねません。不要な500番台エラーを避けるためにも、このパラメータで何が変わるのか理解しておきたいところです。

ALBを使った構成の例

ALB基本構成

ALBはHTTP/HTTPSリクエストをルーティングするロードバランサです。一般的にはL7スイッチと呼ばれる機器に相当し、HTTPリクエストをEC2インスタンスやECSのコンテナ等に割り振ってくれます。上の図はわかりやすくするためEC2インスタンス1つだけの構成ですが、一般には複数のインスタンスを並べ、分散してルーティングします。

502と504の一般的な定義

ステータスコードは標準化団体のIETFにより、RFC 9110で定義されています。なお以前はRFC 7231*1で定義されていましたが、2022年6月に公開されたRFC 9110にて廃止されています。今回の構成のALBは、この文章内でいうところの「gateway」や「proxy」にあたるものになります。

The 502 (Bad Gateway) status code indicates that the server, while acting as a gateway or proxy, received an invalid response from an inbound server it accessed while attempting to fulfill the request.

502は、gatewayからサーバへの要求に対して"無効な"レスポンスを受け取ったことを示しています。一般的にはレスポンスの形式が不適な場合や、サーバから一方的にコネクションを切断された場合に用いられます。

The 504 (Gateway Timeout) status code indicates that the server, while acting as a gateway or proxy, did not receive a timely response from an upstream server it needed to access in order to complete the request.

504は、サーバからのレスポンスが想定した時間内に返されなかったことを示しています。レスポンスが遅れる原因は多岐にわたりますが、通常はその原因をgatewayが知る由が無いので、ステータスコードが示す内容も「時間内に返事が無かった」旨だけです。

実際にはgatewayでステータスコードが生成される場合、起きた事象に対してどのコードを返すかはgatewayの実装に依るものなので注意しましょう。

「504 Gateway Timeout」を引き起こすケース

504 Gateway Timeout

まずは比較的理解しやすい、アイドルタイムアウトの設定と504 Gateway Timeoutの関係から。ALBでは、EC2インスタンス等のターゲットにリクエストを送信した後、一定時間が経過してもレスポンスが返されない場合にALBが「504 Gateway Timeout」を返します。この時に何秒待ってから504を返すかに、ALBのアイドルタイムアウトの秒数が用いられます

前項で述べた通り、ロードバランサとしてはネットワークやターゲットの状態を直接把握しているわけではありません。ALBの場合、ヘルスチェックを含めた普段のリクエストが返されることによって、今も正常に稼働しているのだろうと推定しているにすぎません。なのでALBがターゲットに送る時、ターゲットの稼働状況やネットワークの状況を正確には知らないまま送っています。その上で、本来N秒以内に届くはずのレスポンスが届かない場合は「何かがあったに、違いない…」と判断し、エラー扱いするわけです。

この「何か」が例えば基盤障害で、本当にレスポンスを返せない状態の時の504は真っ当な動作と言えるでしょう。またソースコード上のバグで無限ループに陥ったり、ファイアウォール等の設定ミスでトラフィック拒否・無視されていた場合も、ALBとしては延々と待たされてタイムアウトする形になり、クライアントへ504を返すこととなります。この場合は少なくともタイムアウトの時間が原因という話にはなりません。

一方で、ターゲットも正常稼働してレスポンスが返される見込みもあるのに、平時でも処理がN秒以上時間がかかることがあり、ALBのアイドルタイムアウトに間に合わなくて504を返すことがある。この場合は、意図しない「504 Gateway Timeout」ということになります。故にアイドルタイムアウトの値を大きくすれば、意図しない504を減らすことはできそうです。

「502 Bad Gateway」を引き起こすケース

502 Bad Gatewayを引き起こすケースを理解するには、まずALBとターゲットの間のTCPコネクションの仕組みを理解する必要があります。

TCPコネクションとコネクションの使い回し

ネットワーク上はALBからいきなりターゲットにリクエスト等のデータを送ることもできますが、送ったデータは相手に届かない可能性があります。この対策のために、現在主流のHTTP/1.1やHTTP/2ではTCPを採用しています。TCPでは、お互いにデータ送信を開始する旨を通知し合ってから(TCPコネクションの確立)データを送り、データ送信を終了する際は終わる旨を通知し合います(コネクション解放)。またコネクションがある間にデータを受け取ったら、どれだけのデータを受け取ったかを相手に通知します。こうすることで、万が一どこかのデータが相手に届かなかった場合でもリカバリができるようになります。

イメージとしては下記のような対話で、データ本体以外のやり取りがかなり増えます。実際にはデータ分割等でもう少し増えます。

  • ALB 「これからデータ(HTTPリクエスト)送るよ?」
  • EC2 「OK!こっちもデータ(HTTPレスポンス)送るよ?」
  • ALB 「OK!」「sz2[^[5d@h;uetu ...(/index.html リクエスト)」
  • EC2 「データNo.1まで受け取れた」
  • EC2 「d94uls@zhqe2[ 5eawewev[e ...(レスポンス)」
  • ALB 「データNo.1まで受け取れた」
  • ALB 「もう送信やめるねー」
  • EC2 「OK!こっちも送信やめるねー」
  • ALB 「OK!」

TCPコネクションの例

ただデータが届かなかった際のリカバリのためとはいえ、1リクエスト毎にコネクション確立・解放を繰り返すのは無駄が多そうですね。通常Webブラウザでサイトを開く場合、htmlのテキストだけでなく、そこに記述されている別のパス・URLのjavascriptやcss、画像ファイル等も必要になるので、1ページを開くだけでも複数のリクエストが送信されることが多いでしょう。その際、リクエスト1つを送る度にTCPコネクションの確立・解放をするのは、明らかにネットワークのリソースの無駄です。またレスポンスタイムの観点でも、安全に無くせるやり取りは無くしたいところです。

そのためHTTP/1.1やHTTP/2の仕様上、確立したTCPコネクション1つで複数リクエスト分のデータを送り合うようになっています。この仕様によりコネクション確立の回数が減るので、最初のリクエスト以降はコネクション確立時の遅延が無くなり、ネットワーク帯域も少し節約されるようになりました。なおTCPコネクションを使い回す際の仕様はHTTP/1.1とHTTP/2で大きく異なります。前者は1.0でオプションとして実装された機能「HTTP Keep-Alive*2」で、1.1ではデフォルトで有効になりました。後者は最初からTCPコネクションを使い回す前提で、プロトコルの仕様が作られています。

TCPコネクション1つで次のリクエストも送信する

効率上はコネクションを延々使い続ける方が良いのですが、実際にはコネクションは時々解放されます。もしTCPコネクションの管理上で問題が生じた際に使い続けると全てのリクエストがエラーになってしまうので、それを回避するために定期的に作り直すのです。またコネクション確立中は互いにリソースを使ってメッセージを待ち構えてる状態なので、暇な時はリソースを解放してやる意味もあります。

発生する事象と原因

前項で説明したHTTPの仕様により、TCPコネクションはある程度使い回された上で解放されますが、このTCPコネクション解放で事故が起きて「502 Bad Gateway」を引き起こしてしまうケースがあります。ようやく本題に戻れました。

まずTCPコネクションが解放されるタイミングですが、TCPコネクション確立後、ALBが解放し始めるまでの時間に、ALBのアイドルタイムアウトの値が使われます。またTCPの仕様上はどちらからでも解放し始めることができるので、ターゲット側から解放し始めることも有り得ます。ALB側が一定時間で解放するように、ターゲット側のミドルウェアもまた一定時間で解放するケースが多いです。ただし処理中のリクエストが有ると分かってる状態で解放するわけにはいかないので、レスポンス待ちのALBが解放したり、リクエスト処理中のターゲットが解放することは普通ありません

リクエスト処理中に解放しないなら事故など起こり得ないと思うかもしれませんが、実際にはネットワークの遅延がある故に起こることがあります。ではどうやって事故が起きるのか。これは図を見た方がわかりやすいので、まずはご覧ください。

502 Bad Gatewayを引き起こす例

コネクション確立中、クライアントからALBにリクエストが来ました。そこでALBからターゲットのEC2にリクエストを送りましたが、時を同じくしてEC2側がコネクション解放を始めてしまい、その連絡がALBに届きました。その結果、ALBとしては到達確認やHTTPレスポンスのデータを待ち構えていたのに、実際には「もうデータ送信やめるねー」というコネクション解放の開始手続きを受けてしまいます。そしてALBとしては意思疎通が取れてない"無効な"レスポンスを受けたとして、クライアントに502を返すことになります

この事故が起こる条件ですが、HTTPリクエストの処理が無い時にターゲット側からTCPコネクションを解放し始めると事故の可能性が出てきます。裏を返せば、ALBから先に解放する場合には起こり得ない事故です。この観点だけで考えると、アイドルタイムアウトの値を小さくし、ALBが先んじてコネクション解放をするようにすれば「502 Bad Gateway」を減らすことはできそうです。

ちなみにこのケースでは、タイミング悪くリクエスト送信してしまった時だけ502を返すエラーがおきます。普通はTCPコネクションの挙動まで再現しないので、表向きには再現性が低い不具合に該当します。幸いにしてCloudWatch メトリクスで502を返した回数が確認できるので『不具合報告した人が勘違いしている』なんて扱いはされないでしょうが、知らないと不具合調査で頭を抱えることになるでしょう。

適切な設定値を考える

ここまでの話を見ると、ALBのアイドルタイムアウトが小さいと「504 Gateway Timeout」を引き起こし、大きいと「502 Bad Gateway」を引き起こしてしまう、という話になります。実際、アイドルタイムアウトを最小値の1に設定してやれば、ちょっとした遅れですぐ504が返され始めるでしょうし、前述した条件での502は出なくなりそうです。

しかし実際は、ALBのアイドルタイムアウト単体で考えるわけにはいきません。少なくともALBがターゲットとしているEC2等は気にする必要があるでしょうし、単にエラーが増える/減るだけの話ではありません。というわけで、どのような観点で適正値を考える必要があるのか述べていきます。

1アクセスにおけるタイムアウトの観点(504)

先ほどはALBが待たされてGateway Timeoutする話を書きましたが、HTTPの1アクセスで「タイムアウト」し得る箇所は他にもあります。シンプルな構成のWebサイトでも、

  • データベースのクエリ(RDSなど)
  • Webサーバ(EC2など)内部での処理時間全体
  • ALBでのターゲットからのレスポンス待ち

これぐらいは考慮しておく必要があります。加えて、クライアントが自前のアプリケーションの場合はアプリ内でもレスポンス待ちのタイムアウト設定も考慮する必要があります。ALBの前段にCloudFrontを使う場合、「レスポンスタイムアウト」という設定値が今回の1アクセスのタイムアウトに関わってきます*3

このようにタイムアウトの設定箇所は多くなりがちですが、大事なのは設定値の大小関係です。基本的に前段のシステムほどタイムアウトの設定値を大きく、後段ほど小さくする必要があります*4。ALBでのアイドルタイムアウトを20秒に設定したら、後段のターゲットではWebサーバのタイムアウトを15秒にして超えたら500を返す、さらに後段でデータベースにアクセスする際は5秒でタイムアウトさせて超えたらやはり500を返す、といった具合です。

もし後段のシステムでのタイムアウト値が長いと、クライアントにエラーが返ってるのに裏で処理は成功している、という事態になりかねません。ALBではタイムアウトしてしまって仕方なく504を返したのに、データベースで実行したUPDATE文はその後に成功していて、知らぬ間にデータ更新されていた、と言った具合です。仮に全部失敗するにしても、クライアントにエラーの旨を伝えた上で処理を続けていてはリソースの無駄です。また障害時の原因切り分けのことを考えると、後段のシステムから「間に合わなかった、スマン。」と言ってもらうだけで、基盤障害等で応答すらできない場合との切り分けがやりやすいはずです。

ここまでが大原則となり、具体的に数字を当てはめる段階ではケースバイケースとなってきます。基本方針としては事前に平時の所要時間を調べておき、それより少し大きい時間を設定することになるでしょう。ただし無駄に大きくするメリットは薄いという意識は必要です。たとえばWebサイトで、単純なテキスト取得が30秒かかっても終わらなければ、どこかで致命的な障害が発生してる場合がほとんどです。そのような状態でさらに90秒待ってもらったところで、事態が好転することは稀でしょう。むしろ、どうせエラーになるリクエスト処理・コネクションが解放されないままだと、リソース不足での二次被害を引き起こしかねません

TCPコネクション解放事故防止での観点(502)

TCPコネクション解放での事故を回避する設定ですが、ターゲット側から先にコネクション解放されなければ事故は起こり得ないです。なので、まずはターゲットがどのような条件でコネクション解放を行うのか、ミドルウェア等の設定を見てみましょう。

HTTP/1.1の場合、対応する機能はHTTP Keep-Aliveです。ちなみにTCPのKeep-Aliveとは別物なので調べる際は注意。HTTP Keep-AliveではTCPコネクションを解放する条件として、TCPコネクションを接続し続けた秒数と、1つのTCPコネクションで処理するリクエスト数が当てはまるケースが多いです。たとえばLAMP環境でお馴染みのApacheではKeepAliveTimeoutで解放までの秒数、MaxKeepAliveRequestsで解放までに処理するリクエスト数を設定します。このうちKeepAliveTimeoutに、ALBのアイドルタイムアウトより数秒大きい値を設定すれば502事故はほぼ起きなくなります。MaxKeepAliveRequestsはコネクション接続中のリクエスト総数を考慮して設定することになりますが、極端に小さい値でなければALB側のアイドルタイムアウトでのコネクション解放が先になるでしょう。

HTTP/2は未だ本家で対応しきれてないミドルウェアも多く、プラグイン等で実現させる場合も多いです。どこでHTTP/2を実現しているのか確認した上で各マニュアルを確認しましょう。私が見た限りは、何もリクエスト・レスポンスをやり取りしていない「アイドル状態」の経過時間でタイムアウトさせる「アイドルタイムアウト」を指定するものが多いです。ALBの属性と同じ名称ですね。

繰り返しですが、ターゲットより先にALBからTCPコネクション解放をさせる必要があります。そのためタイムアウト系のパラメータで今回の502事故を回避する場合、ターゲット側のタイムアウトを長くする形になります。前項の話と逆なのがややこしい。

HTTP/1.1,HTTP/2のいずれもターゲット側の設定値だけで回避しやすい上、この観点での影響範囲はターゲット本体に限られてきます。そのため、ALBのアイドルタイムアウトの値は、「504 Gateway Timeout」を考慮した設定値にしておき、ターゲット側での設定値を変更して「502 Bad Gateway」を回避するケースが多いと思われます。

502が発生した事例

最後に、この記事を書くきっかけとなった「502 Bad Gateway」が発生した事例を、ほどほどに架空の話を混ぜながら紹介します。

オンプレミスのサーバを所有し、LAMP環境でシステム構築をしているWebサイトがありました。今回の事例は、そのシステムをAWS上に移行することから始まります。LAMP(Linux + Apache + MySQL + PHP)の移行なのでEC2とRDSを中心に設計し、冗長構成やスケーリングなどのためにAuto Scaling Groupと、今回の記事で取り上げたALBを使うことにしました。

一般公開用とは別に有る技術検証用のシステムから移行し始めましたが、移行してTOPページが表示できるまでは特に問題なく進んでいました。しかし他のページの検証を進めていく中で、時々「502 Bad Gateway」が発生するようになりました。当然その原因を探ることにはなりますが、502が出る条件にリクエスト内容は関係なく、F5キーを連打してみると50回に1回ぐらいの頻度で発生するだけでした。CloudWatchメトリクスを見ると502が発生していたのは明らかですが、どうにも再現させる条件がわからないので、対処しようにも手がかりがありません。そこで同僚に事象を話してみたところ、HTTP/1.1のKeep-Alive関連の設定値に問題があることがわかりました。

技術検証中であまりチューニングを意識してない段階だったため、ALBのアイドルタイムアウトはデフォルトの60秒になっていました。一方で、Apacheの設定ファイルに移行前のものを用いていたので、Keep-Alive関連の設定値は以下のようになっていました。

  • KeepAlive On
  • KeepAliveTimeout 5
  • MaxKeepAliveRequests 100

この設定ではALBのアイドルタイムアウト60秒に対して、ApacheのKeepAliveTimeoutが5秒と短すぎるため、ALBから接続していたTCPコネクションが、常にターゲットのEC2から解放されていました。そのため記事内で説明した仕組みで、「502 Bad Gateway」が発生していたのです。なお、移行前のシステムで問題なかったのは、L7スイッチがサーバとの通信でHTTP Keep-Aliveを使わずに都度解放していたので、サーバ側が一方的に解放する状況にならなかったためでした。

AWSのALBはHTTP Keep-Aliveを用いる前提で設計されているため、ターゲット側でもHTTP Keep-Aliveを有効にした上で、適切な設定値を設定する必要があります。一方でTCPコネクションの維持によるサーバ負荷を考慮して、HTTP Keep-Aliveを使わないようにしたシステム構成も存在します。今回の事例では、L7スイッチにおけるHTTP Keep-Aliveの扱いの差異によって起こった事故でした。

まとめ

今回はALBの属性「アイドルタイムアウト」に着目した記事となりました。AWSの中でも使用頻度が高いサービスですが、パラメータ1つでもそこそこ長い記事になりましたね。プロトコルの話はかなり省略しましたので、細かい仕様は気になったら各々調べて頂ければと思います。アプリケーション専門だと聞きなじみが無いかもしれませんが、TCPとかUDPはWebサイトに限った話でも無いので時折出てきます。

なお本記事執筆時点のALBで使われるプロトコルはHTTP/1.1とHTTP/2、そしてHTTP/2派生のgRPCですが、世の中としては既にHTTP/3も出始めています。そして、HTTP/2まではTCPを採用していると述べましたが、HTTP/3ではこの辺りのプロトコルが大幅に変わるので、ALBがHTTP/3に対応するとこの記事の内容もあまり当てはまらないかもしれません。ALBの対応後すぐに変えられないケースは多いと思いますが、動向は気にしておきたいところです。

参考URL

Elastic Load Balancing(複数のターゲットにわたる着信トラフィックの分配)| AWS

Application Load Balancer のトラブルシューティング - Elastic Load Balancing : 400,500番台のステータスコードに対して、考えられる原因が一通り記載されています。

*1:大部分はRFC7231だが、「401 Unauthorized」などが別のRFCで定義されていた。RFC7231 Section 6.1に記載あり。

*2:TCPにおけるKeep-Aliveとは別物です。

*3:CloudFrontも「タイムアウト」が多くありややこしいですが、今回は省略。

*4:非同期システムはこの限りではない。