TT Lab
はじめる
学ぶ 学習パス コース

nginx障害対応

502と504を自分で作っておいて証拠で分ける

TT Labで続きを見る

目標

502と504を自分で発生させたうえで、ステータスコードではなくerror.logのフレーズから原因を確定できるようになります。特に、同じ504でも引っかかったタイムアウトが異なる2つのケースを区別できるようになります。

なぜ重要なのか

障害対応で最も時間を食うのは、間違った原因を確信することです。502を「バックエンドが死んだ」と決めつけてタイムアウトを上げても、何も起こりません。接続が拒否された失敗には、待つ時間そのものがなかったからです。逆に、504だからといってproxy_read_timeoutを上げればよいわけでもありません。接続の段階で引っかかったのなら、読み取りタイムアウトを30秒に上げても、リクエストは相変わらず2秒で切れます。このラボのステップ5が、まさにその場面です。その違いを生むのは、ログ1行のwhileの後ろのフレーズ1つだけです。

ステップ

  1. /root/ngi/upstream.pyを保存して、バックグラウンドで起動します。ポート9101でアプリが起動し、ポート9109は接続を受け付けないポートになります。ポート9999には何も起動しません。確認: ポート9101の/が200、/slow?d=2が2秒後に200、/bighdr?n=8000のレスポンスにX-Session-Blobヘッダーがある必要があります。
  2. /root/ngi/nginx.confを作成して、nginxを起動します。ポート8088でlistenし、3つのパスを用意します。/app/はポート9101、/dead/はポート9999、/hang/はポート9109にプロキシします。error_logは/root/ngi/logs/error.logにwarnレベルで、アクセスログは/root/ngi/logs/access.logに出力し、フォーマットに$upstream_response_time、$request_time、$upstream_statusをすべて入れます。proxy_connect_timeout 2s;とproxy_read_timeout 3s;を設定します。確認: http://127.0.0.1:8088/app/が200である必要があります(ブラウザーのプレビューで開いて確認できます)。
  3. http://127.0.0.1:8088/dead/を呼び出して、/root/ngi/case-refused.txtを作成します。3行で、プレースホルダーは、順に受け取ったステータスコード、error.logで原因を指しているフレーズそのまま、実際に発動したタイムアウトの名前(なければnone)です。
    status=<받은 상태 코드>
    phrase=<error.log 에서 원인을 지목하는 구절 그대로>
    timeout=<실제로 발동한 타임아웃 이름, 없으면 none>
    
  4. http://127.0.0.1:8088/app/slow?d=10を呼び出して、何秒で切れるかを計測し、/root/ngi/case-read.txtを作成します。4行で、プレースホルダーは、順にステータスコード、error.logのwhileで始まるフレーズそのまま、発動したタイムアウトのディレクティブ名、切れるまでにかかった秒数(整数)です。
    status=<상태 코드>
    phrase=<error.log 의 while 로 시작하는 구절 그대로>
    timeout=<발동한 타임아웃 지시자 이름>
    elapsed=<끊기기까지 걸린 초, 정수>
    
  5. /hang/ブロックの中にproxy_read_timeout 30s;を入れてreloadしてから、http://127.0.0.1:8088/hang/を呼び出します。読み取りに30秒を与えたのに、リクエストははるかに早く切れます。時間を計測して、/root/ngi/case-connect.txtをステップ4と同じ形式で作成します。ステータスコードはステップ4と同じですが、timeoutは異なる必要があります。
  6. http://127.0.0.1:8088/app/bighdr?n=8000を呼び出すと、502になります。原因のフレーズを確認したあと、プロキシバッファーを大きくして200が返るように直します。ディレクティブを1つだけ大きくするとnginxが起動しないので、emergメッセージを読んで、一緒に直してください。/root/ngi/case-header.txtを作成します。4行で、プレースホルダーは、順に修正前のコード、修正後のコード、error.logで原因を指しているフレーズそのまま、ヘッダーサイズを直接決めるディレクティブ名です。
    before=<고치기 전 코드>
    after=<고친 뒤 코드>
    phrase=<error.log 에서 원인을 지목하는 구절 그대로>
    fix=<헤더 크기를 직접 정하는 지시자 이름>
    
    修正後も、ステップ3–5の失敗はそのまま再現される必要があります。
  7. /root/ngi/verdict.csvを作成します。1行目はcase,status,fixで、そのあとに4行が続きます。caseはrefused、read-timeout、connect-timeout、big-headerで、fixはnone、proxy_connect_timeout、proxy_read_timeout、proxy_buffer_sizeのいずれかです。どれがどれと対応するかは、ステップ3–6で見たことから決めてください。
  8. /root/ngi/rca.mdを作成します。## 현상、## 증거、## 원인、## 조치、## 재발방지という5つのh2見出し(韓国語の見出しは、順に「現象」「証拠」「原因」「対処」「再発防止」を意味します)が必要で、ステップ4とステップ5のelapsedの数字と、2つのタイムアウトのディレクティブ名が、本文にそのまま引用されている必要があります。

参考

障害発生器を起動する

/root/ngi/upstream.pyを保存して、バックグラウンドで起動します。ポート9101でアプリが起動し、ポート9109は接続を受け付けないポートになります。ポート9999には何も起動しません。確認: ポート9101の/が200、/slow?d=2が2秒後に200、/bighdr?n=8000のレスポンスにX-Session-Blobヘッダーがある必要があります。

アップストリームが3つ必要です。正常に応答するアプリ、接続を絶対に受け付けないポート、そして誰もリッスンしていないポートです。最初の2つは、Pythonの1つのファイルでまとめて起動します。起動後は、アプリが200を返すか、接続を受け付けないポートが本当にハングするかを、それぞれ確認してください。

