cgroup

このページの目次

リーフグループが自身のクォータを使い切っていないにもかかわらず、タスクが制限されている場合、先祖のリソースが既に枯渇している可能性があります。cgroup の階層、コントローラの有効化、実際のスロットリングは一体として見る必要があります。cgroup namespace への入り込みはパスのビューを変更するだけです。

本稿では v2 の統一階層を主に扱い、v1 では複数のコントローラ階層を使用できます。実際のマウント方法は現在の環境を参照してください。

参照目標読書位置
制限の発生源と管理権限階層、委任
CPU、メモリ、タスクの予算各コントローラ
実際の妨害の有無PSI、統計、ソースコードのエントリ

階層制限は先祖を沿って適用される

子 cgroup の設定は親の制限を回避できません。親の CPU クォータは複数の子グループによって共有消費される可能性があり、親のメモリ制限は子ツリー全体を制約します。リーフファイルのみをチェックすると、真のボトルネックを見逃すことになります。

cgroup.controllers は現在の階層で子グループに利用可能なコントローラを示し、cgroup.subtree_control は子階層に対してどのコントローラを有効にするかを決定します。mkdir しても、すべてのコントローラファイルとポリシーが自動的に整うわけではありません。コントローラは階層を上に下る方向で有効にする必要があります。

通常のドメインコントローラは通常、no-internal-process 制約に従います:内部ノードがリソースを子グループに割り当てる際、通常のワークロードプロセスと子グループを同時に競合させることはできません。ルートおよび threaded モードには専用のルールがあり、すべてを一言で概括することはできません。cgroup v2

委任はシステムルートの直接修正よりも重要

現代のシステムでは systemd などが cgroup ツリーを管理します。アプリケーションは委任されたサブツリーまたはマネージャインターフェースを使用すべきであり、/sys/fs/cgroup の再マウントや上位コントローラの随意な変更が可能であると仮定してはいけません。

許可されたターゲットグループに cgroup.procs を書き込むと、プロセスのすべてのスレッドが移動します。threaded モードでは cgroup.threads は異なる意味を持ちます。移動権限には共通の祖先などの要件も関与しており、ディレクトリが書き込み可能だからといって任意のプロセスを引き込めるわけではありません。プロセスの移動でも、既存のメモリ会計がすべて移転される保証はありません。

CPU:重み、クォータ、配置は別々に設定

インターフェース例意味
cpu.weight200同一レベルの競合相手に対する相対的な重み、デフォルトは 100
cpu.max50000 100000100000 マイクロ秒の周期ごとに、最大 50000 マイクロ秒の CPU 時間を消費可能
cpuset.cpus0-3利用可能な CPU コレクションを要求。有効な値と先祖の制限も確認する必要がある

weight=200 は 2 つの CPU を獲得することを保証するものではなく、また常に他のグループの 2 倍の速度であることを保証するものでもありません。重みは主に該当タスクが共同で競合する際に発揮され、アイドル状態のリソースは使用可能です。

cpu.max のクォータはグループ内の関連タスクによって共同消費されます。2 つのスレッドが 2 つの CPU で同時に実行され、クォータをより早く使い果たす可能性があります。その後、スロットリングされます。これは「各 CPU で固定 50% を実行する」ものではありません。cpu.stat の nr_throttled、throttled_usec などの増分を観察して、クォータが実際にボトルネックとなっているかどうかを判断する必要があります。

cpuset による配置範囲の制限は、これらの CPU の排他利用を意味するものではありません。パーティション化と排他設定には別のルールがあります。cpuset.mems も有効なノードと NUMA 割り当てポリシーと合わせて理解する必要があります。

子グループがそれぞれ 30 ms 残っているのに、なぜ一緒にスロットリングされるのか

親グループ P の cpu.max を 100000 100000、2 つの子グループ A、B をそれぞれ 80000 100000 と設定します。予算関係を際立たせるため、周期を同時に t=0 から開始し、追加のバーストを無効化し、A、B にはそれぞれ持続的に実行可能な通常の公平スケジューラタスクが 1 つずつあり、利用可能な CPU をそれぞれ 1 つ占有していると仮定します。スケジューリングとランタイムスライスの配布の詳細は無視します。

最初の 50 ms のウォールクロック時間において、A は 50 ms の CPU 時間を消費し、B も 50 ms を消費します。親グループの 100 ms の総クォータはすでに使い果たされています。A、B 自身はそれぞれ 30 ms 残っていますが、親グループを越えて実行することはできません。次の周期でクォータが補充されるまで、子グループの上限を引き上げても、この先祖によるボトルネックは解消されません。

