VS Code Dev Container IO hoch: Ausführliche Konfiguration von executeInWSL und Ursachenanalyse

Dokumentiert den vollständigen Fehlersuchprozess für das hohe IO-Problem, das durch die VS Code Dev Container-Erweiterung unter Windows verursacht wird, von der Symptomerkennung bis zur Ursachenanalyse, und löst das Docker‑CLI‑Grenzkommunikationsengpass mit dev.containers.executeInWSL als Kernlösung.

Auf Windows kann die Verwendung der VS Code‑Dev‑Container‑Erweiterung für containerbasiertes Entwickeln bei einigen Benutzern zu deutlichen Systemverlangsamungen führen. Im Task‑Manager lässt sich ein dauerhaft hohes CPU‑ und Festplatten‑Read‑IO des Extension‑Host‑Prozesses beobachten, selbst wenn keine aktive Arbeit ausgeführt wird. Dieser Beitrag dokumentiert den vollständigen Fehlersuchprozess, beginnend mit den Symptomen, über die schrittweise Ursachenfindung bis hin zur Kernlösung.

Symptome

Das System hängt, sobald die Dev‑Container‑Erweiterung eine Verbindung zum Container herstellt. Im Task‑Manager ist zu sehen, dass der Extension Host‑Prozess dauerhaft hohe Festplatten‑Read‑IO und CPU‑Auslastung aufweist, selbst wenn der Benutzer keine Dateien bearbeitet oder das Terminal verwendet. In extremen Fällen wird die gesamte Windows‑Desktop‑Reaktionszeit beeinträchtigt, und der Mauszeiger ruckelt gelegentlich.

Fehlersuche

IO‑Quelle mit Process Monitor lokalisieren

Sysinternals Process Monitor ist das erste Werkzeug für diese Art von Problem. Nach dem Start von Procmon filtert man nach Process Name is Code.exe oder Process Name is Extension Host, um alle ReadFile/WriteFile‑Operationen und deren Pfade zu beobachten. In den Ergebnissen erscheinen häufige Zugriffe auf Pfade, die mit \\pipe\ beginnen – benannte Pipe‑Operationen, die zwischen Docker‑CLI und Docker‑Daemon stattfinden. Das zeigt, dass der Extension‑Host häufig Docker‑CLI aufruft.

VS Code‑Interne Werkzeuge nutzen

Über Help > Toggle Developer Tools öffnet man die Chromium‑DevTools und nimmt im Performance‑Panel ein CPU‑Profil auf. Dort sieht man, dass der Extension‑Host viel Zeit mit dem Starten von Unterprozessen und dem Lesen von stdout‑Pipes verbringt. Setzt man die Protokollstufe von „Dev Containers“ im Output‑Panel auf trace, erscheinen vollständige Befehlssequenzen wie docker inspect --type container, docker version --format, docker exec, docker ps usw. Das Issue #9194 liefert Messwerte: ein einzelner Aufruf von docker inspect --type container dauert etwa 1800 ms, docker version etwa 620 ms.

System‑IO mit Windows Performance Analyzer analysieren

Für tiefere Analysen startet man wpr.exe -start GeneralProfile -filemode, reproduziert das Problem und stoppt die Aufzeichnung mit wpr.exe -stop capture.etl. In Windows Performance Analyzer zeigt die Disk‑I/O‑Ansicht, dass der Extension‑Host der Hauptverursacher ist, und die Process‑Life‑Cycle‑Ansicht enthüllt zahlreiche kurzlebige Unterprozesse, die Docker‑CLI‑Aufrufe darstellen.

Ursachenanalyse

Mehrprozess‑Kommunikationsarchitektur von Dev Container

Die Untersuchung weist auf die mehrstufige Prozesskommunikation von Dev Container hin. Beim Öffnen eines Container‑Workspaces auf Windows wird eine Kette von Prozessen über Windows‑ und Linux‑Grenzen hinweg aufgebaut.

