一覧OS-14-2 · 仮想化と隔離の原理

名前空間とcgroupによるリソース制限

プロセスからの見え方と、使える資源量を別々に確認します。

説明

Linuxの名前空間はPID、マウント、ネットワークなどの見え方を分けます。cgroupはプロセス群の資源使用を管理します。コンテナではこれらを組み合わせますが、役割は同じではありません。

一つのコンテナを表す記録から、ホストと内部のPID、ユーザーID、ファイルの見え方を照合します。続いてCPU時間とメモリの上限を計算し、見え方を変えても資源上限にはならないことを確かめます。

1ホストとコンテナのPID

表のhostは外側、innerは内側のPIDです。名前空間に入る前後で同じプロセスがどう見えるかを取り出します。

tasks = [{"host": 6200, "inner": 1, "name": "init"}, {"host": 6207, "inner": 7, "name": "worker"}]PID対応の記録; print([(t["host"], t["inner"], t["name"]) for t in tasks])内外の番号

ホストPID6207が内側では7です。プロセスが二つに増えたのではなく、見るPID名前空間によって番号が変わっています。親の名前空間からは子側のプロセスを見られますが、内側から外側の全プロセスを同じようには参照できません。

2同じパスに別の内容が見える

ホストとコンテナで/etc/hostnameが別のマウント内容を指す例です。

mount_views = {"host": {"/etc/hostname": b"host-a\n"}, "container": {"/etc/hostname": b"lab-a\n"}}パスから見える内容; print(mount_views["host"]["/etc/hostname"], mount_views["container"]["/etc/hostname"])同じパスの比較

パス文字列は同じでも、参照するマウントの構成が異なり、内容も違います。マウント名前空間は見えるマウントを分ける仕組みです。共有マウントの伝播設定によっては変更が他の名前空間にも届くため、名前空間を作るだけで全共有が消えるわけではありません。

3コンテナ内のrootをホストの一般ユーザーのUIDに対応させる

uid_mapの一行を内側開始0、外側開始100000、長さ65536とします。内部UID0と1000を外側へ変換します。

inside_start, outside_start, length = 0, 100000, 65536UID対応の範囲; internal = [0, 1000]変換する内部UID; print([outside_start + uid - inside_start for uid in internal if inside_start <= uid < inside_start + length])外側のUID

内側の0は外側100000、内側1000は101000です。内部でrootという名前やUID0を持つことは、ホストの初期ユーザー名前空間のUID0と同じ権限を得ることではありません。どの名前空間に対するcapabilityかも関係します。

4CPU使用時間の上限

cgroup v2のcpu.maxが50000 100000という記録を使います。どちらもµsで、期間内に使えるCPU時間の総量を表します。

cpu_max = "50000 100000"cpu.maxの記録; quota, period = map(int, cpu_max.split())量と期間を取り出す; print(quota / period, quota / 1000, period / 1000)比率とms表記

100msの期間にCPU時間50ms、CPU1個分に対して平均0.5相当です。CPU番号を半分にする指定でも、必ず1個だけで動かす指定でもありません。複数スレッドが別CPUで同時に走れば、この総量を早く消費します。

5二つのスレッドが同時に上限を使う

二つのスレッドが各1CPUを連続使用する理想化した区間で、50msの総量を使い切るまでを計算します。バースト設定や他の競合はない条件です。

workers = 2同時に走るスレッド数; elapsed_ms = quota / 1000 / workers総量を使う実時間; print(elapsed_ms, period / 1000 - elapsed_ms)実行時間と残り期間

25msで総量50msを使い、残る75msは次の期間を待つ計算です。実際のスケジューリングでは細部が変わりますが、実行できるCPU数と総CPU時間の上限は別の設定だと分かります。

6メモリ上限と現在の使用量

memory.maxを256MiB、memory.currentを200MiBとした記録です。80MiBの追加要求が上限内に収まるか、まず単純に計算します。

