cd ~/blog/eb-al2023-gunicorn-502

2026-07-22

Elastic Beanstalk を AL2023 に上げたら 502 Bad Gateway が頻発した話

AWSElastic BeanstalknginxGunicornAL2023

Elastic Beanstalk 環境を Python 3.8(Amazon Linux 2)から Python 3.9(Amazon Linux 2023)に上げる作業をしていたときの話です。新環境にデプロイして動作確認をしていると、502 Bad Gateway が頻繁に返ってくるようになりました。

結論: Gunicorn の max_requests を無効化して解決

Gunicorn の --max-requests1 をデフォルトの10から0(無制限)に変更し、workerの自動再起動そのものを止めたところ、502は再現しなくなりました。

環境Gunicorn 設定1000リクエスト中の502
対処前 (AL2023)max-requests=10, jitter=1050回
対処後 (AL2023)max-requests=0(無制限)0回

ただし、なぜAL2023環境だけでworkerの再起動時に502が起きるのかという根本原因は最後まで特定できていません。ここから先は、そこに行き着くまでの調査の経緯です。

症状

nginx/error.log にはこう記録されていました。

2025/04/02 10:33:29 [warn] 2612#2612: could not build optimal types_hash, you should increase either types_hash_max_size: 1024 or types_hash_bucket_size: 64; ignoring types_hash_bucket_size
2025/04/02 10:46:35 [error] 2614#2614: *199 recv() failed (104: Connection reset by peer) while reading response header from upstream, client: <internal>, server: , request: "GET /healthcheck/ HTTP/1.1", upstream: "http://127.0.0.1:8000/healthcheck/"
2025/04/02 11:27:47 [error] 2454#2454: *16 recv() failed (104: Connection reset by peer) while reading response header from upstream, client: <internal>, server: , request: "GET /api/... HTTP/1.1", upstream: "http://127.0.0.1:8000/api/..."

upstream: "http://127.0.0.1:8000" は EB がローカルで立ち上げている Gunicorn です。ELB のヘルスチェック(/healthcheck/)まで巻き込まれていたので、これを放置すると異常なインスタンスとして切り離されかねず、実害のあるレベルの症状でした。

Gunicorn 側にはエラーが出ていない

nginx が Connection reset by peer と言っているので、まず Gunicorn 側を疑いました。ところが journalctl にも gunicorn/error.log にもエラーは出ておらず、あるのは大量の worker 起動ログだけでした。

[2025-04-02 11:27:01 +0900] [2411] [INFO] Listening at: http://127.0.0.1:8000 (2411)
[2025-04-02 11:27:01 +0900] [2411] [INFO] Using worker: gthread
[2025-04-02 11:27:01 +0900] [2439] [INFO] Booting worker with pid: 2439
[2025-04-02 11:27:48 +0900] [2876] [INFO] Booting worker with pid: 2876
[2025-04-02 11:27:51 +0900] [2889] [INFO] Booting worker with pid: 2889
[2025-04-02 11:27:59 +0900] [2902] [INFO] Booting worker with pid: 2902
[2025-04-02 11:28:00 +0900] [2903] [INFO] Booting worker with pid: 2903

数分の間にworkerが何度も起動し直しています。エラーは出ていないのでクラッシュしているわけではなく、何かのタイミングで再起動しているように見えました。

容疑者は3つ、一つずつ試す

その時点で候補に挙がったのは次の3つでした。

  • types_hash_max_size / types_hash_bucket_size2 の warning(設定値不足)
  • Gunicorn の --max-requests1
  • Gunicorn の --workers

一度に複数変えると因果関係がつかめなくなるため、一つずつ検証することにしました。まず types_hash の warning を解消(types_hash_max_size を2048、types_hash_bucket_size を128に増加)してデプロイしましたが、502は変わらず発生しました。この時点でこの warning はハズレだと分かりました3

次に --max-requests を疑いました。Gunicorn のドキュメントを確認すると、デフォルトは10という小さい値になっていました。手で動作確認している最中に502が出た経験もあったので、workerが再起動している間にリクエストが来るとConnection reset by peerになるのでは、という仮説を立てました。

