IO المفرط في حاوية تطوير VS Code: شرح تكوين executeInWSL وتحليل السبب الجذري

تسجيل عملية التحقيق الكاملة لمشكلة IO المفرطة الناتجة عن ملحق حاوية تطوير VS Code على Windows، من تحديد الأعراض إلى تحليل السبب الجذري، وأخيراً حل اختناق اتصال Docker CLI عبر الحدود باستخدام dev.containers.executeInWSL كحل أساسي.

عند استخدام ملحق Dev Container في VS Code للتطوير المبني على الحاويات على Windows، قد يواجه بعض المستخدمين تباطؤاً ملحوظاً في النظام. يمكن ملاحظة ارتفاع مستمر في CPU وIO القرص لعملية Extension Host في إدارة المهام، حتى في حالة عدم النشاط. تسجل هذه المقالة عملية التحقيق الكاملة بدءاً من الأعراض إلى تحديد السبب الجذري وإيجاد الحل الأساسي.

الأعراض

يحدث تباطؤ النظام بعد الاتصال بحاوية التطوير عبر الملحق. يمكن ملاحظة ارتفاع مستمر في IO القرص ومعدل استخدام CPU لعملية Extension Host في إدارة المهام، حتى عندما لا يقوم المستخدم بأي عمليات تحرير أو طرفية. في الحالات القصوى، تتأثر سرعة استجابة سطح مكتب 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، مما يشير إلى أن Extension Host يستدعي Docker CLI بشكل متكرر.

استخدام أدوات VS Code المدمجة للتأكيد

عبر فتح Chromium DevTools باستخدام Help > Toggle Developer Tools، يمكن تسجيل ملف تعريف CPU في لوحة Performance،,可以看到大量时间消耗在子进程的 spawn 和 stdout 管道读取上. 将 Output 面板中的 “Dev Containers” 日志级别设置为 “trace”, 可以看到完整的命令调用序列: docker inspect --type container, docker version --format, docker exec, docker ps 等命令被反复执行. 从日志数据可以量化单次调用的开销: 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. عندما يفتح المستخدم مساحة عمل حاوية عبر ملحق 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) عبر IPC مع Extension Host المحلي (Node.js). يحتاج 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 بدء عملية فرعية جديدة، تتضمن مجموعة من عمليات IO مثل إنشاء العملية، وقراءة أنبوب stdout، وتحليل نتيجة JSON. في الوضع الافتراضي، تحتاج هذه العمليات إلى عبور حدود Windows/WSL، وأداء بروتوكول مشاركة ملفات 9P في WSL2 ضعيف عند التعامل مع IO للملفات الصغيرة والاتصالات القصيرة عالية التردد.

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 بالمنفذ المستهدف داخل الحاوية، وتعيد توجيه البيانات بشكل ثنائي بين المنفذ المحلي والمنفذ في الحاوية. المشكلة هي أنه عند قطع اتصال المتصفح أو العميل الآخر الذي يصل إلى المنفذ المعاد توجيهه، إذا لم تكن هناك آلية تنظيف في الوقت المناسب، ستستمر هذه العمليات في الوجود بدلاً من الإنهاء الطبيعي.

وفقاً للتحليل، تستهلك كل عملية إعادة توجيه منفذةFuori ذاكرة تبلغ حوالي 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

وفقاً للسجلات، عند فقدان الاتصال بالحاوية البعيدة بسبب انقطاع الشبكة أو أسباب أخرى، يوجد خطأ في منطق إعادة الاتصال في 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 ميجابايت في الدقيقة، لأن كل استدعاء متكرر يتراكم فيه سياق غير مُحرر على استدعاء المكدس. تم إصلاح هذه المشكلة في إصدار Remote-Containers 0.221.0-pre-release، لكن بالنسبة للمستخدمين الذين لم يقوموا بتحديث الملحق في الوقت المناسب، يمكن محاكاة انقطاع الشبكة (مثل إيقاف WiFi) لتحفيز هذه المشكلة، ولن يظهر مربع حوار提示 أبداً. هذه مشكلة برمجية مستقلة لا تتعلق بتكوين executeInWSL، لكنها ستتفاقم في إدراك المستخدم لتباطؤ النظام.

تضخيم IO عبر نظام الملفات WSL2

عند استخدام Docker Desktop مع الواجهة الخلفية WSL2 على Windows، توجد مشكلة أداء IO عبر نظام الملفات. يتواصل ملحق VS Code على جانب Windows مع docker daemon في WSL2 عبر pipe، وإذا تضمنت عمليات نظام الملفات داخل الحاوية الوصول إلى مسار /mnt/c (أي الوصول إلى أقراص Windows)، فإنها تحتاج إلى التحويل عبر بروتوكول مشاركة ملفات 9P. أداء بروتوكول 9P في WSL2 أقل بكثير من نظام الملفات الأصلي في سيناريوهات IO للملفات الصغيرة، ويمكن أن يصل زمن العملية الواحدة إلى 3-5 أضعاف المسار الأصلي.

بالإضافة إلى ذلك، وفقاً للتقرير، عند تشغيل حاويات x86 على ARM Mac عبر Rosetta، يتم تعيين قناع CPU affinity لعملية VS Code Server بشكل غير طبيعي لاستخدام نواة واحدة فقط، مما يؤدي إلى إرجاع nproc لـ 1 بدلاً من عدد الأنوية الفعلية. على الرغم من أن هذه المشكلة تظهر بشكل رئيسي على منصة macOS، إلا أنها تكشف عن عدم اتساق في تحكم Dev Container في معلمات جدولة العمليات عبر المنصات، وقد تظهر مشاكل مماثلة في بيئة WSL2 على Windows بأشكال مختلفة. يحرك executeInWSL: true موقع تنفيذ Docker CLI إلى داخل WSL، مما يقلل من تكرار IO عبر نظام الملفات، لكنه لا يمكنه القضاء تماماً على开销 9P الناتج عند الوصول إلى مسار /mnt/c داخل الحاوية.

