Claude Media
Claude Codeセルフホストでpids.maxを実測して決める

Claude Codeセルフホストでpids.maxを実測して決める

セルフホストRunnerのプロセス・スレッド上限を、pids.peakの実測値と--capacityから決める手順。Kubernetes・Docker・systemdごとの設定先と、上限に当たったかを確かめる方法をまとめます。

セルフホストRunnerのプロセス・スレッド上限は、1セッションのピークを測り、同じ上限を共有するセッション数を掛けて決めます。CPUとメモリには目安の数字が示されていますが、プロセス数にはありません。自分の環境で測るしかない項目です。

上限に届くと、カーネルが新しいプロセスとスレッドの作成を断ります。セッション内のコマンド、ビルド、テストが失敗し、断られた呼び出しはEAGAIN(標準メッセージはResource temporarily unavailable)を返します。しかも同じ上限を共有する別のセッションも巻き込まれます。この記事は、測り方、掛け算、設定先、到達の確かめ方を順に通します。

上限は「セッション数 × ピーク」で組み立てる

考え方は一行で済みます。1セッションが必要とする数を測り、その上限を共有するセッションの数を掛けます。

公式の例は具体的です。1セッションのピークが300プロセス・スレッドで、4セッションが同じ上限を共有するなら、合計で1,200が要ります。上限を2,400にすれば、2倍まで膨らむビルドの余裕も残ります。

ここで数えるのはプロセスだけではありません。表の各上限はスレッドも数えます。ビルドツールが多数のスレッドを立てる構成では、プロセス数だけを見ると過小に見積もります。

数字

公式の例で見る上限の組み立て

  • 1セッションのピーク

    300

    プロセスとスレッドの合計

  • 共有するセッション

    4

    --capacity 4 の場合

  • 必要な合計

    1,200

    300 × 4

  • 設定する上限

    2,400

    必要量の2倍

数値は公式の説明例であり、環境ごとに測り直す前提の値です

「上限を高くすれば安全」とも言い切れません。公式は、上限が高いほど重いビルドの余裕が増え、低いほど暴走したforkを早く止められると説明しています。余裕と封じ込めの綱引きなので、測定値の何倍に置くかは自分で決める部分です。

どこに設定するかは実行基盤で決まる

pids.maxはcgroupが持つ上限値のファイル名です。同じ上限でも、設定する場所と、それを分け合う範囲が基盤ごとに違います。

基盤設定先分け合う範囲
Kubernetes設定先ノードのkubelet設定のpodPidsLimit。Pod specでは設定できません分け合う範囲1つのPod内のコンテナ
Docker設定先--pids-limit、Composeではpids_limit分け合う範囲1つのコンテナ
systemd設定先RunnerのunitのTasksMax分け合う範囲1つのunit
ulimit -u設定先Runnerを起動するシェルやサービス分け合う範囲そのユーザーIDで動く、ホスト上のすべて。rootには効きません

Kubernetesだけ、設定場所がワークロード側にありません。podPidsLimitはRunnerを動かすノードのkubelet設定に書く項目なので、マニフェストを書き換えても変わらず、ノード設定を管理する側との調整が要ります。また分け合う範囲がPodなので、サイドカーが同居していればその分も同じ枠から出ます。

kubelet設定ファイルの該当行は、次の形です。

# kubelet の設定ファイル(Runnerを動かすノード側)の一部
podPidsLimit: 2400

Dockerでは、Composeの公式レシピ(--capacity 4)にpids_limitを足す形になります。

services:
  claude-runner:
    image: <your-registry>/claude-runner:latest
    command:
      - self-hosted-runner
      - --capacity
      - "4"
      # 他の引数は公式レシピのまま
    pids_limit: 2400

systemdでは、RunnerのunitにTasksMax=2400を書きます。

TasksMaxを書かなかったunitにも制限はかかります。systemdはDefaultTasksMaxを当て、その既定値はkernel.pid_max・kernel.threads-max・ルートcgroupのpids.maxのうち最小のものの15%です。ホストの規模で値が変わるので、「無制限のはず」と思い込んだ構成がここで当たります。

