Слишком высокая нагрузка на IO в VS Code Dev Container: подробное объяснение конфигурации executeInWSL и анализ корневых причин

Полный процесс диагностики проблемы чрезмерной нагрузки на IO в расширении VS Code Dev Container в Windows, от выявления симптомов до анализа корневых причин, с最终ным решением в виде dev.containers.executeInWSL для устранения узкого места при межплатформенной коммуникации Docker CLI.

При использовании расширения Dev Container в VS Code для контейнерной разработки в Windows некоторые пользователи сталкиваются с явным замедлением системы. В диспетчере задач можно наблюдать постоянно повышенную нагрузку на CPU и дисковый IO процесса Extension Host, которая не снижается даже без активных действий пользователя. В этой статье описан полный процесс диагностики: от выявления симптомов проблемы до определения корневых причин и поиска оптимального решения.

Симптомы проблемы

Замедление системы происходит после подключения расширения Dev Container к контейнеру. В диспетчере задач можно увидеть, что процесс Extension Host постоянно потребляет повышенные ресурсы CPU и дискового IO, и эти показатели не возвращаются к уровню простоя даже тогда, когда пользователь не выполняет никаких действий с редактором или терминалом. В экстремальных случаях отклик всей системы Windows замедляется, курсор мыши периодически подвисает.

Процесс диагностики

Использование Process Monitor для определения источника IO

Sysinternals Process Monitor — это первый инструмент для диагностики подобных проблем. Запустив procmon, установите фильтр Process Name is Code.exe или Process Name is Extension Host, чтобы в реальном времени наблюдать за всеми операциями ReadFile/WriteFile, их путями и частотой. В результатах фильтрации операции с именованными каналами (named pipe), начинающиеся с \\pipe\, происходят аномально часто — до нескольких десятков раз в секунду. Эти операции named pipe соответствуют коммуникации между Docker CLI и Docker daemon, что указывает на частые вызовы Docker CLI со стороны Extension Host.

Использование встроенных инструментов VS Code

Открыв Chromium DevTools через Help > Toggle Developer Tools и записав профиль CPU на панели Performance, можно увидеть, что значительная часть времени Extension Host тратится на создание дочерних процессов и чтение из stdout-каналов. Установив уровень логирования “Dev Containers” в Output на “trace”, можно увидеть полную последовательность вызовов команд: docker inspect --type container, docker version --format, docker exec, docker ps и другие команды выполняются повторно. Согласно данным логов из issue #9194, можно量化ровать накладные расходы одного вызова: docker inspect --type container занимает около 1800 мс, docker version — около 620 мс.

Использование Windows Performance Analyzer для анализа системного IO

Для более глубокого анализа начните запись ETW с помощью wpr.exe -start GeneralProfile -filemode, воспроизведите проблему и остановите запись с помощью wpr.exe -stop capture.etl. Загрузив результат в Windows Performance Analyzer, представление Disk I/O подтверждает, что основной вклад в дисковый IO вносит Extension Host, а представление Process Life Cycle показывает множество короткоживущих дочерних процессов, которые постоянно создаются и уничтожаются — это именно экземпляры вызовов Docker CLI.

Анализ корневых причин

Архитектура межпроцессного взаимодействия Dev Container

Результаты диагностики указывают на архитектуру межпроцессного взаимодействия Dev Container. Когда пользователь открывает контейнерное рабочее пространство через расширение Dev Container в Windows, фактически запускается цепочка межпроцессного взаимодействия, пересекающая границу между 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) общается с локальным 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 daemon, docker exec для выполнения команд зондирования окружения внутри контейнера, а также docker ps для列出运行中的容器. Эти команды выполняются не один раз при запуске, а повторяются с определенной частотой в течение всего жизненного цикла контейнера.

Каждый вызов Docker CLI запускает новый дочерний процесс, что влечет за собой накладные расходы на создание процесса, чтение из stdout-канала, парсинг JSON-результата и другие операции IO. В режиме по умолчанию все эти операции данных需要跨越 Windows/WSL 边界, а протокол 9P файловой системы WSL2 показывает низкую производительность при обработке большого количества мелких файловых операций 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 exec аварийно завершается, а процесс Node.js внутри контейнера продолжает работать, эти сиротские процессы не могут быть корректно освобождены, что приводит к постоянному росту потребления памяти. Эта проблема была частично исправлена в версии VS Code 1.62, но все еще может воспроизводиться при определенных сетевых условиях. Стоит отметить, что утечка перенаправления портов напрямую не связана с executeInWSL — это собственный дефект механизма перенаправления портов VS Code.