証拠が残るプロキシを立てる

/root/ngi/nginx.confを作成して、nginxを起動します。ポート8088でlistenし、3つのパスを用意します。/app/はポート9101、/dead/はポート9999、/hang/はポート9109にプロキシします。error_logは/root/ngi/logs/error.logにwarnレベルで、アクセスログは/root/ngi/logs/access.logに出力し、フォーマットに$upstream_response_time、$request_time、$upstream_statusをすべて入れます。proxy_connect_timeout 2s;とproxy_read_timeout 3s;を設定します。確認: http://127.0.0.1:8088/app/が200である必要があります(ブラウザーのプレビューで開いて確認できます)。

ポートは8088です。ログ2つがこのラボのすべてなので、パスとレベルを正確に設定してください。error_logをerrorレベルにすると、後のラボの警告がすべて消えます。アクセスログのフォーマットには、アップストリームが使った時間、全体の時間、アップストリームのステータスコードの3つが入っている必要があります。

死んだアップストリーム: 502

http://127.0.0.1:8088/dead/を呼び出して、/root/ngi/case-refused.txtを作成します。3行で、プレースホルダーは、順に受け取ったステータスコード、error.logで原因を指しているフレーズそのまま、実際に発動したタイムアウトの名前(なければnone)です。

status=<받은 상태 코드>
phrase=<error.log 에서 원인을 지목하는 구절 그대로>
timeout=<실제로 발동한 타임아웃 이름, 없으면 none>

死んだポートにプロキシするとどのコードが出るか、そしてそのときerror.logの最初の単語が何かを見てください。ここでタイムアウトの話が出てくる余地があるかを自問すれば、timeoutの欄に何を書くかが決まります。

遅いアップストリーム: 504、読み取りタイムアウト

http://127.0.0.1:8088/app/slow?d=10を呼び出して、何秒で切れるかを計測し、/root/ngi/case-read.txtを作成します。4行で、プレースホルダーは、順にステータスコード、error.logのwhileで始まるフレーズそのまま、発動したタイムアウトのディレクティブ名、切れるまでにかかった秒数(整数)です。

status=<상태 코드>
phrase=<error.log 의 while 로 시작하는 구절 그대로>
timeout=<발동한 타임아웃 지시자 이름>
elapsed=<끊기기까지 걸린 초, 정수>

応答まで10秒かかるパスを呼び出して、何秒で切れるかを計測してください。その秒数は、設定ファイルのどこかにそのまま書かれています。error.logのフレーズで、whileの後ろを読めば、どのタイムアウトかが確定します。

上げたタイムアウトは、発動したタイムアウトではなかった

/hang/ブロックの中にproxy_read_timeout 30s;を入れてreloadしてから、http://127.0.0.1:8088/hang/を呼び出します。読み取りに30秒を与えたのに、リクエストははるかに早く切れます。時間を計測して、/root/ngi/case-connect.txtをステップ4と同じ形式で作成します。ステータスコードはステップ4と同じですが、timeoutは異なる必要があります。

まず/hang/ブロックの中にproxy_read_timeout 30sを入れて、reloadしてください。読み取りに30秒を与えると宣言したことになります。それでもリクエストが何秒で切れるかを計測し、error.logのwhileの後ろのフレーズが、ステップ4とどう違うかを見てください。ステータスコードとエラー番号は同じです。

サーバーは正常なのに、特定のユーザーだけ502

http://127.0.0.1:8088/app/bighdr?n=8000を呼び出すと、502になります。原因のフレーズを確認したあと、プロキシバッファーを大きくして200が返るように直します。ディレクティブを1つだけ大きくするとnginxが起動しないので、emergメッセージを読んで、一緒に直してください。/root/ngi/case-header.txtを作成します。4行で、プレースホルダーは、順に修正前のコード、修正後のコード、error.logで原因を指しているフレーズそのまま、ヘッダーサイズを直接決めるディレクティブ名です。

before=<고치기 전 코드>
after=<고친 뒤 코드>
phrase=<error.log 에서 원인을 지목하는 구절 그대로>
fix=<헤더 크기를 직접 정하는 지시자 이름>

修正後も、ステップ3–5の失敗はそのまま再現される必要があります。

レスポンスヘッダーを8000バイトに膨らませるパスがあります。ヘッダーが大きくなっただけで、コードがどうなるかを見てください。直すときにバッファーのディレクティブを1つだけ大きくすると、nginxがそもそも起動しません。そのemergメッセージが、何を一緒に大きくするよう言っているかを読んでください。

4行の判定表

/root/ngi/verdict.csvを作成します。1行目はcase,status,fixで、そのあとに4行が続きます。caseはrefused、read-timeout、connect-timeout、big-headerで、fixはnone、proxy_connect_timeout、proxy_read_timeout、proxy_buffer_sizeのいずれかです。どれがどれと対応するかは、ステップ3–6で見たことから決めてください。

4つの失敗を1つの表にまとめます。fixの欄には、nginxの設定でこの失敗を実際に解決するディレクティブ名を書きますが、nginxの設定では解決できないものが1つあります。その行にはnoneを書きます。

数字が入った障害報告書

/root/ngi/rca.mdを作成します。## 현상、## 증거、## 원인、## 조치、## 재발방지という5つのh2見出し(韓国語の見出しは、順に「現象」「証拠」「原因」「対処」「再発防止」を意味します)が必要で、ステップ4とステップ5のelapsedの数字と、2つのタイムアウトのディレクティブ名が、本文にそのまま引用されている必要があります。

報告書の価値は数字にあります。前のステップで計測しておいた秒数を、そのまま引用してください。再発防止には、「注意する」ではなく、どのディレクティブをどの値にして、どのログの文言にアラートを設定するかを書きます。