--max-requests10から1000--max-requests-jitter10から50--workers1から4に変更してデプロイしたところ、手動での動作確認では502が出なくなったように見えました。ただ、ここで「本当に直ったのか、単に発生確率が下がっただけなのか」を確認しないまま終わらせるのは危ういと指摘され、負荷試験をすることにしました。

1000リクエスト叩いて確率で見る

同時実行数を10に抑えつつ1000リクエスト送り、502の件数を数えるスクリプトを書きました。

Bash
set +m
for i in $(seq 1000); do
  (
    STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X GET "http://<internal-elb>/api/...")
    if [ "$STATUS" -eq 502 ]; then
      echo "Request $i: $STATUS"
    fi
  ) &
  if (( i % 10 == 0 )); then wait; fi
done
wait
set -m

結果は次の通りでした。

環境Gunicorn 設定1000リクエスト中の502
現行 (AL2)max-requests=10, jitter=10, workers=10回
新環境 (AL2023)max-requests=10, jitter=10, workers=4複数回発生
新環境 (AL2023)max-requests=1000, jitter=50, workers=41回

数値を上げたことで発生頻度は大きく下がりましたが、ゼロにはなりませんでした。しかも本番稼働中の現行環境(AL2)はデフォルトに近いmax-requests=10のままで502が一度も出ていません。同じような設定でもAL2かAL2023かで結果が違う。ここで、単なる設定値の問題ではなく、OSの違い自体が絡んでいるらしいと分かりました。

AWSサポートに聞いてみる

ここまでの調査結果を添えてAWSサポートに問い合わせました。回答の要旨は次の通りでした。

  • AL2からAL2023への変更が影響している可能性は否定できない。同じアップグレードでもOSそのものが異なり、構成コンポーネントも変わるため、AL2で動いていたからといってAL2023で同じ動作を期待できるわけではない
  • インストールしたソフトウェアやその依存関係については、AWS側では踏み込んだ支援ができない。継続してテスト・デバッグをしてほしい

想定はしていましたが、アプリケーション側の実装に依存する部分は自分たちで詰めるしかない、という回答でした。

対処の詳細

チームで出た案は「max_requestsをデフォルトの0(無制限)に戻して、workerの自動再起動自体を止めてしまう」でした。メモリリーク対策としてのmax_requestsではありますが、別記事で書いたように、このEB環境はもともと深夜に定期的にインスタンスごと再起動する仕組みを別途持っていたので、workerレベルの自動再起動に頼らなくてもメモリリークは吸収できるはずだという判断でした。

max_requests=0にして同じ負荷試験をかけたところ、1000リクエスト中502は0回でした。

結局、根本原因は分からないまま

max_requestsを無制限にすることで502は再現しなくなりましたが、なぜAL2023だけでこの現象が起きるのかは最後まで特定できませんでした。workerの起動・終了処理の何かがAL2とAL2023で違うのだろう、という推測はチーム内でも出ましたが、確証は得られていません。AWSサポートもアプリケーション側の依存関係には踏み込めないとのことだったので、これ以上の切り分けは自分たちでOSレベルまで潜るしかなさそうです。

今は回避策で運用していますが、workerを再起動させない設定にした分、メモリリークの監視は前より注意して見るようにしています。同じ現象(AL2からAL2023への移行でGunicornのworker再起動まわりに異常が出る)に心当たりのある方がいれば、ぜひ教えてください。

参照

Footnotes

  1. Gunicornの設定で、workerが処理したリクエスト数がこの値に達すると、そのworkerを自動的に再起動する。メモリリーク対策として用意されている。 2

  2. nginxがMIMEタイプなどの内部ハッシュテーブルを構築する際のサイズ・バケット数を指定する設定。値が小さすぎるとこのwarningが出る。

  3. 警告自体は直す価値があるため設定は反映したままにしましたが、502の原因ではありませんでした。