VS Code Dev Container IO مرتفع: شرح إعداد executeInWSL وتحليل السبب الجذري
في Windows، عند استخدام امتداد Dev Container في VS Code لتطوير حاويات، قد يواجه بعض المستخدمين بطء واضح في النظام. يمكن ملاحظة ارتفاع مستمر في استهلاك CPU وقراءة IO للقرص من قبل عملية Extension Host في مدير المهام، حتى دون أي نشاط صريح. يوضح هذا المقال عملية التحقيق الكاملة بدءًا من الظاهرة، مرورًا بتحديد السبب الجذري، وصولًا إلى الحل الأساسي.
الظاهرة
يظهر بطء النظام بعد اتصال امتداد Dev Container بالحاوية. من خلال مدير المهام يمكن ملاحظة أن عملية Extension Host تستهلك قراءات IO للقرص وCPU بشكل مستمر، حتى عندما لا يقوم المستخدم بأي تحرير أو تشغيل طرفية. في الحالات القصوى، يتأثر استجابة سطح مكتب Windows بالكامل، ويظهر تأخر متقطع لمؤشر الفأرة.
عملية التحقيق
تحديد مصدر IO باستخدام Process Monitor
Sysinternals Process Monitor هو الأداة الأولى للتحقق من هذه المشكلة. بعد تشغيل procmon، اضبط الفلتر على Process Name is Code.exe أو Process Name is Extension Host لتراقب جميع عمليات ReadFile/WriteFile والمسارات وترددها. في النتائج، تظهر عمليات named pipe التي تبدأ بـ \\pipe\ بتردد غير طبيعي، تصل إلى عدة عشرات في الثانية. هذه العمليات تمثل التواصل بين Docker CLI وDocker daemon، مما يدل على أن Extension Host يستدعي Docker CLI بشكل متكرر.
التحقق باستخدام أدوات VS Code المدمجة
من خلال Help > Toggle Developer Tools افتح Chromium DevTools، وسجل ملف الأداء في لوحة 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 ms، وdocker version حوالي 620 ms.
تحليل IO على مستوى النظام باستخدام Windows Performance Analyzer
لتحليل أعمق، استخدم 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. عندما يفتح المستخدم مساحة عمل حاوية عبر امتداد 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 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؛ فهو عيب في آلية تحويل المنفذ نفسها.
حلقة إعادة الاتصال في Extension Host
وفقًا لتوثيق microsoft/vscode-remote-release#6178، عندما تُفقد الاتصال بالحاوية بسبب انقطاع الشبكة أو سبب آخر، يحتوي منطق إعادة الاتصال في Extension Host على خطأ: دالة async تستدعي نفسها داخل كتلة 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، توجد مشكلة أساسية في أداء IO عبر أنظمة الملفات. يتواصل امتداد VS Code مع Docker daemon داخل WSL2 عبر pipe، وعند الوصول إلى ملفات على مسار /mnt/c (أي ملفات Windows)، يتم تحويل العملية عبر بروتوكول 9P. هذا التحويل يضيف تأخيرًا كبيرًا؛ حيث يمكن أن تكون مدة العملية 3‑5 مرات أطول من مسار الملفات الأصلي.
بالإضافة إلى ذلك، وفقًا لتقارير microsoft/vscode-remote-release#9372، عند تشغيل حاويات x86 على أجهزة ARM Mac عبر Rosetta، تُضبط قناع affinity للمعالج الخاص بـ VS Code Server على نواة واحدة فقط، مما يجعل nproc يُعيد 1 بدلاً من عدد الأنوية الفعلية. رغم أن هذه المشكلة تقتصر على macOS، فإنها تُظهر عدم تجانس التحكم في معلمات جدولة العمليات عبر المنصات، وقد تظهر بأشكال مختلفة في بيئات WSL2 على Windows. ضبط executeInWSL: true ينقل تنفيذ Docker CLI إلى داخل WSL، مما يقلل من تكرار عمليات IO عبر نظام الملفات، لكنه لا يلغي تمامًا تكلفة الوصول إلى /mnt/c.
الحلول
تبسيط إعدادات تحويل المنفذ
في ملف devcontainer.json، قلل عدد المنافذ في forwardPorts إلى ما هو ضروري فقط. سيقلل ذلك من عدد عمليات تحويل المنفذ وبالتالي يقلل من خطر تسرب العمليات المذكور في issue #5767. إغلاق نوافذ Dev Container غير المستخدمة يحرّر موارد Extension Host وعمليات تحويل المنفذ فورًا.
تحسين Docker Desktop
في لوحة Settings > Resources داخل Docker Desktop، اضبط تخصيص CPU والذاكرة لتجنب عمليات جمع القمامة المتكررة أو الـ swap داخل Docker daemon. احرص على استخدام أحدث نسخة من Docker Desktop، حيث تُحسّن كل نسخة أداء الخلفية WSL2. للسيناريوهات ذات المتطلبات العالية، يمكن تثبيت Docker Engine مباشرة داخل WSL2 وتجاوز طبقة الافتراضية في Docker Desktop، مما يقلل من الحمل الإضافي على حدود Windows/WSL.
تحسين إعدادات VS Code
عطّل الامتدادات غير الضرورية لتقليل حمل Extension Host، خاصة تلك التي تعمل داخل الحاوية وتراقب الملفات (مثل TypeScript language service، ESLint). في settings.json، اضبط files.watcherExclude لاستبعاد node_modules، .git، dist وغيرها من الأدلة الكبيرة لتقليل IO الناتج عن مراقبة النظام. ضبط extensions.autoUpdate: false يمنع تحديثات الخلفية للامتدادات داخل الحاوية التي قد تُحدث عمليات IO إضافية.
بدائل
إذا لم تُرضِ الحلول السابقة متطلبات الأداء، يمكن استخدام امتداد VS Code Remote‑SSH للاتصال مباشرةً بـ WSL2 وإدارة الحاويات باستخدام Docker CLI داخل WSL. بهذه الطريقة يتحول استدعاء Docker CLI من اتصال عبر الحدود إلى اتصال محلي داخل WSL2. بديل آخر هو استخدام Docker Compose لإدارة دورة حياة الحاوية عبر docker compose up -d، ثم الاتصال بالحاوية عبر Remote‑SSH فقط، متجاوزًا آلية الاستطلاع المتكررة في امتداد Dev Container.
تفعيل executeInWSL (الحل الأساسي)
الحلول السابقة تخفّف من مشكلة ارتفاع IO بدرجات متفاوتة، لكن لا تعالج السبب الجذري: تكلفة التواصل عبر 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، وتستخدم Unix socket للتواصل مباشرةً مع Docker daemon، متجنبةً named pipe وبروتوكول 9P.
أضف الإعداد التالي إلى settings.json لتفعيل الميزة:
{
"dev.containers.executeInWSL": true
}
لفهم لماذا يُحسّن هذا الإعداد من أداء IO، قارن مسار التواصل في الوضعين:
الوضع الافتراضي (
executeInWSL: falseأو غير محدد)- Extension Host يعمل على Windows.
- كل استدعاء لـ Docker daemon يُنفّذ عبر
docker.exeعلى Windows. - يتواصل
docker.exeمع Docker daemon عبر named pipe (\\pipe\\). - هذه السلسلة تشمل إنشاء عملية Windows، نقل IO عبر named pipe، وإعادة توجيه stdout، مما يضيف تأخيرًا وعبء IO.
الوضع المحسّن (
executeInWSL: true)- Extension Host لا يزال على Windows، لكنه يستدعي
wsl -d <distro> -e dockerلتشغيل Docker CLI داخل WSL. - Docker CLI داخل WSL يتواصل مع Docker daemon عبر Unix socket (IPC محلي).
- جميع المراحل تظل داخل نواة Linux، متجنبةً overhead الـ named pipe.
- Extension Host لا يزال على Windows، لكنه يستدعي
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 inspect --type container يستغرق ~1800 ms في الوضع الافتراضي، بينما يُقلص إلى مستوى مليثانية عند تفعيل executeInWSL: true. التأثير المتراكم واضح عند تكرار الاستدعاءات.
المشكلات المعروفة والاعتبارات
- مشكلة تشغيل Docker Desktop تلقائيًا: وفقًا لـ issue #9695، لا يتم تشغيل Docker Desktop تلقائيًا عند تفعيل
executeInWSL: true. تم إصلاح ذلك في نسخة 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: عند استبدال Docker Desktop بـ Rancher Desktop، قد يتسبب
executeInWSL: trueفي ظهور خطأ WSL1. لا يزال هذا المفتوح، وقد يحتاج المستخدمون إلى تعطيل الإعداد مؤقتًا. - التنشيط غير المقصود: وفقًا لـ issue #11005، قد يُفعّل
executeInWSL: trueعملية تهيئة Dev Container في مستودعات Windows المحلية. يُنصح بتطبيق الإعداد على مساحة عمل محددة بدلاً من التكوين العام.
الخلاصة
يتطلب التحقيق في مشكلة ارتفاع IO في VS Code Dev Container على Windows اتباع نهج منهجي يبدأ من الظاهرة، يمر بتحليل السبب الجذري، ثم تطبيق حلول مركزة. يوضح تحليل Process Monitor وVS Code DevTools وWindows Performance Analyzer أن المصدر الرئيسي هو استدعاءات Docker CLI المتكررة عبر named pipe. بالإضافة إلى ذلك، تسرب عمليات تحويل المنفذ وحلقة إعادة الاتصال في Extension Host تُفاقم الوضع، بينما يضيف WSL2 قيودًا على أداء IO عبر نظام الملفات.
الحل الأساسي هو تفعيل dev.containers.executeInWSL: true، الذي يزيل تكلفة التواصل عبر الحدود وينقل استدعاءات Docker CLI إلى داخل WSL، محققًا تحسينًا كبيرًا في زمن الاستجابة وتقليل استهلاك IO. تُعد الإجراءات المساعدة (تقليل تحويل المنافذ، تحسين إعدادات Docker Desktop، تحسين إعدادات VS Code) مكملات تُخفّف من العوامل الثانوية. للمستخدمين المتأثرين، يُنصح باتباع خطوات التحقيق المذكورة، ثم تطبيق executeInWSL: true كخطوة أولى، ثم اختيار التحسينات الإضافية وفقًا لسيناريو الاستخدام.