共有サーバー
ユーザーID、ファイルの所有者と権限、プロセスの実行権限で利用者を分けます。
複数の利用者と処理を、どの境界で分けるか
共有サーバーでは、一つのOSへ複数の利用者を収容するために、ファイルと権限を分けてきました。仮想マシンはゲストOSごとに境界を作り、コンテナは一つのカーネル上でプロセス群を分けます。起動時間、利用密度、互換性、権限、攻撃面のどれを重視するかによって、現在も複数の技術が組み合わされています。
レンタルサーバーや共有ホスティングでは、多くの利用者を一つのOSへ収容します。ユーザーIDとファイルの権限は利用者を分けますが、カーネルと、カーネルが管理するプロセスやネットワークは共有されます。chrootは見えるディレクトリツリーを狭め、FreeBSD Jailはプロセス、ユーザー、ネットワークにも制約を広げました。
ハードウェア仮想化はゲストカーネルを分けます。その後、Linuxのnamespaceとcgroupは共有カーネルへ戻り、プロセス群を細かく分ける道を発展させました。さらにgVisorやmicroVMは、コンテナの運用方法を活かしながら、ホストカーネルとの間へ別の実行境界を加えています。
ユーザーID、ファイルの所有者と権限、プロセスの実行権限で利用者を分けます。
rootディレクトリを切り替え、Jailではプロセス、ユーザー、ネットワークの扱いにも制約を加えます。
各仮想マシンがゲストカーネルを持ちます。OSの互換性と境界を得る代わりに、ゲストOSの起動とメモリが必要です。
プロセス群から見える状態と、CPUやメモリなどの使用量を項目ごとに管理します。
rootfs、namespace、cgroupに加え、capabilitycapability従来rootへまとめて与えられていた権限を、操作の種類ごとに分けた単位。、seccompseccompプロセスが呼び出せるシステムコールと引数をfilterで制限する仕組み。、LSMLSMカーネル内の操作へ、アクセス制御を呼び出すhookを用意する共通の枠組み。などを設定してプロセスを起動します。
ユーザー空間のapplication kernelがシステムコールを処理し、コンテナとホストカーネルの間へ境界を加えます。
KVMと最小限のデバイスモデルを使い、仮想マシンの隔離と短い起動時間、少ないメモリ消費を同時に狙います。
PID、マウント構成、ネットワーク構成、ホスト名などを項目ごとに分けます。
CPU、メモリ、I/O、プロセス数などをグループ単位で測り、配分や上限を設定します。
Linuxカーネルは、すべてのプロセスとネットワークインターフェース、マウント構成などを管理しています。その一部だけをプロセス群へ見せると、同じカーネルを使いながら、異なる状態を参照させられます。この参照範囲を分ける機能をnamespacenamespacePID、マウント、ネットワーク、ホスト名などの参照範囲を、プロセス群ごとに分ける仕組み。と呼びます。
内側:PID 1, 7, 12内側:PID 1, 4PID namespaceは階層構造です。外側のnamespaceからは、内側のプロセスも外側のPIDで参照できます。
どのファイルシステムが、ディレクトリツリーのどこへ接続されているかを分けます。ある範囲で行ったmountやunmountを、別の範囲から切り離せます。
hostnameとNIS domain nameを分けます。同じカーネル上でも、プロセス群ごとに異なるホスト名を参照できます。
System V IPCとPOSIX message queueを分けます。識別子やキューを、そのnamespaceに属するプロセス同士で使います。
プロセス番号の見え方を階層的に分けます。新しく作ったPID namespaceでは最初のプロセスがPID 1になり、親側のPID namespaceからは別のPIDで見えます。
ネットワークインターフェース、IPアドレス、経路、ポート、socketなどを分けます。各範囲に独立したネットワーク構成を用意できます。
UID、GID、capabilityの対応を分けます。内側のUID 0を、外側の一般ユーザーのUIDへ対応づける構成も作れます。
プロセスが参照するcgroup階層の起点を分けます。内側からは、割り当てられた位置をcgroup階層のルートとして扱えます。
起動後の経過時間などに使うmonotonic clockとboottime clockの値へ、namespaceごとのずれを設定できます。
新しいnamespaceに属する子プロセスを作る、自分用のnamespaceを作る、既存のnamespaceへ所属を切り替える、といった操作をシステムコールで行います。
clone() / clone3()unshare()setns()/proc/PID/ns/PID namespaceとtime namespaceをunshare()で作る場合、呼び出したプロセス自身ではなく、その後に作る子プロセスが新しいnamespaceへ入ります。
CPU時間やメモリは、同じコンピューター上のプロセスが共有します。ある処理が大量に使うと、ほかの処理へ割り当てられる量が減ります。そこでLinuxは、複数のプロセスをグループにまとめ、使用量を測り、配分や上限を設定できます。このグループをcgroupcgroupプロセス群ごとに、CPU時間、メモリ、I/Oの使用量や優先度を管理する仕組み。と呼びます。
cgroupは親子関係を持つ階層として作ります。親に設定した制約は、その下のグループにも及びます。cgroup v2では、一つの階層でCPU、メモリ、I/Oなどをまとめて管理します。
この下へ、管理したい単位ごとのグループを作ります。
PID 8421PID 8427cpu.weight = 200memory.max = 1 GiBPID 9104PID 9118cpu.weight = 50memory.max = 512 MiBLinuxは、cgroupの階層と設定項目をファイルシステムとして公開します。グループのディレクトリを作り、cgroup.procsへPIDを書いてプロセスを所属させ、cpu.weightやmemory.maxなどのファイルを通して値を設定します。
CPUやメモリなど、管理する対象ごとに使用量を測り、設定を適用するカーネル側の機能をcontrollerと呼びます。cgroup v2の主なcontrollerには、次のものがあります。
処理が競合したときの相対的な配分をcpu.weightで指定し、一定期間に使えるCPU時間の上限をcpu.maxで指定します。
現在の使用量をmemory.currentで確認します。memory.highは使用量を抑える目安、memory.maxは超えられない上限として働きます。
ブロックデバイスへの読み書きについて、io.weightで相対的な配分を、io.maxで帯域や回数の上限を設定します。
現在のプロセス数をpids.currentで確認し、pids.maxで上限を設定します。プロセスを無制限に増やす動作を抑えられます。
プロセス群を実行できるCPUと、メモリを割り当てられるNUMA nodeの範囲を指定します。
controllerごとに異なる階層を組める設計です。構成の自由度が高い一方、controller間でプロセスのグループ分けが一致しない構成も作れました。
統合階層が正式に導入されました。v2で使えるcontrollerはその後も増え、cpuは4.15、cpusetは5.0、プロセス群を一時停止するfreezerは5.2、hugetlbは5.6で加わりました。
アプリを実行するには、実行ファイルとライブラリ、設定ファイルなどが並んだディレクトリツリーも必要です。コンテナを起動するソフトウェアは、これらのファイルからプロセスの/として使うディレクトリツリーを用意します。このツリーをrootファイルシステム、略してrootfsと呼びます。複数のnamespaceとcgroupを設定した後、このrootfsにある実行ファイルからアプリのプロセスを起動します。
/var/lib/containers/webへ置く構成を例にしています。/bin /lib /etc/app/server/tmp/cachepivot_root()などで、このツリーをプロセス側の/にするexecve("/app/server", ...)アプリのプロセスLinuxは、rootディレクトリから枝分かれする一つのディレクトリツリーとしてファイルを扱います。
コンテナコンテナ同じカーネルを共有しながら、プロセスを分離して動かす環境。ごとに、プロセスから見える値と、cgroupが記録する使用量や設定値を切り替えて確認できます。
このプロセス群に設定されたPID、ネットワーク、マウントの各namespaceを通して見える値です。
cpu.max = 200000 100000memory.max = 512 MiB現在の使用量を測る値と、配分や上限を決める設定値は別々に管理されます。
コンテナの権限を狭める場合は、capability、seccomp、LSMなども同じプロセス群へ設定します。これらはアクセス制御とカーネル防御で扱います。PID namespace内のPID 1が担う終了状態の回収とシグナル処理は、systemdとPID 1で説明します。