تфайيل تطبيقات Node.js
تتوفر العديد من أدوات الجهات الخارجية لتфайيل تطبيقات Node.js، ولكن في كثير من الحالات، يكون الخيار الأسهل هو استخدام أداة التфайيل المدمجة في Node.js. تستخدم أداة التфайيل المدمجة أداة التфайيل داخل V8 التي تقوم بعينات المكدس على فترات منتظمة أثناء تنفيذ البرنامج. وهي تسجل نتائج هذه العينات، بالإضافة إلى أحداث التحسين المهمة مثل عمليات تجميع jit، كمجموعة من النقاط:
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 بسيط. سيكون لتطبيقنا مُعالجان، أحدهما لإضافة مستخدمين جدد إلى نظامنا:
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)
})
وآخر للتحقق من صحة محاولات مصادقة المستخدم:
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 الخاصة بك، وهي تُستخدم لأغراض التوضيح فقط. يجب ألا تحاول تصميم آليات المصادقة المشفرة الخاصة بك بشكل عام. من الأفضل بكثير استخدام حلول المصادقة القائمة والمُثبتة.
لنفترض الآن أننا نشرنا تطبيقنا وأن المستخدمين يشكون من ارتفاع زمن الوصول في الطلبات. يمكننا بسهولة تشغيل التطبيق باستخدام أداة التфайيل المدمجة:
NODE_ENV=production node --prof app.js
ووضع بعض الحمل على الخادم باستخدام ab
(ApacheBench):
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 من:
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
:
node --prof-process isolate-0xnnnnnnnnnnnn-v8.log > processed.txt
سيؤدي فتح processed.txt في محرر النصوص المفضل لديك إلى إعطائك أنواعًا مختلفة قليلة من المعلومات. يتم تقسيم الملف إلى أقسام، والتي يتم تقسيمها مرة أخرى حسب اللغة. أولاً، ننظر إلى قسم الملخص ونرى:
[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 ++ التي تستغرق معظم وقت وحدة المعالجة المركزية ونرى:
[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 والتي تتوافق مع توليد التجزئة من كلمة مرور المستخدم. ومع ذلك، قد لا يكون من الواضح على الفور كيف تساهم الإدخالات الأقل من ذلك في تطبيقنا (أو إذا كان الأمر كذلك، فسوف نتظاهر بخلاف ذلك من أجل المثال). لفهم العلاقة بين هذه الوظائف بشكل أفضل، سننظر بعد ذلك إلى قسم [الملف التعلوي (الثقيل)] الذي يوفر معلومات حول المُستدعين الأساسيين لكل دالة. عند فحص هذا القسم، نجد:
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:
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 أعلاه باستخدام الإصدار غير المتزامن من تطبيقك إلى:
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 الخاصة بك.
قد تجد أيضًا كيفية إنشاء رسم بياني للهب مفيدًا.