Skip to content

تфайيل تطبيقات Node.js

تتوفر العديد من أدوات الجهات الخارجية لتфайيل تطبيقات Node.js، ولكن في كثير من الحالات، يكون الخيار الأسهل هو استخدام أداة التфайيل المدمجة في Node.js. تستخدم أداة التфайيل المدمجة أداة التфайيل داخل V8 التي تقوم بعينات المكدس على فترات منتظمة أثناء تنفيذ البرنامج. وهي تسجل نتائج هذه العينات، بالإضافة إلى أحداث التحسين المهمة مثل عمليات تجميع jit، كمجموعة من النقاط:

bash
code-creation,LazyCompile,0,0x2d5000a337a0,396,"bp native array.js:1153:16",0x289f644df68,~
code-creation,LazyCompile,0,0x2d5000a33940,716,"hasOwnProperty native v8natives.js:198:30",0x289f64438d0,~
code-creation,LazyCompile,0,0x2d5000a33c20,284,"ToName native runtime.js:549:16",0x289f643bb28,~
code-creation,Stub,2,0x2d5000a33d40,182,"DoubleToIStub"
code-creation,Stub,2,0x2d5000a33e00,507,"NumberToStringStub"

في الماضي، كنت بحاجة إلى رمز مصدر V8 لتفسير النقاط. لحسن الحظ، تم تقديم أدوات منذ Node.js 4.4.0 تسهل استهلاك هذه المعلومات دون بناء V8 بشكل منفصل من المصدر. دعونا نرى كيف يمكن لأداة التфайيل المدمجة أن تساعد في توفير نظرة ثاقبة على أداء التطبيق.

لتوضيح استخدام أداة التфайيل، سنعمل مع تطبيق Express بسيط. سيكون لتطبيقنا مُعالجان، أحدهما لإضافة مستخدمين جدد إلى نظامنا:

