نظرا: تطبيق http node.js القديم وزيادة الحمل عليه.
الحلول القياسية للمشكلة: قم بإنهاء الخوادم ، وإعادة كتابة كل شيء من 0 ، وتحسين ما تم كتابته بالفعل.
دعونا نحاول متابعة التحسين ومعرفة كيفية العثور على نقاط ضعف التطبيق وتحسينها. وربما تسريع دون لمس سطر واحد من التعليمات البرمجية :)
نرحب بجميع المهتمين تحت القط!
أولاً ، دعنا نقرر تقنية اختبار الأداء. سنهتم بعدد الطلبات المقدمة في ثانية واحدة: rps.
سنقوم بتشغيل التطبيق في الوضع 1 للعامل (عملية واحدة) ، وقياس أداء الشفرة القديمة والتعليمات البرمجية مع التحسينات - الأداء المطلق ليس مهمًا ، والأداء المقارن مهم.
في تطبيق نموذجي مع العديد من المسارات المختلفة ، من المنطقي أولاً العثور على الطلبات الأكثر تحميلًا ، والتي تستغرق معالجتها معظم الوقت. ستسمح لك الأدوات المساعدة مثل
request-log-analizer أو العديد من الأدوات المشابهة باستخراج هذه المعلومات من السجلات.
من ناحية أخرى ، يمكنك الحصول على قائمة حقيقية بالطلبات ورقمهم جميعًا (على سبيل المثال ، باستخدام yandex-tank) - نحصل على ملف تعريف موثوق به.
ولكن من خلال إجراء العديد من التكرارات لتحسين الرمز ، فمن الملائم أكثر استخدام أداة أبسط وأسرع ونوع معين من الطلبات (وبعد تحسين طلب واحد ، ادرس الطلب التالي ، وما إلى ذلك). خياري هو
wrk . علاوة على ذلك ، في حالتي ، عدد الطرق ليس كبيرًا - ليس من الصعب التحقق من كل شيء واحدًا تلو الآخر.
وتجدر الإشارة على الفور إلى أنه من حيث حظر الاستعلامات وتوقعات قاعدة البيانات ، إلخ. تم تحسين التطبيق بالفعل ، كل شيء يعتمد على وحدة المعالجة المركزية: أثناء الاختبارات ، يستهلك العامل وحدة المعالجة المركزية بنسبة 100 ٪.
تستخدم الخوادم المُباعة node.js الإصدار 6 - لنبدأ بها:
الطلبات / ثانية:
1210نحاول في العقدة الثامنة:
الطلبات / ثانية:
2308الملاحظة العاشرة:
الطلبات / ثانية:
2590الفرق واضح. يتم لعب الدور الرئيسي هنا عن طريق تحديث إصدار v8 - كان الكثير من كود v8 المحسن بشكل سيئ في الماضي. ومن أجل عدم التعامل مع طواحين الهواء التي اختفت في node.js v8 ، من الأفضل الترقية على الفور ، ثم القيام بتحسين الشفرة.
ننتقل إلى البحث الفعلي عن الاختناقات: في رأيي ، فإن أفضل أداة لذلك هي flamegraph. ومع ظهور مشروع
0x ، كان الحصول على flamegraph بسيطًا جدًا - بدء 0x بدلاً من العقدة: 0x -o yourscript.js ، قم بإجراء اختبار ، أوقف البرنامج النصي ، انظر إلى النتيجة في المتصفح.
يبدو flamegraph للشفرة المختبرة شيئًا مثل هذا قبل التحسينات:

