VS Code Dev Container IO 과다: executeInWSL 설정 상세 설명과 근본 원인 분석

Windows에서 VS Code Dev Container 확장으로 인한 IO 과다 문제의 전체적인 해결 과정을 기록한 것으로, 현상 파악에서 근본 원인 분석까지 다루며 최종적으로 dev.containers.executeInWSL을 핵심 솔루션으로 Docker CLI 경계 간 통신 병목 문제를 해결합니다.

Windows에서 VS Code의 Dev Container 확장을 사용하여 컨테이너화된 개발을 수행할 때, 일부 사용자는 시스템이 현저히 느려지는 현상을 경험합니다. 작업 관리자에서 Extension Host 프로세스의 CPU 및 디스크 읽기 IO가 지속적으로 높은 것을 확인할 수 있으며, 사용자가 적극적으로 작업을 수행하지 않는 경우에도 이러한 지표가 감소하지 않습니다. 극단적인 경우 전체 Windows 데스크톱의 응답 속도가 영향을 받아 마우스 커서가 간헐적으로 멈추는 현상이 발생합니다.

문제 현상

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) 작업이 비정상적으로 높은 빈도로 나타나며, 초당 수십 번에 달합니다. 이러한 명명된 파이프 작업은 Docker CLI와 Docker 데몬 간의 통신을 나타내며, Extension Host가 Docker CLI를 자주 호출하고 있음을 보여줍니다.

VS Code 내장 도구를 사용한 확인

