Blog
本記事は、PFNのインターンシップを経て現在はアルバイトとして勤務されている松本直樹さんによる寄稿です。
はじめに
京都大学情報学研究科D1の松本直樹です。インターンでは、「キャッシュを利⽤した機械学習・深層学習ワークロードの加速」のテーマで PFN が運用するオブジェクトストレージや分散キャッシュシステムの利便性向上に取り組みました。このブログでは、今年のインターンシップで開発した、あらゆる FUSE 実装を Kubernetes 上で利用できるようにする CSI ドライバ meta-fuse-csi-plugin について紹介します。また、その CSI ドライバを https://github.com/pfnet-research/meta-fuse-csi-plugin で公開しました。
PFNにおけるストレージ環境
PFN では、NFS サーバー [1] の他、 Apache Ozone によるオブジェクトストレージ [2][3] や、分散キャッシュシステム [4] を社内向けに提供しています。
Apache Ozone [5] は S3 互換な API を提供しています。S3 API は GetObject や ListObjects といった API をもち、オブジェクトストレージにおけるデファクトスタンダードな API となっています。そのため、S3 API 互換な Apache Ozone においても、S3 向けに開発された aws-cli [6]などのユーティリティや aws-sdk [7] のような SDK を用いて開発されたソフトウェアを利用できます。
Apache Ozone をはじめとするオブジェクトストレージや S3 API は、高い性能とスケーラビリティを得るために、通常のファイルシステムとは異なり POSIX API を持ちません。そのため、オブジェクトストレージ上のオブジェクトの一覧を ls で取得したり、less でオブジェクトの内容を閲覧するといった操作は出来ず、S3 API 向けに開発されたユーティリティを利用することになります。S3 API 利用を前提としたソフトウェアだけを利用する環境ならば問題はありませんが、PFN のように、オブジェクトストレージ以外にも NFS のように POSIX API を持つストレージを提供する環境においては、対応コストが高くなってしまいます。
そこで、オブジェクトストレージを通常のファイルシステムと同様に扱えるようにする種々のソフトウェア(例: mountpoint-s3 [8])が開発されています。これらを利用することで、通常のファイルシステムと同じ操作でオブジェクトストレージ上のオブジェクトを扱うことが出来ます。S3 API を持つオブジェクトストレージを擬似的なファイルシステムとしてマウントすることで、ファイルアクセスのインタフェースを POSIX API ないしはそれに近いものにすることができます。下図に示すように、ユーザーは通常のファイルシステムを対象とした実装やよく用いるコマンド類で大容量のオブジェクトストレージにアクセスすることができ、利便性の向上を期待できます。
これらの実装では、FUSE (Filesystem in UserSpace) [9] と呼ばれる Linux カーネルの機能を利用しています。FUSE を利用することで、ユーザーランドで動作するファイルシステムを開発することができます。ユーザーランドで実装が出来るため、C 以外にも Go や Rust といった便利な言語を用いて実装することができます。また、ファイルシステムの実装がクラッシュしたとしてもカーネル全体がクラッシュするといった深刻なトラブルを避けられます。
Kubernetes で FUSE を利用する上での課題
PFN においてはセキュリティ上の観点から、FUSE を利用するために必要な権限(CAP_SYS_ADMIN)が一般ユーザーが利用する Pod には付与されていません。したがって、そのままでは一般ユーザーは自身の Pod 内で FUSE の実装を利用することが出来ません。一方で前節で解説したとおり、PFNではKubernetes上で使えるFUSEの導入が強く望まれてきました。
FUSEはLinux カーネルの機能であり、FUSE を利用するためのインターフェースとして、/dev/fuse というデバイスファイルが存在しています。FUSE で実装されたファイルシステムをマウントするためには、/dev/fuse を open(2) して、カーネル側の FUSE モジュールと通信するための file descriptor (fd) を得るとともに、mount(2) によりそのファイルシステムをどこにマウントするかを指定する必要があります。Kubernetes の Pod 内で FUSE をマウントするためには、/dev/fuse を Pod 内で見えるようにしておくことと、権限として “CAP_SYS_ADMIN” を割り当てる必要があります。 CAP_SYS_ADMIN は /dev/fuse をはじめとするデバイスファイルの操作や、mount(2) のようなシステム全体に影響のある操作を可能にする権限であり、特権に相当する操作を行うことができます。そのため、CAP_SYS_ADMIN を付与することで Pod で FUSE を利用できる一方で、他ユーザーの Pod やシステム自体を操作することが可能になるため、非常に大きなリスクを伴う権限でもあります。
このような問題はクラウド上で提供されるKubernetesサービスにおいても同様に生じています。Google Cloud Platform は Google Cloud Storage (GCS) 向けの FUSE 実装として、gcsfuse [10] を提供しています。Google Kubernetes Engine (GKE) 上の pod で gcsfuse を利用するためには、上述の通りCAP_SYS_ADMIN が割り当てられている必要があります。しかし、一般ユーザーの Pod にそのような高い権限を割り当てることはできる限り避けるべきです。そこで、GKE では一般ユーザーの Pod 内において gcsfuse を利用可能にするために、 専用の Container Interface Storage (CSI) ドライバ [11] として、gcs-fuse-csi-driver [12] を提供しています。gcs-fuse-csi-driver を利用することで、一般ユーザーの Pod に対して CAP_SYS_ADMIN を割り当てることなく gcsfuse を利用することが可能です。
インターンでは、gcs-fuse-csi-driver のように一般ユーザーの Pod 内においても FUSE 実装を利用可能にする CSI ドライバの開発を目標としました。
gcs-fuse-csi-driver の内部
まず、gcsfuse を一般ユーザーの pod 内で走らせることが出来る gcs-fuse-csi-driver について調査しました。gcs-fuse-csi-driver を用いてストレージをマウントするために、以下のような Kubernetes Pod のマニフェスト を記述します。
apiVersion: v1 kind: Pod metadata: name: gcs-fuse-csi-example-ephemeral namespace: NAMESPACE annotations: gke-gcsfuse/volumes: "true" spec: terminationGracePeriodSeconds: 60 containers: - image: busybox name: busybox command: ["sleep"] args: ["infinity"] volumeMounts: - name: gcs-fuse-csi-ephemeral mountPath: /data readOnly: true serviceAccountName: KSA_NAME volumes: - name: gcs-fuse-csi-ephemeral csi: driver: gcsfuse.csi.storage.gke.io readOnly: true volumeAttributes: bucketName: BUCKET_NAME mountOptions: "implicit-dirs"
gcs-fuse-csi-driver を用いる際の マニフェスト
(https://cloud.google.com/kubernetes-engine/docs/how-to/persistent-volumes/cloud-storage-fuse-csi-driver より)
このような マニフェスト を記述することにより、busybox コンテナの /data に gcsfuse のファイルシステムがマウントされます。
内部では、CSI ドライバにより mount の処理が行われています。gcs-fuse-csi-driver では2つのコンポーネントが存在します。一つが、クラスタ管理者が作成し特権で稼働する CSI driver Pod、もう一つが、一般ユーザーが作成し特権無しで稼働する Pod において sidecar コンテナとして動作する sidecar_mounter です。CSI driver Pod は CAP_SYS_ADMIN の権限を含む privileged な Pod であり、/dev/fuse の open(2) や mount(2) の操作を行うことが出来る権限を持ちます。この Pod は DaemonSet として各ノードに配置されています。
一般ユーザー Pod 内の sidecar コンテナでは、sidecar_mounter と gcsfuse が動作しています。この sidecar コンテナは MutatingWebhook により gcs-fuse-csi-driver を利用する Pod 内に作成されます。
gcsfuse がマウントされるまでの流れは以下の通りです。
- Container Orchestration system が csi_driver に対して NodePublishVolume を呼び出す。
- /dev/fuse を open(2) し、FUSE の処理に必要な file descriptor (fd) を得る。
- 得た fd とその他のマウントオプションを指定し、mount(2) を行う。
- sidecar container にマウントされた emptyDir に UNIX Domain Socket (UDS) を作成する。作成した UDS において listen(2) で接続を待ち受けるスレッドを立ち上げて、 NodePublishVolume を完了する。
※ 各ノードで動作する CSI driver Pod には /var/lib/kubelet/pods/ がマウントされているため、同じノードで実行されている他の Pod にマウントされている emptyDir をホスト側のファイルシステム経由で操作することができます。 - sidecar_mounter は自コンテナにマウントされた emptyDir に存在する UDS に connect(2) し、mount(2) が完了した FUSE 処理用の fd を受け取る。
- sidecar_mounter は exec.Cmd の ExtraFiles に受け取った FUSE 処理用の fd を指定(=/dev/fd/3 として渡される)し、gcsfuse を起動する。
※ ExtraFiles に指定した fd は、sidecar_mounter が fork(2) + execve(2) によって gcefuse プロセスを起動する際に継承されます。 - gcsfuse は指定されたパス(/dev/fd/3) の fd を用いてカーネルと FUSE の処理に関する通信を行う。
FUSE では、”/dev/fuse” を open(2) し得た fd に対して read(2)/write(2) することでユーザーランドの FUSE 実装とカーネル内の FUSE モジュールが通信しています。”/dev/fuse” のopen(2) と mount(2) についてのみ CAP_SYS_ADMIN が必要であり、一旦マウント処理が終われば、その後の処理はユーザー権限で行うことが出来ます。gcs-fuse-csi-driver では、CAP_SYS_ADMIN が必要な処理を privileged な CSI driver Pod で行います。その後一般ユーザー Pod 内の sidecar コンテナに FUSE の処理に必要な fd を UDS 経由で渡すことで、一般ユーザー Pod に CAP_SYS_ADMIN がなくとも gcsfuse を動作させることが出来ます。
なお、UDS では SCM_RIGHTS と呼ばれるタイプの補助メッセージを用いることで、プロセス間で fd を受け渡すことができます。この仕組みは、今回のような特権が必要な処理を別のプロセスとして切り出す場合において用いられています。
あらゆる FUSE 実装をKubernetesで利用可能にする CSI ドライバ meta-fuse-csi-plugin
上述のように、gcs-fuse-csi-driver では CAP_SYS_ADMIN が必要な処理のみを privileged な Pod で行い、残りの処理を一般権限で動作するユーザー Pod で行うことで gcsfuse をユーザー Pod 内で動作させていました。しかし、gcs-fuse-csi-driver は gcsfuse のために専用に設計されたものであり、ユーザーが自由に FUSE 実装を切り替えて利用することは出来ません。
そこで、様々な FUSE 実装をユーザー Pod 内で動かすことが出来るようにする CSI ドライバである meta-fuse-csi-plugin を開発しました。meta-fuse-csi-plugin では、出来る限り多くの FUSE 実装を無改変でサポートするために、以下の2つのマウント方法を提供しています。
- fd を直接渡す方式 (fuse-starter)
- fusermount3 を模擬する方式 (fusermount3-proxy)
1. fd を直接渡す方式 (fuse-starter)
この方法では、gcs-fuse-csi-driver と同じアプローチで FUSE 実装を起動します。FUSE のユーザーライブラリである libfuse3 [13] では、マウントポイントの代わりに “/dev/fd/X” を指定すると、X を“/dev/fuse” を open したときの fd として解釈して FUSE の処理を行います。また、gcsfuse 等で用いられている Go 言語向けの FUSE ライブラリ jacobsa/fuse においても、同等の機能を提供しています。
この方式が利用できる場合は、gcs-fuse-csi-driver と同様に、fuse-starter が CSI ドライバから fd を受け取り、FUSE 実装の起動時に渡すことで FUSE 実装をマウントできます。
2. fusermount3 を模擬する方式 (fusermount3-proxy)
Rust の fuser crate など、/dev/fd/X で fd を指定できないライブラリを用いた FUSE 実装では、fuse-starter 方式が利用できません。そこで、fusermount3 周りの仕組みを利用した方法を実装しました。
fusermount3 は、libfuse3 を用いて実装された FUSE 実装を一般ユーザーが特権無しでマウントするために用いられています。特権が必要な “/dev/fuse” の open(2) と mount(2) を setuid された fusermount3 に代わりに行ってもらうことで、FUSE 実装本体は一般ユーザーの権限で実行することが出来ます。
FUSE 実装としては、fusermount3 を実行した結果、マウント処理が終了し、”/dev/fuse” を open(2) した fd を受け取るだけになっています。
この仕組みを利用し、呼び出されると CSI ドライバと通信してマウント処理を行い、UDS 経由で受け取った fd を呼び出し側に渡す fusermount3-proxy を開発しました。fusermount3-proxy は fusermount3 と置き換えて利用されることを想定しています。FUSE 実装は置き換えられた fusermount3-proxy を呼び出すと fusermount3 と同じように fd を受け取ることができるため、その fd を用いて FUSE の処理を行います。
種々の FUSE 実装への対応状況
meta-fuse-csi-plugin では、既に mountpoint-s3, goofys [14], s3fs [15], ros3fs [16], gcsfuse, sshfs [17], といった有名な FUSE 実装を無改変で利用できることを確認しています。
表1 各種FUSE に対する meta-fuse-csi-plugin の動作状況
Does it work? | Mount approach | Works without modifications? | |
mountpoint-s3 (S3) | ✔️ | fusermount3-proxy | ✔️ |
goofys (S3) | ✔️ | fusermount3-proxy | ✔️ |
s3fs (S3) | ✔️ | fusermount3-proxy | ✔️ |
ros3fs (S3) | ✔️ | fuse-starter/fusermount3-proxy | ✔️ |
gcsfuse (GCS) | ✔️ | fusermount3-proxy | ✔️ |
sshfs (SSH) | ✔️ | fuse-starter/fusermount3-proxy | ✔️ |
例: mountpoint-s3 における meta-fuse-csi-plugin の利用
meta-fuse-csi-plugin では、ユーザーが利用したい FUSE 実装を利用できるように、MutatingWebhook 等は用いず、マニフェストにすべて設定を記述します。以下のマニフェストでは、fusermount3-proxy と mountpoint-s3 を用いてバケットをマウントしています。なお、S3 互換のオブジェクトストレージとして MinIO を利用しています。
apiVersion: v1 kind: Pod metadata: name: mfcp-example-proxy-mountpoint-s3 namespace: default spec: terminationGracePeriodSeconds: 10 containers: - name: minio image: quay.io/minio/minio:latest command: ["/bin/bash"] args: ["-c", "minio server /data --console-address :9090"] - name: starter image: ghcr.io/pfnet-research/meta-fuse-csi-plugin/mfcp-example-proxy-mountpoint-s3:latest imagePullPolicy: IfNotPresent command: ["/bin/bash"] args: ["-c", "./configure_minio.sh && mount-s3 test-bucket /tmp --endpoint-url \"http://localhost:9000\" -d --allow-other --auto-unmount --foreground --force-path-style"] # "--auto-unmount" forces mountpoint-s3 to use fusermount3 env: - name: FUSERMOUNT3PROXY_FDPASSING_SOCKPATH # UDS path to connect to csi driver value: "/fusermount3-proxy/fuse-csi-ephemeral.sock" - name: AWS_ACCESS_KEY_ID value: "minioadmin" - name: AWS_SECRET_ACCESS_KEY value: "minioadmin" volumeMounts: - name: fuse-fd-passing # dir for UDS mountPath: /fusermount3-proxy - image: busybox name: busybox command: ["/bin/ash"] args: ["-c", "while [[ ! \"$(/bin/mount | grep fuse)\" ]]; do echo \"waiting for mount\" && sleep 1; done; sleep infinity"] volumeMounts: - name: fuse-csi-ephemeral # mounting volume provided by meta-fuse-csi-plugin mountPath: /data readOnly: true mountPropagation: HostToContainer # propagating host-side mount to container-side volumes: - name: fuse-fd-passing # dir for UDS emptyDir: {} - name: fuse-csi-ephemeral # volume with meta-fuse-csi-plugin csi: driver: meta-fuse-csi-plugin.csi.storage.pfn.io readOnly: true volumeAttributes: fdPassingEmptyDirName: fuse-fd-passing fdPassingSocketName: fuse-csi-ephemeral.sock
fusermount3-proxy により mountpoint-s3 をマウントするマニフェスト
一般ユーザーの Pod 内に、 mountpoint-s3 を含む starter コンテナと、mountpoint-s3 によるファイルシステムがマウントされる busybox コンテナを作成しています。
starter コンテナには、fusermount3-proxy が含まれ、 mountpoint-s3 に対して fusermount3 の利用を強制するために、ワークアラウンドとして “–auto-unmount” を指定しています。環境変数 FUSERMOUNT3PROXY_FDPASSING_SOCKPATH は、fusermount3-proxy が CSI ドライバとの通信に利用する UDS を指定しています。その他にはバケットの利用に必要となるアクセストークンを指定しています。volume としては、CSI ドライバとの通信用の UDS が置かれる empty dir を指定しています。
busybox コンテナでは、mountpoint-s3 のマウント先を指定しています。上述した通り、コンテナ起動後にホスト側で再度 mount(2) が実行されるため、volumeMounts で mountPropagation: HostToContainer を指定しています。
volumes では、CSI ドライバとの通信に用いる UDS を置くための emptyDir と、CSI ドライバでマウントを行う volume を指定しています。volumeAttributes として、fusermount3-proxy との通信用の UDS のパスを emptyDir とソケット名を指定しています。
以上のマニフェストを meta-fuse-csi-plugin がデプロイされている Kubernetes クラスタにデプロイすることで、一般ユーザーが特別な権限無しで、自分が利用したい FUSE 実装を自由にユーザーの Pod 内で利用することが出来ます。
今後の展望
一般ユーザーが利用したい FUSE 実装を自由に利用できる CSI ドライバとして、meta-fuse-csi-plugin を開発しました。すでに複数の FUSE 実装で動作することが確認できていますが、実用に向けて2つの懸念点があります。
1つ目は、セキュリティ上の懸念です。privileged なプロセスが open(2) した fd を一般ユーザーの権限で動くプロセスに渡すことでどういったセキュリティ上のリスクが生じうるのか?という点について十分な議論が必要となります。
2つ目は、ユーザーが用意した FUSE 実装に不具合があった場合や、悪意のある操作を行った場合、他の Pod やクラスタ全体に悪影響が出ないか?という点に関する検証です。meta-fuse-csi-plugin は可能な限りシンプルに設計されていますが、異常系においてどういった副作用があるのか、検証が必要になります。
meta-fuse-csi-plugin により、オブジェクトストレージについては mountpoint-s3 などの既存の実装を用いることで通常のファイルシステムと同様な操作が可能になりました。PFNで内製している分散キャッシュシステムについては独自のAPIを持つため既存の実装を利用できず、あらたな FUSE 実装が必要となります。ユーザーがそのような実装を自前で用意した場合においても、meta-fuse-csi-plugin を用いることで Kubernetes クラスタ内で自由に利用することが期待されます。
インターンの感想・謝辞
meta-fuse-csi-plugin の開発においては、Kubernetes のエコシステムや CSI ドライバについて理解が深まるとともに、実際に Kubernetes クラスタを利用するユーザーの方々からの要望を取り込むことでより実用的な CSI ドライバを開発することが出来ました。また、インターン期間は光の如く過ぎ、非常に充実したものでありました。他のインターン生や社員の方との交流を通して、自分の分野に限らず他分野についても視野を広げることができました。
日々の作業やディスカッションでは、メンターの上野さん、上西さんや、Cluster Services、Storage Team の方々に大変お世話になりました。この場を借りて御礼を申し上げます。
メンターより
松本さんのメンターを務めました、PFNのKubernetesベースの機械学習基盤を開発・運用している上野です。もともとの課題としては、gcs-fuse-csi-plugin で実現されている FUSE の Kubernetes 上での利用を、mountpoint-s3 など他のFUSE実装でも使えるようにするため、どういう仕組みなのかを調査していただく、というものでした。CSI Plugin と既存の FUSE 実装の間のよいインターフェースを考えるのは難しかったと思いますが、松本さんは gcs-fuse-csi-plugin で使われている fuse-starter 方式の他に、本稿では fusermount3-proxy とよんでいる方式を新たに実装することで様々なFUSE実装を無変更で利用することができるようになりました。
PFNでは、機械学習/深層学習そのものの研究だけでなく、分散キャッシュシステムやオブジェクトストレージシステムなどそれらを支えるクラスタの研究開発、構築、運用も行っています。もしご興味がある方、我こそはという方がいらっしゃいましたら、ご連絡お待ちしています。
参考文献
[1]: “2022年のPFNの機械学習基盤 – Preferred Networks Research & Development”, https://tech.preferred.jp/ja/blog/ml-kubernetes-cluster-2022/
[2]: “Apache Ozoneをやっていた一年 – Preferred Networks Research & Development”, https://tech.preferred.jp/ja/blog/apache-ozone-year/
[3]: “続・Apache Ozone をやっていた一年 – Preferred Networks Research & Development”, https://tech.preferred.jp/ja/blog/apache-ozone-two-years/
[4]: “深層学習のための分散キャッシュシステム – Preferred Networks Research & Development”, https://tech.preferred.jp/ja/blog/distributed-cache-for-deep-learning/
[5]: “Apache Ozone”, https://ozone.apache.org/
[6]: “GitHub – aws/aws-cli: Universal Command Line Interface for Amazon Web Services”, https://github.com/aws/aws-cli
[7]: “SDK とは何ですか? – SDK の説明 – AWS”, https://aws.amazon.com/jp/what-is/sdk/
[8]: “GitHub – awslabs/mountpoint-s3: A simple, high-throughput file client for mounting an Amazon S3 bucket as a local file system.”, https://github.com/awslabs/mountpoint-s3
[9]: “FUSE — The Linux Kernel documentation”, https://www.kernel.org/doc/html/next/filesystems/fuse.html
[10]: “GitHub – GoogleCloudPlatform/gcsfuse: A user-space file system for interacting with Google Cloud Storage”, https://github.com/GoogleCloudPlatform/gcsfuse
[11]: “Kubernetes CSI Developer Documentation”, https://kubernetes-csi.github.io/docs/
[12]: “Google Cloud Storage FUSE CSI Driver”, https://github.com/GoogleCloudPlatform/gcs-fuse-csi-driver
[13]: “GitHub – libfuse/libfuse: The reference implementation of the Linux FUSE (Filesystem in Userspace) interface”, https://github.com/libfuse/libfuse
[14]: “GitHub – kahing/goofys: a high-performance, POSIX-ish Amazon S3 file system written in Go”, https://github.com/kahing/goofys
[15]: “GitHub – s3fs-fuse/s3fs-fuse: FUSE-based file system backed by Amazon S3”, https://github.com/s3fs-fuse/s3fs-fuse
[16]: “GitHub – akawashiro/ros3fs: ros3fs is a Linux FUSE adapter for AWS S3 and S3 compatible object storages”, https://github.com/akawashiro/ros3fs
[17]: “GitHub – libfuse/sshfs: A network filesystem client to connect to SSH servers”, https://github.com/libfuse/sshfs
Area