VS Code Dev Container Zbyt wysokie IO: szczegółowa konfiguracja executeInWSL i analiza przyczyn źródłowych
Podczas korzystania z rozszerzenia VS Code Dev Container do tworzenia konteneryzowanego środowiska deweloperskiego w systemie Windows, niektórzy użytkownicy doświadczają znacznego spowolnienia systemu. W Menedżerze zadań można zaobserwować, że proces Extension Host wykazuje stale wysokie użycie procesora i operacji wejścia/wyjścia na dysku, nawet gdy użytkownik nie wykonuje żadnych aktywnych operacji. Niniejszy dokument opisuje pełny proces diagnozowania, od zidentyfikowania objawów problemu, przez stopniowe określenie przyczyny źródłowej, aż po znalezienie głównego rozwiązania.
Objawy problemu
Spowolnienie systemu występuje po nawiązaniu połączenia z kontenerem za pomocą rozszerzenia Dev Container. W Menedżerze zadań można zaobserwować, że proces Extension Host wykazuje stale wysokie operacje wejścia/wyjścia na dysku oraz wysokie użycie procesora, nawet gdy użytkownik nie wykonuje żadnych operacji edycji ani terminala, a te wskaźniki nie powracają do poziomu bezczynności. W skrajnych przypadkach czas reakcji całego pulpitu Windows jest zakłócony, a kursor myszy okresowo się zawiesza.
Proces diagnozowania
Lokalizacja źródła IO za pomocą Process Monitor
Sysinternals Process Monitor to pierwsze narzędzie do diagnozowania takich problemów. Po uruchomieniu procmon można ustawić filtr dla Process Name is Code.exe lub Process Name is Extension Host, aby obserwować w czasie rzeczywistym wszystkie operacje ReadFile/WriteFile oraz ich ścieżki i częstotliwość. W przefiltrowanych wynikach operacje na named pipe’ach rozpoczynające się od \\pipe\ występują z nienormalnie wysoką częstotliwością, nawet kilkadziesiąt razy na sekundę. Te operacje named pipe odpowiadają komunikacji między Docker CLI a demonem Docker, co wskazuje, że Extension Host często wywołuje Docker CLI.
Potwierdzenie za pomocą wbudowanych narzędzi VS Code
Otwierając Chromium DevTools poprzez Help > Toggle Developer Tools i nagrywając profil CPU w panelu Performance, można zobaczyć, że Extension Host poświęca dużo czasu na spawnowanie procesów potomnych i odczytywanie ze strumienia stdout. Ustawiając poziom logowania “Dev Containers” w panelu Output na “trace”, można zobaczyć pełną sekwencję wywołań poleceń: docker inspect --type container, docker version --format, docker exec, docker ps i inne polecenia są wykonywane wielokrotnie. Na podstawie danych logu z issue #9194 można określić koszt pojedynczego wywołania: docker inspect --type container trwa około 1800ms, a docker version około 620ms.
Analiza IO na poziomie systemu za pomocą Windows Performance Analyzer
W celu głębszej analizy można rozpocząć nagrywanie ETW za pomocą wpr.exe -start GeneralProfile -filemode, odtworzyć problem, a następnie zatrzymać nagrywanie za pomocą wpr.exe -stop capture.etl. Po załadowaniu wyników w Windows Performance Analyzer widok Disk I/O potwierdza, że Extension Host jest głównym źródłem operacji wejścia/wyjścia na dysku, a widok Process Life Cycle pokazuje liczne krótko żyjące procesy potomne wielokrotnie tworzone i niszczone - te procesy potomne są właśnie instancjami wywołań Docker CLI.
Analiza przyczyn źródłowych
Architektura komunikacji procesów w Dev Container
Wyniki diagnozy wskazują na architekturę wieloprocesowej komunikacji Dev Container. Gdy użytkownik otwiera przestrzeń roboczą kontenera za pomocą rozszerzenia Dev Container w systemie Windows, w rzeczywistości uruchamia wieloprocesowy łańcuch komunikacyjny przekraczający granicę między Windows a Linux.
flowchart LR
subgraph Windows["Windows Host"]
A["VS Code Client<br/>(Electron)"]
B["Extension Host<br/>(Node.js)"]
C["Docker CLI<br/>(Subprocess)"]
end
subgraph WSL2["WSL2 / Docker Desktop"]
D["Docker Daemon<br/>(dockerd)"]
end
subgraph Container["Inside Container"]
E["VS Code Server"]
F["Remote Extension Host"]
G["Port Forwarding Process<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;Lokalny klient VS Code (Electron) komunikuje się przez IPC z lokalnym Extension Host (Node.js). Extension Host musi nawiązać połączenie z VS Code Server wewnątrz kontenera, a to połączenie zależy od Docker CLI jako pośrednika. Po uruchomieniu VS Code Server w kontenerze, Remote Extension Host przejmuje wykonywanie rozszerzeń, a proces przekierowywania portów zapewnia tunel TCP dla lokalnej przeglądarki do dostępu do usług kontenera. Każdy element w tym łańcuchu może stać się źródłem amplifikacji IO.
Mechanizm odpytywania Docker CLI
Rozszerzenie Dev Containers uzyskuje i utrzymuje stan kontenera poprzez częste wywołania poleceń Docker CLI. Na etapie uruchamiania kontenera rozszerzenie wykonuje kolejno docker inspect --type container w celu uzyskania metadanych kontenera, docker version --format w celu sprawdzenia dostępności demona Docker, docker exec w celu wykonania poleceń wykrywania środowiska wewnątrz kontenera, oraz docker ps w celu wyświetlenia uruchomionych kontenerów. Te polecenia nie są wykonywane tylko raz podczas uruchamiania, ale są wywoływane wielokrotnie z określoną częstotliwością przez cały cykl życia kontenera.
Każde wywołanie Docker CLI uruchamia nowy proces potomny, obejmujący koszt tworzenia procesu, odczyt ze strumienia stdout, parsowanie wyniku JSON i szereg innych operacji IO. W trybie domyślnym wszystkie te operacje danych muszą przekraczać granicę Windows/WSL, a protokół udostępniania plików 9P w WSL2 ma słabą wydajność przy obsłudze dużej liczby małych operacji IO i wysokiej częstotliwości krótkich połączeń. Zgodnie z oficjalną dokumentacją Microsoft, operacji między systemami plików należy unikać, ale architektura Dev Container utrudnia całkowite wyeliminowanie tego narzutu.
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 outputWyciek połączeń przekierowywania portów
Mechanizm przekierowywania portów VS Code tworzy niezależny proces potomny Node.js dla każdego przekierowanego portu. Te procesy łączą się przez net.createConnection z docelowym portem wewnątrz kontenera i dwukierunkowo przekazują dane między portem lokalnym a portem kontenera. Problem polega na tym, że gdy przeglądarka lub inny klient uzyskuje dostęp do przekierowanego portu, a następnie rozłącza się, jeśli logika czyszczenia nie jest terminowa, te procesy przekierowywania będą się utrzymywać zamiast normalnie zakończyć działanie.
Zgodnie z analizą w microsoft/vscode-remote-release#5767, każdy wyciekły proces przekierowywania portów zajmuje około 26 MiB pamięci. W środowisku deweloperskim skonfigurowanym z wieloma przekierowanymi portami i częstym dostępem, liczba procesów może w krótkim czasie wzrosnąć z normalnych 2 do kilkudziesięciu. Poniższy fragment kodu przedstawia podstawowy wzorzec procesu przekierowywania portów, gdzie zdarzenie client.on('close') jest kluczowe dla prawidłowego zakończenia procesu.
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));
});
Gdy proces docker exec Docker CLI kończy się nienormalnie, ale proces Node.js wewnątrz kontenera nadal działa, te osierocone procesy nie mogą być prawidłowo odzyskane, co powoduje ciągły wzrost pamięci. Ten problem został częściowo naprawiony po wersji VS Code 1.62, ale może nadal występować w określonych warunkach sieciowych. Warto zauważyć, że wyciek przekierowywania portów nie ma bezpośredniego związku z executeInWSL - jest to wewnętrzna usterka mechanizmu przekierowywania portów VS Code.
Pętla ponownego łączenia Extension Host
Zgodnie z zapisem w microsoft/vscode-remote-release#6178, gdy połączenie z kontenerem zdalnym zostanie utracone z powodu przerwy w sieci lub innych przyczyn, logika ponownego łączenia w Extension Host zawiera błąd: funkcja async rekurencyjnie wywołuje samą siebie w bloku catch, powodując bezczynne obracanie się procesora. Stos wywołań pokazuje, że ta funkcja jest powtarzana w processTicksAndRejections w pętli bez warunku wyjścia.
flowchart TD
A["Connection Lost"] e1@--> B["Async Reconnect Function"]
B e2@--> C{"Connection Success?"}
C e3@-->|Yes| D["Resume Normal"]
C e4@-->|No| 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;Tymczasem pamięć Extension Host rośnie w tempie około 1 MB/minuty, ponieważ każde rekurencyjne wywołanie kumuluje niewydane konteksty na stosie wywołań. Ten problem został naprawiony w wersji Remote-Containers 0.221.0-pre-release, ale dla użytkowników, którzy nie zaktualizowali rozszerzenia na czas, wystarczy symulować jedno rozłączenie sieciowe (np. wyłączenie WiFi), aby wywołać ten problem, a okno dialogowe z informacją o utracie połączenia nigdy się nie pojawi. Jest to niezależna usterka oprogramowania niezwiązana z konfiguracją executeInWSL, ale pogłębia postrzegane przez użytkownika spowolnienie systemu.
Amplifikacja IO między systemami plików WSL2
Podczas korzystania z backendu WSL2 Docker Desktop w systemie Windows istnieje nieodłączny problem wydajności IO między systemami plików. Rozszerzenie VS Code w systemie Windows komunikuje się z demonem Docker w WSL2 przez pipe, a operacje na systemie plików kontenera, jeśli obejmują ścieżkę /mnt/c (tj. dostęp do dysku Windows), wymagają konwersji przez protokół udostępniania plików 9P. Protokół 9P w WSL2 ma znacznie niższą wydajność niż natywny system plików w scenariuszach z dużą liczbą małych operacji IO, a opóźnienie pojedynczej operacji może być 3-5 razy większe niż w przypadku natywnych ścieżek.
Ponadto, zgodnie z raportem w microsoft/vscode-remote-release#9372, podczas uruchamiania kontenera x86 przez Rosetta na ARM Mac, maska CPU affinity procesu VS Code Server jest nieprawidłowo ustawiana na używanie tylko jednego rdzenia, powodując, że nproc zwraca 1 zamiast rzeczywistej liczby rdzeni fizycznych. Chociaż ten problem występuje głównie na platformie macOS, ujawnia on, że Dev Container ma niespójność w kontrolowaniu parametrów planowania procesów na różnych platformach - podobne problemy mogą występować w środowisku WSL2 w Windows w innej formie. executeInWSL: true przenosi wykonywanie Docker CLI do wnętrza WSL, zmniejszając częstotliwość występowania IO między systemami plików, ale nie może całkowicie wyeliminować narzutu 9P powstającego, gdy kontener uzyskuje dostęp do ścieżki /mnt/c.
Rozwiązania
Optymalizacja konfiguracji przekierowywania portów
Uproszczenie konfiguracji forwardPorts w devcontainer.json do zachowania tylko rzeczywiście potrzebnych portów może znacząco zmniejszyć liczbę procesów przekierowywania portów i obniżyć ryzyko wycieku procesów opisanego w issue #5767. Zamykanie nieużywanych okien Dev Container natychmiast zwalnia odpowiednie zasoby Extension Host i przekierowywania portów.
Optymalizacja Docker Desktop
W panelu Settings > Resources w Docker Desktop należy odpowiednio dostosować alokację CPU i pamięci, aby uniknąć częstego garbage collection lub operacji swap przez demona Docker z powodu niewystarczających zasobów. Należy używać najnowszej wersji Docker Desktop, ponieważ wydajność backendu WSL2 jest poprawiana w każdej wersji. W scenariuszach o wysokich wymaganiach wydajnościowych można rozważyć bezpośrednią instalację Docker Engine wewnątrz WSL2, pomijając warstwę wirtualizacji Docker Desktop, co dodatkowo eliminuje dodatkowy narzut granicy Windows/WSL.
Optymalizacja konfiguracji VS Code
Wyłączenie niepotrzebnych rozszerzeń może zmniejszyć obciążenie Extension Host, szczególnie tych, które działają w kontenerze zdalnym i mają funkcje监视owania plików (takich jak usługa językowa TypeScript, ESLint itp.). Ustawienie files.watcherExclude w settings.json w celu wykluczenia dużych katalogów takich jak node_modules, .git, dist może zmniejszyć IO generowane przez监视owanie systemu plików. Ustawienie extensions.autoUpdate: false może zapobiec dodatkowym operacjom sieciowym i dyskowym wyzwalanym przez aktualizacje rozszerzeń w tle w środowisku kontenera.
Alternatywne rozwiązania
Jeśli powyższe środki nadal nie spełniają wymagań wydajnościowych, można rozważyć użycie rozszerzenia VS Code Remote-SSH do połączenia z WSL2 i bezpośrednie używanie Docker CLI wewnątrz WSL2 do zarządzania kontenerami. To podejście zmienia wywołania Docker CLI z komunikacji między granicą Windows/WSL na lokalną komunikację wewnątrz WSL2. Innym sposobem jest użycie Docker Compose do zarządzania cyklem życia kontenera - po uruchomieniu usług za pomocą docker compose up -d można używać tylko Remote-SSH do łączenia się z kontenerem w celu tworzenia oprogramowania, całkowicie pomijając mechanizm odpytywania rozszerzenia Dev Container.
Włączenie executeInWSL (rozwiązanie główne)
Powyższe środki mogą łagodzić problem nadmiernego IO w różnym stopniu, ale albo tylko zmniejszają częstotliwość IO (optymalizacja przekierowywania portów), albo tylko optymalizują alokację zasobów (opymalizacja Docker Desktop), nie sięgając do głównej przyczyny problemu: narzutu komunikacji named pipe przy wywołaniach Docker CLI przekraczających granicę Windows/WSL. dev.containers.executeInWSL jest bezpośrednim rozwiązaniem tej głównej przyczyny.
Zgodnie z jasnym wyjaśnieniem członka zespołu VS Code, chrmarti, w microsoft/vscode-remote-release#9194, to ustawienie określa, czy polecenie docker jest uruchamiane po stronie Windows czy wewnątrz WSL. Po ustawieniu na true, wszystkie wywołania Docker CLI (w tym docker inspect, docker version, docker exec, docker ps itp.) będą wykonywane wewnątrz WSL, komunikując się bezpośrednio z demonem Docker przez Unix socket, omijając w ten sposób narzut konwersji named pipe i protokołu 9P na granicy Windows/WSL.
Aby włączyć to ustawienie, należy dodać następującą konfigurację w settings.json:
{
"dev.containers.executeInWSL": true
}
Aby zrozumieć, dlaczego ta konfiguracja może znacząco poprawić wydajność IO, należy porównać różnice w ścieżkach komunikacji w dwóch trybach. W trybie domyślnym (executeInWSL: false lub nie ustawione), Extension Host VS Code działa w systemie Windows, a za każdym razem, gdy potrzebuje interakcji z demonem Docker, uruchamia podproces docker.exe w systemie Windows przez spawn. Ten podproces komunikuje się z demonem Docker przez named pipe (ścieżka rozpoczynająca się od \\pipe\), przekraczając granicę Windows/WSL. Ta ścieżka obejmuje trzy etapy: tworzenie procesu Windows, IO named pipe przekraczające granicę i przesyłanie strumienia stdout z powrotem, z których każdy wprowadza dodatkowe opóźnienie i narzut IO. Gdy executeInWSL: true, Extension Host zmienia się na wykonywanie Docker CLI wewnątrz WSL przez wsl -d <distro> -e docker. Docker CLI działa natywnie wewnątrz WSL i komunikuje się z demonem Docker przez Unix socket (lokalny IPC) w tej samej instancji WSL. Ta ścieżka jest całkowicie realizowana wewnątrz przestrzeni jądra Linux, unikając narzutu named pipe przekraczającego granicę.
flowchart TB
subgraph Default["Default Mode (executeInWSL: false)"]
direction LR
A1["Extension Host<br/>(Windows)"]
A2["docker.exe<br/>(Windows Subprocess)"]
A3["named pipe<br/>(\\\\pipe\\\\)"]
A4["Docker Daemon<br/>(WSL2)"]
A1 e1@--> A2
A2 e2@--> A3
A3 e3@--> A4
end
subgraph Optimized["Optimized Mode (executeInWSL: true)"]
direction LR
B1["Extension Host<br/>(Windows)"]
B2["wsl -e docker<br/>(Inside WSL)"]
B3["Unix socket<br/>(Local 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;Na podstawie skwantyfikowanych danych z etapu diagnozowania można zweryfikować tę poprawę: w trybie domyślnym pojedyncze wywołanie docker inspect --type container trwa około 1800ms, a wywołanie docker version trwa około 620ms - te opóźnienia wynikają głównie z narzutu komunikacji named pipe i tworzenia procesu na granicy Windows/WSL. Po włączeniu executeInWSL: true, Docker CLI komunikuje się z daemonem przez Unix socket wewnątrz WSL, a opóźnienie pojedynczego wywołania może spaść do poziomu milisekund, a skumulowany efekt poprawy jest szczególnie znaczący.
Znane problemy i uwagi
Chociaż dev.containers.executeInWSL może skutecznie poprawić wydajność IO, należy pamiętać o następujących znanych problemach podczas jego używania.
Problem automatycznego uruchamiania Docker Desktop: issue #9695 raportuje, że przy executeInWSL: true Docker Desktop nie uruchamia się automatycznie. Ten problem został naprawiony w wersji Dev Containers 0.353.0-pre-release, więc użytkownicy używający nowszych wersji nie powinni już go doświadczać.
Przekierowanie usług WSL: issue #9194 raportuje, że nawet po ustawieniu executeInWSL na false, rozszerzenie nadal próbuje połączyć się z WSL (do przekierowywania display/ssh-agent/gpg-agent). To zachowanie zostało naprawione w wersji 0.337.0-pre-release poprzez dodanie nowego elementu ustawień dev.containers.wslServiceForwarding, który pozwala użytkownikom niezależnie wyłączyć przekierowanie usług WSL.
Kompatybilność z Rancher Desktop: issue #10722 raportuje, że przy użyciu Rancher Desktop zamiast Docker Desktop, executeInWSL: true wyzwala błąd WSL1. Ten problem jest obecnie nadal otwarty, a użytkownicy Rancher Desktop mogą tymczasowo potrzebować wyłączyć to ustawienie.
Nieoczekiwana aktywacja: issue #11005 raportuje, że executeInWSL: true nieoczekiwanie inicjuje proces Dev Container w lokalnym repozytorium Windows. Ten problem jest również otwarty, a dotknięci użytkownicy mogą rozważyć ograniczenie tego ustawienia do określonych obszarów roboczych zamiast konfiguracji globalnej.
Podsumowanie
Diagnozowanie problemu nadmiernego IO Dev Container w systemie Windows wymaga stopniowego określania przyczyny źródłowej na podstawie objawów. Za pomocą Process Monitor można potwierdzić, że głównym źródłem IO jest komunikacja named pipe Docker CLI, a za pomocą wbudowanych narzędzi VS Code i Windows Performance Analyzer można dalej kwantyfikować częstotliwość wywołań i opóźnienia. Analiza przyczyn źródłowych ujawnia cztery nakładające się czynniki: wysokoczęstotliwościowe odpytywanie Docker CLI przekraczające granicę jest głównym wąskim gardłem wydajności, wyciek połączeń procesów przekierowywania portów i pętla ponownego łączenia Extension Host to potwierdzone błędy oprogramowania, a amplifikacja IO między systemami plików WSL2 to nieodłączne ograniczenie na poziomie platformy.
Wśród rozwiązań dev.containers.executeInWSL: true jest najważniejszym środkiem - bezpośrednio eliminuje narzut komunikacji named pipe Docker CLI przekraczającej granicę Windows/WSL, przenosząc wykonywanie wysokoczęstotliwościowych wywołań CLI z Windows do wnętrza WSL i realizując lokalny IPC przez Unix socket. Pozostałe rozwiązania (optymalizacja przekierowywania portów, optymalizacja Docker Desktop, optymalizacja konfiguracji VS Code) służą jako środki pomocnicze, łagodząc wpływ innych czynników w różnym stopniu. Użytkownicy dotknięci tym problemem powinni najpierw potwierdzić przyczynę źródłową zgodnie z procesem diagnozowania w tym dokumencie, a następnie w pierwszej kolejności włączyć executeInWSL: true, a następnie wybrać odpowiednie strategie optymalizacji pomocniczej na podstawie rzeczywistego scenariusza.