الحفاظ على سرعة Node.js: الأدوات والتقنيات والنصائح لإنشاء خوادم Node.js عالية الأداء

نشرت: 2022-03-10
ملخص سريع ↬ تعد Node نظامًا أساسيًا متعدد الاستخدامات ، ولكن أحد التطبيقات السائدة هو إنشاء عمليات متصلة بالشبكة. في هذه المقالة ، سنركز على تحديد السمات الأكثر شيوعًا لهذه: خوادم الويب HTTP.

إذا كنت تقوم ببناء أي شيء باستخدام Node.js لفترة كافية ، فلا شك أنك عانيت من آلام مشاكل السرعة غير المتوقعة. جافا سكريبت هي لغة غير متزامنة ذات حدث. يمكن أن يجعل ذلك التفكير في الأداء صعبًا ، كما سيتضح. كشفت الشعبية المتزايدة لـ Node.js عن الحاجة إلى الأدوات والتقنيات والتفكير المناسب لقيود JavaScript من جانب الخادم.

عندما يتعلق الأمر بالأداء ، فإن ما يعمل في المتصفح لا يناسب بالضرورة Node.js. لذا ، كيف يمكننا التأكد من أن تنفيذ Node.js سريع ومناسب للغرض؟ دعنا نسير من خلال مثال عملي.

أدوات

Node هي منصة متعددة الاستخدامات ، ولكن أحد التطبيقات السائدة هو إنشاء عمليات متصلة بالشبكة. سنركز على تحديد السمات الأكثر شيوعًا لهذه: خوادم الويب HTTP.

سنحتاج إلى أداة يمكنها تفجير الخادم بالعديد من الطلبات أثناء قياس الأداء. على سبيل المثال ، يمكننا استخدام AutoCannon:

 npm install -g autocannon

تتضمن أدوات قياس أداء HTTP الجيدة الأخرى Apache Bench (ab) و wrk2 ، ولكن AutoCannon مكتوب في Node ، ويوفر ضغط تحميل مشابهًا (أو أكبر في بعض الأحيان) ، كما أنه سهل التثبيت على أنظمة التشغيل Windows و Linux و Mac OS X.

المزيد بعد القفز! أكمل القراءة أدناه ↓

بعد أن أنشأنا قياسًا أساسيًا للأداء ، إذا قررنا أن عمليتنا يمكن أن تكون أسرع ، فسنحتاج إلى طريقة ما لتشخيص مشاكل العملية. تعد Node Clinic أداة رائعة لتشخيص مشكلات الأداء المختلفة ، والتي يمكن تثبيتها أيضًا باستخدام npm:

 npm install -g clinic

هذا في الواقع يثبت مجموعة من الأدوات. سنستخدم Clinic Doctor و Clinic Flame (غلاف حول 0 x) أثناء ذهابنا.

ملاحظة : بالنسبة لهذا المثال العملي ، سنحتاج إلى Node 8.11.2 أو أعلى.

الرمز

مثالنا هو خادم REST بسيط مع مورد واحد: حمولة JSON كبيرة معروضة كمسار GET في /seed/v1 . الخادم عبارة عن مجلد app يتكون من ملف package.json (اعتمادًا على restify 7.1.0 ) وملف index.js وملف util.js.

يبدو ملف index.js لخادمنا كما يلي:

 'use strict' const restify = require('restify') const { etagger, timestamp, fetchContent } = require('./util')() const server = restify.createServer() server.use(etagger().bind(server)) server.get('/seed/v1', function (req, res, next) { fetchContent(req.url, (err, content) => { if (err) return next(err) res.send({data: content, url: req.url, ts: timestamp()}) next() }) }) server.listen(3000)

يمثل هذا الخادم الحالة الشائعة لخدمة المحتوى الديناميكي المخزن مؤقتًا للعميل. يتم تحقيق ذلك باستخدام الوسيطة etagger ، التي تحسب رأس ETag لأحدث حالة من المحتوى.