flowchart LR
    subgraph Windows["Windows‑Host"]
        A["VS Code Client<br/>(Electron)"]
        B["Extension Host<br/>(Node.js)"]
        C["Docker CLI<br/>(Unterprozess)"]
    end

    subgraph WSL2["WSL2 / Docker Desktop"]
        D["Docker Daemon<br/>(dockerd)"]
    end

    subgraph Container["Im Container"]
        E["VS Code Server"]
        F["Remote Extension Host"]
        G["Port‑Forward‑Prozess<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;

Der lokale VS Code‑Client (Electron) kommuniziert per IPC mit dem lokalen Extension‑Host (Node.js). Der Extension‑Host muss eine Verbindung zum VS Code‑Server im Container herstellen, wobei Docker‑CLI als Vermittler dient. Nach dem Start des Servers übernimmt der Remote‑Extension‑Host die Ausführung von Erweiterungen, und der Port‑Forward‑Prozess stellt einen TCP‑Tunnel für den lokalen Browser bereit. Jeder dieser Schritte kann zu erhöhtem IO führen.

Docker‑CLI‑Polling‑Mechanismus

Die Dev‑Containers‑Erweiterung ruft Docker‑CLI häufig auf, um den Container‑Status zu ermitteln und zu erhalten. Beim Start eines Containers führt sie nacheinander docker inspect --type container, docker version --format, docker exec und docker ps aus. Diese Aufrufe wiederholen sich nicht nur beim Start, sondern während der gesamten Lebensdauer des Containers in regelmäßigen Intervallen.

Jeder Aufruf startet einen neuen Unterprozess, erzeugt Prozess‑Overhead, liest stdout‑Pipes und parst JSON‑Ergebnisse – alles IO‑intensiv. Standardmäßig müssen diese Daten die Windows/WSL‑Grenze überqueren, und das 9P‑Dateisystem von WSL2 ist bei vielen kleinen Datei‑IO‑Operationen und häufigen Kurzverbindungen leistungsschwach. Microsoft empfiehlt, Datei‑system‑übergreifende Zugriffe zu minimieren, doch das Design von Dev Container macht dies schwierig.

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 request
    DD-->>CLI: JSON response
    CLI-->>EH: stdout pipe output
    EH->>CLI: spawn docker version
    CLI->>DD: named pipe request
    DD-->>CLI: version info
    CLI-->>EH: stdout pipe output
    EH->>CLI: spawn docker exec
    CLI->>DD: named pipe request
    DD-->>CLI: execution result
    CLI-->>EH: stdout pipe output

Port‑Forward‑Leckage

Jeder weitergeleitete Port erzeugt einen eigenen Node.js‑Unterprozess, der über net.createConnection mit dem Ziel im Container verbindet und Daten bidirektional weiterleitet. Wenn ein Client die Verbindung schließt, aber die Aufräum‑Logik nicht rechtzeitig greift, verbleiben diese Prozesse und verbrauchen Speicher.

Laut Issue #5767 belegt jeder undichte Port‑Forward‑Prozess etwa 26 MiB. In einer Umgebung mit vielen weitergeleiteten Ports kann die Prozesszahl von 2 auf Dutzende ansteigen. Der folgende Code zeigt das Kernmuster, wobei das client.on('close')‑Ereignis entscheidend für das ordnungsgemäße Beenden ist.

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));
});

Wenn ein docker exec‑Prozess abnormal endet, während der zugehörige Node‑Prozess im Container weiterläuft, können diese verwaisten Prozesse nicht freigegeben werden, was zu weiterem Speicherwachstum führt. Der Bug wurde teilweise in VS Code 1.62 behoben, tritt jedoch unter bestimmten Netzwerkbedingungen weiterhin auf. Hinweis: Das Leck ist nicht direkt mit executeInWSL verbunden, sondern ein eigenständiger Fehler im Port‑Forward‑Mechanismus von VS Code.

Extension‑Host‑Reconnect‑Schleife

Issue #6178 beschreibt einen Bug, bei dem bei Verbindungsabbrüchen zum Remote‑Container ein async‑Funktionsaufruf im catch‑Block sich selbst rekursiv aufruft, was zu einer CPU‑Spinnschleife führt. Der Aufruf‑Stack zeigt wiederholte Durchläufe in processTicksAndRejections ohne Abbruchbedingung.

flowchart TD
    A["Verbindung verloren"] e1@--> B["async Reconnect‑Funktion"]
    B e2@--> C{"Verbindung erfolgreich?"}
    C e3@-->|Ja| D["Normalbetrieb"]
    C e4@-->|Nein| E["catch‑Block"]
    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;

Parallel dazu wächst der Speicherverbrauch des Extension‑Hosts um etwa 1 MiB pro Minute, weil jede rekursive Aufrufinstanz Kontext im Stack hält. Der Bug wurde in Remote‑Containers 0.221.0‑pre‑release behoben; Nutzer, die nicht aktualisieren, können das Problem durch kurzes Trennen des Netzwerks (z. B. WLAN aus‑/einschalten) reproduzieren. Das Problem ist unabhängig von executeInWSL, verschärft jedoch die wahrgenommene Systemverlangsamung.

