أداء الإدخال / الإخراج من جانب الخادم: العقدة مقابل PHP مقابل Java مقابل Go
نشرت: 2022-03-11يمكن أن يعني فهم نموذج الإدخال / الإخراج (I / O) لتطبيقك الفرق بين التطبيق الذي يتعامل مع الحمل الذي يتعرض له ، والتطبيق الذي يتداعى في مواجهة حالات الاستخدام الواقعية. ربما بينما يكون تطبيقك صغيرًا ولا يخدم أحمالًا عالية ، فقد يكون أقل أهمية بكثير. ولكن مع زيادة عبء حركة المرور على تطبيقك ، فإن العمل باستخدام نموذج الإدخال / الإخراج الخاطئ يمكن أن يوصلك إلى عالم من الأذى.
ومثل معظم المواقف التي تكون فيها الطرق المتعددة ممكنة ، فالمسألة ليست فقط أيهما أفضل ، إنها مسألة فهم المفاضلات. لنقم بجولة عبر مناظر I / O ونرى ما يمكننا التجسس عليه.
في هذه المقالة ، سنقارن Node و Java و Go و PHP مع Apache ، ونناقش كيف تصوغ اللغات المختلفة I / O ، ومزايا وعيوب كل نموذج ، وننتهي ببعض المعايير الأولية. إذا كنت قلقًا بشأن أداء الإدخال / الإخراج لتطبيق الويب التالي ، فهذه المقالة مناسبة لك.
أساسيات الإدخال / الإخراج: تحديث سريع
لفهم العوامل المتضمنة في الإدخال / الإخراج ، يجب علينا أولاً مراجعة المفاهيم لأسفل على مستوى نظام التشغيل. في حين أنه من غير المحتمل أن يتعامل مع العديد من هذه المفاهيم بشكل مباشر ، فإنك تتعامل معها بشكل غير مباشر من خلال بيئة وقت تشغيل التطبيق الخاص بك طوال الوقت. والتفاصيل مهمة.
مكالمات النظام
أولاً لدينا استدعاءات النظام والتي يمكن وصفها كالتالي:
- يجب أن يطلب برنامجك (في "أرض المستخدم" ، كما يقولون) من نواة نظام التشغيل إجراء عملية إدخال / إخراج نيابة عنها.
- "syscall" هي الوسيلة التي يطلب من خلالها برنامجك من kernel أن تفعل شيئًا ما. تختلف تفاصيل كيفية تنفيذ ذلك بين أنظمة تشغيل ولكن المفهوم الأساسي هو نفسه. ستكون هناك بعض التعليمات المحددة التي تنقل التحكم من برنامجك إلى النواة (مثل استدعاء الوظيفة ولكن مع بعض الصلصة الخاصة للتعامل مع هذا الموقف تحديدًا). بشكل عام ، يتم حظر عمليات syscalls ، مما يعني أن برنامجك ينتظر عودة kernel إلى التعليمات البرمجية الخاصة بك.
- ينفذ kernel عملية الإدخال / الإخراج الأساسية على الجهاز الفعلي المعني (القرص ، بطاقة الشبكة ، إلخ) والرد على طلب النظام. في العالم الحقيقي ، قد يتعين على kernel القيام بعدد من الأشياء لتلبية طلبك بما في ذلك انتظار أن يكون الجهاز جاهزًا ، وتحديث حالته الداخلية ، وما إلى ذلك ، ولكن بصفتك مطور تطبيق ، فأنت لا تهتم بذلك. هذا هو عمل النواة.
حظر مقابل المكالمات غير المحظورة
الآن ، لقد قلت للتو أعلاه أن عمليات النظام تمنع ، وهذا صحيح بالمعنى العام. ومع ذلك ، يتم تصنيف بعض المكالمات على أنها "غير محظورة" ، مما يعني أن النواة تأخذ طلبك ، وتضعه في قائمة انتظار أو مخزن مؤقت في مكان ما ، ثم يعود فورًا دون انتظار حدوث الإدخال / الإخراج الفعلي. لذلك "يحجب" لفترة زمنية قصيرة جدًا ، فقط بما يكفي لإدراج طلبك.
قد تساعد بعض الأمثلة (لمكالمات النظام في Linux) في توضيح: - read()
عبارة عن مكالمة حظر - تقوم بتمريرها بمقبض يوضح الملف ومخزنًا مؤقتًا لمكان تسليم البيانات التي تقرأها ، وترجع المكالمة عندما تكون البيانات موجودة. لاحظ أن هذا يتميز بكونه لطيفًا وبسيطًا. - epoll_create()
و epoll_ctl()
و epoll_wait()
هي مكالمات تتيح لك ، على التوالي ، إنشاء مجموعة من المقابض للاستماع إليها وإضافة / إزالة معالجات من تلك المجموعة ثم حظرها حتى يكون هناك أي نشاط. يتيح لك هذا التحكم بكفاءة في عدد كبير من عمليات الإدخال / الإخراج باستخدام مؤشر ترابط واحد ، لكنني أتقدم على نفسي. يعد هذا أمرًا رائعًا إذا كنت بحاجة إلى الوظيفة ، ولكن كما ترى من المؤكد أنه أكثر تعقيدًا في الاستخدام.
من المهم فهم ترتيب مقدار الاختلاف في التوقيت هنا. إذا كان أحد نواة وحدة المعالجة المركزية يعمل بسرعة 3 جيجاهرتز ، دون الدخول في تحسينات يمكن أن تقوم بها وحدة المعالجة المركزية ، فإنها تؤدي 3 مليارات دورة في الثانية (أو 3 دورات في النانو ثانية). قد يستغرق استدعاء النظام غير المحظور 10 ثوانٍ من الدورات حتى يكتمل - أو "بضع نانوثانية نسبيًا". قد تستغرق المكالمة التي تحظر تلقي المعلومات عبر الشبكة وقتًا أطول بكثير - دعنا نقول على سبيل المثال 200 مللي ثانية (1/5 من الثانية). ودعونا نقول ، على سبيل المثال ، استغرقت المكالمة غير المحظورة 20 نانوثانية ، واستغرقت مكالمة الحظر 20000000 نانوثانية. لقد انتظرت العملية الخاصة بك 10 ملايين مرة أكثر من أجل مكالمة الحظر.
يوفر kernel الوسائل للقيام بكل من حظر الإدخال / الإخراج ("القراءة من اتصال الشبكة هذا وإعطائي البيانات") والإدخال / الإخراج غير المحظور ("أخبرني عندما يكون لأي من اتصالات الشبكة هذه بيانات جديدة"). وأي آلية يتم استخدامها سوف تمنع عملية الاستدعاء لفترات زمنية مختلفة بشكل كبير.
الجدولة
الشيء الثالث المهم الذي يجب اتباعه هو ما يحدث عندما يكون لديك الكثير من سلاسل الرسائل أو العمليات التي تبدأ في الحظر.
لأغراضنا ، لا يوجد فرق كبير بين الخيط والعملية. في الحياة الواقعية ، يتمثل الاختلاف الأكثر وضوحًا فيما يتعلق بالأداء في أنه نظرًا لأن الخيوط تشترك في نفس الذاكرة ، ولكل من العمليات مساحة ذاكرة خاصة بها ، فإن عمليات منفصلة تميل إلى شغل الكثير من الذاكرة. ولكن عندما نتحدث عن الجدولة ، فإن ما يتلخص فيه حقًا هو قائمة بالأشياء (سلاسل العمليات والعمليات على حد سواء) التي يحتاج كل منها للحصول على جزء من وقت التنفيذ على نوى وحدة المعالجة المركزية المتاحة. إذا كان لديك 300 موضوع قيد التشغيل و 8 مراكز لتشغيلها ، فيجب عليك تقسيم الوقت بحيث يحصل كل واحد على نصيبه ، مع تشغيل كل نواة لفترة قصيرة من الوقت ثم الانتقال إلى الخيط التالي. يتم ذلك من خلال "تبديل السياق" ، مما يجعل تبديل وحدة المعالجة المركزية من تشغيل مؤشر ترابط / عملية إلى التالية.
رموز تبديل السياق هذه لها تكلفة مرتبطة بها - فهي تستغرق بعض الوقت. في بعض الحالات السريعة ، قد يكون أقل من 100 نانوثانية ، ولكن ليس من غير المألوف أن تستغرق 1000 نانوثانية أو أكثر اعتمادًا على تفاصيل التنفيذ ، وسرعة المعالج / البنية ، وذاكرة التخزين المؤقت لوحدة المعالجة المركزية ، وما إلى ذلك.
وكلما زاد عدد الخيوط (أو العمليات) ، زاد تبديل السياق. عندما نتحدث عن آلاف الخيوط ومئات النانو ثانية لكل منها ، يمكن أن تصبح الأمور بطيئة للغاية.
ومع ذلك ، فإن المكالمات غير المحظورة في جوهرها تخبر النواة "اتصل بي فقط عندما يكون لديك بعض البيانات أو الأحداث الجديدة على أحد هذه الاتصالات." تم تصميم هذه المكالمات غير المحظورة للتعامل بكفاءة مع أحمال الإدخال / الإخراج الكبيرة وتقليل تبديل السياق.
معي حتى الآن؟ لأنه يأتي الآن الجزء الممتع: لنلق نظرة على ما تفعله بعض اللغات الشائعة بهذه الأدوات ونستخلص بعض الاستنتاجات حول المفاضلات بين سهولة الاستخدام والأداء ... وغيرها من الحكايات المثيرة للاهتمام.
كملاحظة ، في حين أن الأمثلة الموضحة في هذه المقالة تافهة (وجزئية ، مع عرض البتات ذات الصلة فقط) ؛ الوصول إلى قاعدة البيانات وأنظمة التخزين المؤقت الخارجية (memcache ، وآخرون) وأي شيء يتطلب I / O سينتهي به الأمر إلى إجراء نوع من مكالمات الإدخال / الإخراج تحت غطاء المحرك والذي سيكون له نفس تأثير الأمثلة البسيطة الموضحة. أيضًا ، بالنسبة للسيناريوهات التي يتم فيها وصف الإدخال / الإخراج بأنه "حظر" (PHP ، Java) ، فإن طلب HTTP والاستجابة يقرأ ويكتبان هما بحد ذاته يحظر المكالمات: مرة أخرى ، المزيد من الإدخال / الإخراج مخفي في النظام مع مشكلات الأداء المصاحبة أن تأخذ في الاعتبار.
هناك الكثير من العوامل التي تدخل في اختيار لغة برمجة لمشروع ما. هناك الكثير من العوامل عندما تفكر في الأداء فقط. ولكن ، إذا كنت قلقًا من أن برنامجك سيكون مقيدًا بشكل أساسي من خلال الإدخال / الإخراج ، وإذا كان أداء الإدخال / الإخراج ناجحًا أو معطلاً لمشروعك ، فهذه هي الأشياء التي تحتاج إلى معرفتها.
منهج "حافظ على البساطة": PHP
في التسعينيات ، كان الكثير من الناس يرتدون أحذية كونفيرس ويكتبون نصوص CGI بلغة بيرل. ثم ظهرت لغة PHP ، وبقدر ما يحب بعض الناس التخلص منها ، فقد جعلت جعل صفحات الويب الديناميكية أسهل بكثير.
النموذج الذي يستخدمه PHP بسيط إلى حد ما. هناك بعض الاختلافات في ذلك ولكن يبدو خادم PHP المتوسط الخاص بك كما يلي:
يأتي طلب HTTP من متصفح المستخدم ويصل إلى خادم الويب Apache. ينشئ Apache عملية منفصلة لكل طلب ، مع بعض التحسينات لإعادة استخدامها لتقليل عدد العمليات التي يتعين عليها القيام بها (إنشاء العمليات بطيء نسبيًا). يقوم Apache باستدعاء PHP ويخبرها بتشغيل ملف .php
المناسب على القرص. كود PHP ينفذ ويحظر مكالمات الإدخال / الإخراج. يمكنك استدعاء file_get_contents()
في PHP وتحت الغطاء ، تقوم بإجراء مكالمات read()
وتنتظر النتائج.
وبالطبع يتم تضمين الشفرة الفعلية في صفحتك مباشرةً ، والعمليات تمنع:
<?php // blocking file I/O $file_data = file_get_contents('/path/to/file.dat'); // blocking network I/O $curl = curl_init('http://example.com/example-microservice'); $result = curl_exec($curl); // some more blocking network I/O $result = $db->query('SELECT id, data FROM examples ORDER BY id DESC limit 100'); ?>
من حيث كيفية تكامل هذا مع النظام ، فالأمر كما يلي:
بسيط جدًا: عملية واحدة لكل طلب. حظر مكالمات I / O فقط. مميزات؟ إنه بسيط ويعمل. عيب؟ اضربها مع 20000 عميل في نفس الوقت وسيشتعل الخادم الخاص بك. لا يتسع هذا الأسلوب بشكل جيد لأن الأدوات التي توفرها النواة للتعامل مع الإدخال / الإخراج عالي الحجم (epoll ، إلخ) لا يتم استخدامها. ولزيادة الطين بلة ، فإن تشغيل عملية منفصلة لكل طلب يميل إلى استخدام الكثير من موارد النظام ، وخاصة الذاكرة ، والتي غالبًا ما تكون أول شيء تنفد منه في سيناريو مثل هذا.
ملحوظة: النهج المستخدم في Ruby يشبه إلى حد بعيد نهج PHP ، ويمكن اعتباره متشابهًا لأغراضنا بشكل عام وعامة ومموج يدويًا.
النهج متعدد الخيوط: جافا
لذا جاءت Java ، في الوقت الذي اشتريت فيه اسم نطاقك الأول وكان رائعًا أن تقول عشوائيًا "dot com" بعد جملة. وتحتوي Java على خاصية multithreading مضمنة في اللغة ، والتي (خاصة عند إنشائها) رائعة جدًا.
تعمل معظم خوادم ويب Java عن طريق بدء سلسلة تنفيذ جديدة لكل طلب يأتي ثم في هذا الموضوع تستدعي الوظيفة التي كتبتها بصفتك مطور التطبيق.
يميل إجراء I / O في Java Servlet إلى الظهور بالشكل التالي:
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // blocking file I/O InputStream fileIs = new FileInputStream("/path/to/file"); // blocking network I/O URLConnection urlConnection = (new URL("http://example.com/example-microservice")).openConnection(); InputStream netIs = urlConnection.getInputStream(); // some more blocking network I/O out.println("..."); }
نظرًا لأن طريقة doGet
أعلاه تتوافق مع طلب واحد ويتم تشغيلها في مؤشر ترابط خاص بها ، فبدلاً من عملية منفصلة لكل طلب تتطلب ذاكرته الخاصة ، لدينا سلسلة منفصلة. يحتوي هذا على بعض الامتيازات اللطيفة ، مثل القدرة على مشاركة الحالة ، والبيانات المخزنة مؤقتًا ، وما إلى ذلك بين سلاسل الرسائل لأنهم يستطيعون الوصول إلى ذاكرة بعضهم البعض ، لكن التأثير على كيفية تفاعلها مع الجدول لا يزال مطابقًا تقريبًا لما يتم إجراؤه في PHP المثال السابق. يحصل كل طلب على مؤشر ترابط جديد وكتلة عمليات الإدخال / الإخراج المختلفة داخل مؤشر الترابط هذا حتى تتم معالجة الطلب بالكامل. يتم تجميع الخيوط لتقليل تكلفة إنشائها وتدميرها ، ولكن مع ذلك ، فإن آلاف الوصلات تعني آلاف الخيوط وهو أمر سيئ بالنسبة إلى المجدول.
معلم مهم هو أنه في الإصدار 1.4 من Java (وترقية مهمة مرة أخرى في 1.7) اكتسبت القدرة على إجراء مكالمات I / O غير محظورة. معظم التطبيقات والويب وغير ذلك ، لا تستخدمها ، لكنها متوفرة على الأقل. تحاول بعض خوادم الويب Java الاستفادة من ذلك بطرق مختلفة ؛ ومع ذلك ، فإن الغالبية العظمى من تطبيقات Java المنشورة لا تزال تعمل كما هو موضح أعلاه.
تقربنا Java بشكل أكبر ولديها بالتأكيد بعض الوظائف الجيدة للإدخال / الإخراج ، لكنها لا تزال لا تحل مشكلة ما يحدث عندما يكون لديك تطبيق مرتبط بشدة بالإدخال / الإخراج يتم قصفه الأرض مع عدة آلاف من خيوط الحجب.
عدم حظر الإدخال / الإخراج كمواطن من الدرجة الأولى: العقدة
الطفل المشهور في الكتلة عندما يتعلق الأمر بإدخال / إخراج أفضل هو Node.js. تم إخبار أي شخص لديه حتى أقصر مقدمة عن Node أنه "غير محظور" وأنه يتعامل مع I / O بكفاءة. وهذا صحيح بشكل عام. لكن الشيطان يكمن في التفاصيل والوسائل التي تحققت بها هذه السحر عندما يتعلق الأمر بالأداء.
يتمثل التحول النموذجي الذي تنفذه Node بشكل أساسي في أنه بدلاً من القول بشكل أساسي "اكتب الكود الخاص بك هنا للتعامل مع الطلب" ، فإنهم بدلاً من ذلك يقولون "اكتب الرمز هنا لبدء معالجة الطلب". في كل مرة تحتاج إلى القيام بشيء يتضمن I / O ، تقوم بإجراء الطلب وتعطي وظيفة رد الاتصال التي ستتصل بها Node عند الانتهاء.

