VS Code Dev Container IO слишком высокий: подробный разбор конфигурации executeInWSL и анализ причины
В Windows при использовании расширения Dev Container в VS Code для контейнерной разработки некоторые пользователи сталкиваются с заметным зависанием системы. В Диспетчере задач можно увидеть, что процесс Extension Host постоянно потребляет высокий CPU и диск для чтения, даже без активных действий. В этой статье описан полный процесс расследования, начиная от симптомов и заканчивая поиском корневой причины и её решением.
Симптомы проблемы
Зависание системы происходит после подключения к контейнеру через расширение Dev Container. В Диспетчере задач видно, что процесс Extension Host постоянно показывает высокий уровень чтения диска и загрузки CPU, даже если пользователь не редактирует файлы и не использует терминал. В экстремальных случаях откликается медленно весь рабочий стол Windows, курсор мыши периодически «залипает».
Процесс расследования
Использование Process Monitor для определения источника IO
Sysinternals Process Monitor — первый инструмент для расследования подобных проблем. После запуска procmon задайте фильтр Process Name is Code.exe или Process Name is Extension Host, чтобы в реальном времени наблюдать все операции ReadFile/WriteFile, их пути и частоту. В результатах фильтра пути, начинающиеся с \\pipe\, появляются с аномально высокой частотой — десятки раз в секунду. Эти операции named pipe отвечают за коммуникацию между Docker CLI и Docker daemon, что указывает на частый вызов Docker CLI из Extension Host.
Проверка встроенными средствами VS Code
Откройте Help > Toggle Developer Tools и в панели Performance запишите профиль CPU. Вы увидите, что большая часть времени Extension Host тратит на создание подпроцессов и чтение их stdout. Установив уровень журнала «Dev Containers» в «trace», в Output‑панели появятся полные последовательности команд: docker inspect --type container, docker version --format, docker exec, docker ps и т.д. По данным из issue #9194 один вызов docker inspect --type container занимает около 1800 мс, а docker version — около 620 мс.
Анализ системного IO с помощью Windows Performance Analyzer
Для более глубокого анализа используйте wpr.exe -start GeneralProfile -filemode, воспроизведите проблему, затем wpr.exe -stop capture.etl и загрузите полученный файл в Windows Performance Analyzer. В представлении Disk I/O видно, что Extension Host является главным источником чтения диска, а в представлении Process Life Cycle отображается множество короткоживущих подпроцессов, которые постоянно создаются и уничтожаются — это именно вызовы Docker CLI.
Анализ причины
Архитектура межпроцессного взаимодействия Dev Container
Результаты расследования указывают на многопроцессную коммуникацию Dev Container. При открытии рабочего пространства в контейнере из Windows запускается цепочка процессов, пересекающая границу Windows‑Linux.
flowchart LR
subgraph Windows["Windows хост"]
A["VS Code Client<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) общается с локальным Extension Host (Node.js) через IPC. 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 exec (для проверки среды) и docker ps. Эти команды вызываются не один раз, а периодически в течение всего жизненного цикла контейнера.
Каждый вызов Docker CLI порождает новый подпроцесс, что влечёт за собой накладные расходы на создание процесса, чтение stdout и парсинг JSON‑ответов. По умолчанию все эти операции проходят через границу Windows/WSL, а протокол 9P в WSL2 плохо справляется с большим количеством мелких файлов и частыми короткими соединениями. Согласно рекомендациям 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 для соединения с целевым портом внутри контейнера и двунаправленно передают данные между локальным и контейнерным портом. Если клиент (браузер или другое приложение) разрывает соединение, а логика очистки не срабатывает, процесс остаётся живым.
По данным issue #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 exec завершается аварийно, а процесс Node.js внутри контейнера продолжает работать, такие «сиротские» процессы не могут быть собраны, что приводит к росту потребления памяти. Проблема частично исправлена в VS Code 1.62, но в определённых сетевых условиях может воспроизводиться вновь. Заметьте, что утечка портов не связана напрямую с executeInWSL; это отдельный баг механизма перенаправления.
Цикл повторного подключения Extension Host
Согласно issue #6178, при потере соединения с удалённым контейнером логика повторного подключения содержит ошибку: асинхронная функция рекурсивно вызывает себя в блоке 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 MiB в минуту из‑за накопления контекстов в стеке. Проблема исправлена в Remote‑Containers 0.221.0‑pre‑release, но пользователи, не обновившие расширение, могут воспроизвести её, просто отключив Wi‑Fi. Диалоговое окно с предупреждением о разрыве соединения не появляется. Это отдельный баг, не связанный с executeInWSL, но усиливающий ощущение «зависания».
Увеличение IO при работе через файловую систему WSL2
При использовании Docker Desktop с бекендом WSL2 в Windows существует фундаментальная проблема производительности при переходе между файловыми системами. Расширение VS Code в Windows общается с Docker daemon в WSL2 через pipe; если внутри контейнера происходит доступ к файлам на /mnt/c (т.е. к Windows‑диску), запросы проходят через протокол 9P, который значительно медленнее нативных файловых систем. Операции могут занимать в 3‑5 раз больше времени, чем при работе с Linux‑файлами.
Кроме того, согласно issue #9372, на ARM‑Mac при запуске x86‑контейнеров через Rosetta процесс VS Code Server получает маску CPU affinity, ограничивая его одним ядром, из‑за чего nproc возвращает 1 вместо реального количества ядер. Хотя проблема в основном относится к macOS, она демонстрирует несоответствие управления параметрами планировщика в разных платформах, и аналогичные эффекты могут наблюдаться в Windows WSL2. Параметр executeInWSL: true перемещает выполнение Docker CLI внутрь WSL, уменьшая частоту переходов через границу файловой системы, но полностью избавиться от накладных расходов при доступе к /mnt/c нельзя.
Решения
Уменьшить количество перенаправляемых портов
В devcontainer.json оставьте только действительно нужные порты в forwardPorts. Это существенно сокращает количество процессов перенаправления и снижает риск утечки, описанной в issue #5767. Закрытие неиспользуемых окон Dev Container также мгновенно освобождает ресурсы Extension Host и процессов перенаправления.
Оптимизация Docker Desktop
В настройках Docker Desktop → Resources отрегулируйте количество CPU и объём памяти, чтобы Docker daemon не приходилось часто выполнять сборку мусора или использовать swap. Обязательно используйте последнюю версию Docker Desktop — в каждой версии улучшается производительность WSL2‑бэкенда. Для особо требовательных сценариев можно установить Docker Engine непосредственно в WSL2, минуя виртуализацию Docker Desktop, что устраняет дополнительный overhead границы Windows/WSL.
Оптимизация конфигурации VS Code
Отключите ненужные расширения, особенно те, которые работают в удалённом контейнере и активно следят за файловой системой (TypeScript, ESLint и т.п.). В settings.json задайте files.watcherExclude для node_modules, .git, dist и т.д., чтобы уменьшить нагрузку от мониторинга файлов. Установите extensions.autoUpdate: false, чтобы фоновые обновления не инициировали лишние сетевые и дисковые операции.
Альтернативные подходы
Если перечисленные меры не дают достаточного прироста производительности, рассмотрите использование VS Code Remote‑SSH для подключения к WSL2 и управления контейнерами напрямую через Docker CLI внутри WSL. Другой вариант — управлять жизненным циклом контейнеров с помощью Docker Compose (docker compose up -d), а затем подключаться к уже запущенному контейнеру через Remote‑SSH, полностью обходя механизм опроса Dev Container.
Включить executeInWSL (ключевое решение)
Все перечисленные меры лишь частично снижают нагрузку, тогда как коренная причина — накладные расходы на передачу named pipe между Windows и WSL при каждом вызове Docker CLI. Параметр dev.containers.executeInWSL решает эту проблему напрямую.
Как указал chrmarti в issue #9194, этот параметр определяет, где будет запускаться команда docker: в Windows или внутри WSL. При значении true все вызовы (docker inspect, docker version, docker exec, docker ps и др.) выполняются внутри WSL, используя Unix‑socket для прямой связи с Docker daemon, тем самым обходя named pipe и 9P.
Добавьте в settings.json следующее:
{
"dev.containers.executeInWSL": true
}
Почему это работает? Сравним два пути коммуникации.
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;В режиме по умолчанию каждый вызов docker требует создания Windows‑процесса, передачи данных через named pipe и чтения stdout, что добавляет задержки и IO. При executeInWSL: true Docker CLI запускается внутри WSL и общается с daemon через Unix‑socket, полностью находясь в Linux‑ядре, что сокращает задержку до миллисекунд.
Известные проблемы и предостережения
- Автозапуск Docker Desktop: в issue #9695 сообщалось, что при
executeInWSL: trueDocker Desktop не запускается автоматически. Проблема исправлена в Dev Containers 0.353.0‑pre‑release. - Перенаправление WSL‑служб: даже при
executeInWSL: falseрасширение пытается подключаться к WSL для перенаправления display/ssh‑agent/gpg‑agent. Управлять этим теперь можно через параметрdev.containers.wslServiceForwarding(добавлен в 0.337.0‑pre‑release). - Совместимость с Rancher Desktop: при использовании Rancher Desktop вместо Docker Desktop параметр
executeInWSL: trueвызывает ошибку WSL1. Проблема пока открыта. - Неожидательная активация: в issue #11005 отмечено, что
executeInWSL: trueможет неожиданно запускать процесс инициализации Dev Container в локальном Windows‑репозитории. Рекомендуется ограничивать настройку областью конкретного рабочего пространства, а не глобально.
Заключение
Для устранения высокой нагрузки IO в VS Code Dev Container на Windows необходимо последовательно исследовать симптомы, определить, что основной источник — частый вызов Docker CLI через named pipe, а также учитывать дополнительные факторы: утечки процессов перенаправления портов, цикл повторного подключения Extension Host и ограничения файловой системы WSL2. Наиболее эффективным решением является включение dev.containers.executeInWSL: true, которое полностью устраняет накладные расходы на межплатформенную коммуникацию Docker CLI. Остальные меры (оптимизация портов, настройка Docker Desktop и VS Code) служат вспомогательными и помогают дополнительно снизить нагрузку. Пользователям, столкнувшимся с этой проблемой, рекомендуется сначала включить executeInWSL, а затем при необходимости применить дополнительные оптимизации.