ulimit -uは別物です。cgroupではなくユーザーIDに対する上限なので、同じユーザーで動く無関係なプロセスも数に入ります。Runnerを専用ユーザーで動かしていない場合は、数え方がcgroupより広くなる点に注意してください。

1セッションのピークを測る

測定は、Runnerを1つだけ動かした状態で行います。ホストを他のRunnerと共有していると、どのプロセスがどのRunnerのものか切り分けられません。Kubernetesではそのノードが測定対象のホストです。

手順

ピークを測る5段階

  1. 1

    Runnerを単独で起動する

    --capacity 1で、他のRunnerがいないホストと環境で起動します。セッションはまだ始めません。

  2. 2

    cgroupのディレクトリを探す

    コンテナの外のホスト側で、RunnerのプロセスIDから/proc/<pid>/cgroupを読みます。

  3. 3

    代表的なビルドを1セッションで流す

    ビルド後にpids.peakを読みます。無ければpids.currentを1秒ごとに読み、最大値を控えます。

  4. 4

    共有するセッション数を掛ける

    1つのPod・コンテナ・unitなら--capacity、複数のRunnerで共有するなら容量の合計を掛けます。

  5. 5

    上限をその結果より上に置く

    同じ枠を使う別コンテナのピークも足してから決めます。

セッションを始める前にディレクトリを突き止めておくのは、ビルドが始まってからではpids.currentの推移を取り逃すためです。

cgroupのディレクトリは、/proc/<pid>/cgroupの中身で決まります。:pids:を含む行があればcgroup v1で、ディレクトリは/sys/fs/cgroup/pidsの後ろにその行の:pids:以降を続けたものです。行が0::で始まる1本だけならcgroup v2で、/sys/fs/cgroupの後ろに0::以降を続けます。

cgroup v2のホストなら、次のスクリプトで記録できます。公式の手順を書き下した例で、pgrepの絞り込みは環境に合わせて変えてください。

PID=$(pgrep -f self-hosted-runner | head -1)
DIR=/sys/fs/cgroup$(sed -n 's/^0:://p' /proc/$PID/cgroup)
echo "$DIR"
ls "$DIR" | grep '^pids\.'

pids.peakが一覧にあれば、セッションの終了後に読むだけです。

cat "$DIR/pids.peak"

無い場合は、ビルド中にpids.currentを1秒ごとに読み、最高値を残します。

max=0
while sleep 1; do
  cur=$(cat "$DIR/pids.current")
  [ "$cur" -gt "$max" ] && max=$cur && echo "peak so far: $max"
done

1秒の間に立ち上がって消えたピークは見えません。この方法で得た値は低めの見積もりとして扱い、掛け算の後に余裕を多めに見ます。

測るときに外しやすい点

ビルドの内容で結果は大きく変わります。公式は、リポジトリの代表的なビルドを実際に流すことを求めています。軽い対話だけで測ると、CIで走る本番のテストスイートとの差が丸ごと余裕不足になります。

掛ける数も確認が要ります。1つのRunnerのPod・コンテナ・unitに収まるなら--capacityですが、複数のRunnerが同じ上限を共有する構成では、各Runnerの容量の合計を掛けます。--capacityはそのRunnerが同時に抱えるセッションの上限で、CPUやメモリの見積もりでも同じ掛け算をします。その目安はClaude Codeセルフホスト環境の本番運用にまとめてあります。

上限に当たったかを確かめる

症状は、ビルドやテストが突然Resource temporarily unavailableで落ちることです。原因の切り分けは2つの確認で足ります。

  • 到達したか: ホストのカーネルログをdmesgなどで検索し、cgroup: fork rejected by pids controller inとcgroupのパスを含む行を探します。カーネルはそのcgroupで最初に拒否したときだけ記録するため、ログ全体を検索します
  • どの値が効いているか: Runnerのcgroupディレクトリと、その上のすべてのディレクトリのpids.maxを読みます。最も低い値が実効の上限で、maxはその階層に制限がないことを示します
dmesg | grep 'fork rejected by pids controller'
cat "$DIR/pids.max"

カーネルが記録するのは最初の拒否だけなので、何回当たったかはログから分かりません。dmesgのリングバッファが流れていれば、行そのものも消えます。そのときはpids.peakとpids.maxを比べて判断します。

切り分け

Resource temporarily unavailable が出たときの切り分け

  1. Qdmesg に fork rejected by pids controller の行があるか
    答えがここで原因を絞れない

    pids.maxが原因ではない可能性があります。ただしログが流れて行が消えていることもあるので、pids.peakとpids.maxも見ます。この2つが離れているときは、メモリの上限やulimit -uなど、別の制限を疑います。

    答えがはい
  2. Q行に出た cgroup のパスは Runner のものか
    答えが別の枠で拒否されている

    そのパスの階層と、上位ディレクトリのpids.maxを読み、実効の値を確かめます。

    答えがはい
  3. Q実効の上限は、ピーク × 共有するセッション数に足りているか
    答えが上限が低すぎる

    測り直したピークで掛け算をやり直し、設定先(podPidsLimit・--pids-limit・TasksMax)の値を上げます。

    答えがはい

上限に余裕がある場合

拒否が出ているのに計算上は足りているなら、測ったピークが小さすぎたか、同じ枠を共有する別のコンテナが数に入っていないかのどちらかです。1秒間隔のサンプリングの取りこぼしや、軽いビルドで測った可能性も含めて確かめ、重いビルドでpids.peakを測り直します。

出力の中に同じ文言がある別の原因もあります。Runnerの起動スクリプトが標準出力をRunnerと共有し、パイプが満杯になったときもResource temporarily unavailableの書き込みエラーで終わります。こちらはpids上限とは無関係で、スクリプトの出力先を分ければ解消します。

設定で迷うところ

落とし穴

Pod specに書いても pids の上限は変わらない

KubernetesのpodPidsLimitはkubelet側の設定です。Pod specには同じ役割の項目がなく、マニフェストを直しても反映されません。ノードの設定を管理するチームとの調整が必要になります。

もう1つ、上限を共有する範囲が基盤で違う点も混乱のもとです。KubernetesはPod内の全コンテナ、Dockerは1コンテナ、systemdは1 unitです。1コンテナ1セッションの構成(--capacity 1)なら掛け算は不要ですが、--capacityを上げた構成では同じ枠を複数のセッションが分け合います。

公式はメモリについて、--capacity 1でコンテナごとに上限をかける構成を示しています。--pids-limitもコンテナ単位なので、同じ構成なら1つのセッションの暴走が他のセッションの枠を食いません(pidsへの当てはめは記事側の推論です)。逆に容量を上げるなら、掛け算の上限を決めたうえで、暴走した1セッションが全体を止めるリスクを受け入れることになります。

まとめ

pidsの上限は、公式の数字を写す項目ではなく、自分のビルドを測って置く項目です。順序は「単独のRunnerでpids.peakを取る → 共有するセッション数を掛ける → 他のコンテナのピークを足す → その上に上限を置く」で、設定先はKubernetesならノードのkubelet、Dockerなら--pids-limit、systemdならTasksMaxです。

運用に入ってからは、dmesgのfork rejected by pids controllerを検索する習慣がいちばん安く効きます。測定時より重いビルドを足した日や、--capacityを上げた日に、あらためてpids.peakを測り直してください。

アイドル中のセッションが残すプロセスの扱いはClaude Codeのhold-idle-fileで残プロセス中のセッションを保持するで、Runner起動の全体像はClaude Codeセルフホスト環境でオンデマンドRunnerを起動するで扱っています。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. code.claude.com/docs/en/self-hosted-environments-deploy
  2. code.claude.com/docs/en/self-hosted-environments-reference
この記事を共有:XはてブLinkedIn