0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

500系のステータスコードを返すWebサーバーを作ってみた

Last updated at Posted at 2025-03-30

概要

500系のステータスコードに関する理解を深めるために、敢えて500系を返すWebサーバーを作ってみました。

Webサーバーが500系を返す場合の原因調査の難しさ

Webサーバーを運用していると、Load Balancerが502や504などのステータスコードを返すことがあります。最初に観測したときは戸惑いますが、ログやメトリクスを調査していくうちに、500系のステータスコードが返る際のパターンや傾向が何となく見えてきます。以下はその例です。

「504が返る時は、ストレージのスループットやIOPSが高い値で貼り付いてるよね」
「502が返る時は、複数台あるバックエンドのWebサーバーの一つが一瞬止まってたよね」
「503が返る時は、そもそもバックエンドのWebサーバーが動いていないよね」

こうした経験則が積み重なってくるのは、運用の上では決して悪いことではありません。例えば、504が増え始めたら「ストレージの負荷が高まっているかもしれない」と判断し、ストレージのログやメトリクスを優先的に調査することで、素早く原因にたどり着ける可能性があります。推測が的中していれば、ストレージのスペックを上げて504を返さないように対処できます。

しかし、もし「ストレージの負荷は低いのに504が大量に返っていた」という場合はどうでしょうか?

このケースでは、せっかく積み上げてきたノウハウがほぼ役に立たず、初心者のようにゼロから調査をやり直すことになります。マシン、ネットワーク、ストレージといった各種ログやメトリクスを一つひとつ確認していくことになりますが、このアプローチは原因がわかるまでに非常に時間がかかるうえ、最終的に特定できないまま終わってしまうことも珍しくありません。

「他のタスクをすべてストップして504対応の調査に多くの時間とエネルギーを注いだのに、結局問題の原因を特定できなかった…」という残念な結果に終わることも少なくないでしょう。

500系のステータスコードを理解する

調査の底なし沼に落ちてしまうのを回避する第一歩は、「500系の各ステータスコードが何を意味するのか?」を理解することです。各ステータスコードの意味がわかると、見当外れな調査をしなくて済みます。

そこで、RFC 9110に書かれた5xxのステータスコードの定義を見てみましょう。

500系は500から505までの定義がずらりと書かれていますが、502と504のエラーだけ抜粋します。

502 Bad Gateway

サーバーがゲートウェイまたはプロキシとして機能している間、リクエストを満たそうとしている間にアクセスしたインバウンドサーバーから無効な応答を受け取ったことを示しています。

504 Gateway Timeout

サーバーがゲートウェイまたはプロキシとして機能している間、リクエストを完了するためにアクセスする必要があるアップストリームサーバーからタイムリーな応答を受け取らなかったことを示しています。

この文言で「ゲートウェイまたはプロキシ」と言っているのは、NginxやApache、AWS ALBに相当するものです。また「インバウンドサーバー」や「アップストリームサーバー」は、Node.js、Django、Flaskなどのプロキシの背後にあるアプリケーションサーバーを指します。

試しに具体的なサービス名で、502と504の説明を言い換えてみます。

502 Bad Gateway

NginxやApacheがプロキシとして機能している間、リクエストを満たそうとしている間にアクセスしたNode.jsやDjangoから無効な応答を受け取ったことを示しています。

504 Gateway Timeout

NginxやApacheがプロキシとして機能している間、リクエストを完了するためにアクセスする必要があるNode.jsやDjangoからタイムリーな応答を受け取らなかったことを示しています。

NginxやNode.jsを知っている人であれば、502と504のイメージが少し掴めてきたかもしれません。
よりイメージを明確にするために、502と504の状況を図にしてみました。

502.drawio.png

504.drawio.png

502は「アプリサーバーは応答は返しているけど、プロキシがレスポンスの内容が適切でないと判断して返すステータスコード」、また504は「アプリサーバーが制限時間内にレスポンスしなかった時に返すステータスコード」と理解すればよさそうです。

5xxを返すWebサーバーを実装

500系のステータスコードが実際にどんな時に返るかを体感するために、以下のGithubリポジトリに「敢えて500系を返すWebサーバー作成環境」を格納しました。

このリポジトリでは、リバースプロキシにNginx、アプリケーションサーバーにFlask(Pythonのパッケージ)を使っています。

Nginxの設定

Nginxのコンフィグは以下です。

default.conf
server {
    listen 80;

    location / {
        proxy_pass         http://app:8080;
        proxy_read_timeout 3s;
    }
}

Nginxはポートは80でリッスンし、appというホスト名で8080で待ち受けているFlaskのアプリケーションサーバーにそのままリクエストを転送します。
また、proxy_read_timeout3s にし、アプリケーションサーバーからのレスポンスデータの受信が3秒間途切れた場合にタイムアウトするようにしています。実運用ではこんな小さな値にしないと思いますが、今回は5xxのステータスコードを返すために敢えて3秒にしました。

アプリサーバーのソースコード

続いて、PythonのFlaskを使って書いたソースコードは以下です。

app.py
from flask import Flask
import time
import sys

app = Flask(__name__)

# 200が返る
@app.route("/")
def index():
  return "Hello, this is a simple HTTP error simulator!", 200

# 504が返る
@app.route("/slow")
def slow_response():
  time.sleep(5)
  return "This response took a long time", 200

# 502が返る
@app.route('/abort')
def abort():
  sys.exit(1)

# 500が返る(Flaskが500を返すので)
@app.route('/nothing')
def nothing():
  return None

if __name__ == "__main__":
  app.run(host="0.0.0.0", port=8080)

ネタバレしてますが、コメントにはNginx経由で各パスにリクエストした際に返るステータスコードを書いています。

サーバ(コンテナ)の起動

実際にリポジトリをクローンして、

git clone https://github.com/showchan33/http-error-simulator.git

NginxとFlaskのコンテナを起動します。

cd http-error-simulator/docker
docker compose up -d

新たに2つのコンテナが起動することを確認します。

$ docker ps
CONTAINER ID   IMAGE                            COMMAND                   CREATED         STATUS         PORTS                             NAMES
c31226303119   nginx:latest                     "/docker-entrypoint.…"   7 seconds ago   Up 6 seconds   80/tcp                            nginx
c656fc6ab7e4   http-error-simulator             "python app.py"           7 seconds ago   Up 7 seconds   8080/tcp                          flask_app

リクエストしてステータスコードの確認

curlでリクエストするために、nginxのコンテナに入ります。

docker exec -it nginx bash

この後、各パスにFlaskへの直接リクエストした場合と、Nginx経由でリクエストした場合でどのようなステータスコードが返るのかを確認してみます。

/ へのリクエスト(200)

ソースコード

@app.route("/")
def index():
  return "Hello, this is a simple HTTP error simulator!", 200

Flask直接

# curl -I http://app:8080/ 2>/dev/null | head -n1
HTTP/1.1 200 OK

Nginx経由

# curl -I http://localhost/ 2>/dev/null | head -n1
HTTP/1.1 200 OK

当たり前ですが、両方とも200が返ります。

/slow へのリクエスト(504)

ソースコード

@app.route("/slow")
def slow_response():
  time.sleep(5)
  return "This response took a long time", 200

Flask直接

# curl -I http://app:8080/slow 2>/dev/null | head -n1
(5秒後)
HTTP/1.1 200 OK

Nginx経由

# curl -I http://localhost/slow 2>/dev/null | head -n1
(3秒後)
HTTP/1.1 504 Gateway Time-out

Flaskに直接リクエストしたら200が返ったのに、Nginx経由だと504が返ってしまいました。これはNginxのコンフィグで proxy_read_timeout3s となっており、3秒間Flaskから何のデータも受信しなかったことが原因です。今回は極端に値を小さくして意図的に504を発生させましたが、実運用のWebサーバーでもプロキシ側のタイムアウト値が適切でない場合、アプリケーションは200で返せるはずのリクエストを504で返してしまう可能性があります。そのため504エラーが頻発する場合は、プロキシのタイムアウト値が小さすぎないかを確認しましょう。

/abort へのリクエスト(502)

ソースコード

@app.route('/abort')
def abort():
  sys.exit(1)

Flask直接

# curl -I http://app:8080/abort
curl: (52) Empty reply from server

Nginx経由

