はじめに
Kubernetes v1.24ではdockershimが削除されます。そのため、このタイミングでContainer RuntimeをDockerから別のものへ切り替える人もいるのではないでしょうか。
Container RuntimeをDockerからcontainerdへ変更した際に、私達のチームではnode-exporterで取得するPVC/PVのファイルシステムのサイズが正しく取得できない事象が発生しました。
そこで、本ドキュメントでは、本事象の原因調査し対応した事例をベースに、Mount Propagationの挙動を紹介します。
なお、本事象はprometheus/node_exporterのIssue #474で修正済みのため、本IssueがFixされる前のnode-exporterのManifestを利用している場合や設定値のアップデートで漏れていた場合などで発生します。prometheus-community/helm-chartsではこの修正コミット、prometheus-operatorではPR #1806にて修正済みです。
発生事象
Dockerからcontainerdへ切り替えた際、ルートディレクトリ(/
)のMount Propagationの挙動で注意が必要です。
例えば、Podにてnode-exporterを実行しており、ルートディレクトリ(/
)をマウントする際、以下のManifest(node-exporterのPodのManifest(一部抜粋))のようにcontainers.volumeMounts.mountPropagation
の指定を省略している場合、
...
containers:
- name: node-exporter
image: prom/node-exporter:v1.3.1
...
args:
...
- --path.rootfs
- "/rootfs"
...
volumeMounts:
- mountPath: /rootfs
name: root-volume
readOnly: true
...
volumes:
- name: root-volume
hostPath:
path: /
...
PVC/PVでマウントした外部ストレージのボリューム上のファイルシステムのメトリクス(e.g. node_filesystem_size_bytes
など)の値が間違った値となります。
(例: 間違ったメトリクスの値)
- ノードのルートディレクトリに30Giのディスクを割り当て
- 同じノード上のPodにPVC/PVで50Giのボリュームを割り当て
上記環境において、PVC/PVのボリュームのサイズについて、上述のManifestのnode-exporterが返すnode_filesystem_size_bytes
を確認すると以下の値がそれぞれ返ってきます。
- Docker: 50Gi
- containerd: 30Gi
Dockerは正しくPVC/PVのボリュームの値が取得できますが、containerdは間違った値(ルートディレクトリの値)となります。
※ 説明のため簡略化しサイズを示していますが、メトリクスの正確な値はGiをBytesに変換且つFileSystemの管理情報サイズなどが引かれた値となります
対象方法
node-exporterの場合、ルートディレクトリ(/
)のマウントオプションにmountPropagation: HostToContainer
を追加すると正しいメトリクスが取得できます。
@@ -156,6 +156,7 @@
name: sys-volume
readOnly: true
- mountPath: /rootfs
+ mountPropagation: HostToContainer
name: root-volume
readOnly: true
- mountPath: /var/run/dbus/system_bus_socket
なぜ、このような現象が発生するのか
本現象は、containerdに限らず、Docker以外のContainer Runtimeで発生する可能性があります。
この現象の解説では、PVC/PVを使いコンテナにボリュームをマウントする際、どのようにマウントされているのかの構成の理解と、マウントにおけるbindマウントのMount Propagationの理解が前提知識として必要となります。
コンテナにどのようにPVC/PVで作られたボリュームがマウントされているのかの構成については「Kubernetesにおけるストレージ関連のメトリクス一覧 : PVのマウントと監視範囲」をご参照ください。
Mount Propargationについては、以下に簡単な説明を行います。
(前提知識) Mount Propagation とは
KubernetesにおけるMount propergationとは、コンテナによってマウントされたボリュームを、同じPod内の他コンテナまたは同じノード上の他Podにマウント情報を共有できる機能です。
Kubernetesでは、Manifestのcontainers.volumeMounts
の mountPropagation
フィールドにて設定できます。
Linux KernelではShared Subtreesとも呼ばれています。
ここでは、Kubernetesにて指定できるオプションのうち、良く利用するであろうデフォルトのmountPropagation: None
(Linuxではprivate
オプション)とmountPropagation: HostToContainer
(Linuxではrslave
オプション)について、その挙動の違いをUbuntu20.04上で動作させつつ解説します。
まず、事前に/mnt
ディレクトリ以下に、以下のディレクトリ/ファイルを準備します。
/mnt# tree
.
├── bar
│ ├── d
│ ├── e
│ └── f
├── baz
├── foo
│ ├── a
│ ├── b
│ └── c
└── qux
次に、/mnt/foo
を/mnt/baz
,/mnt/qux
にbindマウントします。
/mnt# mount --bind /mnt/foo /mnt/baz
/mnt# mount --bind /mnt/foo /mnt/qux
/mnt# tree
.
├── bar
│ ├── d
│ ├── e
│ └── f
├── baz
│ ├── a
│ ├── b
│ └── c
├── foo
│ ├── a
│ ├── b
│ └── c
└── qux
├── a
├── b
└── c
ここまでは、bindマウントの基本的な動作になります。
この時のPropagationを見てみます。
/mnt# findmnt -o TARGET,PROPAGATION |grep /mnt
├─/mnt/baz shared
├─/mnt/qux shared
shared
となっています。このモードは、全てのマウント情報が伝播されるモードです。
shared/rsharedとKubernetesでの注意点
Kubernetesでは、mountPropagation: Bidirectional
を指定した場合、上記で説明したshared
時のマウント情報の伝播をサブツリーまで再帰的に伝播させるオプションであるrshared
の動きとなります。
ただし、このモードはbindマウントしたディレクトリ(コンテナからアクセスできるディレクトリ)からホストの元のディレクトリへ情報が双方向に伝播(Propagtion)されるため、ホスト(ノード)を破壊する恐れがあります。そのため、利用する際は十分に気をつける必要があります。
Kubernetesの公式ドキュメントでも以下の注意書きが記載されています。
Warning: Bidirectional mount propagation can be dangerous. It can
damage the host operating system and therefore it is allowed only in
privileged containers. Familiarity with Linux kernel behavior is strongly
recommended. In addition, any volume mounts created by containers in pods
must be destroyed (unmounted) by the containers on termination.
次に、private
とrslave
の動作を確認します。
/mnt/baz
をprivate
に、/mnt/qux
をrslave
(slave
を再帰的にサブツリーに伝播するオプション)に設定し、マウント情報の伝播について確認します。
/mnt# mount --make-private /mnt/baz
/mnt# mount --make-rslave /mnt/qux
/mnt# findmnt -o TARGET,PROPAGATION |grep /mnt
├─/mnt/baz private
├─/mnt/qux private,slave
# /mnt/fooの配下にbarディレクトリを作成しbindマウントします
/mnt# mkdir /mnt/foo/bar
/mnt# tree
.
├── bar
│ ├── d
│ ├── e
│ └── f
├── baz
│ ├── a
│ ├── b
│ ├── bar
│ └── c
├── foo
│ ├── a
│ ├── b
│ ├── bar
│ └── c
└── qux
├── a
├── b
├── bar
└── c
# /mnt/bar を /mnt/foo/bar へbindマウントし /mnt/baz (private), /mnt/qux (rslave) へのマウント情報の伝播を確認します
/mnt# mount --bind /mnt/bar /mnt/foo/bar
/mnt# tree
.
├── bar
│ ├── d
│ ├── e
│ └── f
├── baz
│ ├── a
│ ├── b
│ ├── bar
│ └── c
├── foo
│ ├── a
│ ├── b
│ ├── bar
│ │ ├── d
│ │ ├── e
│ │ └── f
│ └── c
└── qux
├── a
├── b
├── bar
│ ├── d
│ ├── e
│ └── f
└── r
このようにprivate
オプションの/mnt/baz
ではbarディレクトリのマウント情報が一切伝達されず、rslave
オプションの/mnt/qux
ではbarディレクトリのマウント情報が伝播されます。
Dockerとcontainerdでのnode-exporterの挙動の違い
Kubernetesでは、mountPropagation
を省略した場合、デフォルト値として mountPropagation: None
(private
オプション)となります。
上記のMount Propagationの説明より、containerdではnode-exporterのようなPodにてルートディレクトリをマウントする際、rslave
オプション(mountPropagation: HostToContainer
)の付与が必要になります。
もし、今回の事例のようにprivate
オプションとしてしまった場合、node-exporterでファイルシステムの情報を取得しているSystem Callのstatfsからは、新たにbindマウントされたマウントの情報(PVでマウントされるボリュームの情報)が伝播されずルートディレクト自身の情報が返ってしまうためです。
一方、Dockerでは後方互換の維持のためルートディレクトリをマウントする際、自動的にrslave
オプションの指定となるスペシャルな実装が入っています。
そのため、DockerをContainer Runtimeとして利用している場合は、node-exporterでルートディレクトリをマウントする際、PodにてmountPropagation
を省略してもDocker内ではrslave
オプションでマウントされるため、問題が発生しません。
感想
今回、mountPropagation
の設定の有無で、node-exporterのようにルートディレクトリをマウントする場合においては、Dockerとcontainerdで挙動に違いが出る点を紹介しました。
私達のチームではcontainerdしか試していませんが、本現象はDocker以外のContainer Runtimeでも発生する問題と推察します。
また、node-exporter以外でもルートディレクトリをマウントしているようなPodの場合でも発生する可能性があります。
もし、bindマウントを行なっているKubernetesのPVC/PVのような環境で、ファイルシステムの情報が正しくないと思った際に疑ってみるポイントの一つですので、Mount Propagationというものがあるという知識はあると良いかもしれません。
また、本ドキュメントでは、必要な箇所に限定しMount Propagationの説明を簡単に紹介していますが、このMount Propagationは他にもオプションがあり奥が深いものの一つです。
興味のある方はLinux KernelのShared Subtreesを読むと良いでしょう。