maximum = 256 * 1024**2; current = 200 * 1024**2; requested = 80 * 1024**2上限・使用・追加要求; print((maximum-current) // 1024**2, (current+requested) // 1024**2, current+requested <= maximum)余裕と追加後の量

余裕は56MiBで、追加後は280MiBとなり上限を超えます。ただし、この加算だけで即座のOOMを断定できません。カーネルは回収可能なメモリを回収し、抑えられない場合にOOM処理へ進みます。memory.highとmemory.maxでも意味が異なります。

7回収可能量で結果が変わる

40MiBを回収できた場合と、16MiBしか回収できない場合で、追加後の量を比べます。

reclaimable = [40, 16]回収量の二条件; projected = [(current + requested - amount * 1024**2) // 1024**2 for amount in reclaimable]回収後の量; print([(value, value <= 256) for value in projected])上限との比較

40MiB回収できれば240MiBで上限内、16MiBなら264MiBで上限を超えます。実際の原因を調べるときはmemory.events等の記録も使い、単なる使用量の大きさだけでOOMと決めません。

8名前空間を共有しても資源上限は別に設定できる

同じPID名前空間を使う二つのプロセス群に、異なるcgroupのCPU上限を持たせた表を表示します。

groups = [{"pid_ns": "ns-A", "cgroup": "batch", "cpu.max": "50000 100000"}, {"pid_ns": "ns-A", "cgroup": "api", "cpu.max": "200000 100000"}]見え方と資源上限の対応; print(*groups, sep="\n")同じ名前空間の二群

PIDの見え方は共通でも、CPU上限は0.5個分と2個分に分かれています。名前空間、cgroup、権限、システムコール制限はそれぞれ別の役割を持ちます。通常のLinuxコンテナではホストカーネルを共有するため、別カーネルを動かすVMとの境界も異なります。

まとめ

名前空間は資源の見え方を分け、cgroupはプロセス群の使用量や配分を管理します。

内部のUID0をホストのUID0と同じ資格情報として扱えません。

CPUの数と使用時間の総量、メモリ要求と回収後の量をそれぞれ確認します。

操作履歴と解説を振り返れます。

Python 3

1
tasks = [{"host": 6200, "inner": 1, "name": "init"}, {"host": 6207, "inner": 7, "name": "worker"}]; print([(t["host"], t["inner"], t["name"]) for t in tasks])
[(6200, 1, 'init'), (6207, 7, 'worker')]
2
mount_views = {"host": {"/etc/hostname": b"host-a\n"}, "container": {"/etc/hostname": b"lab-a\n"}}; print(mount_views["host"]["/etc/hostname"], mount_views["container"]["/etc/hostname"])
b'host-a\n' b'lab-a\n'
3
inside_start, outside_start, length = 0, 100000, 65536; internal = [0, 1000]; print([outside_start + uid - inside_start for uid in internal if inside_start <= uid < inside_start + length])
[100000, 101000]
4
cpu_max = "50000 100000"; quota, period = map(int, cpu_max.split()); print(quota / period, quota / 1000, period / 1000)
0.5 50.0 100.0
5
workers = 2; elapsed_ms = quota / 1000 / workers; print(elapsed_ms, period / 1000 - elapsed_ms)
25.0 75.0
6
maximum = 256 * 1024**2; current = 200 * 1024**2; requested = 80 * 1024**2; print((maximum-current) // 1024**2, (current+requested) // 1024**2, current+requested <= maximum)
56 280 False
7
reclaimable = [40, 16]; projected = [(current + requested - amount * 1024**2) // 1024**2 for amount in reclaimable]; print([(value, value <= 256) for value in projected])
[(240, True), (264, False)]
8
groups = [{"pid_ns": "ns-A", "cgroup": "batch", "cpu.max": "50000 100000"}, {"pid_ns": "ns-A", "cgroup": "api", "cpu.max": "200000 100000"}]; print(*groups, sep="\n")
{'pid_ns': 'ns-A', 'cgroup': 'batch', 'cpu.max': '50000 100000'}
{'pid_ns': 'ns-A', 'cgroup': 'api', 'cpu.max': '200000 100000'}

演習完了