設問と適用場面
あるLinuxサービスが短命なヘルパープロセスを繰り返し起動しています。新規ジョブがfork: Resource temporarily unavailableで断続的に失敗するようになりました。CPUと常駐メモリ(RSS)は正常に見えますが、psを実行すると、1つの親プロセスの配下に状態Zの子プロセスが数千個存在することが示されます。コンテナ内では、pids.currentがpids.maxに近づいており、max内のpids.eventsカウンターが増加しています。
ゾンビプロセスとは何かを説明し、回収(reap)されていない子プロセスが障害の原因であるかを証明し、可能であれば再起動せずにサービスを復旧させ、再発を防止してください。通常のホストと、アプリケーションがPID 1として実行されている可能性のあるコンテナの両方を対象としてください。プロセス数やデプロイ構成は面接上の前提条件です。優れた回答では、fork()がEAGAINを返す原因となり得る他の制限についても検証する必要があります。
この問題は、Linuxのプロセスライフサイクルとインシデント診断が中心的なスキルであるため、generalの設問です。公開されているLinuxの面接ガイドにはゾンビ対孤児プロセスの問題が含まれており、LinuxおよびDockerのドキュメントには運用の境界が記載されています。これにより、特定の企業や面接頻度に依存しない、代表的で普遍的な設問となっています。
面接官が評価しているポイント
第1の評価ポイントは、候補者がプロセスの状態とリソースの症状を明確に切り離して捉えられているかです。ゾンビプロセスはすでに終了しています。カーネルは、親プロセスがwait系システムコールで回収するまで、PID、終了ステータス、アカウンティング情報を保持します。通常のCPUも終了したプロセスのユーザー空間メモリも消費しませんが、有限なプロセステーブル/PIDスロットを占有し続けます。
第2の評価ポイントは、所有権(オーナーシップ)の理解です。問題のあるコンポーネントは通常、子プロセスを起動したにもかかわらず回収しなかった生存中の親プロセスです。ゾンビにSIGKILLを送信しても、再度終了させることはできません。また、シェルが無関係なプロセスの子に対してwait()を呼び出すこともできません。候補者は、対策を講じる前に、親プロセス、そのスーパーバイザー、デプロイメントユニット、および子プロセスの管理コードを特定する必要があります。
第3の評価ポイントは、因果関係の診断です。EAGAINからのfork()は、ユーザーごとのRLIMIT_NPROC、システム全体のthreads-max、pid_max、またはcgroupのpids.max制限を意味する可能性があります。ゾンビの存在は強力な証拠ですが、これらのチェックを省略してよい理由にはなりません。スレッドや無関係なプロセスも同じ上限に影響を与えている可能性があります。
最後の評価ポイントは、バースト、エラー、シャットダウン、およびコンテナ環境に耐えうる修正を行えるかです。シグナルは合流(coalesce)する可能性があるため、waitpid()ごとに1回のSIGCHLD呼び出しを行うだけでは不十分です。親プロセスは終了したすべての子プロセスを回収(drain)しなければならず、コンテナには孤児となった子孫プロセスを適切に引き取って回収するPID 1が必要です。
回答前に確認すべき質問
- 障害はどこで発生していますか? ホスト全体のエラー、単一のユーザーアカウント、単一のコンテナでは、上限の種類や影響範囲(blast radius)が異なります。
- 正確なエラーとシステムコールは何ですか?
EAGAINはプロセス/スレッドの制限を示唆し、ENOMEMやアプリケーションのキュー拒否は別の原因パスを辿ります。 Zのエントリは単一のPPIDに集中していますか? 単一の支配的なPPIDはプロセスの所有権パスを特定します。多数のPPIDがある場合は、共通ラッパーや壊れたコンテナinitパターンが疑われます。- 親プロセスは生存し、正常で、監視(スーパーバイズ)されていますか? 生存している親プロセスは修正や安全な再起動が可能です。終了した場合、子プロセスは最も近いサブリーパー(subreaper)またはnamespaceのinitに親が付け替えられ、それらが回収を行う必要があります。
- コンテナ内でアプリケーションはPID 1として動作していますか? PID 1には孤児プロセスを回収する特別な責任があります。ランタイムには軽量なinitプロセスが必要になる場合があります。
- トラフィックやジョブの受け入れをドレイン(縮退)できますか? 新規のforkを停止し処理中の作業を保護した後のほうが、制御された安全な再起動が可能です。
- 子プロセスの終了から何を保持する必要がありますか? 終了コード、エラー出力、再試行の判断によって、回収処理をブロッキング呼び出し、イベントループ、ワーカープール、ランタイム固有のプロセスAPIのどこに配置すべきかが決まります。
30秒回答フレームワーク
「ゾンビプロセスはすでに終了しており、親プロセスが終了ステータスを回収していない状態です。まずZ状態をカウントしてPPIDごとにグループ化し、その親プロセスを調査して、時系列データと失敗したforkを照合します。また、EAGAINには複数の制限要因が考えられるため、RLIMIT_NPROC、threads-max、pid_max、およびcgroupのpids.current、pids.max、pids.eventsも確認します。ゾンビをkill -9で直接解消することはできないため、新規プロセスの生成を停止し、トラフィックをドレインした上で、機能しているサブリーパーやPID 1が子プロセスを引き取って回収できるよう、親プロセスを安全に再起動または修復します。恒久的な対策としては、エラー時やシャットダウン時も含め、終了した子プロセスが残らないよう親プロセスがwaitpid(-1, ..., WNOHANG)を完全にドレインするようにします。コンテナ環境でアプリがPID 1の責務を果たせない場合は適切なinitを使用し、バースト負荷テストを実施してゾンビ数とPID使用量が一定の範囲内に収まることを検証します。」
ステップごとの詳細解説
ステップ1:プロセスの状態とオーナーを特定する
PIDの空きがすでに逼迫している状況で、大きなパイプラインを作成しないコマンドから開始します。
ps -eo pid=,ppid=,stat=,etime=,comm= | awk '$3 ~ /^Z/'
ps -eo ppid=,stat= | awk '$2 ~ /^Z/ { count[$1]++ } END { for (p in count) print count[p], p }' | sort -nrpsにおいて、Zで始まるステータスはゾンビです。/proc/PID/statでも状態ZとPPIDを確認できます。切り詰められたプロセス名だけでなく正確にPPIDを確認し、生存している親プロセスを調査します。
ps -o pid=,ppid=,stat=,lstart=,etime=,cmd= -p PARENT_PID
cat /proc/PARENT_PID/status
cat /proc/PARENT_PID/limitsゾンビの数、新規ゾンビの発生率、親プロセスのデプロイバージョン、および最初の障害発生時刻を記録します。計画的なシャットダウン中に一時的に残る安定したカウントと、ジョブごとに増加するカウントは性質が異なります。
ステップ2:どの上限が新規の子プロセスを拒否したかを証明する
ユーザー向けのエラーメッセージ「Resource temporarily unavailable」から短絡的に「メモリ不足」と判断してはいけません。Linuxでは、fork()がEAGAINを返す要因がいくつか文書化されています。
- 実ユーザーの
RLIMIT_NPROC /proc/sys/kernel/threads-max/proc/sys/kernel/pid_max- 有効なcgroupのPIDs制限
cgroup v2の場合、ルートパスを前提とせず、サービスの実際のcgroup内のファイルを調べます。
cat /proc/PARENT_PID/cgroup
cat /sys/fs/cgroup/SERVICE_CGROUP/pids.current
cat /sys/fs/cgroup/SERVICE_CGROUP/pids.max
cat /sys/fs/cgroup/SERVICE_CGROUP/pids.eventsmaxイベントカウンターの増加は、forkがPIDsコントローラーの上限に達したことを直接証明します。pids.currentをゾンビ数や実行中のスレッド/プロセス数と比較します。コントローラーは子孫全体のタスクをカウントするため、リソースを消費しているのはゾンビだけではない可能性があります。ホスト上では、ユーザーごとのタスク数とシステム全体の合計も制限値と比較します。ゾンビの増加、残りのPID余裕、EAGAIN、および親プロセスの生成レートが時間軸上で一致したとき、因果関係が最も強固に証明されます。
ステップ3:一般的な「解決策」が失敗する理由を説明する
プロセスはすでに終了しているため、kill -9 ZOMBIE_PIDを実行しても終了ロジックは走りません。残っているカーネルレコードは、その親プロセスまたは後から引き取ったプロセス(adopter)が待機(wait)した場合にのみ消滅します。シェルの組み込みコマンドwaitは、そのシェル自身の子プロセスしか管理できません。
盲目的にpids.max、pid_max、またはRLIMIT_NPROCを引き上げると復旧までの時間は稼げるかもしれませんが、リークはそのまま残り、次回障害時の影響範囲を拡大させる可能性があります。ランダムな生存プロセスをkillすればスロットは空きますが、親プロセスの問題は解決しません。再起動すればプロセスクリー全体が破棄されるため機能はしますが、証拠が消え、回避可能なダウンタイムが発生します。
また、状態Dとも区別してください。D状態(割り込み不可のスリープ)のプロセスは生存しており、カーネル内で待機しています。SIGKILLが効かないように見えても、診断内容は異なります。すべての応答しないPIDをゾンビとして扱うと、調査の方向性を誤ることになります。
ステップ4:親プロセスの制御された移行によるサービス復旧
まず、障害のある親プロセスが残りのスロットを消費しないよう、新規ジョブの受け入れを停止またはスロットリングします。状態を変更する前に、管理用セッションを1つ確保し、親プロセスのログ、/procの証拠、制限値、バージョン情報を収集します。
親プロセスに子プロセスの管理ループを安全に再生成するドキュメント化されたリロード機能がある場合は、それを使用します。ない場合は、処理中の作業をドレインし、スーパーバイザー経由でその親プロセスを再起動します。親プロセスが終了すると、回収されていなかった子孫プロセスは最も近い子サブリーパーまたはPID namespaceのinitに引き取られます。適切に実装された引き取り手はそれらのプロセスをwaitします。受け入れを再開する前に、ゾンビ数が減少したことを確認します。
引き取り手がそれらを回収しない場合、ワーカーのみを再起動しても復旧は完了しません。ホスト上では、サービスのスーパーバイザー/サブリーパーを調査します。コンテナ内では、namespaceのPID 1が最終的な責任を持つため、コンテナの制御された再作成が必要になる場合があります。PID制限の引き上げは、スロットリングと修正版のデプロイを組み合わせた、文書化された一時的な安全マージン確保策としてのみ許容されます。
ステップ5:バーストやエラーを処理できる回収処理の実装
特定の子プロセスが終了するまでブロックする必要がある親プロセスの場合は、waitpid(child_pid, ...)を呼び出し、割り込みを適切に処理します。非同期な親プロセスの場合は、SIGCHLDでイベントループを起床させ、取得可能なすべてのステータスを回収(drain)するように構成します。
for (;;) {
pid_t pid = waitpid(-1, &status, WNOHANG);
if (pid > 0) {
record_child_result(pid, status);
continue;
}
if (pid == 0) break;
if (errno == EINTR) continue;
if (errno == ECHILD) break;
report_wait_error(errno);
break;
}このループは通常のイベントループのコンテキストに配置する必要があります。素のシグナルハンドラー内では、フラグの設定やself-pipeへの書き込みなど、ランタイムのシグナル安全(signal-safety)規則で許可された操作のみを行うべきです。複数の子プロセスの終了が1つのシグナル通知にまとめられる(合流する)ことがあるため、ドレイン処理が極めて重要になります。生成と登録の間の失敗、タイムアウト、キャンセル、親プロセスのシャットダウン、およびあらゆる再試行パスをカバーしてください。マネージドランタイムでも、個々の子プロセスの完了をawaitまたは明示的に消費するという同等のルールが適用されます。
明示的にSIGCHLDを無視するかSA_NOCLDWAITを使用すると、サポートされているシステムでは自動クリーンアップを要求できますが、通常の終了ステータス収集が行えなくなり、移植性やAPI上の影響が生じます。これは意図的な設計上の選択肢であり、結果を必要とするワーカーマネージャーの手抜き策として使うべきではありません。
ステップ6:コンテナのPID 1と検証を修正計画に含める
コンテナのメインプロセスは、自身が起動したプロセスに対して責任を持ち、子孫プロセスの引き取り手となる場合があります。アプリケーションがPID 1としてシグナルの転送やプロセスの回収を正しく行えない場合は、Dockerの--initや同等のCompose設定など、ランタイムの軽量init機能配下で実行します。initプロセスを導入しても、アプリケーションが結果を必要とする直接の子プロセスをwaitする責務が免除されるわけではありません。孤児プロセスの引き取り漏れを防ぐためのものです。
当初の並行数を上回るバースト負荷に加え、子プロセスの意図的な失敗や急速な終了を発生させて修正を検証します。受け入れ基準には以下を含める必要があります。
- 起動されたすべての子プロセスにつき、完了ステータスが1回消費されること。
- バースト後、ゾンビ数がゼロまたは文書化された短時間の制限値内に戻ること。
pids.currentが増加し続けずに安定し、pids.eventsで新たな制限到達が記録されないこと。- 想定負荷下で
fork()/spawnのEAGAINが発生しないこと。 - シャットダウン時に子プロセスがドレインまたは終了され、その後回収されること。
- 親プロセスがクラッシュした場合でも、テスト済みのサブリーパーまたはPID 1に子孫プロセスが引き取られること。
- ジョブ生成が失敗する前に、ゾンビの増加率とPID残容量に対してアラートが発報すること。
質の高い模範回答
「まず、エントリが実際にZ状態で始まっているかを確認し、PPIDでグループ化して、支配的な生存親プロセスを調査します。ゾンビプロセスは実行を完了しているため、CPUやRSSが正常なのは想定どおりです。カーネルは、親プロセスがwait系関数を呼び出すまでPIDと終了ステータスを保持します。そのため、ゾンビに対してkill -9を実行しても無効であり、親プロセスこそが修復対象となります。
次に、fork失敗を引き起こした制限要因を特定します。親プロセスに対してはRLIMIT_NPROCを確認し、ホスト上ではthreads-maxとpid_maxを確認し、サービスのcgroup内ではpids.current、pids.max、pids.eventsを読み取ります。エラーの発生と同時にmaxカウンターが増加し、グループが上限に近づいていれば、cgroupの制限が原因であると証明されます。すべてのスロットをゾンビのせいと決めつけないよう、実行中のスレッドや子孫タスクもカウントします。
復旧手順としては、新規ジョブをスロットリングし、診断情報を保存し、処理中の作業をドレインした上で、スーパーバイザーを通じて親プロセスを再起動またはリロードします。親のゾンビプロセスは、正常に機能しているサブリーパーまたはnamespaceのPID 1によって引き取られ、回収されるはずです。コンテナのPID 1が適切に引き取れていない場合は、initを有効にした構成でコンテナを再デプロイします。制限の引き上げは一時的なマージン確保にすぎません。
恒久的なコード修正は、すべての子プロセスの結果を漏れなく消費することです。同期処理を行う親は対象のPIDを正確にwaitします。イベント駆動型の親はSIGCHLDを起床契機として扱い、ノンブロッキングのwaitpid()で完了した子プロセスがなくなるまでループ処理を行います。その際、spawn失敗、キャンセル、シャットダウンも考慮します。急速な終了、強制終了、親プロセスのクラッシュ、グレースフルシャットダウンの条件下で負荷テストを実施します。完了処理が1対1で行われ、ゾンビ数が抑制され、PID使用量が安定し、新たな制限イベントが発生しないことが確認されて初めてリリース可能と判断します。」
よくある間違い
- 各ゾンビに
kill -9を送信する → 子プロセスはすでに終了しているため、シグナルを送っても終了ステータスは回収されません → 親プロセス/引き取り手プロセスを特定し、修復、リロード、または再起動します。 - メモリリークと呼ぶ → ゾンビが保持しているのは終了したプロセスの通常のアドレス空間ではなく、カーネルの管理情報です → プロセステーブル/PIDの枯渇として説明し、実際に制限となっているリソースを測定してください。
- 1つの
psスナップショットからcgroup枯渇と決めつける →EAGAINには複数の制限があり、生存スレッドもタスク枠を消費します → 制限値、カウンター、タスク数、タイムスタンプを相互に関連付けて分析してください。 - シグナル1回につき
waitpid()を1回だけ呼び出す → 子プロセスの終了シグナルは合流(coalesce)することがあり、ステータスが回収されずに残る原因になります → 処理可能なプロセスがないと報告されるまで、ノンブロッキングのwaitをドレイン(ループ実行)してください。 pids.maxの引き上げを恒久対策とする → 問題のある親プロセスはスロットをリークし続けます → 引き上げは復旧時の一時的な猶予としてのみ使用し、スロットリングと修正プログラムのデプロイを併せて計画してください。- initプロセスを追加しただけで直接の子プロセスを放置する → アプリケーションには直接の子プロセスの結果取得と再試行の責務が依然としてあります → 直接の子プロセスは自身でwaitし、initはPID 1の責務や引き取られた子孫プロセスの処理に利用してください。
- 証拠収集前に再起動する → どの制限やコードパスで障害が発生したかを証明できないままインシデントが消失してしまいます → 空き容量が許す限り、まずPPID、制限値、カウンター、バージョン、発生レート、ログを記録してください。
関連質問と回答
営業時間中に親プロセスを再起動できない場合はどうしますか?
まずリークが発生しているパスを遮断します。該当ジョブタイプの無効化、並行数の削減、または正常なレプリカへのトラフィックのルーティングを行います。ポリシーで許可されている場合は、管理アクセスやヘルスチェックのキャパシティを維持できる程度にのみ、有効なPIDs制限を一時的に引き上げます。単一のカウント数だけでなく増加傾向の傾きを監視し、保守的な枯渇予測時間を算出します。生存している親プロセスは、サポートされた修復アクションを受け付けられる場合にのみ自身の子プロセスを回収できます。外部プロセスが代わりにwaitすることはできません。一時的なマージンが消費される前に、制御された親プロセスの切り替えを計画します。
コンテナ内でのみゾンビが発生する場合はどうしますか?
PID namespaceの視点に入り、namespaceのPID 1とゾンビのPPIDを特定します。ホストまたはオーケストレーター側からサービスのcgroupパスとPIDsカウンターを確認します。アプリケーションがPID 1として動作しており、回収やシグナル転送を実装していない場合は、軽量なinitプロセスを導入します。ラッパーがPID 1の場合は、早期に終了していないか、単一の子プロセスのみをwaitしていないかを確認します。シグナルが適切にアプリケーションに届き、子プロセスが終了し、猶予期間(grace period)が切れる前にすべてのステータスが回収されるよう、コンテナの終了処理をテストします。
なぜSIGCHLDハンドラーの呼び出し1回が子プロセスの終了1回と一致しないのですか?
従来のシグナルは通知であり、子プロセスごとに1イベントずつ保持される永続キューではありません。プロセスがシグナルを処理する前に複数の子プロセスが終了する可能性があり、その場合通知は合流(coalesce)します。堅牢な設計パターンは「通知を受け取ったら子プロセスの状態を確認する」とし、終了した子プロセスがいなくなるまでノンブロッキングのwaitを繰り返すことです。アプリケーションは返された各PIDを正確に1回ずつ記録します。
SIGCHLDをSIG_IGNに設定すれば問題は解決しますか?
Linuxにおいて、明示的にSIGCHLDを無視するかSA_NOCLDWAITを設定するとゾンビプロセスの挙動は変わりますが、アプリケーションはwait()を通じた通常の終了ステータス収集に依存できなくなります。デフォルトの動作が「無視」と表現されていることと、明示的にSIG_IGNを設定することは同義ではありません。このモードは、子プロセスの結果がビジネスロジック上まったく不要であり、言語やランタイムの仕様が確認できている場合にのみ使用してください。通常のワーカー管理では、明示的な完了アカウンティングが必要です。
ユーザー側でfork失敗が発生する前にどのようにアラートを発報しますか?
親プロセスごとの継続的なゾンビ増加率、cgroup PIDの残余枠低下、pids.events:maxの増加、およびプロセスの生成(spawn)失敗をトリガーとしてアラートを設定します。これらのシグナルをジョブのスループットと組み合わせることで、正当な一時的バーストによるカウント増加だけで不要な通知が飛ばないようにします。スーパーバイザー、テレメトリ、復旧コマンド用のリソースを十分に確保した上で、非本番のnamespaceで制御されたリークを発生させ、アラートの動作を検証します。