プロンプトとスコープ
あるNodeエンドポイントが複数の外部呼び出しを実行し、通常5〜35秒かかります。Node側は60秒を許容していますが、NGINXはデフォルトのproxy_read_timeoutを維持しています。本番環境のユーザーには時折502が表示されますが、アプリケーションログには後からビジネス書き込みが完了したと記録されます。負荷がかかると、長時間保持される接続によって接続プールも消費されます。
クライアント、NGINX、Node、ダウンストリームの依存関係、およびジョブストアのタイムラインを描いてください。502の原因、クライアントに失敗が表示された後も処理が完了できる理由、そして同期レスポンス、ストリーミング、非同期キューをそれぞれいつ使用すべきかを説明してください。数値は面接用の前提条件です。核心となるスキルは、レイヤー間のタイムアウトセマンティクス、キャンセル処理、副作用の境界、分離、および修正の検証であり、これはバックエンドの設問です。
面接官が評価するポイント
優秀な候補者は、すべての502を安易にリトライするのではなく、接続の切断、アプリケーションのデッドライン、ビジネス処理の完了を明確に区別します。また、NGINXのread timeoutがレスポンス全体ではなく、アップストリームの読み取り間のアイドル時間を測定していること、およびAbortSignal.timeout()はそのシグナルを実際にリッスンしている処理にしか通知されないことを理解しています。
さらに、プロキシ、ゲートウェイ、ロードバランサー、SDK、クライアントのデフォルト設定を把握し、必要に応じてHTTPリクエストを長時間ジョブから切り離し、修正によって重複書き込み、接続リーク、リトライ増幅が発生しないことを証明できます。
最初に確認すべき明確化のための質問
- 502はNGINXによって生成されたものか、それともNodeから返されて書き換えられたものか?ヘッダー、プロキシのエラーログ、アップストリームのアクセスログを比較します。
- タイムアウト発生時にビジネス上の副作用はコミットされていたか?結果が不明な処理を盲目的に再実行することはできません。
- 結果はこのリクエスト内で返さなければならないか?最終結果のみが必要な場合、35秒の同期接続は不要です。
- アップストリームは継続的にバイトを送信しているか?その場合、読み取りアイドルのタイムアウトとリクエスト全体のデッドラインは異なる意味を持ちます。
- キャンセルはデータベース、HTTPクライアント、外部SDKに到達しているか?ブラウザの接続を切断しても、すべての処理が停止するわけではありません。
- 同時実行数、接続プールの使用状況、イベントループの遅延、キューの深さはどのように変化するか?数値を変更する前にボトルネックを確認します。
30秒での回答
「クライアント、NGINX、Node、ダウンストリーム呼び出し、ストレージ全体で1つのトレースIDを紐付け、どのレイヤーがいつ502を発行したかを特定します。NGINXのproxy_read_timeoutは読み取り間のアイドル時間を制限するものであり、Nodeの60秒のリクエスト設定はクライアント切断後にジョブが停止することを保証しません。同期レスポンスの場合、1つのエンドツーエンドのデッドラインを定義し、プロキシ、アプリケーション、ダウンストリーム間でクリーンアップのマージンを確保します。処理がインタラクションの予算を超える場合は、ジョブを永続化してIDを返し、ワーカーに実行させます。障害注入を行い、重複した副作用、接続の枯渇、リトライ増幅が発生しないことを検証します。」
ステップごとの解決策
タイムラインの整理から始めます。クライアントがリクエストを送信し、NGINXがそれを転送し、Nodeが処理を開始します。Nodeが十分な時間レスポンスバイトを送信しない場合、NGINXの読み取りタイマーが満了してアップストリーム接続が切断され、502または504が生成される可能性があります。Nodeは同じ瞬間にキャンセルを受信しない場合があり、すでに書き込みをコミットした後に計算を終了することがあります。そのため、ユーザーから見える失敗と完了したビジネス操作が共存し得ます。
NGINXのアクセス/エラーログ、Nodeのリクエスト開始/終了/中断イベント、ダウンストリーム呼び出し、データベースコミットを、トレースID、リクエストID、ジョブIDで紐付けます。upstream_response_time、ハンドラーの所要時間、クライアントの受信時刻、副作用のコミット時刻を比較します。これにより、プロキシのアイドルタイムアウト、アプリケーションのデッドライン、ダウンストリームのタイムアウト、クライアントの切断を区別できます。アプリケーションの「成功」ログだけでは不十分です。
NGINXのproxy_read_timeoutはデフォルトで60秒であり、2回の読み取り間の最長アイドル間隔を測定します。バイトを受信するとその間隔はリセットされます。これを増やすとプロキシの許容時間は変わりますが、クライアントのデッドラインや接続コストは変わりません。変更する場合は、プロキシ、アプリケーション、クライアントの上限を1つの表に記録し、レスポンスの送信、クリーンアップ、ネットワークジッターのためのマージンを確保します。
NodeはAbortSignal.timeout()でデッドラインを作成し、キャンセルをサポートするfetch、データベース、またはSDK呼び出しにシグナルを渡すことができます。キャンセルは協調的(cooperative)です。シグナルを無視するライブラリは処理を継続する可能性があります。すでにコミットされたトランザクションは、中断してもなかったことにはできません。したがって、すべての副作用には、冪等性キー、ステートマシン、またはaccepted、running、succeeded、failed、unknownなどの明示的な状態を持つ補償パスが必要です。
ジョブのp99がインタラクション予算を大幅に超える場合は、非同期境界を設けます。APIは入力を検証し、ジョブレコードまたはキューメッセージを書き込み、202とジョブIDを返します。ワーカーがジョブをリースしてリトライを実行し、結果を永続化します。クライアントはステータスをポーリングまたは購読します。ジョブの作成と完了は冪等でなければならず、ワーカーの再起動から回復可能であり、重複配信によって決済やリソース作成が重複してはなりません。キューを追加すると、ストレージ、ワーカー、およびデッドレター運用のためのキャパシティとアラートが必要になります。
ストリーミングが適しているのは、結果をチャンクとして安全に生成でき、すべての仲介者が長時間レスポンスをサポートし、合計処理時間が制限されている場合のみです。ハートビートは、合計デッドライン、出力制限、またはキャンセルパスの代わりにはなりません。無制限の処理を隠すためにストリーミングを使用しないでください。
既知のプロキシアイドルタイムアウトを設定し、それを超えるダウンストリームの無応答を注入する、さまざまなフェーズでクライアントを切断する、ワーカーを再起動する、同時実行テストを実行する、などの方法で修正を検証します。論理ジョブごとに最大1回の成功した副作用、デッドラインを超えたリトライの防止、接続およびキュー使用量の制限、すべてのレイヤーのタイムスタンプを再構築できるトレースを確認します。
模範解答
「私は単に30秒を5分に変更することから始めることはしません。まずNGINX、Node、ダウンストリーム、データベースのタイムスタンプを紐付け、プロキシが502を生成したのかNodeが返したのかを判断します。proxy_read_timeoutは読み取り間のアイドル間隔であり、Nodeのリクエストデッドラインとビジネスの完了は別個のイベントです。そのため、Nodeが処理を継続して副作用をコミットしている間にプロキシが接続を切断する可能性があります。
同期レスポンスの場合、1つのエンドツーエンドデッドラインを定義し、残りの予算をダウンストリームに伝播させ、AbortSignal.timeout()を使用して、各SDKがキャンセルを検知することを確認します。不明な書き込み結果には、冪等性キーとステータス参照が必要です。ジョブのp99がインタラクション予算を超える場合、APIはジョブを永続化してIDとともに202を返すべきです。ワーカーが実行、リトライ、ステータスの永続化を行います。プロキシのアイドルタイムアウト、クライアントの切断、ワーカーの再起動、重複配信を注入し、重複した副作用が発生しないこと、接続使用量が制限されていること、完全なトレースが得られることを確認します。」
よくある間違い
- NGINXのタイムアウトのみを増やす → 長時間接続と同時実行の負荷が残る → ビジネスデッドラインと非同期境界を定義する。
- すべての502をリトライする → 元のジョブがコミットされている可能性がある → ステータスを照会し、冪等性キーを再利用する。
proxy_read_timeoutを合計レスポンス時間として扱う → 小さなチャンクが継続するとリクエストが無期限に維持される → 合計デッドラインと出力上限を追加する。- クライアントの切断でNodeが停止すると決めつける → 多くのライブラリがキャンセルを無視する → レイヤーごとにabort、接続、トランザクションの挙動を検証する。
- 無制限の処理を隠すためにハートビートを使う → リソースリークは依然として発生する → 合計時間、バイト数、同時実行数を制限する。
- 同期ハンドラー内で35秒のジョブを実行する → HTTP接続がすべての負荷を背負う → ジョブを永続化してワーカーを使用する。
- Nodeのログしか見ない → プロキシのエラーとタイミングが見落とされる → プロキシのアクセス/エラーログとアップストリームの所要時間を収集する。
- 冪等な状態を持たずにキューを追加する → 重複配信によって二重請求が発生する → ユニークなジョブキーと条件付きステータス更新を使用する。
フォローアップ質問と回答
フォローアップ 1: proxy_read_timeoutを60秒に変更するだけで十分ですか?
不十分です。これは読み取り間のアイドル時間を制御するだけであり、別のレイヤーでより短いデッドラインが設定されている可能性があり、接続自体もリソースを消費し続けます。まずインタラクション予算を定義し、その上で各レイヤーの設定を整合させてください。
フォローアップ 2: クライアントが切断された後、Nodeはどのように処理を停止しますか?
リクエストのcloseイベントを監視し、コントローラーをアボートして、そのシグナルをキャンセル可能な処理に渡します。キャンセル不可能な呼び出しについては、キャパシティを分離し、遅れて届いた結果を安全に破棄します。コミット済みの副作用には依然として冪等性とリコンシリエーションが必要です。
フォローアップ 3: ストリーミングはどのような場合適切ですか?
結果を安全にチャンク分割でき、すべての仲介者が長時間レスポンスをサポートし、全体の所要時間が制限されている場合です。ハートビート間隔、合計デッドライン、出力制限、およびキャンセルパスを維持してください。
フォローアップ 4: キューはどのようにして重複実行を防ぎますか?
ビジネスリクエストから1つのジョブキーを生成し、ストレージ内で一意性を強制します。ワーカーはリースを使用し、完了処理には条件付き更新を使用し、外部への副作用には同一の冪等性キーを再利用します。
フォローアップ 5: 修正が機能していることをどのように証明しますか?
ステージング環境で、プロキシのアイドルタイムアウト、ダウンストリームの遅延応答、クライアント切断、ネットワークリセット、ワーカーの再起動を注入します。各レイヤーのタイムスタンプを比較し、エラーの発生元、接続使用量、重複副作用、キュー遅延を検証します。
フォローアップ 6: ユーザーに502が返されているのに、アプリケーションログに成功と記録されるのはなぜですか?
プロキシが先に接続を切断したか、後からレスポンスが失われた可能性があります。完了ログはコードが終了したことを証明するだけであり、レスポンスが届いたことを証明するものではありません。プロキシのステータス、Nodeの書き込み結果、クライアントの観測結果を紐付けて確認してください。
フォローアップ 7: 非同期実行にはどのようなコストがかかりますか?
状態ストレージ、ワーカー、リトライ、デッドレター、結果整合性の管理が必要になります。その代わり、HTTPのライフタイムがジョブの所要時間から切り離され、同時実行とリプレイを独立して制御できるようになります。