VS Code Dev Container の IO 過負荷: executeInWSL 設定の詳細解説と根本原因分析
Windows 上で VS Code の Dev Container 拡張機能を使用してコンテナ開発を行う際、システムが明らかにカクつく現象に遭遇するユーザーがいます。タスクマネージャーでは、Extension Host プロセスの CPU およびディスク読み取り IO が継続的に高止まりしており、ユーザーが何も操作していない状態でも低下しないことが確認できます。本記事では、この問題の現象から出発し、段階的に根本原因を特定して核心的な解決策を見つけるまでの完全なトラブルシューティング過程を記録します。
問題の現象
システムのカクつきは、Dev Container 拡張機能を使用してコンテナに接続した後に発生します。タスクマネージャーを通じて、Extension Host プロセスのディスク読み取り IO と CPU 使用率が継続的に高止まりしていることが観察できます。ユーザーが編集やターミナル操作を一切行っていない場合でも、これらの指標はアイドルレベルまで低下しません。極端な場合、Windows デスクトップ全体の応答速度に影響が及び、マウスカーソルが断続的にカクつく現象が発生します。
トラブルシューティング過程
Process Monitor を使用して IO の発生源を特定
Sysinternals Process Monitor は、この種の問題を調査する際の最初のステップとなるツールです。procmon を起動し、フィルター条件を Process Name is Code.exe または Process Name is Extension Host に設定することで、すべての ReadFile/WriteFile 操作のパスと頻度をリアルタイムで観察できます。フィルター結果では、パスが \\pipe\ で始まる named pipe 操作の頻度が異常に高く、1 秒間に数十回に達することがあります。これらの named pipe 操作は Docker CLI と Docker daemon 間の通信に対応しており、Extension Host が Docker CLI を頻繁に呼び出していることを示しています。
VS Code 組み込みツールを使用して確認
Help > Toggle Developer Tools を通じて Chromium DevTools を開き、Performance パネルで CPU プロファイルを記録すると、Extension Host の時間の大部分が子プロセスの spawn と stdout パイプの読み取りに費やされていることがわかります。Output パネルの「Dev Containers」ログレベルを「trace」に設定すると、完全なコマンド呼び出しシーケンスを確認できます。docker inspect --type container、docker version --format、docker exec、docker ps などのコマンドが繰り返し実行されています。issue #9194 のログデータから、1 回の呼び出しにかかるオーバーヘッドを定量化できます。docker inspect --type container は約 1800ms、docker version は約 620ms を要します。
Windows Performance Analyzer を使用してシステムレベルの IO を分析
より深い分析のために、wpr.exe -start GeneralProfile -filemode を使用して ETW 記録を開始し、問題を再現した後に wpr.exe -stop capture.etl で記録を停止し、Windows Performance Analyzer で結果を読み込みます。Disk I/O ビューは Extension Host がディスク読み取り IO の主要な要因であることを確認し、Process Life Cycle ビューは多数の短命な子プロセスが繰り返し作成および破棄されていることを示しています。これらの子プロセスはまさに Docker CLI の呼び出しインスタンスです。
根本原因分析
Dev Container のプロセス間通信アーキテクチャ
調査結果は、Dev Container のマルチプロセス通信アーキテクチャを指し示しています。ユーザーが Windows 上で Dev Container 拡張機能を使用してコンテナワークスペースを開くと、実際には Windows と Linux の境界を越えるマルチプロセス通信リンクが起動されます。
flowchart LR
subgraph Windows["Windows ホストマシン"]
A["VS Code クライアント<br/>(Electron)"]
B["Extension Host<br/>(Node.js)"]
C["Docker CLI<br/>(子プロセス)"]
end
subgraph WSL2["WSL2 / Docker Desktop"]
D["Docker Daemon<br/>(dockerd)"]
end
subgraph Container["コンテナ内部"]
E["VS Code Server"]
F["Remote Extension Host"]
G["ポートフォワーディングプロセス<br/>(Node.js)"]
end
A e1@--> B
B e2@--> C
C e3@--> D
B e4@--> E
E e5@--> F
F e6@--> G
classDef win fill:#E3F2FD,stroke:#1565C0,stroke-width:1px,color:#0D47A1;
classDef wsl fill:#FFF8E1,stroke:#EF6C00,stroke-width:1px,color:#E65100;
classDef ctr fill:#E8F5E9,stroke:#2E7D32,stroke-width:1px,color:#1B5E20;
classDef animate stroke:#EF6C00,stroke-width:2px,stroke-dasharray: 9\,5,stroke-dashoffset: 900,animation: dash 25s linear infinite;
class A,B,C win;
class D wsl;
class E,F,G ctr;
class e1,e2,e3,e4,e5,e6 animate;ローカルの VS Code クライアント (Electron) は IPC を介してローカルの Extension Host (Node.js) と通信します。Extension Host はコンテナ内の VS Code Server と接続を確立する必要があり、この接続は Docker CLI を仲介として依存しています。コンテナ内の VS Code Server が起動すると、Remote Extension Host が拡張機能の実行を引き継ぎ、ポートフォワーディングプロセスがローカルブラウザからコンテナサービスへのアクセスに TCP トンネルを提供します。このリンクの各段階が IO 増幅の原因となる可能性があります。
Docker CLI のポーリングメカニズム
Dev Containers 拡張機能は、Docker CLI コマンドを頻繁に呼び出すことでコンテナの状態を取得および維持します。コンテナ起動段階では、拡張機能は docker inspect --type container を実行してコンテナメタデータを取得し、docker version --format で Docker daemon の可用性を確認し、docker exec でコンテナ内で環境検出コマンドを実行し、docker ps で実行中のコンテナを一覧表示します。これらのコマンドは起動時に一度だけ実行されるのではなく、コンテナのライフサイクル全体を通じて一定の頻度で繰り返し呼び出されます。
Docker CLI を呼び出すたびに新しい子プロセスが起動され、プロセス作成のオーバーヘッド、stdout パイプの読み取り、JSON 結果の解析など、一連の IO 操作が伴います。デフォルトモードでは、これらの操作のデータはすべて Windows/WSL 境界を越える必要があり、WSL2 の 9P ファイル共有プロトコルは多数の小ファイル IO や高頻度の短接続を処理する際のパフォーマンスが劣ります。Microsoft 公式ドキュメント の推奨事項によれば、ファイルシステム間の操作は可能な限り避けるべきですが、Dev Container のアーキテクチャ設計により、このオーバーヘッドを完全に回避することは困難です。
sequenceDiagram
participant EH as Extension Host
participant CLI as Docker CLI
participant DD as Docker Daemon
EH->>CLI: spawn docker inspect
CLI->>DD: named pipe リクエスト
DD-->>CLI: JSON レスポンス
CLI-->>EH: stdout パイプ出力
EH->>CLI: spawn docker version
CLI->>DD: named pipe リクエスト
DD-->>CLI: バージョン情報
CLI-->>EH: stdout パイプ出力
EH->>CLI: spawn docker exec
CLI->>DD: named pipe リクエスト
DD-->>CLI: 実行結果
CLI-->>EH: stdout パイプ出力ポートフォワーディング接続のリーク
VS Code のポートフォワーディングメカニズムは、フォワーディングポートごとに独立した Node.js 子プロセスを作成します。これらのプロセスは net.createConnection を介してコンテナ内のターゲットポートに接続し、ローカルポートとコンテナポート間で双方向にデータを転送します。問題となるのは、ブラウザや他のクライアントがフォワーディングポートにアクセスして接続を切断した後、クリーンアップロジックがタイムリーに実行されない場合、これらのフォワーディングプロセスが正常に終了せずに存続し続けることです。
microsoft/vscode-remote-release#5767 の分析によると、リークしたポートフォワーディングプロセスはそれぞれ約 26 MiB のメモリを占有します。複数のフォワーディングポートが構成され、頻繁にアクセスされる開発環境では、プロセス数が短時間で通常の 2 個から数十個に増加する可能性があります。以下のコードスニペットはポートフォワーディングプロセスのコアパターンを示しており、client.on('close') イベント処理がプロセスが正常に終了できるかどうかの鍵となります。
const net = require('net');
process.stdin.pause();
const client = net.createConnection({ port: 36187 }, () => {
client.pipe(process.stdout);
process.stdin.pipe(client);
});
client.on('close', function (hadError) {
console.error(hadError ? 'Remote close with error' : 'Remote close');
process.exit(hadError ? 1 : 0);
});
client.on('error', function (err) {
process.stderr.write(err && (err.stack || err.message) || String(err));
});
Docker CLI の docker exec プロセスが異常終了し、コンテナ内の Node.js プロセスが実行中のままの場合、これらの孤児プロセスは正常に回収されず、メモリが継続的に増加します。この問題は VS Code 1.62 バージョン以降で部分的に修正されましたが、特定のネットワーク条件下では依然として再現する可能性があります。注目すべきは、ポートフォワーディングのリークは executeInWSL と直接的な関連がなく、VS Code のポートフォワーディングメカニズム自体のソフトウェア欠陥であることです。
Extension Host の再接続ループ
microsoft/vscode-remote-release#6178 の記録によると、ネットワーク切断やその他の理由でリモートコンテナへの接続が失われた場合、Extension Host の再接続ロジックにバグが存在します。ある async 関数が catch ブロック内で自身を再帰的に呼び出し、CPU が空転します。コールスタックは、この関数が processTicksAndRejections 内で繰り返しループしており、終了条件がないことを示しています。
flowchart TD
A["接続の喪失"] e1@--> B["async 再接続関数"]
B e2@--> C{"接続成功?"}
C e3@-->|はい| D["正常に復帰"]
C e4@-->|いいえ| E["catch ブロック"]
E e5@--> B
classDef start fill:#FFEBEE,stroke:#C62828,stroke-width:1px,color:#B71C1C;
classDef work fill:#E8F5E9,stroke:#2E7D32,stroke-width:1px,color:#1B5E20;
classDef check fill:#FFF8E1,stroke:#EF6C00,stroke-width:1px,color:#E65100;
classDef animate stroke:#EF6C00,stroke-width:2px,stroke-dasharray: 9\,5,stroke-dashoffset: 900,animation: dash 25s linear infinite;
class A start;
class B,D work;
class C,E check;
class e1,e2,e3,e4,e5 animate;同時に、Extension Host のメモリは約 1 MB/分の速度で継続的に増加します。これは、再帰呼び出しのたびに解放されていないコンテキストがコールスタック上に蓄積されるためです。この問題は Remote-Containers 0.221.0-pre-release バージョンで修正されましたが、拡張機能をタイムリーに更新していないユーザーの場合、ネットワーク切断を 1 回シミュレートする(例:WiFi をオフにする)だけでこの問題がトリガーされ、接続切断のダイアログボックスは決して表示されません。これは独立したソフトウェアバグであり、executeInWSL 設定とは無関係ですが、ユーザーが認識するシステムのカクつきを悪化させます。
WSL2 のファイルシステム間 IO 増幅
Windows 上で Docker Desktop の WSL2 バックエンドを使用する場合、固有のファイルシステム間 IO パフォーマンス問題が存在します。Windows 側の VS Code 拡張機能は pipe を介して WSL2 内の Docker daemon と通信しますが、コンテナ内のファイルシステム操作が /mnt/c パス(Windows ディスクへのアクセス)を伴う場合、9P ファイル共有プロトコルへの変換を経る必要があります。WSL2 の 9P プロトコルは、多数の小ファイル IO シナリオを処理する際のパフォーマンスがネイティブファイルシステムより著しく劣り、1 回の操作の遅延はネイティブパスの 3〜5 倍に達する可能性があります。
さらに、microsoft/vscode-remote-release#9372 の報告によると、ARM Mac 上で Rosetta を介して x86 コンテナを実行する場合、VS Code Server プロセスの CPU アフィニティマスクが異常に単一コアのみを使用するように設定され、nproc が実際の物理コア数ではなく 1 を返す原因となります。この問題は主に macOS プラットフォームで発生しますが、Dev Container がクロスプラットフォームシナリオにおいてプロセススケジューリングパラメータの制御に一貫性がないことを示しており、同様の問題が Windows の WSL2 環境でも異なる形で発生する可能性があります。executeInWSL: true は Docker CLI の実行場所を WSL 内部に移すことでファイルシステム間 IO の発生頻度を減らしますが、コンテナ内で /mnt/c パスにアクセスする際に発生する 9P オーバーヘッドを完全に排除することはできません。
解決策
ポートフォワーディング設定の簡素化
devcontainer.json 内の forwardPorts 設定を簡素化し、実際に必要なポートのみを保持することで、ポートフォワーディングプロセスの数を大幅に削減し、issue #5767 で説明されているプロセスリークのリスクを低減できます。使用していない Dev Container ウィンドウを閉じることで、対応する Extension Host とポートフォワーディングリソースを即座に解放することもできます。
Docker Desktop の最適化
Docker Desktop の Settings > Resources パネルで、CPU とメモリの割り当てを適切に調整し、リソース不足により Docker daemon が頻繁にガベージコレクションやスワップ操作を行うのを回避します。WSL2 バックエンドのパフォーマンスはバージョンごとに改善されているため、Docker Desktop の最新バージョンを使用していることを確認してください。パフォーマンス要件が高いシナリオでは、WSL2 内部に直接 Docker Engine をインストールし、Docker Desktop の仮想化レイヤーをバイパスすることを検討できます。これにより、Windows/WSL 境界の追加オーバーヘッドをさらに排除できます。
VS Code 設定の最適化
不要な拡張機能を無効にすることで Extension Host の負荷を軽減できます。特に、リモートコンテナ内で実行され、ファイル監視機能を持つ拡張機能(TypeScript 言語サービス、ESLint など)が対象です。settings.json で files.watcherExclude を設定し、node_modules、.git、dist などの大規模ディレクトリを除外することで、ファイルシステム監視によって生成される IO を削減できます。extensions.autoUpdate: false を設定することで、バックグラウンドでの拡張機能更新がコンテナ環境で追加のネットワークおよびディスク操作をトリガーするのを回避できます。
代替案
上記の対策でもパフォーマンス要件を満たせない場合、VS Code Remote-SSH 拡張機能を使用して WSL2 に接続し、WSL2 内部で直接 Docker CLI を使用してコンテナを管理することを検討できます。この方法により、Docker CLI の呼び出しは Windows/WSL 間の境界間通信から WSL2 内部のローカル通信に変わります。別の方法として、Docker Compose を使用してコンテナのライフサイクルを管理し、docker compose up -d でサービスを起動した後、Remote-SSH のみを使用してコンテナに接続して開発を行い、Dev Container 拡張機能のポーリングメカニズムを完全にバイパスする方法もあります。
executeInWSL の有効化(核心対策)
上記の対策は IO 過負荷の問題をある程度緩和できますが、IO の頻度を減らすだけ(ポートフォワーディングの簡素化)か、リソース割り当てを最適化するだけ(Docker Desktop の最適化)であり、問題の根本原因である Docker CLI 呼び出しが Windows/WSL 境界を越える際に発生する named pipe 通信オーバーヘッドには触れていません。dev.containers.executeInWSL はまさにこの根本原因に対する直接的な解決策です。
VS Code チームメンバーの chrmarti が microsoft/vscode-remote-release#9194 で明確に説明しているように、この設定は docker コマンドを Windows 側で実行するか WSL 内部で実行するかを決定します。これを true に設定すると、すべての Docker CLI 呼び出し(docker inspect、docker version、docker exec、docker ps などを含む)が WSL 内部で実行され、Unix socket を介して Docker daemon と直接通信するため、Windows/WSL 境界上の named pipe および 9P プロトコル変換のオーバーヘッドをバイパスできます。
settings.json に以下の設定を追加するだけで有効化できます。
{
"dev.containers.executeInWSL": true
}
この設定がなぜ IO パフォーマンスを大幅に改善するのかを理解するには、2 つのモード間の通信パスの違いを比較する必要があります。デフォルトモード(executeInWSL: false または未設定)では、VS Code Extension Host は Windows 上で実行され、Docker daemon と対話する必要があるたびに spawn を介して Windows 上の docker.exe 子プロセスを起動します。この子プロセスは named pipe(パスが \\pipe\ で始まる)を介して Windows/WSL 境界を越え、Docker daemon と通信します。このパスには、Windows プロセスの作成、named pipe の境界間 IO、および stdout パイプの戻り伝送という 3 つの段階が含まれ、各段階が追加の遅延と IO オーバーヘッドを導入します。executeInWSL: true の場合、Extension Host は代わりに wsl -d <distro> -e docker を介して WSL 内部で Docker CLI を実行します。Docker CLI は WSL 内部でネイティブに実行され、Unix socket(ローカル IPC)を介して同じ WSL インスタンス内の Docker daemon と通信します。このパスは完全に Linux カーネル空間内で完了し、named pipe の境界間オーバーヘッドを回避します。
flowchart TB
subgraph Default["デフォルトモード (executeInWSL: false)"]
direction LR
A1["Extension Host<br/>(Windows)"]
A2["docker.exe<br/>(Windows 子プロセス)"]
A3["named pipe<br/>(\\\\pipe\\\\)"]
A4["Docker Daemon<br/>(WSL2)"]
A1 e1@--> A2
A2 e2@--> A3
A3 e3@--> A4
end
subgraph Optimized["最適化モード (executeInWSL: true)"]
direction LR
B1["Extension Host<br/>(Windows)"]
B2["wsl -e docker<br/>(WSL 内部)"]
B3["Unix socket<br/>(ローカル IPC)"]
B4["Docker Daemon<br/>(WSL2)"]
B1 e4@--> B2
B2 e5@--> B3
B3 e6@--> B4
end
Default ~~~ Optimized
classDef win fill:#E3F2FD,stroke:#1565C0,stroke-width:1px,color:#0D47A1;
classDef wsl fill:#FFF8E1,stroke:#EF6C00,stroke-width:1px,color:#E65100;
classDef fast fill:#E8F5E9,stroke:#2E7D32,stroke-width:1px,color:#1B5E20;
classDef animate stroke:#EF6C00,stroke-width:2px,stroke-dasharray: 9\,5,stroke-dashoffset: 900,animation: dash 25s linear infinite;
class A1,A2 win;
class A3,A4 wsl;
class B1 win;
class B2,B3,B4 fast;
class e1,e2,e3,e4,e5,e6 animate;調査段階の定量化データから、この改善を検証できます。デフォルトモードでは、1 回の docker inspect --type container 呼び出しに約 1800ms、docker version 呼び出しに約 620ms を要します。これらの遅延は主に Windows/WSL 境界上の named pipe 通信とプロセス作成のオーバーヘッドに起因します。executeInWSL: true を有効にすると、Docker CLI は WSL 内部で Unix socket を介して daemon と通信するため、1 回の呼び出しの遅延はミリ秒レベルにまで低下し、累積効果による改善は特に顕著です。
既知の問題と注意事項
dev.containers.executeInWSL は IO パフォーマンスを効果的に改善できますが、使用時には以下の既知の問題に注意する必要があります。
Docker Desktop の自動起動問題: issue #9695 によると、executeInWSL: true の場合、Docker Desktop が自動起動しません。この問題は Dev Containers 0.353.0-pre-release バージョンで修正されており、より新しいバージョンを使用しているユーザーはもはやこの問題に遭遇しないはずです。
WSL サービス転送: issue #9194 によると、executeInWSL を false に設定しても、拡張機能は WSL への接続を試みます(display/ssh-agent/gpg-agent 転送用)。この動作は 0.337.0-pre-release バージョンで新たに追加された dev.containers.wslServiceForwarding 設定項目によって制御されるようになり、ユーザーは WSL サービス転送を独立して無効にできます。
Rancher Desktop の互換性: issue #10722 によると、Docker Desktop の代わりに Rancher Desktop を使用する場合、executeInWSL: true が WSL1 エラープロンプトをトリガーします。この問題は現在 open 状態のままであり、Rancher Desktop ユーザーは一時的にこの設定を無効にする必要があるかもしれません。
予期せぬアクティベーション: issue #11005 によると、executeInWSL: true がローカルの Windows リポジトリで予期せず Dev Container 初期化プロセスをトリガーします。この問題も open 状態のままであり、影響を受けるユーザーは、この設定をグローバル構成ではなく特定のワークスペースに限定することを検討できます。
まとめ
Windows 上での VS Code Dev Container の IO 過負荷問題を調査するには、現象から出発して段階的に根本原因を特定する必要があります。Process Monitor を使用することで、IO の主要な発生源が Docker CLI の named pipe 通信であることを確認でき、VS Code 組み込みツールと Windows Performance Analyzer を使用することで、呼び出し頻度と遅延をさらに定量化できます。根本原因分析は 4 つの複合要因を明らかにしました。Docker CLI の高頻度な境界間ポーリングが最も主要なパフォーマンスボトルネックであり、ポートフォワーディングプロセスの接続リークと Extension Host の再接続ループは確認済みのソフトウェアバグ、WSL2 のファイルシステム間 IO 増幅はプラットフォームレベルの固有の制限です。
解決策の中で、dev.containers.executeInWSL: true が最も核心的な対策です。これは Docker CLI が Windows/WSL 境界を越える際の named pipe 通信オーバーヘッドを直接排除し、高頻度な CLI 呼び出しの実行場所を Windows から WSL 内部に移し、Unix socket を介してローカル IPC を完了します。残りの対策(ポートフォワーディングの簡素化、Docker Desktop の最適化、VS Code 設定の最適化)は補助的な対策として、他の要因による影響をある程度緩和します。この問題に悩まされているユーザーには、本記事の調査フローに従って根本原因を確認した後、優先的に executeInWSL: true を有効化し、その後実際のシナリオに応じて適切な補助最適化戦略を選択することを推奨します。