يوفر ملف الاستخدامات.

 'use strict' require('events').defaultMaxListeners = Infinity const crypto = require('crypto') module.exports = () => { const content = crypto.rng(5000).toString('hex') const ONE_MINUTE = 60000 var last = Date.now() function timestamp () { var now = Date.now() if (now — last >= ONE_MINUTE) last = now return last } function etagger () { var cache = {} var afterEventAttached = false function attachAfterEvent (server) { if (attachAfterEvent === true) return afterEventAttached = true server.on('after', (req, res) => { if (res.statusCode !== 200) return if (!res._body) return const key = crypto.createHash('sha512') .update(req.url) .digest() .toString('hex') const etag = crypto.createHash('sha512') .update(JSON.stringify(res._body)) .digest() .toString('hex') if (cache[key] !== etag) cache[key] = etag }) } return function (req, res, next) { attachAfterEvent(this) const key = crypto.createHash('sha512') .update(req.url) .digest() .toString('hex') if (key in cache) res.set('Etag', cache[key]) res.set('Cache-Control', 'public, max-age=120') next() } } function fetchContent (url, cb) { setImmediate(() => { if (url !== '/seed/v1') cb(Object.assign(Error('Not Found'), {statusCode: 404})) else cb(null, content) }) } return { timestamp, etagger, fetchContent } }

لا تأخذ هذا الرمز بأي حال من الأحوال كمثال لأفضل الممارسات! توجد العديد من روائح الأكواد في هذا الملف ، لكننا سنحددها أثناء قيامنا بقياس التطبيق وتوصيفه.

للحصول على المصدر الكامل لنقطة البداية ، يمكن العثور على الخادم البطيء هنا.

التنميط

من أجل التشكيل الجانبي ، نحتاج إلى محطتين ، واحدة لبدء التطبيق ، والأخرى لاختبار الحمل.

في محطة واحدة ، داخل app ، يمكننا تشغيل المجلد:

 node index.js

في محطة أخرى ، يمكننا تحديدها على النحو التالي:

 autocannon -c100 localhost:3000/seed/v1

سيؤدي هذا إلى فتح 100 اتصال متزامن وقصف الخادم بطلبات لمدة عشر ثوانٍ.