Цикл переподключения Extension Host

Согласно записям в microsoft/vscode-remote-release#6178, при потере подключения к удаленному контейнеру из-за сетевого сбоя или по другим причинам в логике переподключения Extension Host есть баг: асинхронная функция рекурсивно вызывает себя в блоке catch, что приводит к холостому потреблению CPU. Стек вызовов показывает, что эта функция повторяется в processTicksAndRejections без условия выхода.

flowchart TD
    A["Потеря подключения"] e1@--> B["Асинхронная функция переподключения"]
    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 МБ/мин, поскольку каждый рекурсивный вызов накапливает неосвобожденный контекст в стеке вызовов. Эта проблема была исправлена в версии Remote-Containers 0.221.0-pre-release, однако для пользователей, которые не обновили расширение вовремя, достаточно один раз имитировать разрыв сети (например, отключить WiFi), чтобы спровоцировать эту проблему, и диалоговое окно с уведомлением о разрыве подключения никогда не появится. Это независимый программный баг, не связанный с конфигурацией executeInWSL, но он усугубляет воспринимаемое пользователем замедление системы.

Усиление IO при межфайловых операциях в WSL2

При использовании Docker Desktop с backend WSL2 в Windows существует присущая проблема производительности межфайловых операций. Расширение VS Code на стороне Windows общается с Docker daemon в WSL2 через pipe, а файловые операции внутри контейтера, если они затрагивают пути /mnt/c (доступ к дискам Windows), требуют преобразования через протокол 9P файловой системы. Протокол 9P в WSL2 показывает значительно более низкую производительность по сравнению с нативной файловой системой при обработке большого количества мелких файловых операций IO, а задержка одной операции может быть в 3-5 раз выше, чем при использовании нативных путей.

Кроме того, согласно отчету в microsoft/vscode-remote-release#9372, при запуске x86-контейнеров через Rosetta на ARM Mac процесс VS Code Server аномально устанавливает маску CPU affinity только на одно ядро, что приводит к тому, что nproc возвращает 1 вместо реального количества физических ядер. Хотя эта проблема в основном проявляется на платформе macOS, она выявляет несоответствие в управлении параметрами планирования процессов Dev Container на разных платформах; аналогичные проблемы могут проявляться в разных формах в среде WSL2 в Windows. executeInWSL: true перемещает выполнение Docker CLI внутрь WSL, уменьшая частоту межфайловых операций IO, но не может полностью устранить накладные расходы 9P при доступе контейтера к путям /mnt/c.

Решения

Оптимизация конфигурации перенаправления портов

Оптимизация конфигурации forwardPorts в devcontainer.json с оставлением только реально необходимых портов может значительно сократить количество процессов перенаправления портов и снизить риск утечки процессов, описанный в issue #5767. Закрытие неиспользуемых окон Dev Container также немедленно освободит соответствующие ресурсы Extension Host и перенаправления портов.

Оптимизация Docker Desktop

В панели Settings > Resources Docker Desktop适当调整 CPU 和内存分配, избегайте того, чтобы Docker daemon из-за недостатка ресурсов часто выполнял сборку мусора или операции swap. Убедитесь, что используете последнюю версию Docker Desktop, поскольку производительность backend WSL2 улучшается в каждой версии. Для сценариев с высокими требованиями к производительности можно рассмотреть установку Docker Engine непосредственно внутри WSL2, обходя виртуализационный слой Docker Desktop и дополнительно устраняя накладные расходы на границу Windows/WSL.

Оптимизация конфигурации VS Code

Отключение ненужных расширений может снизить нагрузку на Extension Host, особенно тех, которые выполняются в удаленном контейнере и имеют функции мониторинга файлов (например, языковой сервис TypeScript, ESLint и др.). Установка files.watcherExclude в settings.json для исключения больших каталогов, таких как node_modules, .git, dist, может уменьшить IO, создаваемый мониторингом файловой системы. Установка extensions.autoUpdate: false позволяет избежать фоновых обновлений расширений, которые могут вызвать дополнительные сетевые и дисковые операции в среде контейнера.

Альтернативные решения

Если указанные меры все еще не удовлетворяют требованиям к производительности, можно рассмотреть использование расширения VS Code Remote-SSH для подключения к WSL2 и непосредственное использование Docker CLI внутри WSL2 для управления контейнерами. Этот подход превращает вызовы Docker CLI из межграничной коммуникации Windows/WSL во внутреннюю локальную коммуникацию WSL2. Другой способ — использовать Docker Compose для управления жизненным циклом контейнеров: запустив сервисы через docker compose up -d, использовать Remote-SSH для подключения к контейнеру для разработки, полностью обходя механизм опроса расширения Dev Container.