2つのCPUが同じ親グループのクォータを並行して消費 タスク A 実行中:50 msのCPUを消費 親グループのクォータ枯渇、進行停止 タスク B 実行中:50 msのCPUを消費 親グループのクォータ枯渇、進行停止 0 ms 50 ms 100 ms 50 msのウォールクロック × 2つのCPU = 親グループの100 msのCPU予算 同一周期・バーストなし。子に各30 ms残っていても、祖先の上限を受けます。
def one_period(active):
    parent_left = 100
    leaf_left = {"A": 80, "B": 80}
    used = {"A": 0, "B": 0}
    for _ in range(100):  # 100個の1ms時間グリッド、各タスクは最大1つのCPUを占有
        for name in active:
            if parent_left > 0 and leaf_left[name] > 0:
                parent_left -= 1
                leaf_left[name] -= 1
                used[name] += 1
    return used, leaf_left, parent_left

assert one_period(("A", "B")) == ({"A": 50, "B": 50}, {"A": 30, "B": 30}, 0)
assert one_period(("A",)) == ({"A": 80, "B": 0}, {"A": 0, "B": 80}, 20)

A のみが実行されている場合、むしろ 80 ms を消費でき、その後自身の上限によって制限され、親グループには 20 ms が残ります。これらの 2 つの軌跡は、診断にはタスクの競合状況と各層の cpu.stat 増分の両方を同時に確認する必要があることを示しています。モデルの 50 ms は理想的な時間グリッドの結果であり、Linux が任意の周期アライメント、スライスキャッシュ、スケジューラクラス、またはマシン負荷に対して正確なタイミング保証を行うものではありません。CPU コントローラ

重みは別の階層計算です。ルート直下の P、Q の重みが同じで、P 直下の A、B の重みが同じ、Q 直下には C のみがいる場合、各グループが同じ CPU 容量を継続的に競合し、他の制約がない比例モデルでは、P、Q はまずそれぞれ半分ずつ獲得し、A、B はさらに P の半分を分け合います。最終的に A:1/4、B:1/4、C:1/2 となります。3 つのリーフの同じ重みを単純に均等に 1/3 ずつ割り当てると、中間階層が失われます。あるグループがリソースを必要としなくなった場合、この飽和競合の比率はもはや適用されません。

A と B を別々に実行し、それらがどのように先祖のクォータを共同消費するかを見ます。特定の子グループに残高がある場合でも、先祖は兄弟によってすでに枯渇している可能性があります。子グループのクォータは、そのグループのために予約された独立した CPU リソースではありません。

R は本期間の残りの CPU 時間です。実際の多層階層では、関連するすべての先祖を沿って確認する必要があります。図では親子の 2 層のみを残しています。

図を準備しています
子グループにクォータが残っているのに、なぜ実行を継続できないのか?

子グループの設定クォータは予約されたシェアではなく、先祖を回避することもできません。利用可能な量は min(子グループの残り, 先祖の残り) によって制限され、先祖が兄弟の 1 つによって消費されると、もう 1 つに影響します。

メモリ:high は圧力制御、max は境界

memory.high と memory.max の役割は異なる memory.high を超える 回収とスロットリングの圧力;一時的に超えることができる memory.max に到達 制限と回収を試みる;失敗すると memcg OOM になる可能性あり memory.events と memory.pressure を組み合わせる 境界超過回数、OOM/kill イベント、実際の待機時間を区別する high 自体は OOM kill をトリガーしない。単純な閾値順序で実行される完全なカーネル呼び出し図ではない。
インターフェース関連条件に達した後の意味
memory.high回収とスロットリングの圧力をかけ、一時的に突破可能。OOM killer はトリガーされない
memory.max硬制限下でまず回収などを行うが、満たせない場合は memcg OOM に陥る可能性がある

これらを「1 バイトでも超えたら即座にプロセスを殺す」という単一のスイッチに統合することはできません。

memory.low はベストエフォート回収保護であり、memory.min はより強力な保護セマンティクスを提供しますが、有効な保護は先祖と競合割り当ての影響を受けるため、事前に物理 RAM の一塊を割り当てるものではありません。memory.swap.max はスワップに対して独立した制御を行います。

memory.oom.group は、対応する OOM 処理がワークロードをグループ単位で扱うよう要求できます。「high での回収失敗」から直接「oom.group により全グループを殺す」へ矢印を引くことはできません。イベントが発生した階層とトリガー原因を区別する必要があります。

回収の選択はページの状態、活性、書き戻し可能性、スワップ、ポリシーなどに依存し、キャッシュをすべてクリアしてから匿名ページをスワップアウトするという固定の順序が保証されるわけではありません。memory.current には対応して会計処理されたメモリが含まれており、あるプロセスの RSS に等しいものではありません。

I/O とタスク数

io.max はデバイスごとにバイト数または IOPS で制限し、フォーマット内のメジャー/マイナーデバイス番号は実際のターゲットデバイスから取得する必要があります。io.weight の効果はブロック層と対応するスケジューラ/ポリシーのサポートに依存します。キャッシュ書き込み、バックグラウンド書き戻し、および下位層デバイスパスは目に見える表現に影響を与え、任意の read/write 呼び出しレートを直接ブロックデバイスのレートとして扱うことはできません。

pids.max はカーネルタスク数を制限し、スレッドもカウントされます。制限に達すると、対応する fork/clone の作成が阻止され、典型的なエラーは EAGAIN です。より低い上限を設定するか、既存のタスクに移動しても、超過したタスクが自動的に殺されるわけではありません。pids.current と pids.events を観察する必要があります。