الحلول

تبسيط تكوين إعادة توجيه المنافذ

يمكن أن يقلل تبسيط تكوين forwardPorts في devcontainer.json للاحتفاظ فقط بالمنافذ المطلوبة فعلياً عدد عمليات إعادة توجيه المنافذ بشكل ملحوظ، مما يقلل من مخاطر تسرب العمليات الموصوف في المشكلة. إغلاق نوافذ Dev Container غير المستخدمة يمكن أن يطلق موارد Extension Host وإعادة توجيه المنافذ المقابلة على الفور.

تحسين Docker Desktop

في لوحة Settings > Resources في Docker Desktop، قم بضبط تخصيص CPU والذاكرة بشكل مناسب لتجنب قيام docker daemon بجمع البيانات المهملة أو عمليات المبادلة المتكررة بسبب عدم كفاية الموارد. تأكد من استخدام أحدث إصدار من Docker Desktop، حيث يتحسن أداء الواجهة الخلفية WSL2 في كل إصدار. للسيناريوهات التي تتطلب أداءً عالياً،可以考虑 في تثبيت Docker Engine مباشرة داخل WSL2، تجاوز طبقةvirtualization في 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، يحدد هذا الإعداد ما إذا كان أمر docker يعمل على جانب Windows أو داخل WSL. بعد تعيينه إلى true، ستُنفذ جميع استدعاءات Docker CLI (بما في ذلك docker inspect وdocker version وdocker exec وdocker ps وما إلى ذلك) داخل WSL، وتتصل بـ docker daemon عبر Unix socket،,从而 يتجاوز overhead تحويل بروتوكول 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 طريقة التنفيذ إلى wsl -d <distro> -e docker للتنفيذ داخل WSL. يعمل Docker CLI بشكل أصلي داخل WSL، ويتواصل مع docker daemon في نفس مثيل WSL عبر Unix socket (IPC محلي). يكتمل هذا المسار تماماً داخل مساحة نواة Linux، مما يتجنب overhead跨边界 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 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 مللي ثانية، واستدعاء docker version حوالي 620 مللي ثانية،comes mainly from named pipe跨边界 communication and process creation overhead on the Windows/WSL boundary. After enabling executeInWSL: true, Docker CLI communicates with daemon via Unix socket inside WSL, and the latency of a single call can drop to millisecond level, with the cumulative effect improvement being particularly significant.

المشاكل المعروفة والملاحظات

على الرغم من أن dev.containers.executeInWSL يمكن أن يحسن أداء IO بشكل فعال، إلا أن هناك مشاكل معروفة يجب مراعاتها عند استخدامه.

مشكلة البدء التلقائي لـ Docker Desktop: تم الإبلاغ عن أنه عند executeInWSL: true، لا يبدأ Docker Desktop تلقائياً. تم إصلاح هذه المشكلة في إصدار Dev Containers 0.353.0-pre-release، ويجب على المستخدمين الذين يستخدمون الإصدارات الأحدث عدم مواجهة هذه المشكلة بعد الآن.

إعادة توجيه خدمة WSL: تم الإبلاغ عن أنه حتى عند تعيين executeInWSL إلى false، لا يزال الملحق يحاول الاتصال بـ WSL (لإعادة توجيه display/ssh-agent/gpg-agent). تم التحكم في هذا السلوك في إصدار 0.337.0-pre-release من خلال إضافة عنصر إعداد جديد dev.containers.wslServiceForwarding، ويمكن للمستخدمين إيقاف إعادة توجيه خدمة WSL بشكل مستقل.

توافق Rancher Desktop: تم الإبلاغ عن أنه عند استخدام Rancher Desktop بدلاً من Docker Desktop، سيؤدي executeInWSL: true إلى ظهور رسالة خطأ WSL1. هذه المشكلة لا تزال في حالة open، وقد يحتاج مستخدمو Rancher Desktop إلى تعطيل هذا الإعداد مؤقتاً.

التنشيط غير المتوقع: تم الإبلاغ عن أن executeInWSL: true سيؤدي إلى تنشيط غير متوقع لعملية تهيئة Dev Container في مستودع Windows المحلي. هذه المشكلة أيضاً في حالة open،可以考虑 للمستخدمين المتأثرين تقييد هذا الإعداد لمساحة عمل محددة بدلاً من التكوين العام.

ملخص

يتطلب التحقيق في مشكلة IO المفرط لـ 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 هو الإجراء الأكثر جوهرية، لأنه يقضي مباشرة على overhead اتصال named pipe لـ Docker CLI عبر حدود Windows/WSL، وينقل موقع تنفيذ استدعاءات CLI عالية التردد من Windows إلى داخل WSL، ويكمل IPC المحلي عبر Unix socket. الإجراءات الأخرى (تبسيط إعادة توجيه المنافذ، تحسين Docker Desktop، تحسين تكوين VS Code) serve as auxiliary measures to mitigate impacts from other factors to varying degrees. للمستخدمين الذين يعانون من هذه المشكلة، يُوصى باتباع عملية التحقيق في هذه المقالة لتأكيد السبب الجذري، ثم تمكين executeInWSL: true كأولوية، ثم اختيار استراتيجيات التحسين المساعدة المناسبة بناءً على السيناريو الفعلي.