Help > Toggle Developer Tools를 통해 Chromium DevTools를 열고 Performance 패널에서 CPU 프로파일을 녹화하면, Extension Host가 하위 프로세스 생성 및 stdout 파이프 읽기에 많은 시간을 소비하는 것을 볼 수 있습니다. Output 패널에서 “Dev Containers” 로그 수준을 “trace"로 설정하면 전체 명령 호출 시퀀스를 볼 수 있습니다: docker inspect --type container, docker version --format, docker exec, docker exec 명령이 반복적으로 실행됩니다. issue #9194의 로그 데이터에서 단일 호출의 비용을 정량화할 수 있습니다: 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 데몬<br/>(dockerd)"]
    end

    subgraph Container["컨테이너 내부"]
        E["VS Code 서버"]
        F["원격 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 서버와 연결을 설정해야 하며, 이 연결은 Docker CLI를 중개자로 사용합니다. 컨테이너 내의 VS Code 서버가 시작되면 Remote Extension Host가 확장 실행을接管하고, 포트 포워딩 프로세스는 로컬 브라우저가 컨테이너 서비스에 액세스할 수 있는 TCP 터널을 제공합니다. 이 링크의 모든 구성 요소가 IO 증폭의 원인이 될 수 있습니다.

Docker CLI 폴링 메커니즘

Dev Containers 확장은 Docker CLI 명령을 자주 호출하여 컨테이너 상태를 가져오고 유지합니다. 컨테이너 시작 단계에서 확장은 순서대로 docker inspect --type container를 실행하여 컨테이너 메타데이터를 가져오고, docker version --format으로 Docker 데몬 가용성을 확인하며, 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 데몬

    EH->>CLI: spawn docker inspect
    CLI->>DD: 명명된 파이프 요청
    DD-->>CLI: JSON 응답
    CLI-->>EH: stdout 파이프 출력
    EH->>CLI: spawn docker version
    CLI->>DD: 명명된 파이프 요청
    DD-->>CLI: 버전 정보
    CLI-->>EH: stdout 파이프 출력
    EH->>CLI: spawn docker exec
    CLI->>DD: 명명된 파이프 요청
    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의 재연결 로직에 버그가 있습니다: 비동기 함수가 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의 메모리가 약 1MB/분의 속도로 지속적으로 증가하며, 이는 각 재귀 호출이 호출 스택에 해제되지 않은 컨텍스트가 누적되기 때문입니다. 이 문제는 Remote-Containers 0.221.0-pre-release 버전에서 수정되었지만, 확장을 적시에 업데이트하지 않은 사용자의 경우 네트워크 연결 끊김을 한 번만 시뮬레이션하면(예: WiFi 끄기) 이 문제를 트리거할 수 있으며, 연결 끊김 알림 대화상자가永远표시되지 않습니다. 이는 executeInWSL 설정과 관련이 없는 독립적인 소프트웨어 버그이지만, 사용자가 인지하는 시스템 지연을 악화시킵니다.

WSL2 파일 시스템 간 IO 증폭

Windows에서 Docker Desktop의 WSL2 백엔드를 사용할 때, 본질적인 파일 시스템 간 IO 성능 문제가 있습니다. Windows 측의 VS Code 확장은 파이프를 통해 WSL2의 Docker 데몬과 통신하며, 컨테이너 내의 파일 시스템 작업이 /mnt/c 경로(Windows 디스크 액세스)를 포함하는 경우 9P 파일 공유 프로토콜의 변환이 필요합니다. WSL2의 9P 프로토콜은大量의 소형 파일 IO 시나리오에서 성능이原生 파일 시스템보다 현저히 낮으며, 단일 작업의 지연은原生 경로의 3-5배에 달할 수 있습니다.

또한 microsoft/vscode-remote-release#9372의 보고에 따르면, ARM Mac에서 Rosetta를 통해 x86 컨테이너를 실행할 때 VS Code 서버 프로세스의 CPU affinity mask가 비정상적으로 단일 코어만 사용하도록 설정되어 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 데몬이 리소스 부족으로 인해 빈번한 가비지 컬렉션이나 스왑 작업을 수행하지 않도록 합니다. 최신 버전의 Docker Desktop을 사용해야 합니다. WSL2 백엔드의 성능은 각 버전에서 개선되고 있습니다. 성능 요구 사항이 높은 시나리오의 경우 Docker Desktop의 가상화 계층을 우회하기 위해 WSL2 내부에 직접 Docker Engine을 설치하여 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 과다 문제를 완화할 수 있지만, 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 내부에서 실행되어 Unix 소켓을 통해 Docker 데몬과 직접 통신함으로써 Windows/WSL 경계의 명명된 파이프 및 9P 프로토콜 변환开销을 우회합니다.

settings.json에 다음 구성을 추가하여 활성화할 수 있습니다:

{
  "dev.containers.executeInWSL": true

이 설정이 IO 성능을 현저히 개선하는 이유를 이해하려면 두 모드의 통신 경로 차이를 비교해야 합니다. 기본 모드에서(executeInWSL: false 또는 미설정) VS Code Extension Host는 Windows에서 실행되며, Docker 데몬과 상호작용해야 할 때마다 spawn을 통해 Windows의 docker.exe 하위 프로세스를 시작합니다. 해당 하위 프로세스는 명명된 파이프(경로가 \\pipe\로 시작)를 통해 Windows/WSL 경계를跨越하여 Docker 데몬과 통신합니다. 이 경로는 Windows 프로세스 생성, 명명된 파이프 경계 간 IO, stdout 파이프 회신의 세 단계를 포함하며, 각 단계가 추가 지연과 IO开销을 야기합니다. executeInWSL: true일 때 Extension Host는 wsl -d <distro> -e docker를 통해 WSL 내부에서 Docker CLI를 실행합니다. Docker CLI는 WSL 내부에서原生으로 실행되어 Unix 소켓(로컬 IPC)을 통해 동일한 WSL 实例의 Docker 데몬과 통신합니다. 이 경로는 완전히 Linux 커널 공간 내에서 완료되어 명명된 파이프의 경계 간开销을 방지합니다.

flowchart TB
    subgraph Default["기본 모드 (executeInWSL: false)"]
        direction LR
        A1["Extension Host<br/>(Windows)"]
        A2["docker.exe<br/>(Windows 하위 프로세스)"]
        A3["명명된 파이프<br/>(\\\\pipe\\\\)"]
        A4["Docker 데몬<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 데몬<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 호출은 약 1800ms, docker version 호출은 약 620ms가 소요되며, 이러한 지연은 주로 Windows/WSL 경계의 명명된 파이프 통신 및 프로세스 생성开销에서 발생합니다. executeInWSL: true를 활성화하면 Docker CLI가 WSL 내부에서 Unix 소켓을 통해 데몬과 통신하므로 단일 호출의 지연이 밀리초 수준으로 감소하며, 누적 효과의 개선이 특히 두드러집니다.

알려진 문제점 및 주의 사항

dev.containers.executeInWSL은 IO 성능을 효과적으로 개선할 수 있지만, 사용 시 다음의 알려진 문제점에 주의해야 합니다.

Docker Desktop 자동 시작 문제: issue #9695에서 보고된 바와 같이, executeInWSL: true일 때 Docker Desktop이 자동으로 시작되지 않습니다. 이 문제는 Dev Containers 0.353.0-pre-release 버전에서 수정되었으며, 최신 버전을 사용하는 사용자는 더 이상 이 문제를 겪지 않아야 합니다.

WSL 서비스 포워딩: issue #9194에서 보고된 바와 같이, executeInWSLfalse로 설정하더라도 확장이 여전히 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의 명명된 파이프 통신임을 확인할 수 있으며, VS Code 내장 도구 및 Windows Performance Analyzer를 통해 호출 빈도와 지연을 추가로 정량화할 수 있습니다. 근본 원인 분석은 네 가지叠加 요소를 보여줍니다: Docker CLI의 고빈도 경계 간 폴링이 가장 주요한 성능 병목이며, 포트 포워딩 프로세스의 연결 누수 및 Extension Host의 재연결 루프는 확인된 소프트웨어 버그이며, WSL2 파일 시스템 간 IO 증폭은 플랫폼 수준의 본질적인 제한입니다.

솔루션에서 dev.containers.executeInWSL: true가 가장 핵심적인 조치로, Docker CLI가 Windows/WSL 경계를跨越하는 명명된 파이프 통신开销을 직접消除하여 고빈도 CLI 호출의 실행 위치를 Windows에서 WSL 내부로 이동하고 Unix 소켓을 통해 로컬 IPC를 완료합니다. 기타 조치(포트 포워딩精简, Docker Desktop 최적화, VS Code 구성 최적화)는 다른 요인의 영향을不同程度에서 완화하는 보조 조치입니다. 이 문제로困扰받는 사용자는本文의排查 프로세스에 따라 근본 원인을 확인한 후 executeInWSL: true를 우선 활성화한 다음 실제 시나리오에 따라 적절한 보조 최적화 전략을 선택하는 것이 좋습니다.