PSI は使用量だけでなく待機を観察

指標説明
some少なくとも 1 つのタスクがそのリソースによって妨害された時間の割合
full該当範囲のすべての非アイドルタスクが妨害された時間
avg10/60/300対応する平均パーセンテージ
total累積マイクロ秒、パーセンテージではない

範囲と単位を明確にして初めて、待機とリソース使用量を一緒に分析できます。

CPU 使用率が高いことは、有用な作業が十分に実行されている可能性があり、PSI は待機損失を観察するのに役立ちます。メモリ使用率が高いことも、主に価値あるキャッシュである可能性があり、直接メモリ圧力と同等視すべきではありません。システムレベルの CPU full には特別な意味があり、メモリ full の解釈を機械的に適用することはできません。PSI

some と full:2 つのタスクの待機区間のマージ方法

ある cgroup の I/O 圧力を例に、100 ms を観察します。非アイドルタスクが A、B の 2 つのみおり、両方が終始作業需求を持っていると仮定します。A は [0,40) ms で I/O を待機し、B は [20,60) ms で I/O を待機し、他の時間は各自が作業を進行できます。

区間 msAB圧力時間への貢献
0–20妨害中進行可能some が 20 ms 増加
20–40妨害中妨害中some、full がそれぞれ 20 ms 増加
40–60進行可能妨害中some が 20 ms 増加
60–100進行可能進行可能どちらも増加しない

したがって、some=60 ms、full=20 ms となります。2 つのタスクのそれぞれの 40 ms の待機時間を単純に足すと 80 ms になりますが、それはタスク待機時間の合計であり、some がウォールクロック区間の和集合を数えたものではありません。

def pressure_ms(blocked, window=100):
    assert blocked and window > 0
    points = {0, window}
    for intervals in blocked:
        for start, end in intervals:
            assert 0 <= start <= end <= window
            points.update((start, end))
    points = sorted(points)
    some = full = 0
    for start, end in zip(points, points[1:]):
        waiting = [any(a <= start < b for a, b in intervals) for intervals in blocked]
        if any(waiting):
            some += end - start
        if all(waiting):
            full += end - start
    return some, full

waits = [[(0, 40)], [(20, 60)]]
assert pressure_ms(waits) == (60, 20)
assert pressure_ms(waits + [[]]) == (60, 0)  # C は常に進行可能、full は 0 に
assert pressure_ms([[(0, 10)], [(10, 20)]]) == (20, 0)
assert pressure_ms([[], []]) == (0, 0)
assert pressure_ms([[(0, 40), (20, 60)]]) == (60, 60)

コードは区間を分割してカウントするもので、固定された非アイドルタスク集合における教育的モデルです。実際のカーネルでは、タスクの状態変化と統計範囲に基づいて会計処理を行うため、作成されたすべてのスレッドを永久に full の分母に含めることはできません。例えば、作業がなくなり自発的にアイドル状態になったスレッドは、「常に進行中」と見なされて full をゼロに引き下げる要因として扱われるべきではありません。PSI 定義

2 回のサンプリング間隔が 100000 μs で、some の total が 60000 μs 増加した場合、これはサンプリングウィンドウ内で約 60% の妨害時間であることを示します。これはファイル内の avg10 の再計算式ではありません。avg10/60/300 はカーネルが維持する平滑平均です。読み取り間隔、カウンターのリセット、グループのライフサイクルも確認し、異なる範囲の total を単純に減算しないでください。

現在のプロセスの読み取り専用チェック

cat /proc/self/cgroup
findmnt -t cgroup2
cat /proc/pressure/cpu
cat /proc/pressure/memory
cat /proc/pressure/io

実際の cgroup マウントルートとプロセスパスに基づいて所属グループを特定し、cpu.stat、memory.events、memory.pressure、pids.current などの利用可能なファイルを読み取ります。コンテナは階層の一部のみを公開する場合があり、/proc パスも namespace の影響を受けるため、文字列を無条件でホストルートのディレクトリに連結しないでください。

診断順序は次のようになります:所属グループと先祖の制約を確認し、コントローラが有効化されているかチェックし、統計増分と実際の要求レイテンシを比較します。カウンターを読み取ってもすべての因果関係が説明されるわけではありませんが、「制限が設定されていること」と「実際に制限されていること」を区別できます。

カーネル読書エントリ

kernel/cgroup はコア階層とメンバーシップ関係を管理し、mm/memcontrol.c はメモリ会計と制御を管理し、block/blk-cgroup.c はブロック層関連を管理します。コントローラの css はサブシステム状態であり、すべてのコントローラが同じオブジェクトを request_queue にアタッチするわけではありません。

ユーザーから見えるインターフェース契約からソースコードに入り、特定バージョンの charge、reclaim、またはスケジューリングパスを確認することが、数十行のコメントを一般的な try_charge 呼び出し図として偽装するよりも信頼できます。隔離ビューのもう半分は ネームスペース を参照してください。