أسفل الفلاتر ، اترك التطبيق ، وإدخالات - فقط رمز التطبيق ووحدات الطرف الثالث.
كلما كان الشريط أوسع ، زاد الوقت الذي يقضيه في أداء هذه الوظيفة (بما في ذلك المكالمات المتداخلة).
سنتعامل مع الجزء المركزي الأكبر.
بادئ ذي بدء ، نسلط الضوء على الوظائف غير المحسنة. لقد وجدت عدد قليل من هذه في التطبيق.
علاوة على ذلك ، تعتبر الوظائف العليا مرشحة نموذجية للتحسين. تصطف الوظائف المتبقية بخطوات متساوية نسبيًا - تساهم كل وظيفة بجزء صغير من التأخير ، ولا يوجد قائد واضح.
ثم يمكن استخدام خوارزمية بسيطة من الإجراءات: لتحسين الوظائف الأوسع ، والانتقال من واحد إلى آخر. ولكنني اخترت طريقة مختلفة: لتحسين البدء من نقطة الدخول إلى التطبيق (معالج الطلب في http.createServer). في نهاية الوظيفة قيد الدراسة ، بدلاً من استدعاء الوظائف التالية ، أكمل معالجة الطلب باستجابة وهمية وأدرس أداء هذه الوظيفة المحددة. بعد تحسينه ، تتحرك الإجابة الزائفة على طول مكدس المكالمة إلى الوظيفة التالية ، إلخ.
نتيجة ملائمة لهذا النهج: يمكنك رؤية rps في ظروف مثالية (مع وظيفة بدء واحدة فقط ، rps قريبة من الحد الأقصى rps لتطبيق hode world node.js) ، ومع المزيد من حركة كعب الاستجابة في التطبيق ، لاحظ مساهمة الوظيفة المدروسة في انخفاض الأداء في آه في الثانية.
لذلك ، نترك فقط وظيفة البداية ، نحصل على:
الطلبات / ثانية:
16176
من خلال توصيل الفلاتر الأساسية ، v8 ، يمكنك أن ترى أن الوظيفة بأكملها تقريبًا قيد التحقيق تتكون من إرسال إجابة ، وتسجيل الدخول والأشياء الأخرى المحسنة بشكل سيئ - نذهب إلى أبعد من ذلك.
ننتقل إلى الوظيفة التالية:
الطلبات / ثانية:
16111لم يتغير شيء - مزيد من الغطس:
الطلبات / ثانية:
13330
عملائنا! يمكن ملاحظة أن دالة getByUrl المعنية تحتل جزءًا كبيرًا من دالة البدء - والتي ترتبط بشكل جيد مع هبوط rps.
نحن ننظر بعناية إلى ما يحدث فيه (قم بتشغيل Core ، v8):
تحدث الكثير من الأشياء ... نحن ندخن الشفرة ونحسنها:
for (var i in this.data) { if (this[i]._options.regexp_obj.test(url)) return this[i]; } return null;
تتحول إلى
let result = null; for (let i=0; i<this.length && !result; i++) { if (this[i]._options.regexp_obj.test(url)) result = this[i]; }
في هذه الحالة ، البساطة هي أسرع بكثير من .. في
احصل على طلبات / ثانية:
16015
بصريا ، الوظيفة "تنكمش" وتحتل جزءًا أصغر بكثير من دالة البداية.
في المعلومات التفصيلية حول الوظيفة ، تم تبسيط كل شيء أيضًا إلى حد كبير:
ننتقل إلى الوظيفة التالية.
الطلبات / ثانية:
13316
تحتوي هذه الوظيفة على الكثير من وظائف الصفيف ، وعلى الرغم من التسارع الكبير في الإصدارات الأخيرة من node.js ، إلا أنها لا تزال أبطأ من الحلقات البسيطة: change [] .map and filter. بانتظام للحصول على
الطلبات / ثانية:
15067
وهكذا مرة بعد مرة ، لكل وظيفة لاحقة.
بعض التحسينات المفيدة الأخرى: بالنسبة للتجزئة مع مجموعة مفاتيح متغيرة ديناميكيًا ، يمكن أن تكون الخريطة الجديدة () أسرع بنسبة 40٪ من العادية {} ؛
Math.round (el * 100) / 100 أسرع مرتين من toFixed (2).
في flamegraph للوظائف الأساسية و v8 ، يمكنك رؤية كل من الإدخالات الغامضة و StringPrototypeSplit الناطقة تمامًا أو v8 :: internal :: Runtime_StringToNumber ، وإذا كان هذا جزءًا مهمًا من تنفيذ التعليمات البرمجية ، فحاول تحسين ، على سبيل المثال ، ببساطة إعادة كتابة التعليمات البرمجية التي لا تنفذ هذه العمليات.
على سبيل المثال ، يمكن أن يؤدي استبدال الانقسام بمكالمات indexOf و substring المتعددة إلى مضاعفات أداء مضاعفة.
الموضوع الكبير والمعقد المنفصل هو التحسين jit ، أو بالأحرى ، الوظائف المعزولة.
إذا كانت هناك نسبة كبيرة من هذه الوظائف ، فسيكون من الضروري التعامل معها.
يمكن أن تساعد دراسة مدروسة لمخرجات العقدة --trace_file_names --trace_opt_verbose --trace-deopt --trace_opt هنا.
على سبيل المثال ، خطوط النموذج
deoptimizing (DEOPT soft): ابدأ 0x2bcf38b2d079 <وظيفة JS getTime ... تعليقات غير كافية من النوع للتشغيل الثنائي أدت إلى الخط
عودة فال> = 10؟ فال: '0' + فال ؛
استبدال
return (val> = 10؟ '': '0') + val ؛
صحح الوضع.
هناك الكثير من المعلومات لمحرك V8 القديم لأسباب وطرق مكافحة إلغاء الوظائف الوظيفية:
github.com/P0lip/v8-deoptimize-reasons - قائمة ،
www.netguru.co/blog/tracing-patterns-hinder-performance - تحليل الأسباب النموذجية ،
www.html5rocks.com/en/tutorials/speed/v8 - حول تحسينات الإصدار 8 ، أعتقد أن
الشيء نفسه ينطبق على محرك الإصدار 8 الحالي.
لكن العديد من المشاكل لم تعد ذات صلة بالإصدار 8 الجديد.
على أي حال ، بعد كل التحسينات ، تمكنت من الحصول على الطلبات / ثانية:
9971 ، أي ستسرع مرتين تقريبًا بسبب الانتقال إلى أحدث إصدار من node.js ، وأربع مرات أخرى بسبب تحسين الشفرة.
آمل أن تكون هذه التجربة مفيدة لشخص آخر.