يشبه رمز العقدة النموذجي لإجراء عملية الإدخال / الإخراج في طلب ما على النحو التالي:
http.createServer(function(request, response) { fs.readFile('/path/to/file', 'utf8', function(err, data) { response.end(data); }); });
كما ترى ، هناك نوعان من وظائف رد الاتصال هنا. يتم استدعاء الأول عند بدء الطلب ، ويتم استدعاء الثاني عند توفر بيانات الملف.
ما يفعله هذا هو في الأساس إعطاء Node فرصة للتعامل بكفاءة مع I / O بين عمليات الاسترجاعات هذه. السيناريو الذي سيكون أكثر ملاءمة هو المكان الذي تجري فيه استدعاء قاعدة بيانات في Node ، لكنني لن أزعج هذا المثال لأنه نفس المبدأ بالضبط: تبدأ استدعاء قاعدة البيانات ، وتعطي Node وظيفة رد ، يؤدي عمليات الإدخال / الإخراج بشكل منفصل باستخدام المكالمات غير المحظورة ، ثم يستدعي وظيفة رد الاتصال عندما تكون البيانات التي طلبتها متاحة. تسمى آلية ترتيب مكالمات الإدخال / الإخراج هذه والسماح للعقدة بمعالجتها ثم الحصول على رد اتصال باسم "حلقة الأحداث". وهي تعمل بشكل جيد
ومع ذلك ، هناك فائدة لهذا النموذج. تحت الغطاء ، السبب وراء ذلك يرتبط كثيرًا بكيفية تنفيذ محرك V8 JavaScript (محرك Chrome JS الذي تستخدمه Node) 1 أكثر من أي شيء آخر. كود JS الذي تكتبه يعمل في سلسلة واحدة. فكر في ذلك للحظة. هذا يعني أنه بينما يتم تنفيذ الإدخال / الإخراج باستخدام تقنيات فعالة غير قابلة للحظر ، يمكن لـ JS الخاص بك أن يقوم بعمليات مرتبطة بوحدة المعالجة المركزية تعمل في مؤشر ترابط واحد ، كل جزء من التعليمات البرمجية يحظر التالي. من الأمثلة الشائعة على المكان الذي قد يظهر فيه ذلك هو تكرار سجلات قاعدة البيانات لمعالجتها بطريقة ما قبل إخراجها إلى العميل. إليك مثال يوضح كيفية عمل ذلك:
var handler = function(request, response) { connection.query('SELECT ...', function (err, rows) { if (err) { throw err }; for (var i = 0; i < rows.length; i++) { // do processing on each row } response.end(...); // write out the results }) };
بينما تقوم Node بمعالجة الإدخال / الإخراج بكفاءة ، فإن حلقة for
في المثال أعلاه تستخدم دورات وحدة المعالجة المركزية داخل مؤشر ترابط واحد فقط. هذا يعني أنه إذا كان لديك 10000 اتصال ، فإن هذه الحلقة يمكن أن تجلب التطبيق بأكمله إلى الزحف ، اعتمادًا على المدة التي يستغرقها. يجب أن يتشارك كل طلب في جزء من الوقت ، واحدًا تلو الآخر ، في سلسلة المحادثات الرئيسية الخاصة بك.
الفرضية التي يعتمد عليها هذا المفهوم بالكامل هي أن عمليات الإدخال / الإخراج هي أبطأ جزء ، وبالتالي فمن الأهمية بمكان التعامل معها بكفاءة ، حتى لو كان ذلك يعني القيام بمعالجة أخرى بشكل متسلسل. هذا صحيح في بعض الحالات ، ولكن ليس على الإطلاق.
النقطة الأخرى هي أنه على الرغم من أن هذا مجرد رأي ، إلا أنه قد يكون من الممل جدًا كتابة مجموعة من عمليات الاسترجاعات المتداخلة ويجادل البعض في أنها تجعل متابعة الكود أكثر صعوبة. ليس من غير المألوف رؤية عمليات الاسترجاعات متداخلة في أربعة أو خمسة مستويات أو حتى أكثر في عمق كود العقدة.
عدنا مرة أخرى إلى المقايضات. يعمل نموذج العقدة بشكل جيد إذا كانت مشكلة الأداء الرئيسية الخاصة بك هي الإدخال / الإخراج. ومع ذلك ، فإن كعب أخيل هو أنه يمكنك الانتقال إلى وظيفة تتعامل مع طلب HTTP ووضع رمز كثيف لوحدة المعالجة المركزية وإحضار كل اتصال إلى الزحف إذا لم تكن حريصًا.
غير محجوب بشكل طبيعي: Go
قبل أن أدخل إلى قسم Go ، من المناسب أن أفصح عن أنني معجب بـ Go. لقد استخدمتها في العديد من المشاريع وأنا مؤيد صراحة لمزايا الإنتاجية ، وأراها في عملي عندما أستخدمها.
بعد قولي هذا ، دعنا نلقي نظرة على كيفية تعاملها مع I / O. تتمثل إحدى السمات الرئيسية للغة Go في احتوائها على برنامج الجدولة الخاص بها. بدلاً من كل سلسلة تنفيذ تتوافق مع مؤشر ترابط واحد لنظام التشغيل ، فإنها تعمل بمفهوم "goroutines". ويمكن لـ Go runtime تعيين goroutine إلى مؤشر ترابط OS وجعله ينفذه أو يعلقه وجعله غير مرتبط بخيط OS ، بناءً على ما يفعله هذا goroutine. يتم التعامل مع كل طلب يأتي من خادم HTTP الخاص بـ Go في Goroutine منفصلة.
يبدو الرسم التخطيطي لكيفية عمل المجدول كما يلي:
تحت الغطاء ، يتم تنفيذ ذلك من خلال نقاط مختلفة في وقت تشغيل Go الذي ينفذ استدعاء I / O من خلال تقديم طلب للكتابة / القراءة / الاتصال / وما إلى ذلك ، ضع goroutine الحالي في وضع السكون ، مع المعلومات لتنبيه goroutine مرة أخرى حتى عندما يمكن اتخاذ مزيد من الإجراءات.
في الواقع ، يقوم Go runtime بعمل شيء لا يختلف بشكل رهيب عما تفعله Node ، باستثناء أن آلية رد الاتصال مدمجة في تنفيذ مكالمة الإدخال / الإخراج وتتفاعل مع المجدول تلقائيًا. كما أنه لا يعاني من قيود الاضطرار إلى تشغيل كل كود المعالج الخاص بك في نفس سلسلة الرسائل ، وسوف يقوم Go تلقائيًا بتعيين Goroutines الخاصة بك إلى العديد من مؤشرات الترابط التي يراها مناسبة بناءً على المنطق الموجود في برنامج الجدولة الخاص به. والنتيجة هي رمز مثل هذا:
func ServeHTTP(w http.ResponseWriter, r *http.Request) { // the underlying network call here is non-blocking rows, err := db.Query("SELECT ...") for _, row := range rows { // do something with the rows, // each request in its own goroutine } w.Write(...) // write the response, also non-blocking }
كما ترون أعلاه ، فإن بنية الشفرة الأساسية لما نقوم به تشبه تلك الموجودة في الأساليب الأكثر بساطة ، ومع ذلك فهي تحقق عمليات إدخال / إخراج غير معطلة تحت الغطاء.
في معظم الحالات ، ينتهي هذا الأمر بكونه "أفضل ما في العالمين". يتم استخدام الإدخال / الإخراج غير المحظور لجميع الأشياء المهمة ، ولكن يبدو أن الكود الخاص بك يحظر ، وبالتالي يميل إلى أن يكون أسهل في الفهم والصيانة. التفاعل بين جدولة Go وجدولة نظام التشغيل يعالج الباقي. إنه ليس سحرًا كاملًا ، وإذا قمت ببناء نظام كبير ، فإن الأمر يستحق تخصيص الوقت لفهم المزيد من التفاصيل حول كيفية عمله ؛ ولكن في نفس الوقت ، البيئة التي تحصل عليها "خارج الصندوق" تعمل وتتوسع بشكل جيد.
قد يكون لدى Go عيوبه ، ولكن بشكل عام ، فإن الطريقة التي يتعامل بها مع الإدخال / الإخراج ليست من بينها.
أكاذيب وأكاذيب ملعونه ومعايير
من الصعب إعطاء توقيتات دقيقة لتبديل السياق المتضمن مع هذه النماذج المختلفة. يمكنني أيضًا أن أزعم أنه أقل فائدة بالنسبة لك. لذا بدلاً من ذلك ، سأقدم لك بعض المعايير الأساسية التي تقارن أداء خادم HTTP الإجمالي لبيئات الخادم هذه. ضع في اعتبارك أن الكثير من العوامل متضمنة في أداء مسار طلب / استجابة HTTP الكامل من طرف إلى طرف ، والأرقام المقدمة هنا ليست سوى بعض العينات التي جمعتها معًا لإعطاء مقارنة أساسية.
لكل من هذه البيئات ، كتبت الكود المناسب للقراءة في ملف 64 كيلو بايت مع بايت عشوائي ، وقمت بتشغيل تجزئة SHA-256 عليه عدد N من المرات (يتم تحديد N في سلسلة استعلام عنوان URL ، على سبيل المثال ، .../test.php?n=100
) واطبع التجزئة الناتجة في شكل سداسي عشري. لقد اخترت هذا لأنه طريقة بسيطة للغاية لتشغيل نفس المعايير مع بعض الإدخال / الإخراج المتسق وطريقة محكومة لزيادة استخدام وحدة المعالجة المركزية.
راجع ملاحظات المعيار هذه للحصول على مزيد من التفاصيل حول البيئات المستخدمة.
أولاً ، دعنا نلقي نظرة على بعض أمثلة التزامن المنخفض. تشغيل 2000 تكرار مع 300 طلب متزامن وتجزئة واحدة فقط لكل طلب (N = 1) يعطينا هذا:
من الصعب استخلاص استنتاج من هذا الرسم البياني فقط ، ولكن هذا بالنسبة لي يبدو أنه ، في هذا الحجم من الاتصال والحساب ، نرى أوقاتًا تتعلق بالتنفيذ العام للغات نفسها ، وأكثر من ذلك بكثير I / O. لاحظ أن اللغات التي تعتبر "لغات برمجة" (كتابة فضفاضة ، تفسير ديناميكي) تؤدي أبطأ أداء.
ولكن ماذا يحدث إذا زدنا N إلى 1000 ، لا يزال لدينا 300 طلب متزامن - نفس الحمل ولكن 100x تكرار تجزئة أكثر (تحميل أكبر بكثير لوحدة المعالجة المركزية):
فجأة ، انخفض أداء Node بشكل كبير ، لأن العمليات كثيفة الاستخدام لوحدة المعالجة المركزية في كل طلب تحظر بعضها البعض. ومن المثير للاهتمام أن أداء PHP يتحسن كثيرًا (مقارنة بالآخرين) ويتفوق على Java في هذا الاختبار. (من الجدير بالذكر أن تطبيق SHA-256 في PHP مكتوب بلغة C وأن مسار التنفيذ يقضي الكثير من الوقت في هذه الحلقة ، نظرًا لأننا نجري 1000 تكرار تجزئة الآن).
لنجرب الآن 5000 اتصال متزامن (مع N = 1) - أو أقرب ما يمكن من ذلك. لسوء الحظ ، بالنسبة لمعظم هذه البيئات ، لم يكن معدل الفشل ضئيلًا. بالنسبة إلى هذا المخطط ، سننظر إلى العدد الإجمالي للطلبات في الثانية. كلما ارتفع كان ذلك أفضل :
والصورة تبدو مختلفة تماما. إنه تخمين ، لكن يبدو أنه في حجم الاتصال العالي ، يبدو أن الحمل الزائد لكل اتصال المتضمن في إنتاج عمليات جديدة والذاكرة الإضافية المرتبطة به في PHP + Apache أصبح عاملاً مهيمناً ويعزز أداء PHP. من الواضح أن Go هو الفائز هنا ، يليه Java و Node وأخيراً PHP.
على الرغم من أن العوامل المرتبطة بالإنتاجية الإجمالية كثيرة وتختلف أيضًا على نطاق واسع من تطبيق إلى آخر ، فكلما فهمت أكثر عن شجاعة ما يجري تحت غطاء المحرك والمفاضلات التي ينطوي عليها الأمر ، كان ذلك أفضل حالًا.
باختصار
مع كل ما سبق ، من الواضح تمامًا أنه مع تطور اللغات ، تطورت معها حلول التعامل مع التطبيقات واسعة النطاق التي تقوم بالكثير من عمليات الإدخال / الإخراج.
لكي نكون منصفين ، فإن كلا من PHP و Java ، على الرغم من الأوصاف الواردة في هذه المقالة ، لديها تطبيقات I / O غير المحظورة متاحة للاستخدام في تطبيقات الويب. ولكن هذه ليست شائعة مثل الأساليب الموصوفة أعلاه ، وسيتعين أخذ النفقات التشغيلية المصاحبة لصيانة الخوادم باستخدام مثل هذه الأساليب في الاعتبار. ناهيك عن أنه يجب هيكلة التعليمات البرمجية الخاصة بك بطريقة تعمل مع مثل هذه البيئات ؛ عادةً لا يعمل تطبيق الويب PHP أو Java "العادي" الخاص بك بدون تعديلات كبيرة في مثل هذه البيئة.
على سبيل المقارنة ، إذا أخذنا في الاعتبار بعض العوامل المهمة التي تؤثر على الأداء بالإضافة إلى سهولة الاستخدام ، فسنحصل على هذا:
لغة | الخيوط مقابل العمليات | الإدخال / الإخراج غير المحظور | سهولة الاستعمال |
---|---|---|---|
بي أتش بي | العمليات | رقم | |
جافا | الخيوط | متاح | يتطلب عمليات الاسترجاعات |
Node.js | الخيوط | نعم | يتطلب عمليات الاسترجاعات |
اذهب | خيوط (Goroutines) | نعم | لا حاجة لردود النداء |
ستكون الخيوط عمومًا أكثر كفاءة في الذاكرة من العمليات ، لأنها تشترك في نفس مساحة الذاكرة في حين أن العمليات لا تشترك. بدمج ذلك مع العوامل المتعلقة بالإدخال / الإخراج غير المحظور ، يمكننا أن نرى ذلك على الأقل مع العوامل المذكورة أعلاه ، حيث ننتقل إلى أسفل القائمة حيث يتحسن الإعداد العام فيما يتعلق بالإدخال / الإخراج. لذلك إذا كان علي اختيار فائز في المسابقة أعلاه ، فمن المؤكد أنه سيكون Go.
ومع ذلك ، من الناحية العملية ، فإن اختيار البيئة التي يتم فيها إنشاء تطبيقك يرتبط ارتباطًا وثيقًا بمعرفة فريقك بالبيئة المذكورة ، والإنتاجية الإجمالية التي يمكنك تحقيقها معها. لذلك قد لا يكون من المنطقي أن يقوم كل فريق بالتعمق والبدء في تطوير تطبيقات وخدمات الويب في Node أو Go. في الواقع ، غالبًا ما يُشار إلى العثور على مطورين أو معرفة فريقك الداخلي على أنه السبب الرئيسي لعدم استخدام لغة و / أو بيئة مختلفة. ومع ذلك ، فقد تغيرت الأوقات على مدار الخمسة عشر عامًا الماضية أو نحو ذلك ، كثيرًا.
نأمل أن يساعد ما سبق في رسم صورة أوضح لما يحدث تحت الغطاء ويعطيك بعض الأفكار حول كيفية التعامل مع قابلية التوسع في العالم الحقيقي لتطبيقك. نتمنى لك السعادة في الإدخال والإخراج!