WSL2‑Dateisystem‑IO‑Verstärkung

Bei Verwendung von Docker Desktop mit WSL2‑Backend entstehen inhärente IO‑Leistungsverluste über Dateisystemgrenzen hinweg. Wenn die VS Code‑Erweiterung über Pipes mit dem Docker‑Daemon in WSL2 kommuniziert und innerhalb des Containers Pfade wie /mnt/c (Windows‑Laufwerke) verwendet, muss jede Dateioperation über das 9P‑Protokoll übersetzt werden. 9P ist bei vielen kleinen Datei‑IO‑Operationen 3‑5 × langsamer als native Dateisysteme.

Ein weiteres Problem, das in Issue #9372 beschrieben wird, betrifft ARM‑Macs, bei denen die CPU‑Affinity des VS Code‑Servers fälschlicherweise auf einen einzelnen Kern beschränkt wird. Obwohl das primär macOS‑spezifisch ist, zeigt es, dass Dev Container plattformübergreifend inkonsistente Prozess‑Scheduling‑Parameter nutzt – ein ähnliches Phänomen kann auch unter Windows‑WSL2 auftreten. executeInWSL: true verlagert die Ausführung von Docker‑CLI in die WSL‑Umgebung und reduziert damit die Häufigkeit von 9P‑Übersetzungen, eliminiert jedoch nicht vollständig die Kosten beim Zugriff auf /mnt/c.

Lösungen

Port‑Forward‑Konfiguration reduzieren