Включение executeInWSL (основное решение)

Вышеуказанные меры могут в разной степени решить проблему высокой нагрузки IO, но они либо только уменьшают частоту IO (оптимизация перенаправления портов), либо только оптимизируют распределение ресурсов (оптимизация Docker Desktop), не затрагивая корень проблемы: накладные расходы на коммуникацию named pipe при вызовах Docker CLI, пересекающих границу Windows/WSL. Именно для этой корневой причины 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 и общаться с Docker daemon напрямую через Unix-сокет, thereby обходя накладные расходы на named pipe и преобразование протокола 9P на границе Windows/WSL.

Добавьте следующую конфигурацию в settings.json для включения:

{
  "dev.containers.executeInWSL": true
}

Чтобы понять, почему эта конфигурация значительно улучшает производительность IO, необходимо сравнить различия в путях коммуникации между двумя режимами. В режиме по умолчанию (executeInWSL: false или не установлено) Extension Host работает в Windows, и каждый раз, когда требуется взаимодействие с Docker daemon, он запускает дочерний процесс docker.exe в Windows через spawn. Этот дочерний процесс общается с Docker daemon через named pipe (путь начинается с \\pipe\), пересекая границу Windows/WSL. Этот путь включает три этапа: создание процесса Windows, межграничный IO named pipe и возврат данных через stdout-канал, каждый из которых добавляет дополнительные задержки и накладные расходы IO. Когда executeInWSL: true, Extension Host вместо этого выполняет Docker CLI внутри WSL через wsl -d <distro> -e docker. Docker CLI работает нативно внутри WSL и общается с Docker daemon через Unix-сокет (локальный IPC) в том же экземпляре WSL. Этот путь полностью выполняется в пространстве ядра 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-сокет<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 inspect --type container занимает около 1800 мс, вызов docker version — около 620 мс, эти задержки в основном происходят из-за накладных расходов на коммуникацию named pipe на границе Windows/WSL и создание процесса. После включения executeInWSL: true Docker CLI выполняется внутри WSL и общается с daemon через Unix-сокет, задержка одного вызова снижается до миллисекундного уровня, и кумулятивный эффект особенно значителен.

Известные проблемы и примечания

Хотя 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 сообщается, что при использовании Rancher Desktop вместо Docker Desktop параметр executeInWSL: true вызывает ошибку WSL1. Эта проблема в настоящее время все еще находится в открытом состоянии, пользователям Rancher Desktop может потребоваться временно отключить эту настройку.

Неожиданная активация: в issue #11005 сообщается, что executeInWSL: true неожиданно инициирует процесс инициализации Dev Container в локальном репозитории Windows. Эта проблема также находится в открытом состоянии, затронутые пользователи могут рассмотреть ограничение этой настройки для конкретных рабочих пространств вместо глобальной конфигурации.

Заключение

Диагностика проблемы высокой нагрузки IO VS Code Dev Container в Windows требует постепенного определения корневых причин, отталкиваясь от симптомов. С помощью Process Monitor можно подтвердить, что основным источником IO является коммуникация named pipe Docker CLI; с помощью встроенных инструментов VS Code и Windows Performance Analyzer можно дополнительно量化ровать частоту вызовов и задержки. Анализ корневых причин выявляет четыре накладывающихся фактора: высокочастотный межграничный опрос Docker CLI является основным узким производительности, утечка подключений процессов перенаправления портов и цикл переподключения Extension Host являются подтвержденными программными багами, а усиление IO при межфайловых операциях в WSL2 является присущим ограничением платформы.

Среди решений dev.containers.executeInWSL: true является наиболее важной мерой — оно напрямую устраняет накладные расходы на коммуникацию named pipe при пересечении границы Windows/WSL для Docker CLI, перемещая выполнение высокочастотных вызовов CLI из Windows внутрь WSL и осуществляя локальный IPC через Unix-сокет. Остальные решения (оптимизация перенаправления портов, оптимизация Docker Desktop, оптимизация конфигурации VS Code) служат вспомогательными мерами, в разной степени смягчая влияние других факторов. Для пользователей, страдающих от этой проблемы, рекомендуется после подтверждения корневых причин с помощью диагностического процесса в этой статье в первую очередь включить executeInWSL: true, а затем выбрать подходящие вспомогательные стратегии оптимизации в зависимости от конкретного сценария.