يجب أن تكون النتائج مشابهة لما يلي (تشغيل اختبار 10s @ https://localhost:3000/seed/v1 - 100 اتصال):

ستات متوسط ستديف الأعلى
الكمون (مللي ثانية) 3086.81 1725.2 5554
مطلوب / ثانية 23.1 19.18 65
بايت / ثانية 237.98 كيلو بايت 197.7 كيلو بايت 688.13 كيلو بايت
231 طلبًا في 10 ثوانٍ ، تمت قراءة 2.4 ميغابايت

ستختلف النتائج حسب الجهاز. ومع ذلك ، مع الأخذ في الاعتبار أن خادم Node.js "Hello World" قادر بسهولة على استقبال ثلاثين ألف طلب في الثانية على هذا الجهاز الذي ينتج هذه النتائج ، فإن 23 طلبًا في الثانية بمتوسط ​​زمن انتقال يتجاوز 3 ثوانٍ يعد أمرًا كئيبًا.

التشخيص

اكتشاف منطقة المشكلة

يمكننا تشخيص التطبيق بأمر واحد ، وذلك بفضل أمر عيادة الطبيب على المنفذ. داخل مجلد app نقوم بتشغيل:

 clinic doctor --on-port='autocannon -c100 localhost:$PORT/seed/v1' -- node index.js

سيؤدي هذا إلى إنشاء ملف HTML يتم فتحه تلقائيًا في متصفحنا عند اكتمال التنميط.

يجب أن تبدو النتائج كما يلي:

اكتشف طبيب العيادة مشكلة في حلقة الأحداث
نتائج طبيب العيادة

يخبرنا الطبيب أنه من المحتمل أن يكون لدينا مشكلة في Event Loop.

إلى جانب الرسالة بالقرب من الجزء العلوي من واجهة المستخدم ، يمكننا أيضًا أن نرى أن مخطط Event Loop باللون الأحمر ، ويظهر تأخيرًا متزايدًا باستمرار. قبل أن نتعمق في ما يعنيه هذا ، دعونا أولاً نفهم تأثير المشكلة التي تم تشخيصها على المقاييس الأخرى.

يمكننا أن نرى أن وحدة المعالجة المركزية ثابتة أو أعلى بنسبة 100٪ لأن العملية تعمل بجد لمعالجة الطلبات في قائمة الانتظار. يستخدم محرك جافا سكريبت Node's (V8) في الواقع نواتين لوحدة المعالجة المركزية في هذه الحالة لأن الجهاز متعدد النواة ويستخدم V8 خيطين. واحد لحلقة الحدث والآخر لجمع القمامة. عندما نرى ارتفاع وحدة المعالجة المركزية بنسبة تصل إلى 120٪ في بعض الحالات ، فإن العملية تجمع العناصر المتعلقة بالطلبات التي تم التعامل معها.

نرى هذا مرتبطًا في الرسم البياني للذاكرة. الخط المتصل في مخطط الذاكرة هو مقياس الكومة المستخدمة. في أي وقت يكون هناك ارتفاع في وحدة المعالجة المركزية ، نرى انخفاضًا في خط Heap Used ، مما يدل على أنه يتم إلغاء تخصيص الذاكرة.

لا تتأثر المقابض النشطة بتأخير تكرار الحدث. المقبض النشط هو كائن يمثل إما الإدخال / الإخراج (مثل المقبس أو مقبض الملف) أو جهاز ضبط الوقت (مثل setInterval ). أصدرنا تعليمات إلى AutoCannon لفتح 100 اتصال ( -c100 ). تحافظ المقابض النشطة على عدد ثابت يبلغ 103. الثلاثة الأخرى هي مقابض لـ STDOUT و STDERR ومقبض الخادم نفسه.

إذا نقرنا على لوحة التوصيات أسفل الشاشة ، فسنرى شيئًا مشابهًا لما يلي:

تم فتح لوحة توصيات طبيب العيادة
عرض التوصيات الخاصة بقضية معينة

التخفيف قصير المدى

قد يستغرق تحليل السبب الجذري لمشكلات الأداء الخطيرة وقتًا. في حالة وجود مشروع يتم نشره بشكل مباشر ، يجدر إضافة حماية من التحميل الزائد إلى الخوادم أو الخدمات. تتمثل فكرة الحماية من التحميل الزائد في مراقبة تأخير حلقة الحدث (من بين أشياء أخرى) ، والاستجابة بـ "503 Service Unavailable" في حالة تجاوز الحد الأدنى. هذا يسمح لموازن التحميل بالفشل في الحالات الأخرى ، أو في أسوأ الحالات يعني أنه سيتعين على المستخدمين التحديث. يمكن أن توفر وحدة الحماية من الحمل الزائد هذا الحد الأدنى من النفقات العامة لـ Express و Koa و Restify. يحتوي إطار عمل Hapi على إعداد تكوين تحميل يوفر نفس الحماية.

فهم منطقة المشكلة

كما يوضح الشرح المختصر في Clinic Doctor ، إذا تأخرت Event Loop إلى المستوى الذي نلاحظه ، فمن المحتمل جدًا أن تقوم وظيفة واحدة أو أكثر "بحظر" Event Loop.

من المهم بشكل خاص مع Node.js التعرف على خاصية JavaScript الأساسية هذه: لا يمكن أن تحدث الأحداث غير المتزامنة حتى اكتمال تنفيذ التعليمات البرمجية حاليًا.

هذا هو السبب في أن setTimeout لا يمكن أن تكون دقيقة.

على سبيل المثال ، جرب تشغيل ما يلي في DevTools بالمتصفح أو Node REPL:

 console.time('timeout') setTimeout(console.timeEnd, 100, 'timeout') let n = 1e7 while (n--) Math.random()

لن يكون قياس الوقت الناتج 100 مللي ثانية. من المحتمل أن يكون في نطاق 150 مللي ثانية إلى 250 مللي ثانية. قام setTimeout بجدولة عملية غير متزامنة ( console.timeEnd ) ، لكن التعليمات البرمجية المنفذة حاليًا لم تكتمل بعد ؛ هناك سطرين آخرين. يُعرف الكود الجاري تنفيذه باسم "التجزئة" الحالية. لكي تكتمل العلامة ، يجب استدعاء Math.random عشرة ملايين مرة. إذا استغرق هذا 100 مللي ثانية ، فسيكون إجمالي الوقت قبل انتهاء المهلة 200 مللي ثانية (بالإضافة إلى المدة التي تستغرقها وظيفة setTimeout المهلة مسبقًا ، وعادة ما تكون بضع مللي ثانية).

في سياق جانب الخادم ، إذا كانت العملية في العلامة الحالية تستغرق وقتًا طويلاً لإكمال الطلبات ، فلا يمكن معالجة الطلبات ، ولا يمكن أن يحدث جلب البيانات لأنه لن يتم تنفيذ التعليمات البرمجية غير المتزامنة حتى اكتمال العلامة الحالية. هذا يعني أن التعليمات البرمجية باهظة الثمن من الناحية الحسابية ستبطئ جميع التفاعلات مع الخادم. لذلك يوصى بتقسيم العمل المكثف على الموارد إلى عمليات منفصلة واستدعائها من الخادم الرئيسي ، وهذا سوف يتجنب الحالات التي نادرًا ما تستخدم ولكن المسار باهظ التكلفة يؤدي إلى إبطاء أداء الطرق الأخرى المستخدمة بشكل متكرر ولكنها غير مكلفة.

يحتوي خادم المثال على بعض التعليمات البرمجية التي تحظر Event Loop ، لذا فإن الخطوة التالية هي تحديد موقع هذا الرمز.

التحليل

تتمثل إحدى طرق التحديد السريع للشفرة ذات الأداء الضعيف في إنشاء الرسم البياني للهب وتحليله. يمثل الرسم البياني للهب استدعاءات الوظائف ككتل تجلس فوق بعضها البعض - ليس بمرور الوقت ولكن بشكل إجمالي. والسبب في تسميته بـ "مخطط اللهب" هو أنه يستخدم عادةً نظام ألوان برتقالي إلى أحمر ، حيث يكون اللون الأحمر هو "الأكثر سخونة" ، مما يعني أنه من المحتمل أن يحجب حلقة الحدث. يتم إجراء التقاط البيانات للرسم البياني للهب من خلال أخذ عينات من وحدة المعالجة المركزية - مما يعني أنه يتم أخذ لقطة للوظيفة التي يتم تنفيذها حاليًا ومكدسها. يتم تحديد الحرارة من خلال النسبة المئوية للوقت أثناء التنميط التي تكون فيها وظيفة معينة في الجزء العلوي من المكدس (مثل الوظيفة التي يتم تنفيذها حاليًا) لكل عينة. إذا لم تكن هذه هي آخر وظيفة يتم استدعاؤها داخل هذا المكدس ، فمن المحتمل أن تحظر حلقة الحدث.

دعنا نستخدم clinic flame لإنشاء رسم بياني لهب لتطبيق المثال:

 clinic flame --on-port='autocannon -c100 localhost:$PORT/seed/v1' -- node index.js

يجب أن تفتح النتيجة في متصفحنا بشيء مثل ما يلي:

يوضح الرسم البياني لهب العيادة أن server.on هو عنق الزجاجة
تصور الرسم البياني لهب العيادة

يمثل عرض الكتلة مقدار الوقت المستغرق في وحدة المعالجة المركزية بشكل عام. يمكن ملاحظة أن ثلاث مجموعات رئيسية تستهلك معظم الوقت ، وكلها تسلط الضوء على server.on باعتباره أهم وظيفة. في الحقيقة ، المجموعات الثلاثة كلها متشابهة. يتباعدون لأنه أثناء التنميط ، يتم التعامل مع الوظائف المحسّنة وغير المحسّنة كإطارات استدعاء منفصلة. يتم تحسين الوظائف المسبوقة بـ * بواسطة محرك JavaScript ، وتلك التي تبدأ بـ ~ لا يتم تحسينها. إذا لم تكن الحالة المحسّنة مهمة بالنسبة لنا ، فيمكننا تبسيط الرسم البياني بشكل أكبر بالضغط على زر دمج. يجب أن يؤدي هذا إلى عرض مشابه لما يلي:

دمج الرسم البياني للهب
دمج الرسم البياني للهب

منذ البداية ، يمكننا أن نستنتج أن الكود المخالف موجود في ملف util.js من كود التطبيق.

الوظيفة البطيئة هي أيضًا معالج حدث: الوظائف التي تؤدي إلى الوظيفة هي جزء من وحدة events الأساسية ، و server.on هو اسم احتياطي لوظيفة مجهولة يتم توفيرها كوظيفة معالجة الحدث. يمكننا أيضًا أن نرى أن هذا الرمز ليس في نفس علامة الرمز الذي يعالج الطلب بالفعل. إذا كان الأمر كذلك ، فستكون الوظائف من وحدات http الأساسية stream net المكدس.

يمكن إيجاد مثل هذه الوظائف الأساسية من خلال توسيع أجزاء أخرى أصغر بكثير من الرسم البياني للهب. على سبيل المثال ، حاول استخدام مدخلات البحث في الجزء العلوي الأيمن من واجهة المستخدم للبحث عن send (اسم كل من الأساليب الداخلية restify و http الداخلية). يجب أن يكون على يمين الرسم البياني (يتم فرز الوظائف أبجديًا):

يحتوي الرسم البياني للهب على كتلتين صغيرتين مميزتين تمثلان وظيفة معالجة HTTP
البحث في الرسم البياني للهب عن وظائف معالجة HTTP

لاحظ كيف تكون جميع كتل معالجة HTTP الفعلية صغيرة نسبيًا.

يمكننا النقر فوق إحدى الكتل المميزة باللون السماوي والتي ستتوسع لإظهار وظائف مثل writeHead write في ملف http_outgoing.js (جزء من مكتبة Node core http ):

تم تكبير الرسم البياني للهب في طريقة عرض مختلفة تعرض الأكوام ذات الصلة بـ HTTP
توسيع الرسم البياني للهب إلى حزم HTTP ذات الصلة

يمكننا النقر فوق كل الأكوام للعودة إلى العرض الرئيسي.

النقطة الأساسية هنا هي أنه على الرغم من أن الوظيفة server.on ليست في نفس علامة رمز معالجة الطلب الفعلي ، إلا أنها لا تزال تؤثر على أداء الخادم الكلي عن طريق تأخير تنفيذ التعليمات البرمجية ذات الأداء.

تصحيح

نعلم من الرسم البياني للهب أن الوظيفة الإشكالية هي معالج الحدث الذي تم تمريره إلى server.on في ملف util.js.

لنلقي نظرة:

 server.on('after', (req, res) => { if (res.statusCode !== 200) return if (!res._body) return const key = crypto.createHash('sha512') .update(req.url) .digest() .toString('hex') const etag = crypto.createHash('sha512') .update(JSON.stringify(res._body)) .digest() .toString('hex') if (cache[key] !== etag) cache[key] = etag })

من المعروف أن التشفير يميل إلى أن يكون مكلفًا ، كما هو الحال مع التسلسل ( JSON.stringify ) ولكن لماذا لا تظهر في الرسم البياني للهب؟ هذه العمليات موجودة في العينات الملتقطة ، لكنها مخفية خلف مرشح cpp . إذا ضغطنا على زر cpp ، فسنرى شيئًا كالتالي:

تم الكشف عن الكتل الإضافية المتعلقة بـ C ++ في الرسم البياني للهب (العرض الرئيسي)
الكشف عن أطر التسلسل والتشفير C ++

تظهر الآن تعليمات V8 الداخلية المتعلقة بكل من التسلسل والتشفير على أنها أكثر الحزم سخونة وتستهلك معظم الوقت. تستدعي طريقة JSON.stringify رمز C ++ مباشرةً ؛ هذا هو السبب في أننا لا نرى وظيفة جافا سكريبت. في حالة التشفير ، توجد وظائف مثل createHash و update في البيانات ، لكنها إما مضمنة (مما يعني أنها تختفي في العرض المدمج) أو أصغر من أن يتم عرضها.

بمجرد أن نبدأ في التفكير بشأن الكود في وظيفة etagger ، يمكن أن يتضح سريعًا أنه سيئ التصميم. لماذا نأخذ نسخة server من سياق الوظيفة؟ هناك الكثير من التجزئة ، هل كل ذلك ضروري؟ لا يوجد أيضًا دعم رأس If-None-Match في التنفيذ والذي من شأنه أن يخفف بعض الحمل في بعض سيناريوهات العالم الحقيقي لأن العملاء سيقدمون فقط طلبًا رئيسيًا لتحديد الحداثة.

دعنا نتجاهل كل هذه النقاط في الوقت الحالي ونتحقق من صحة النتيجة التي تفيد بأن العمل الفعلي الذي يتم إجراؤه في server.on هو بالفعل عنق الزجاجة. يمكن تحقيق ذلك من خلال ضبط server.on code على وظيفة فارغة وإنشاء مخطط flamegraph جديد.

قم بتعديل وظيفة etagger إلى ما يلي:

 function etagger () { var cache = {} var afterEventAttached = false function attachAfterEvent (server) { if (attachAfterEvent === true) return afterEventAttached = true server.on('after', (req, res) => {}) } return function (req, res, next) { attachAfterEvent(this) const key = crypto.createHash('sha512') .update(req.url) .digest() .toString('hex') if (key in cache) res.set('Etag', cache[key]) res.set('Cache-Control', 'public, max-age=120') next() } }

أصبحت وظيفة مستمع الحدث التي تم تمريرها إلى server.on الآن عبارة عن وظيفة no-op.

لنقم بتشغيل clinic flame مرة أخرى:

 clinic flame --on-port='autocannon -c100 localhost:$PORT/seed/v1' -- node index.js

يجب أن ينتج عن ذلك رسم بياني للهب مشابه لما يلي:

يوضح الرسم البياني Flame أن مكدسات نظام أحداث Node.js لا تزال تمثل عنق الزجاجة
الرسم البياني للشعلة للخادم عندما يكون server.on دالة فارغة

هذا يبدو أفضل ، ويجب أن نلاحظ زيادة في الطلب في الثانية. ولكن لماذا هذا الحدث الذي ينبعث منه الكود ساخن جدًا؟ نتوقع في هذه المرحلة أن يستغرق كود معالجة HTTP معظم وقت وحدة المعالجة المركزية ، فلا يوجد شيء يتم تنفيذه على الإطلاق في حدث server.on .

هذا النوع من الاختناق ناتج عن وظيفة يتم تنفيذها أكثر مما ينبغي.

قد تكون الشفرة المشبوهة التالية في الجزء العلوي من util.js دليلًا:

 require('events').defaultMaxListeners = Infinity

دعنا نزيل هذا السطر ونبدأ عمليتنا --trace-warnings :

 node --trace-warnings index.js

إذا قمنا بالتوصيف مع AutoCannon في محطة أخرى ، مثل:

 autocannon -c100 localhost:3000/seed/v1

ستخرج عمليتنا شيئًا مشابهًا لـ:

 (node:96371) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 after listeners added. Use emitter.setMaxListeners() to increase limit at _addListener (events.js:280:19) at Server.addListener (events.js:297:10) at attachAfterEvent (/Users/davidclements/z/nearForm/keeping-node-fast/slow/util.js:22:14) at Server. (/Users/davidclements/z/nearForm/keeping-node-fast/slow/util.js:25:7) at call (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/chain.js:164:9) at next (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/chain.js:120:9) at Chain.run (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/chain.js:123:5) at Server._runUse (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/server.js:976:19) at Server._runRoute (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/server.js:918:10) at Server._afterPre (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/server.js:888:10) (node:96371) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 after listeners added. Use emitter.setMaxListeners() to increase limit at _addListener (events.js:280:19) at Server.addListener (events.js:297:10) at attachAfterEvent (/Users/davidclements/z/nearForm/keeping-node-fast/slow/util.js:22:14) at Server. (/Users/davidclements/z/nearForm/keeping-node-fast/slow/util.js:25:7) at call (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/chain.js:164:9) at next (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/chain.js:120:9) at Chain.run (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/chain.js:123:5) at Server._runUse (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/server.js:976:19) at Server._runRoute (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/server.js:918:10) at Server._afterPre (/Users/davidclements/z/nearForm/keeping-node-fast/slow/node_modules/restify/lib/server.js:888:10)

تخبرنا العقدة أنه يتم إرفاق الكثير من الأحداث بكائن الخادم . هذا غريب لأن هناك قيمة منطقية تتحقق مما إذا كان الحدث قد تم إرفاقه ، ثم يعود مبكرًا ، مما يجعل attachAfterEvent في وضع عدم التشغيل بعد إرفاق الحدث الأول.

دعنا نلقي نظرة على وظيفة attachAfterEvent :

 var afterEventAttached = false function attachAfterEvent (server) { if (attachAfterEvent === true) return afterEventAttached = true server.on('after', (req, res) => {}) }

الفحص الشرطي خاطئ! يتحقق ما إذا كان attachAfterEvent صحيحًا بدلاً من afterEventAttached . هذا يعني أنه يتم إرفاق حدث جديد بطبعة server عند كل طلب ، ثم يتم تشغيل جميع الأحداث المرفقة السابقة بعد كل طلب. عذرًا!

التحسين

الآن بعد أن اكتشفنا مناطق المشكلات ، دعنا نرى ما إذا كان بإمكاننا جعل الخادم أسرع.

أمر سهل الحصول عليه

دعنا نعيد كود المستمع server.on (بدلاً من دالة فارغة) ونستخدم الاسم المنطقي الصحيح في الفحص الشرطي. تبدو وظيفة etagger بنا على النحو التالي:

 function etagger () { var cache = {} var afterEventAttached = false function attachAfterEvent (server) { if (afterEventAttached === true) return afterEventAttached = true server.on('after', (req, res) => { if (res.statusCode !== 200) return if (!res._body) return const key = crypto.createHash('sha512') .update(req.url) .digest() .toString('hex') const etag = crypto.createHash('sha512') .update(JSON.stringify(res._body)) .digest() .toString('hex') if (cache[key] !== etag) cache[key] = etag }) } return function (req, res, next) { attachAfterEvent(this) const key = crypto.createHash('sha512') .update(req.url) .digest() .toString('hex') if (key in cache) res.set('Etag', cache[key]) res.set('Cache-Control', 'public, max-age=120') next() } }

الآن نتحقق من الإصلاح من خلال التنميط مرة أخرى. ابدأ الخادم في محطة واحدة:

 node index.js

ثم ملف التعريف مع AutoCannon:

 autocannon -c100 localhost:3000/seed/v1

يجب أن نرى النتائج في مكان ما في نطاق تحسين 200 مرة (تشغيل اختبار 10s @ https://localhost:3000/seed/v1 - 100 اتصال):

ستات متوسط ستديف الأعلى
الكمون (مللي ثانية) 19.47 4.29 103
مطلوب / ثانية 5011.11 506.2 5487
بايت / ثانية 51.8 م 5.45 ميجا بايت 58.72 م
50 ألف طلب في 10 ثوانٍ ، قراءة 519.64 ميغابايت

من المهم الموازنة بين تخفيضات تكلفة الخادم المحتملة وتكاليف التطوير. نحن بحاجة إلى تحديد ، في سياقاتنا الظرفية ، إلى أي مدى نحتاج إلى الذهاب في تحسين المشروع. وإلا ، فقد يكون من السهل جدًا وضع 80٪ من الجهد في 20٪ من تحسينات السرعة. هل معوقات المشروع تبرر ذلك؟

في بعض السيناريوهات ، قد يكون من المناسب تحقيق تحسن بمقدار 200 مرة باستخدام ثمار معلقة منخفضة وتسميتها يوميًا. في حالات أخرى ، قد نرغب في جعل التنفيذ بأسرع ما يمكن. انها حقا تعتمد على أولويات المشروع.

إحدى طرق التحكم في إنفاق الموارد هي تحديد الهدف. على سبيل المثال ، تحسين 10 مرات ، أو 4000 طلب في الثانية. بناء هذا على احتياجات العمل هو الأكثر منطقية. على سبيل المثال ، إذا كانت تكاليف الخادم تزيد بنسبة 100٪ عن الميزانية ، فيمكننا تحديد هدف لتحسين الضعف.

أخذ المزيد

إذا قمنا بإنتاج رسم بياني لهب جديد لخادمنا ، فسنرى شيئًا مشابهًا لما يلي:

لا يزال الرسم البياني للهب يُظهر server.on باعتباره عنق الزجاجة ، ولكنه عنق زجاجة أصغر
رسم بياني للهب بعد إجراء إصلاح الخلل في الأداء

لا يزال مستمع الحدث يمثل عنق الزجاجة ، ولا يزال يشغل ثلث وقت وحدة المعالجة المركزية أثناء التنميط (يبلغ العرض حوالي ثلث الرسم البياني بأكمله).

ما هي المكاسب الإضافية التي يمكن تحقيقها ، وهل التغييرات (إلى جانب الاضطرابات المرتبطة بها) تستحق القيام بها؟

من خلال التنفيذ المحسن ، والذي هو مع ذلك أكثر تقييدًا إلى حد ما ، يمكن تحقيق خصائص الأداء التالية (تشغيل اختبار 10s @ https://localhost:3000/seed/v1 - 10 اتصالات):

ستات متوسط ستديف الأعلى
الكمون (مللي ثانية) 0.64 0.86 17
مطلوب / ثانية 8330.91 757.63 8991
بايت / ثانية 84.17 م 7.64 ميجابايت 92.27 م
92 ألف طلب في 11 ثانية ، قراءة 937.22 ميغابايت

في حين أن التحسين 1.6x مهم ، فإنه يعتمد على الموقف ما إذا كانت الجهود والتغييرات وتعطيل التعليمات البرمجية اللازمة لإنشاء هذا التحسين مبررة. خاصة عند مقارنتها بتحسين 200x على التنفيذ الأصلي مع إصلاح خطأ واحد.

لتحقيق هذا التحسين ، تم استخدام نفس الأسلوب التكراري للملف الشخصي ، وإنشاء flamegraph ، والتحليل ، والتصحيح ، والتحسين للوصول إلى الخادم النهائي المُحسَّن ، والذي يمكن العثور على الكود الخاص به هنا.

التغييرات النهائية للوصول إلى 8000 متطلب / ثانية كانت:

  • لا تبني كائنات ثم تسلسل ، أنشئ سلسلة من JSON مباشرة ؛
  • استخدم شيئًا فريدًا حول المحتوى لتحديد Etag ، بدلاً من إنشاء تجزئة ؛
  • لا تقم بتجزئة عنوان URL ، استخدمه مباشرة كمفتاح.

تكون هذه التغييرات أكثر تعقيدًا بشكل طفيف ، وتزيد من اضطراب قاعدة الشفرة ، وتترك البرمجيات الوسيطة etagger أقل مرونة قليلاً لأنها تضع العبء على المسار لتوفير قيمة Etag . لكنها تحقق 3000 طلب إضافي في الثانية على آلة التنميط.

دعنا نلقي نظرة على الرسم البياني للهب لهذه التحسينات النهائية:

يوضح الرسم البياني للهب أن الكود الداخلي المتعلق بوحدة الشبكة هو الآن عنق الزجاجة
رسم بياني صحي للهب بعد كل تحسينات في الأداء

الجزء الأكثر سخونة من الرسم البياني للهب هو جزء من نواة العقدة ، في الوحدة النمطية net . هذا مثالي.

منع مشاكل الأداء

للتقريب ، إليك بعض الاقتراحات حول طرق منع مشاكل الأداء قبل نشرها.

يمكن أن يؤدي استخدام أدوات الأداء كنقاط تفتيش غير رسمية أثناء التطوير إلى تصفية أخطاء الأداء قبل أن تتحول إلى مرحلة الإنتاج. يوصى بجعل AutoCannon and Clinic (أو ما يعادلها) جزءًا من أدوات التطوير اليومية.

عند الشراء في إطار عمل ، اكتشف ما هي سياسته المتعلقة بالأداء. إذا كان إطار العمل لا يعطي الأولوية للأداء ، فمن المهم التحقق مما إذا كان ذلك يتوافق مع ممارسات البنية التحتية وأهداف العمل. على سبيل المثال ، استثمرت Restify بوضوح (منذ إصدار الإصدار 7) في تحسين أداء المكتبة. ومع ذلك ، إذا كانت التكلفة المنخفضة والسرعة العالية يمثلان أولوية مطلقة ، ففكر في Fastify الذي تم قياسه بنسبة 17٪ أسرع بواسطة مساهم Restify.

احترس من اختيارات المكتبات الأخرى ذات التأثير الواسع - لا سيما التفكير في التسجيل. نظرًا لأن المطورين يقومون بإصلاح المشكلات ، فقد يقررون إضافة مخرجات سجل إضافية للمساعدة في تصحيح المشكلات ذات الصلة في المستقبل. إذا تم استخدام مسجل غير فعال ، فقد يؤدي ذلك إلى اختناق الأداء بمرور الوقت بعد نمط حكاية الضفدع المغلي. مسجل pino هو أسرع مسجل JSON محدد سطر جديد متاح لـ Node.js.

أخيرًا ، تذكر دائمًا أن Event Loop هي مورد مشترك. يتم تقييد خادم Node.js في النهاية بواسطة أبطأ منطق في المسار الأكثر سخونة.