ANTIGRAVITY LABEN
記事一覧/連携・プラグイン
連携・プラグイン/2026-08-08上級

止めたはずの MCP サーバが残り続ける — 停止方法 4 種を 84 回測って見えた setsid の壁

セッションを閉じたのに MCP サーバがポートを握ったまま残る。停止方法 4 種を 84 試行で測ると、プロセスグループ宛の SIGTERM は孤児率 0% でも、子が setsid した瞬間に 100% へ戻りました。/proc の ppid を辿る子孫スイープの実装まで。

mcp15processsigtermsetsidteardownmeasurement

プレミアム記事

朝いちばんにセッションを開いたら、MCP サーバが 1 台だけ立ち上がりませんでした。

ログには address already in use の一行。ポートを握っていたのは、前夜のセッションで止めたはずの、まさに同じサーバでした。

セッションは確かに閉じています。ホスト側のプロセスも消えています。それでも子は生きている。個人開発で回している構成なので、朝の一台が止まっていることに気づけました。台数と人数が増えれば、この手の取り残しは静かに積み上がります。

手元の ps を眺めながら、「止めた」と「止まった」の間にどれだけの隙間があるのかを、一度きちんと測っておきたくなりました。

停止方法だけを変えて、同じツリーを 84 回作り直す

MCP サーバは多くの場合、ホストから子プロセスとして起動されます。sh -c を挟み、ランチャを挟み、その先に実体がいる。この途中の一段でもシグナルを飲み込めば、末端は取り残されます。

そこで、実運用に近い 3 段のツリーを最小構成で再現しました。末端は TCP ポートを掴んだまま待ち続けるだけのサーバです。ポートを持たせたのは、「生きているかどうか」を主観ではなく次の bind の成否で判定したかったからです。

# srv.py — ポートを掴んで待つだけの末端プロセス
import socket, sys, os, time
 
port, mode, tag = int(sys.argv[1]), sys.argv[2], sys.argv[3]
if mode == "own_group":
    os.setsid()                     # 自分で新しいセッション/プロセスグループを作る
s = socket.socket()
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(("127.0.0.1", port))
s.listen(8)
open(tag + ".srv.pid", "w").write(str(os.getpid()))
while True:
    time.sleep(0.05)
# wrap.py — 子を起動して待つだけのラッパー(シグナルを転送しない)
import subprocess, sys, os
 
port, mode, tag = sys.argv[1], sys.argv[2], sys.argv[3]
open(tag + ".wrap.pid", "w").write(str(os.getpid()))
subprocess.Popen([sys.executable, "srv.py", port, mode, tag]).wait()

シグナルを転送しないラッパーは、意地悪な仮定ではありません。パッケージランナーやシェルスクリプト経由でサーバを起動している構成では、むしろこちらが既定の振る舞いです。

ホスト側は start_new_session=True でツリーを起動し、LISTEN を確認してから停止方法を適用します。停止後は 1.5 秒を上限に、末端プロセスの生存とポートの再 bind 可否を 10ms ごとに観測しました。

各条件 12 試行、全 7 条件で 84 試行。起動から LISTEN までは中央値 41.5ms で、条件間の差はありませんでした。

PID に SIGTERM を送る — 12 回中 12 回、孤児が残る

最初はいちばん素朴な方法です。ホストが把握している先頭の PID に SIGTERM を送ります。

os.kill(top.pid, signal.SIGTERM)

結果は、孤児率 100%、ポート占有率 100%。12 回すべてで末端サーバが生き残り、12 回すべてで次の bind が失敗しました。

理由は単純です。SIGTERM はプロセス 1 つに届くだけで、子孫には伝播しません。ラッパーは素直に終了し、その瞬間に末端は親を失って PID 1 に引き取られる。ホストから見れば「起動したプロセスは消えた」ので、成功に見えてしまいます。

ここが厄介なところでした。停止処理の戻り値は正常です。ログにも異常は出ません。翌朝の address already in use まで、誰も失敗に気づけません。

ここまでお読みいただきありがとうございます。

この記事の続きを読む

この先には、実装コードやベンチマーク結果など、実務でお役に立てる内容をご用意しています。このサイトは広告を掲載しておらず、サーバーや開発にかかる費用はメンバーの皆様のご支援で成り立っています。もしお役に立てていましたら、ご支援いただけますと大変ありがたいです。

この記事で得られること
停止方法4種×84試行の実測。PID 宛 SIGTERM は孤児率 100%、プロセスグループ宛なら 0%、ただし子が setsid した瞬間に 100% へ戻る
TERM→猶予→KILL のエスカレーションは猶予 500ms が丸ごと無駄になる。12 回中 0 回が猶予中に終了し、正しく届けば 10.4ms で終わる
/proc の ppid を辿る子孫スイープの完全な実装。zombie 判定と PID 再利用ガードまで含めて 10.8ms で確実に止める
Stripe による安全な決済 · いつでもキャンセル可能

この記事を購入する

この先の内容をすべてお読みいただけます。一度のご購入で、いつでも何度でもアクセスできます。このサイトは広告を掲載しておらず、皆さまのご支援がサーバー費用などの運営を支えています。

または
メンバーシップなら全記事が読み放題 →
シェア

お読みいただきありがとうございます

Antigravity Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

関連記事

連携・プラグイン2026-05-06
Antigravity × Stripe カスタムMCPサーバー実装ガイド — AIエージェントが課金処理を自律実行する本番設計
Stripe課金処理をAntigravityのAIエージェントに委ねるカスタムMCPサーバーを実装する。サブスクリプション管理・Webhook・マルチテナント対応まで、TypeScriptで本番品質のMCPサーバーを構築する完全実装ガイド。
連携・プラグイン2026-05-04
Antigravity から Gmail を操作するまで:Google Workspace MCP の設定と実践パターン
Gmail・Google カレンダー・Sheets の操作を Antigravity から AI で直接実行できる MCP 連携の設定手順と実用的なワークフロー例を解説。日常の繰り返し作業を減らすための具体的なセットアップ付き。
連携・プラグイン2026-05-03
Antigravity × MCP Toolbox for Databases: BigQuery・AlloyDB・Spanner をエージェントから直接操作する
Google謹製MCPサーバー「MCP Toolbox for Databases」でAntigravityエージェントをBigQuery・AlloyDB・Spannerに接続する実践ガイド。MCP Store経由のセットアップから自然言語クエリ・スキーマ解析まで、v1.0.0の新機能を活かした開発ワークフローを解説します。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →