cgroup v2のpids.max、プロセス数とスレッド数を分けて読む

淡い紫の大きな枠の内側に小さな緑と桃色の枠と細い糸の束が入り、一本の糸が外側へ垂れる抽象的な構図。

プロセスの一覧には数個しか見えていないのに、新しいスレッドを作れなくなることがあります。Linuxのcgroup v2で使うpids.maxを読むときは、アプリケーションの個数だけでなく、その中で動くスレッドと、親グループに含まれる別の仕事まで見る必要があります。本稿ではLinux 6.8の文書と実装を対象に、何を数え、どこで追加が止まるのかを整理します。[1][2]

簡単にまとめると?

  • pidsコントローラーが数える単位は、カーネルがTIDで識別するタスクです。一つのプロセスに複数のスレッドがあれば、その分も数えます。
  • 子グループに余裕があっても、祖先グループの合計が上限に達していれば、新しいタスクの生成は拒否されます。
  • 上限を現在数より低く設定しても、既存タスクをその数まで自動終了させる仕組みではありません。
  • CPU時間、メモリー量、タスク数は別の制約です。プロセス一覧だけでなく、対象と祖先の現在数・上限を併せて確認します。

「プロセス数」という名前だけでは数え方が分からない

cgroupは、仕事を階層的なグループに分け、資源の利用を管理するLinuxの仕組みです。pidsコントローラーは、その中で新しく作られるタスクの数を制限します。メモリーにまだ余裕があっても、タスクを大量に増やせば別の制約へ先に到達し得るため、メモリー制御とは別に用意されています。[1、PID節]

通常の説明では、プロセスは実行中のプログラムの単位、スレッドはその中の実行の流れとして区別します。ところが、このコントローラーのPIDという語は、文書が明記するようにTID、つまり個々のタスクを識別する番号を指します。メインのスレッドも一つに数えます。「プロセスが二つだから使用数は二つ」とは限りません。[1]

たとえば、一つ目のプロセスがメインを含めて三スレッド、二つ目が一スレッドなら、合計は四タスクです。同じグループ内のタスクだけでなく、その配下のグループにあるタスクもpids.currentへ含まれます。どの階層の値を読んでいるのかを揃えないと、プロセス一覧との比較を誤ります。

子に四つ分の余裕があっても、あと一つで止まる例

独自の小さな計数モデルで考えます。親Pの上限を十タスクとし、その下に子Aと子Bを置きます。P自身にはタスクがなく、Aには前述の二プロセス・四タスク、Bには一プロセス・五タスクがあるとします。Aの上限は八、Bの上限も八です。親Pの現在数は、四足す五で九になります。

Aだけを見ると、八という上限まであと四つ増やせそうです。しかし、P全体ではあと一つしか空きがありません。Aで新しいスレッドを一つ作ると、Aは五、Pは十となり、このモデルでは許可されます。続けてもう一つ作ろうとすると、Aの六は上限八以下でも、Pの十一が上限十を超えるため拒否されます。拒否後の現在数はAが五、Pが十のままです。

Linux 6.8のpids_try_chargeは、対象グループから祖先へ順に数を加えて上限を確かめ、途中で超過したら加算を取り消して-EAGAINを返します。[2] この動作を、ここでは競合のない逐次の計数として再現しています。実際のカーネルで負荷をかけた実験ではなく、同時生成・終了や他の生成失敗要因も扱っていません。

親上限十の独自モデル。開始時はA四・B五で合計九。Aの生成一回目は合計十となり許可、二回目は親上限のため拒否され合計十のまま。Bの一タスク解放後は合計九となり、Aの再生成が許可される。
子AとBの上限はそれぞれ八、親Pは十。拒否された生成は現在数へ残らない。図は条件を固定した独自の計数モデルで、Linux上の性能測定ではない。

Bのタスクが一つ解放され、カウントが四へ減れば、Pは九に戻ります。この後でAが一つ生成する例では、Aが六、Bが四、Pが十となって許可されます。同じAの要求でも、兄弟グループの使用状況が結果を変えることが分かります。Aの設定を八からさらに大きくしても、親Pの十という制約は取り除けません。

現在数が上限より大きくても、ただちに異常とは限らない

上限は、常に現在数をその値以下へ押し戻す命令ではありません。前の例のAが六タスクを持つ状態で、Aのpids.maxを四へ下げたとします。文書は、このような設定変更やタスクのグループ間移動によって、pids.current > pids.maxになる場合があると説明しています。[1]

この設定変更だけで、六つのうち二つが自動的に終了するわけではありません。Linux 6.8の上限書込み処理は上限値を更新し、新しいタスクの生成時には別途その値を検査します。[2、pids_max_write] したがって「六から四へ設定したのに、現在数が六のまま」という観測だけで、上限が無効だとは判断できません。

また、現在数が四まで下がっても、次の一つを加えると五になるため、上限四のままでは生成できません。他の制約がない単純なモデルなら、三以下になって初めて一つ分の余地が生まれます。上限と現在数が等しい状態を「まだ一つ追加できる」と読み違えないことが大切です。実機では解放がカウントへ反映された時点と、別の生成が進んだ時点にも注意が必要です。

調べる順番は、数える単位、所属、祖先の順

新規生成が失敗したときは、まず何が増えようとしていたのかを確認します。別プロセスの起動なのか、既存プロセスのスレッド追加なのかで、通常のプロセス一覧から見える変化が異なります。次に、失敗した処理が属するグループと、観測しているpids.currentの階層を合わせます。最後に、対象だけでなく祖先側の上限と合計数を辿ります。

pids.maxの値がmaxなら、そのグループ自身には数値上限を置いていないという意味です。祖先の上限までなくなるわけではありません。[2] 子ごとの設定を比較する際にも、「Aには八、Bにも八だから親全体で十六まで使える」と足し算せず、親の条件を別に確かめます。

ここで示したEAGAINは、pidsコントローラーが生成を拒否する経路の説明です。生成APIが返したエラーだけから、この上限が原因だと断定するものではありません。利用しているLinuxの版、コントローラーの有効化、管理ソフトが設定した所属と上限、同時に動く処理を照合する必要があります。CPU時間を配分する設定やメモリーの上限を増やしても、タスク数の制約を直接解消したことにはなりません。何が不足しているのかを単位ごとに分けると、変更すべき設定を絞り込めます。

出典

  1. Linux Kernel Documentation v6.8「Control Group v2」、Introduction、PID、PID Interface Files。公式文書。本稿は6.8の記述を対象とし、最新のすべての版や管理ソフトで同じ表示になるとはしていない。
  2. Linux v6.8、kernel/cgroup/pids.c。pids_try_charge、pids_can_attach、pids_can_fork、pids_release、pids_max_write。公式ソース。数値例と比較図は、以上の条件を単純化したQuantum Sea独自の計数モデル。