コンテナとカーネルの関係性について勉強したので、cgroup v2を使って自前でリソース制限をかけたコンテナを動かしてみる
こんにちは。SaaSの会社でSREをやっている@yashirookです。
はじめに
本業ではKubernetesクラスタを運用しており、Kubernetesの背後で動作しているLinux Kernelの要素技術について理解を深めたいと考え、以下のコンテンツから学んでいました。
- 書籍 「コンテナセキュリティ コンテナ化されたアプリケーションを保護する要素技術」, Liz Rice著, 株式会社インプレス
- ブログ記事「コンテナ技術入門 - 仮想化との違いを知り、要素技術を触って学ぼう」, @hayajoさん
いずれのコンテンツも、コンテナを実現するための要素技術(cgroup, namespace, capability, chroot, overlayfsなど)を使って、実際に手を動かしながらコンテナを動かしていくことができます。
上記のコンテンツを通して、コンテナは各種の制限が加えられているだけで、実体はホスト上のプロセスであるということを強く実感しました。
コンテナ型仮想化というとカーネルを共有して軽量プロセスを動かす仮想化方式であることを認識されている方は多いと思いますが、一度手で動かしてみると腹落ち感がかなり増しました。興味を持たれた方は上記のコンテンツを参考にぜひ試してみてください。実際に手を動かして試すことを強くお勧めします。
本記事で書くこと
本記事では、上記のコンテンツに関連して追加で、cgroup v2を使ってコンテナにリソース制限をかけるのを試してみたので、その内容をアウトプットしてみます。
cgroup v2について
cgroup v2に関する背景
cgroup(Control Group)はプロセスに対してリソース(CPU, Memory, IOなど)の使用制限をかけるしくみで、カーネルにより提供されています。
Kubernetesにおいては、Podが使用できるCPUやメモリリソースを Requests/Limits
の制限をかけることがベストプラクティスとされており、この制限はcgroupの機能を利用して実現されています。
Kubernetesの登場時はcgroup v1が利用されていましたが、cgroupのアーキテクチャやリソース管理のためのcgroup controllerの振る舞いを改善するため、cgroup v2が登場しています。cgroup v2の登場の背景や変更点などは、カーネルの公式文書を参照するといいと思います。
Kubernetesにおいては、1.25のリリースで、cgroup v2はGAとなりました。また、2024年8月時点で最新版のKubernetes v1.31では、cgroup v1はメンテナンスモードへの移行が案内されており、現時点ではcgroup v2の振る舞いを理解しておくことは有用だと考えます。
冒頭に紹介したコンテンツはcgroup v1をベースに記載されているので、cgroup v2でコンテナを動かしてみようと思います。
cgroup v2 でコンテナを動かしてみる
動作環境
VagrantとVirtualBoxを使って仮想マシン(Ubuntu 22.04 LTS)を立ち上げ、環境を構成しています。
- ホストマシン
- Mac OS: 13.6.7(22G720)
- CPU: 2.3 GHz デュアルコアIntel Core i5
- Vagrant: 2.4.1
- VirtualBox: 7.0.18 r162988 (Qt5.15.2)
- 仮想マシン
- OS: Ubuntu 22.04.4 LTS
- カーネル: 5.15.0-118-generic
- Docker: Docker Engine - Community 27.1.1
Vagrantfileの内容は拡大
# -*- mode: ruby -*-
# vi: set ft=ruby :
Vagrant.configure("2") do |config|
config.vm.box = "ubuntu/jammy64"
config.vm.provider "virtualbox" do |vb|
vb.memory = "1024"
vb.cpus = 2
end
config.vm.provision "shell", inline: <<-SHELL
apt-get update
apt-get install apt-transport-https ca-certificates curl software-properties-common jq
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
apt-get update
apt-get install -y docker-ce
apt-get install -y cgdb
apt-get install -y cgroup-tools
apt-get install -y make gcc
SHELL
end
適当なディレクトリに上記のVagrantfileを配置し、ディレクトリを移動した上で以下を実行して仮想マシンの立ち上げと、シェルへの接続を行います。
$ vagrant up
$ vagrant ssh
vagrant@ubuntu-jammy:~$ whoami
vagrant
以降、以下の手順で検証を進めていきます。
- コンテナが利用するルートファイルシステムの準備
- cgroupの作成
- コンテナの起動と検証
- cgroupの値の変更
- コンテナの再起動とリソース
ルートファイルシステムの準備
まず、コンテナを起動するためのルートファイルシステムをbashイメージから取得します。ルートファイルシステムのコピーをとってから、コンテナは削除しておきます。
vagrant@ubuntu-jammy:~$ ROOTFS=$(mktemp -d)
vagrant@ubuntu-jammy:~$ CID=$(sudo docker container create bash)
vagrant@ubuntu-jammy:~$ sudo docker container export $CID | tar -x -C $ROOTFS
vagrant@ubuntu-jammy:~$ ln -s /usr/local/bin/bash $ROOTFS/bin/bash
vagrant@ubuntu-jammy:~$ sudo docker container rm $CID
e3088b16f1c7f0a09be37d5c2771236588ee48fc3256093617e5dacaf8262959
cgroupをつくる
仮想マシンにおけるcgroupのバージョンを確認します。
vagrant@ubuntu-jammy:~$ stat -fc %T /sys/fs/cgroup/
cgroup2fs
cgroup2fs
になっていれば、group v2が使われています。
次に、コンテナプロセスに割り当てるためのcgroupを手動でつくります。
vagrant@ubuntu-jammy:~$ sudo cgcreate -t vagrant:vagrant -a vagrant:vagrant -g cpu,memory:test-cgroupv2
上記のコマンドは、test-cgroupv2
という名前のcgroupを作成しています。-gオプションで有効化するコントローラーとcgroupのパスを指定します。ここで、パスのrootは /sys/fs/cgroup
になります。
また、-a, -gはcgroupのサブシステムやタスクにアクセスできるuid, gidを指定しています。
それでは、cgroupが作られたことを/sys/fs/cgroup/test-cgroupv2/
を見て確認します。
vagrant@ubuntu-jammy:~$ ls /sys/fs/cgroup/test-cgroupv2/
cgroup.controllers
# 中略
cpu.max
# 中略
memory.max
# 中略
pids.current
# 以下略
リソースアクセスの制限や、プロセスに関する統計情報を取得するための各種ファイルが作成されています。一度CPU, メモリの上限値を定義するファイル(cpu.max
, memory.max
)をそれぞれ確認してみましょう。
vagrant@ubuntu-jammy:~$ cat /sys/fs/cgroup/test-cgroupv2/cpu.max
max 100000
vagrant@ubuntu-jammy:~$ cat /sys/fs/cgroup/test-cgroupv2/memory.max
max
ここで、max
は最大値が明示的に指定されていないことを示しています。また、cpu.max
に関しては、(制限値) (期間)がマイクロ秒単位で記載されています。
コンテナの起動
以下のコマンドを実行し、コンテナを起動します。
vagrant@ubuntu-jammy:~$ sudo mount -t proc proc $ROOTFS/proc
vagrant@ubuntu-jammy:~$ sudo cgexec -g cpu,memory:test-cgroupv2 unshare -mupf --mount-proc=$ROOTFS/proc chroot $ROOTFS /bin/sh
cgexec
コマンドを使うと、cgroupを指定してコマンドを実行できます。コマンドにはunshare
コマンドを渡しており、このコマンドによりnamespaceを制限したプロセスを実行します。
細かいオプションはmanコマンドで確認できますが、今回はmount, uts, pid namespaceを新規に作成し、プロセスに割り当てています。
シェルが立ち上がるので、プロセスを確認してみると、ホストの仮想マシンとは異なる名前空間でプロセスが動作していることが確認できます。
/ # ps aux
PID USER TIME COMMAND
1 root 0:00 /bin/sh
21 root 0:00 ps aux
それでは、コンテナの中でCPUを使ってみましょう。yesコマンドを実行します。
/ # yes > /dev/null
別ターミナルで仮想マシンからプロセスIDを取得し、CPUの使用状況を確認します。
vagrant@ubuntu-jammy:~$ ps -C yes
PID TTY TIME CMD
1844 pts/1 00:01:06 yes
vagrant@ubuntu-jammy:~$ top -p 1844
Tasks: 1 total, 1 running, 0 sleeping, 0 stopped,
%Cpu(s): 46.7 us, 6.7 sy, 0.0 ni, 46.7 id, 0.0 wa, 0.0
MiB Mem : 957.3 total, 72.1 free, 172.0 used,
MiB Swap: 0.0 total, 0.0 free, 0.0 used.
PID USER PR NI VIRT RES SHR S %CPU
1871 root 20 0 1612 4 0 R 100.0
yesプロセスはCPUを100%利用していることがわかります。確認が終わったら、yesプロセスを終了しておきます。
vagrant@ubuntu-jammy:~$ sudo kill -9 1844
割り当てリソースの変更
それでは、これらのファイルの値を変更し、リソースの使用に制限を加えてみましょう。
それぞれの設定値は、cgset
コマンドを実行するか、ファイルに直接制限値を書き込むことで設定できます。今回は前者で実行してみます。
# CPUを20%に制限
vagrant@ubuntu-jammy:~$ cgset -r cpu.max='200000 1000000' test-cgroupv2
# 確認
vagrant@ubuntu-jammy:~$ cat /sys/fs/cgroup/test-cgroupv2/cpu.max
200000 1000000
# メモリを128MBに制限
vagrant@ubuntu-jammy:~$ cgset -r memory.max=128M test-cgroupv2
# 確認(単位はbyte)
vagrant@ubuntu-jammy:~$ cat /sys/fs/cgroup/test-cgroupv2/memory.max
134217728
それぞれ設定値が変更されていることが確認できました。この状態で再度コンテナの起動を行い、yesコマンドを実行したあと、プロセスのCPU使用率を確認してみます。
# コンテナの起動、yesコマンドの実行
vagrant@ubuntu-jammy:~$ sudo cgexec -g cpu,memory:test-cgroupv2 unshare -mupf --mount-proc=$ROOTFS/proc chroot $ROOTFS /bin/sh
/ # yes > /dev/null
#別ターミナルで実行
vagrant@ubuntu-jammy:~$ ps -C yes
PID TTY TIME CMD
1990 pts/1 00:00:23 yes
vagrant@ubuntu-jammy:~$ top -p 1990
top - 07:16:11 up 1:22, 3 users, load average: 0.00, 0.0
Tasks: 1 total, 1 running, 0 sleeping, 0 stopped,
%Cpu(s): 8.4 us, 1.9 sy, 0.0 ni, 89.0 id, 0.5 wa, 0.0
MiB Mem : 957.3 total, 450.1 free, 176.4 used,
MiB Swap: 0.0 total, 0.0 free, 0.0 used.
PID USER PR NI VIRT RES SHR S %CPU
1990 root 20 0 1612 4 0 R 20.0
先ほどとは異なり、CPUの使用率が20%で飽和していることが確認できました。cgroupで設定した設定値が、実際のコンテナプロセスのリソースを制限していることがわかりますね。
今回検証は省きますが、メモリについても同様に制限を加えています。
Kubernetesとcgroup v2
公式ドキュメントによると、cgroup v2を利用することで以下が利点があるとされています。
・統合された単一階層設計のAPI
・より安全なコンテナへのサブツリーの移譲
・Pressure Stall Informationなどの新機能
・強化されたリソース割り当て管理と複数リソース間の隔離
・異なるタイプのメモリー割り当ての統一(ネットワークメモリー、カーネルメモリーなど)
・ページキャッシュの書き戻しといった、非即時のリソース変更
cgroup v1で動かしている場合、Requests/Limitsは以下のように利用されていました。
- Requests
- Podのスケジュール先の決定
- 実際にcgroupが確保するメモリの量を保証するものではない
- Limits
- コンテナのメモリ使用量の上限の決定
- 上限量を超えた場合、カーネルのOOM Killerによってプロセスが終了される
Requestsで設定したメモリ量が保証されないことや、Limitsで指定した値を超えてしまうとOOM Killerによりプロセスが強制終了になってしまい、さまざまなアプリケーションのメモリ管理の要求に対応する柔軟性はありませんでした。
cgroup v2を利用することで、Memory QoSfeature gateを有効化できます。Kubernetes v1.22でα機能として登場し、v1.31時点でもまだαの機能です。
Memory QoSを利用することで、Requests/Limitsは以下の役割を提供することになります。
- Requests
- Podのスケジュール先の決定
- cgroupの
memory.min
にマッピングされる- cgroupが最低限確保するメモリの量を保証する
- Limits
- コンテナのメモリ使用量の上限の決定(
memory.max
)- 上限量を超えた場合、カーネルのOOM Killerによってプロセスが終了される
- この値にthrottling factor(デフォルトは0.9)をかけて、
memory.high
を設定する- 一定の閾値を超えた場合に、スロットリングすることができる
- コンテナのメモリ使用量の上限の決定(
cgroup v2になってmemory.high
やmemory.min
のインターフェースが登場したことで、メモリ管理における柔軟性が高まっていることがわかりました。
詳しく知りたい方は、公式ブログのコンテンツが詳しいので確認してみてください。
まとめ
本記事では、Kubernetesの文脈におけるcgroup v2の背景を紹介したあと、実際にcgroup v2でリソースに制限をかけたコンテナを動かしてみることで、マニフェストで抽象化されている裏で動いている機能を体験しました。
また、KubernetesにおいてはMemory QoSという機能を有効化することで、メモリ管理の柔軟性が高められることを少し紹介しました。Kubernetes v1.31時点ではαの機能ですが、昇格していくのが楽しみな機能で、これからウォッチしたいと感じました。
cgroup v2以外にも、Kubernetesにおけるuser namespaceの利用や、AppArmor(Kubernetes v1.31でGAになりました🎉)などのカーネルが提供する機能との接続が改善されてきており、こちらも動かしてみたいなと思います。
改めてですが、カーネルの機能を利用して自分でコンテナに相当するプロセスを動かしてみて、自分が普段入れているマニフェストの裏側を感じられ、非常に勉強になりました。
インプットに使って、本記事における検証のベースにさせていただいている記事を再掲しておきます。学びのあるコンテンツを提供してくださり、この場を借りて感謝を表明させていただきたいと思います。ありがとうございました。
- 書籍 「コンテナセキュリティ コンテナ化されたアプリケーションを保護する要素技術」, Liz Rice著, 株式会社インプレス
- ブログ記事「コンテナ技術入門 - 仮想化との違いを知り、要素技術を触って学ぼう」, @hayajoさん
Discussion