Windows 2.1.7.0 用の Docker Desktop の最新リリース には、Samba の代わりに FUSE を使用したまったく新しいファイル共有システムがあります。リリース した最初のブログ投稿 では、この新しい実装のパフォーマンスの向上を紹介し、フィードバックを提供する方法について説明します。 ぜひお試しいただき、ご意見をお聞かせください。 次に、新しいアーキテクチャについてさらに洞察を提供するために、詳細について説明します。
新しいアーキテクチャ
Hyper-V 仮想ネットワーク上で実行される Samba の代わりに、新しいシステムは、ハイパーバイザー ソケット上で gRPC 上で実行されているユーザー空間 (FUSE) サーバーのファイルシステムを使用します。
次の図は、PHP ファイルの読み取りなど、コンテナーからの 1 つの要求によって使用されるパスを示しています。
ステップ(1)では、コンテナ内のWebサーバーは、カーネルの仮想ファイルシステム(VFS)レイヤーによって処理されるLinuxシステムコールである「read」を呼び出します。 VFSはモジュール式であり、多くの異なるファイルシステムの実装をサポートしています。 私たちの場合、ユーザースペース(FUSE)のファイルシステムを使用して、「FUSEクライアント」というラベルの付いたVM内で実行されているヘルパープロセスにリクエストを送信します。 このプロセスは、Docker エンジンと同じ名前空間内で実行されます。 FUSEクライアントは一部のリクエストをローカルで処理できますが、ホストファイルシステムにアクセスする必要がある場合は、「ハイパーバイザーソケット」を介してホストに接続します。
ハイパーバイザーソケット
ハイパーバイザーソケットは、VM が相互に、およびホストと通信できるようにする共有メモリ通信メカニズムです。 ハイパーバイザーソケットには、通常の仮想ネットワークを使用するよりも、次のような多くの利点があります。
- トラフィックは仮想イーサネット/IPネットワーク上を流れないため、ファイアウォールポリシーの影響を受けません
- トラフィックはIPのようにルーティングされないため、VPNクライアントによって誤ってルーティングされることはありません
- トラフィックは共有メモリを使用するため、マシンを離れることはできず、第三者がトラフィックを傍受することを心配する必要はありません
Docker Desktop はすでにこれらのソケットを使用して Docker API を転送し、コンテナー ポートを転送しており、Windows でのファイル共有にも使用しています。
図に戻ると、FUSEクライアントはAF_VSOCKアドレスファミリを使用してソケットを作成します(手順(3)を参照)。 カーネルには、ハイパーバイザーごとに 1 つずつ、多数の低レベルトランスポートが含まれています。 基になるハイパーバイザーは Hyper-V であるため、VMBus トランスポートを使用します。 ステップ(4)では、ファイルシステム要求が共有メモリに書き込まれ、WindowsカーネルのVMBus実装によって読み取られます。 Windows で実行されている FUSE サーバーのユーザー空間プロセスは、手順 (5) で AF_HYPERV ソケットを介してファイルシステム要求を読み取ります。
ヒューズサーバー
開く/閉じる/読み取り/書き込みなどの要求は、通常のWindowsプロセスとして実行されているFUSEサーバーによって受信されます。 最後に、手順 (6) で、FUSE サーバーは Windows API を使用して読み取りまたは書き込みを実行し、結果を呼び出し元に返します。
FUSEサーバーは、Dockerアプリを実行しているユーザーとして実行されるため、ユーザーのファイルとフォルダーにのみアクセスできます。 ローカル管理者アカウントを使用して VM にドライブをマウントする場合、以前の設計で発生する可能性があるように、VM が他のファイルにアクセスする可能性はありません。
イベントインジェクション
Linux でファイルが変更されると、カーネルは inotify イベントを生成します。 関心のあるアプリケーションは、これらのイベントを監視し、アクションを実行できます。 たとえば、Reactアプリは
$ npm start
この ビデオに示すように、iNotifyイベントを監視し、コードが変更されると自動的に再コンパイルし、ブラウザーを自動的に更新するようにトリガーします。 以前のバージョンのWindows上のDockerデスクトップでは、inotifyイベントを生成できなかったため、これらの開発スタイルは機能しませんでした。
inotify イベントの挿入は非常に困難です。 通常、FUSEのようなLinux VFS実装はイベント自体を生成しません。代わりに、上位レイヤーの共通コードは、アクションを実行する副作用としてイベントを生成します。 たとえば、VFS の "unlink" が呼び出されて正常に返された場合、"unlink" イベントが生成されます。 では、ユーザーがWindowsで「リンク解除」を呼び出すと、Linuxはそれをどのように知るのでしょうか。
Docker デスクトップは、ユーザーが docker run -v
を実行したときにホスト上のイベントを監視します。 ホストで "リンク解除" イベントを受信すると、"リンク解除を挿入してください" という要求が gRPC 経由で Linux VM に転送されます。 次の図は、一連の操作を示しています。
LinuxのFUSEクライアント内のよく知られたpidを持つスレッドは、ディレクトリが実際にはすでに削除されているにもかかわらず、「unlink」を呼び出してリクエストを「再生」します。 FUSEクライアントは、この既知のpidからのリクエストをインターセプトし、リンク解除がまだ行われていないふりをします。 たとえば、FUSE_GETATTRが呼び出されると、FUSEクライアントは(ENENETではなく)「はい、ディレクトリはまだここにあります」と言います。 FUSE_UNLINKが呼び出されると、FUSEクライアントは(ENOENTの代わりに)「はい、うまくいきました」と言います。 FUSE_UNLINKが成功した結果、Linux カーネルは inotify イベントを生成します。
キャッシング
上のアーキテクチャ図からわかるように、各 I/O 要求を完了するには、複数のユーザー/カーネル遷移と VM/ホスト遷移を行う必要があります。 つまり、ファイル システム操作の待機時間は、すべてのファイルが VM 内でローカルである場合よりもはるかに長くなります。 カーネルキャッシュを積極的に使用することでこれを軽減したため、多くのリクエストを完全に回避できます。 私たち:
- FUSE_GETATTR要求を最小限に抑えるファイル属性キャッシュを使用する
- カーネルページキャッシュにディレクトリの内容をキャッシュするFOPEN_CACHE_DIRを設定します
- ファイルの内容をキャッシュするFOPEN_KEEP_CACHEを設定する
- 最大要求サイズを増やすようにCAP_MAX_PAGESを設定しました
- 最新のFUSEパッチがバックポートされた最新の4.19シリーズカーネルを使用しています
非常に多くのキャッシュを有効にしたため、キャッシュの無効化を慎重に処理する必要があります。 ユーザーが docker run -v を実行し、inotify イベントインジェクションのファイルシステムイベントを監視している場合、これらのイベントを使用してキャッシュエントリを無効にします。 docker run -v が終了し、ウォッチが無効になると、すべてのキャッシュエントリが無効になります。
未来の進化
キャッシュをさらに積極的に使用することで、パフォーマンスをさらに向上させるためのアイデアがたくさんあります。 たとえば、上記の Symfony ベンチマークでは、キャッシュされたケースの残りの FUSE 呼び出しの大部分は、ファイルハンドルを開いたり閉じたりする呼び出しです。ファイルの内容自体はキャッシュされていますが(変更されていません)。 これらのオープンコールとクローズコールを遅延させ、必要な場合にのみ呼び出すことができる場合があります。
新しいファイルシステムの実装は、WSL 2(現在、初期のWindows Insiderビルドで利用可能)には、9Pを使用するネイティブファイル共有モードがすでにあるため、関係ありません。 もちろん、ベンチマーク、最適化、ユーザーからのフィードバックの組み込みを継続して、すべてのOSバージョンで利用可能な最良のファイル共有実装を常に使用します。