# curl -I http://localhost/abort 2>/dev/null | head -n1
HTTP/1.1 502 Bad Gateway

このケースでは、Flaskが sys.exit(1) でリクエスト処理を終了しています。そのため、Flaskに直接リクエストするとレスポンスが何も返らずに切断され、curlはエラー終了します。もちろんステータスコードも返りません。一方、Nginx経由でリクエストすると、Nginxは「Flaskが異常終了して応答を返さなかった」ことを検知し 、クライアントに "502 Bad Gateway" を返します。

尚、Nginx(プロキシ)にリクエストした場合に502を返してしまう原因は、Flask(アプリケーションサーバー)の処理以外に起因する場合もあります。例えばNginxのコンフィグで、以下のようにバックエンドサーバーにプロキシするポート番号を間違ってしまった場合を考えます。

default.conf
server {
    listen 80;

    location / {
        proxy_pass         http://app:8081; # 8080を8081と間違う
        proxy_read_timeout 3s;
    }
}

この場合、Flaskは問題なく動いていてもNginxにリクエストすると502が返ってしまいます。

## Flask直接
# curl -I http://app:8080/ 2>/dev/null | head -n1
HTTP/1.1 200 OK

## Nginx経由
# curl -I http://localhost/ 2>/dev/null | head -n1
HTTP/1.1 502 Bad Gateway

したがって、502が発生した場合にアプリケーションサーバーの問題だけを疑って調査しても原因を特定できない可能性があることに注意が必要です。プロキシとアプリケーションサーバー間のネットワーク的な問題がないかも確認してみましょう。

/nothing へのリクエスト(500)

ソースコード

@app.route('/nothing')
def nothing():
  return None

Flask直接

# curl -I http://app:8080/nothing 2>/dev/null | head -n1
HTTP/1.1 500 INTERNAL SERVER ERROR

Nginx経由

# curl -I http://localhost/nothing 2>/dev/null | head -n1
HTTP/1.1 500 INTERNAL SERVER ERROR

このケースは単純に、Flaskが500を返したのでNginxもクライアントにそのまま500を返しています。このことより、プロキシは「バックエンドの状態を自ら判断して500系のステータスコードを返す」ケースと、「バックエンドが返した500系のステータスコードをそのまま返す」ケースの2つがあることがわかります。

おわりに

500系ステータスコードについて調べて実装してみて、私自身も少し理解が深まりました。最後に502と504の違いを恋愛にたとえられる!?と思いつき、生成AIに質問して返ってきたコメントを共有して本記事を締めくくります。

質問

502と504の違いを恋愛にたとえると、504はプロキシ側から切断するので「バックエンドがプロキシにフラれた」、502はその反対なので「プロキシがバックエンドにフラれた」という認識でいいでしょうか?

生成AIの回答

そのたとえ、なかなか面白いですね!方向性としては合っていますが、もう少し明確にするとこうなります。

  • 502 Bad Gateway → 「プロキシがバックエンドにフラれた」
    → バックエンドが期待通りの応答を返さなかった(エラー応答や無効なレスポンスを返した、そもそもダウンしていた など)

    • 例:
      プロキシ(Aさん)が恋人(バックエンドのBさん)に話しかけたが、意味不明な返事が返ってきた、または完全に無視された。Aさんは困ってしまい、第三者(クライアント)に「Bさん、ダメだった…(502)」と伝える。
  • 504 Gateway Timeout → 「バックエンドがプロキシにフラれた」
    → プロキシがバックエンドからの応答を待っていたが、制限時間内に返事がなかったため諦めた

    • 例:
      プロキシ(Aさん)が恋人(バックエンドのBさん)にメッセージを送ったが、何も返信が来ないまま時間切れに。Aさんは「Bさん、待ったけどダメだった…(504)」と第三者(クライアント)に報告する。

つまり、502はバックエンドが変な応答をしたり死んでいる場合、504はバックエンドが沈黙したまま時間切れになった場合 の違いですね!


(以下は筆者のコメントです)
日本の曲にたとえると、502は「好きだからウソついた」T-BOLANの「すれ違いの純情」、504は「会いたくて会いたくていつまでも待っている」西野カナの「遠くても」、がそれぞれの振る舞いに近いかもしれません。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?