Entfernen Sie nicht benötigte Einträge aus forwardPorts in devcontainer.json. Das verringert die Anzahl der Port‑Forward‑Prozesse und senkt das Risiko von Lecks (siehe Issue #5767). Schließen Sie ungenutzte Dev‑Container‑Fenster, um zugehörige Extension‑Host‑ und Port‑Forward‑Ressourcen sofort freizugeben.

Docker Desktop optimieren

Passen Sie in Settings > Resources die CPU‑ und Speicherzuweisungen an, um zu verhindern, dass der Docker‑Daemon wegen Ressourcenmangels häufig Garbage‑Collection‑ oder Swap‑Operationen ausführt. Verwenden Sie stets die neueste Docker‑Desktop‑Version, da jede Version Verbesserungen für das WSL2‑Backend enthält. Für besonders performance‑kritische Szenarien kann Docker Engine direkt in WSL2 installiert werden, um die Virtualisierungsschicht von Docker Desktop zu umgehen.

VS Code‑Konfiguration optimieren

Deaktivieren Sie nicht benötigte Erweiterungen, insbesondere solche, die im Remote‑Container laufen und Dateisystem‑Watcher aktivieren (z. B. TypeScript‑Language‑Service, ESLint). In settings.json können Sie files.watcherExclude nutzen, um große Ordner wie node_modules, .git oder dist auszuschließen und so IO durch Dateisystem‑Überwachung zu reduzieren. Setzen Sie extensions.autoUpdate: false, um Hintergrund‑Updates zu vermeiden, die zusätzliche Netzwerk‑ und Festplatten‑IO auslösen.

Alternative Ansätze

Falls die oben genannten Maßnahmen nicht ausreichen, können Sie VS Code Remote‑SSH verwenden, um direkt zu WSL2 zu verbinden und Docker‑CLI dort zu steuern. Dadurch entfällt das Polling der Dev‑Container‑Erweiterung. Alternativ lässt sich Docker Compose einsetzen, um Container‑Lebenszyklen zu verwalten (docker compose up -d), und anschließend per Remote‑SSH auf den Container zugreifen – komplett ohne Dev‑Container‑Polling.

executeInWSL aktivieren (Kernlösung)

Die vorherigen Maßnahmen mildern das Problem, greifen jedoch nicht an die eigentliche Ursache: die kostenintensive named‑pipe‑Kommunikation von Docker‑CLI über die Windows/WSL‑Grenze. Die Einstellung dev.containers.executeInWSL löst genau dieses Problem.

Wie im Issue #9194 von chrmarti erklärt, bestimmt diese Einstellung, ob docker auf der Windows‑Seite oder innerhalb von WSL ausgeführt wird. Wird sie auf true gesetzt, laufen alle Docker‑CLI‑Aufrufe (z. B. docker inspect, docker version, docker exec, docker ps) in WSL und kommunizieren über einen Unix‑Socket direkt mit dem Docker‑Daemon, wodurch named‑pipe‑ und 9P‑Overhead entfallen.

Fügen Sie Folgendes zu settings.json hinzu:

{
  "dev.containers.executeInWSL": true
}

Warum das die IO‑Leistung verbessert

Im Standardmodus (executeInWSL: false oder nicht gesetzt) läuft der Extension‑Host unter Windows. Jeder Docker‑Daemon‑Aufruf startet docker.exe als Windows‑Unterprozess, der über eine named pipe (\\pipe\\) mit dem Daemon in WSL2 kommuniziert. Dieser Pfad umfasst drei kostenintensive Schritte: Windows‑Prozess‑Erstellung, named‑pipe‑IO über die Grenze und Rückleitung von stdout‑Daten.

Bei executeInWSL: true wird stattdessen wsl -d <distro> -e docker verwendet. Docker‑CLI läuft nativ in WSL, kommuniziert über einen Unix‑Socket (lokales IPC) mit dem Daemon im selben WSL‑Instanz. Der gesamte Pfad bleibt innerhalb des Linux‑Kernels, wodurch die zusätzlichen Latenzen entfallen.

flowchart TB
    subgraph Default["Standardmodus (executeInWSL: false)"]
        direction LR
        A1["Extension Host<br/>(Windows)"]
        A2["docker.exe<br/>(Windows‑Unterprozess)"]
        A3["named pipe<br/>(\\\\pipe\\\\)"]
        A4["Docker Daemon<br/>(WSL2)"]
        A1 e1@--> A2
        A2 e2@--> A3
        A3 e3@--> A4
    end

    subgraph Optimized["Optimierter Modus (executeInWSL: true)"]
        direction LR
        B1["Extension Host<br/>(Windows)"]
        B2["wsl -e docker<br/>(innerhalb WSL)"]
        B3["Unix socket<br/>(lokales 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;

Messungen zeigen, dass ein einzelner docker inspect --type container‑Aufruf im Standardmodus etwa 1800 ms dauert, während derselbe Aufruf im optimierten Modus nur wenige Millisekunden benötigt. Der kumulative Effekt ist bei häufigen Aufrufen enorm.

Bekannte Probleme und Hinweise

  • Docker Desktop Autostart: Issue #9695 berichtet, dass Docker Desktop bei executeInWSL: true nicht automatisch startet. Der Bug wurde in Dev Containers 0.353.0‑pre‑release behoben.
  • WSL‑Service‑Forwarding: Issue #9194 zeigt, dass selbst bei executeInWSL: false die Erweiterung versucht, WSL‑Dienste (display/ssh‑agent/gpg‑agent) zu forwarden. Ab Version 0.337.0‑pre‑release kann dies über dev.containers.wslServiceForwarding deaktiviert werden.
  • Rancher Desktop‑Kompatibilität: Issue #10722 beschreibt, dass executeInWSL: true unter Rancher Desktop zu WSL1‑Fehlern führt. Der Bug ist noch offen; Rancher‑Desktop‑Nutzer sollten die Einstellung vorerst deaktivieren.
  • Unerwartete Aktivierung: Issue #11005 berichtet, dass executeInWSL: true in lokalen Windows‑Repos die Dev‑Container‑Initialisierung auslöst. Betroffene können die Einstellung pro Workspace statt global setzen.

Fazit

Die Fehlersuche bei hohem IO‑Verbrauch von VS Code Dev Container unter Windows erfordert ein systematisches Vorgehen: zunächst die Symptome identifizieren, dann mit Process Monitor, VS Code‑Tools und dem Windows Performance Analyzer die IO‑Quellen lokalisieren. Die Ursachenanalyse zeigt vier Hauptfaktoren: häufige Docker‑CLI‑Aufrufe über die Windows/WSL‑Grenze, Port‑Forward‑Leckagen, eine Reconnect‑Schleife im Extension‑Host und die grundsätzliche IO‑Verstärkung von WSL2‑Dateisystemen.

Unter den vorgeschlagenen Lösungen ist dev.containers.executeInWSL: true die zentrale Maßnahme, da sie den größten Overhead – die named‑pipe‑Kommunikation – eliminiert. Weitere Optimierungen (Port‑Forward‑Reduktion, Docker‑Desktop‑Tuning, VS Code‑Einstellungen) ergänzen die Kernlösung und mildern die übrigen Faktoren. Nutzer, die von diesem Problem betroffen sind, sollten zunächst die Ursachenanalyse durchführen, dann executeInWSL aktivieren und abschließend die ergänzenden Optimierungen je nach Bedarf anwenden.