隔離の境界はどこにあるのか
一言でいうと
VMはハードウェアの境界で分離し、コンテナはカーネルの機能(namespace/cgroup)で分離します。この一文から、残りの違いがすべて導かれます。
なぜ必要なのか
「コンテナを使うか、VMを使うか」は、今でもよく出る質問です。答えは、ワークロードではなく、どの種類の分離が必要かで決まります。
どう動くのか
[VM] [컨테이너]
앱 앱
게스트 라이브러리 라이브러리
게스트 커널 <- 별개! (호스트 커널을 공유)
가상 하드웨어 namespace + cgroup
하이퍼바이저 컨테이너 런타임
호스트 커널 호스트 커널
このコードブロックの韓国語は、VMの列が、上から順に、アプリ、ゲストのライブラリ、ゲストカーネル(別個のもの)、仮想ハードウェア、ハイパーバイザー、ホストカーネルを指し、コンテナの列が、アプリ、ライブラリ、ホストカーネルの共有、namespaceとcgroup、コンテナランタイム、ホストカーネルを指す、という意味です。
コンテナには、ゲストカーネルがありません。そのため、起動が速く(数十ms)、メモリのオーバーヘッドがほとんどなく、密度が高いです。その代わり、カーネルの脆弱性1つで、分離がまるごと崩れます。
| 項目 | VM | コンテナ |
|---|---|---|
| カーネル | ゲストごとに別個 | ホストと共有 |
| 起動時間 | 数十秒 | 数十ms |
| メモリのオーバーヘッド | ゲストカーネル + ハイパーバイザー | ほとんどなし |
| 密度 | ホストあたり数十 | ホストあたり数百 |
| 分離の強さ | 強い(ハードウェアの境界) | 弱い(カーネルの境界) |
| 別のOS/カーネル | 可能 | 不可能 |
| イメージサイズ | GB | MB |
どのようなときに何を選ぶか
VMを選ぶべき場合
- 互いに信頼しないテナントを1つのホストに載せるとき(マルチテナンシー)
- 別のカーネルバージョンや別のOSが必要なとき(Windows、古いカーネルへの依存)
- カーネルモジュールをロードしたり、カーネルパラメーターを変えたりする必要があるとき
- 規制が、ハードウェアレベルの分離を求めるとき
コンテナを選ぶべき場合
- 同じ組織の信頼できるワークロードを、多く載せるとき
- デプロイの周期が短く、起動時間が重要なとき
- イメージベースのイミュータブルなインフラがほしいとき
その中間: microVM
FirecrackerやKata Containersのようなものが、両者の間を埋めます。VMの分離の境界を維持しながら、起動時間とオーバーヘッドをコンテナに近いところまで減らしたものです。デバイスモデルを極端に減らし(virtioを数個だけ)、起動経路を短くして、100ms前後で立ち上がります。AWS LambdaがFirecrackerの上で動いています。
Kata Containersはさらに一歩進んで、コンテナインターフェース(OCI/CRI)を維持しながら、内部では軽量VMを起動します。Kubernetesの観点ではただのPodですが、実際にはVMの境界で分離されます。RuntimeClassでワークロードごとに選べるので、「このネームスペースだけ強い分離」のようなポリシーが可能です。
現場での姿
「コンテナでカーネルパラメーターを変えたいのに、できません」。当然です。カーネルがホストと共有されているので、コンテナの中でsysctl -wを行うと、ホスト全体に影響します。そのため、ほとんどが塞がれています。本当に必要なら、ノードレベルで設定するか(DaemonSet、ノードのブートストラップ)、VMに行く必要があります。
このラボ環境が、まさにその例です。capabilityが取り除かれたコンテナなので、mount、tcpdump、iptablesが使えません。そのため、このカリキュラムは、その制約を回避する代わりに、制約そのものを教材にしました。どの作業がなぜ特権を要求するのかを知ることが、そのまま分離の構造を理解することです。
密度とリソース管理が異なる理由
VMとコンテナは、リソースの扱い方も異なり、その違いが、運用でよく人を驚かせます。
VMのメモリは、あらかじめ確保されます。4GBを与えたVMは、その中でどれだけ使っても、ホストでその分を占めます(バルーンドライバーのような仕組みで一部を返せますが、即座ではありません)。一方、コンテナのメモリは、実際に使った分だけが確保されます。上限は上限にすぎず、使わなければ、ほかのコンテナが使います。そのため、コンテナ側では、リクエスト量の合計より実際の使用量がずっと小さい状態が正常で、これが密度の源泉です。
その代わり、コンテナはオーバーコミットのリスクを負います。上限の合計がノードのメモリより大きく割り当てられていて、複数が同時に多く使うと、ノードがメモリ圧迫に陥り、カーネルがプロセスを選んで殺します。このとき死ぬのは、最も多く使ったものではなく、リクエスト量に対する超過が大きいものなので、リクエスト量を書いていないワークロードが先に犠牲になります。
CPUは、性格がまた違います。メモリと違って、CPUはあとで取り戻せるので、上限を超えたら殺す代わりに、前に見たスロットリングで遅くします。そのため、CPUの上限は余裕をもって、メモリの上限は正確に設定するのが、一般的な助言になります。
VMの中でコンテナを動かすときは、2つの層が重なります。VMに与えたメモリがノード全体のメモリになり、その中でコンテナたちが再び分け合います。このとき、VM側でメモリを回収する仕組みが有効になっていると、ノードは、自分のメモリが減ったことを知らないままスケジュールを続けて、予測しにくい失敗が起きます。仮想化されたノードでは、メモリ回収機能をオフにするのが一般的な推奨である理由が、これです。
前に見たmicroVMは、この2つの折衷でもあります。分離はVMですが、メモリのオーバーヘッドが小さいので、密度を大きく失わずに、カーネルの境界を得られます。
次にすること
次のコースでrootlessコンテナを扱います。特権なしでもコンテナが動く理由(user namespace)を掘り下げると、このモジュールで述べた「カーネルの境界で分離する」という文が、具体的なシステムコールとファイルに置き換わります。