javascript
app.get('/newUser', (req, res) => {
  let username = req.query.username || ''
  const password = req.query.password || ''
  username = username.replace(/[!@#$%^&*]/g, '')
  if (!username || !password || users[username]) {
    return res.sendStatus(400)
  }
  const salt = crypto.randomBytes(128).toString('base64')
  const hash = crypto.pbkdf2Sync(password, salt, 10000, 512, 'sha512')
  users[username] = { salt, hash }
  res.sendStatus(200)
})

وآخر للتحقق من صحة محاولات مصادقة المستخدم:

javascript
app.get('/auth', (req, res) => => {
  let username = req.query.username || '';
  const password = req.query.password || '';
  username = username.replace(/[!@#$%^&*]/g, '');
  if (!username || !password || !users[username]) {
    return res.sendStatus(400);
  }
  const { salt, hash } = users[username];
  const encryptHash = crypto.pbkdf2Sync(password, salt, 10000, 512, 'sha512');
  if (crypto.timingSafeEqual(hash, encryptHash)) {
    res.sendStatus(200);
  } else {
    res.sendStatus(401);
  }
});

يرجى ملاحظة أن هذه ليست مُعالجات مُوصى بها لمصادقة المستخدمين في تطبيقات Node.js الخاصة بك، وهي تُستخدم لأغراض التوضيح فقط. يجب ألا تحاول تصميم آليات المصادقة المشفرة الخاصة بك بشكل عام. من الأفضل بكثير استخدام حلول المصادقة القائمة والمُثبتة.

لنفترض الآن أننا نشرنا تطبيقنا وأن المستخدمين يشكون من ارتفاع زمن الوصول في الطلبات. يمكننا بسهولة تشغيل التطبيق باستخدام أداة التфайيل المدمجة:

bash
NODE_ENV=production node --prof app.js

ووضع بعض الحمل على الخادم باستخدام ab (ApacheBench):

bash
curl -X GET "http://localhost:8080/newUser?username=matt&password=password"
ab -k -c 20 -n 250 "http://localhost:8080/auth?username=matt&password=password"

وحصل على إخراج ab من:

bash
Concurrency Level:      20
Time taken for tests:   46.932 seconds
Complete requests:      250
Failed requests:        0
Keep-Alive requests:    250
Total transferred:      50250 bytes
HTML transferred:       500 bytes
Requests per second:    5.33 [#/sec] (mean)
Time per request:       3754.556 [ms] (mean)
Time per request:       187.728 [ms] (mean, across all concurrent requests)
Transfer rate:          1.05 [Kbytes/sec] received
...
Percentage of the requests served within a certain time (ms)
  50%   3755
  66%   3804
  75%   3818
  80%   3825
  90%   3845
  95%   3858
  98%   3874
  99%   3875
 100%   4225 (longest request)

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

بما أننا قمنا بتشغيل تطبيقنا باستخدام خيار --prof، فقد تم إنشاء ملف نقطة في نفس الدليل مثل تشغيل التطبيق المحلي. يجب أن يكون له الشكل isolate-0xnnnnnnnnnnnn-v8.log (حيث n هو رقم).

من أجل فهم هذا الملف، نحتاج إلى استخدام مُعالج النقاط المُضمّن مع ثنائي Node.js. لتشغيل المُعالج، استخدم علم --prof-process:

bash
node --prof-process isolate-0xnnnnnnnnnnnn-v8.log > processed.txt

سيؤدي فتح processed.txt في محرر النصوص المفضل لديك إلى إعطائك أنواعًا مختلفة قليلة من المعلومات. يتم تقسيم الملف إلى أقسام، والتي يتم تقسيمها مرة أخرى حسب اللغة. أولاً، ننظر إلى قسم الملخص ونرى:

bash
[Summary]:
   ticks  total  nonlib   name
     79    0.2%    0.2%  JavaScript
  36703   97.2%   99.2%  C++
      7    0.0%    0.0%  GC
    767    2.0%          Shared libraries
    215    0.6%          Unaccounted

يخبرنا هذا أن 97٪ من جميع العينات التي تم جمعها حدثت في رمز C ++، وأنه عند عرض الأقسام الأخرى من الإخراج المُعالَج، يجب أن نولي اهتمامًا أكبر للعمل الذي يتم إنجازه في C ++ (على عكس JavaScript). مع وضع هذا في الاعتبار، نجد قسم [C ++] التالي الذي يحتوي على معلومات حول وظائف C ++ التي تستغرق معظم وقت وحدة المعالجة المركزية ونرى:

bash
 [C++]:
   ticks  total  nonlib   name
  19557   51.8%   52.9%  node::crypto::PBKDF2(v8::FunctionCallbackInfo<v8::Value> const&)
   4510   11.9%   12.2%  _sha1_block_data_order
   3165    8.4%    8.6%  _malloc_zone_malloc

نرى أن أعلى 3 إدخالات تمثل 72.1٪ من وقت وحدة المعالجة المركزية الذي استغرقه البرنامج. من هذا الإخراج، نرى على الفور أن ما لا يقل عن 51.8٪ من وقت وحدة المعالجة المركزية يُستهلك بواسطة دالة تُسمى PBKDF2 والتي تتوافق مع توليد التجزئة من كلمة مرور المستخدم. ومع ذلك، قد لا يكون من الواضح على الفور كيف تساهم الإدخالات الأقل من ذلك في تطبيقنا (أو إذا كان الأمر كذلك، فسوف نتظاهر بخلاف ذلك من أجل المثال). لفهم العلاقة بين هذه الوظائف بشكل أفضل، سننظر بعد ذلك إلى قسم [الملف التعلوي (الثقيل)] الذي يوفر معلومات حول المُستدعين الأساسيين لكل دالة. عند فحص هذا القسم، نجد:

bash
  ticks parent  name
  19557   51.8%  node::crypto::PBKDF2(v8::FunctionCallbackInfo<v8::Value> const&)
  19557  100.0%    v8::internal::Builtins::~Builtins()
  19557  100.0%      LazyCompile: ~pbkdf2 crypto.js:557:16
   4510   11.9%  _sha1_block_data_order
   4510  100.0%    LazyCompile: *pbkdf2 crypto.js:557:16
   4510  100.0%      LazyCompile: *exports.pbkdf2Sync crypto.js:552:30
   3165    8.4%  _malloc_zone_malloc
   3161   99.9%    LazyCompile: *pbkdf2 crypto.js:557:16
   3161  100.0%      LazyCompile: *exports.pbkdf2Sync crypto.js:552:30

يتطلب تحليل هذا القسم مزيدًا من العمل من عدد النقاط الخام أعلاه. ضمن كل "مكدسات المكالمات" أعلاه، تُخبرك النسبة المئوية في عمود الوالد بالنسبة المئوية للعينات التي تم فيها استدعاء الدالة في الصف أعلاه بواسطة الدالة في الصف الحالي. على سبيل المثال، في "مكدس المكالمات" الأوسط أعلاه لـ _sha1_block_data_order، نرى أن _sha1_block_data_order حدثت في 11.9٪ من العينات، وهو ما عرفناه من العدّ الخام أعلاه. ومع ذلك، هنا، يمكننا أيضًا معرفة أنه تم استدعاءه دائمًا بواسطة دالة pbkdf2 داخل وحدة crypto في Node.js. نرى أنه وبالمثل، تم استدعاء _malloc_zone_malloc تقريبًا حصريًا بواسطة نفس دالة pbkdf2. وبالتالي، باستخدام المعلومات الموجودة في هذه العرض، يمكننا أن نقول أن حساب التجزئة من كلمة مرور المستخدم لا يمثل فقط 51.8٪ من الأعلى، ولكن أيضًا كل وقت وحدة المعالجة المركزية في أفضل 3 وظائف مُعينة لأن المكالمات إلى _sha1_block_data_order و _malloc_zone_malloc تم إجراؤها نيابة عن دالة pbkdf2.

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

لتصحيح هذه المشكلة، قم بإجراء تعديل صغير على المُعالجات المذكورة أعلاه لاستخدام الإصدار غير المتزامن من دالة pbkdf2:

javascript
app.get('/auth', (req, res) => {
  let username = req.query.username || ''
  const password = req.query.password || ''
  username = username.replace(/[!@#$%^&*]/g, '')
  if (!username || !password || !users[username]) {
    return res.sendStatus(400)
  }
  crypto.pbkdf2(password, users[username].salt, 10000, 512, 'sha512', (err, hash) => {
    if (users[username].hash.toString() === hash.toString()) {
      res.sendStatus(200)
    } else {
      res.sendStatus(401)
    }
  })
})

يؤدي تشغيل جديد لمعيار ab أعلاه باستخدام الإصدار غير المتزامن من تطبيقك إلى:

bash
Concurrency Level:      20
Time taken for tests:   12.846 seconds
Complete requests:      250
Failed requests:        0
Keep-Alive requests:    250
Total transferred:      50250 bytes
HTML transferred:       500 bytes
Requests per second:    19.46 [#/sec] (mean)
Time per request:       1027.689 [ms] (mean)
Time per request:       51.384 [ms] (mean, across all concurrent requests)
Transfer rate:          3.82 [Kbytes/sec] received
...
Percentage of the requests served within a certain time (ms)
  50%   1018
  66%   1035
  75%   1041
  80%   1043
  90%   1049
  95%   1063
  98%   1070
  99%   1071
 100%   1079 (longest request)

رائع! أصبح تطبيقك الآن يقدم حوالي 20 طلبًا في الثانية، أي ما يقارب 4 أضعاف ما كان عليه مع توليد التجزئة المتزامن. بالإضافة إلى ذلك، انخفض متوسط زمن الوصول من 4 ثوانٍ قبل ذلك إلى أكثر من ثانية واحدة.

آمل أن تكون قد رأيت، من خلال التحقيق في أداء هذا المثال (المُصطنع بصراحة)، كيف يمكن أن يساعدك مُعالج نقطة V8 في الحصول على فهم أفضل لأداء تطبيقات Node.js الخاصة بك.

قد تجد أيضًا كيفية إنشاء رسم بياني للهب مفيدًا.