تحسين أداء الخادم: التخزين المؤقت والفهارس
خادم يرد أسرع ويتحمل زوّارًا أكثر ⏱ نحو 45 دقيقة.
- تشرح حلقة الأحداث وما يحجب خادم
Node. - تضيف تخزينًا مؤقتًا بمدة وإبطال.
- تستخدم
ETagوCache-Controlو 304. - تضيف فهارس وتقرأ خطة الاستعلام، وتقلل البيانات المرسلة.
١خيط واحد يخدم الجميع
Node.js يشغّل كودك على خيط واحد وحلقة أحداث Event Loop: حين ينتظر طلبٌ قاعدةَ البيانات أو ملفًّا، ينتقل Node لخدمة طلب آخر. لهذا يخدم آلاف الاتصالات بذاكرة قليلة. لكن هذا يعني أيضًا: حساب ثقيل متزامن يجمّد الخادم كله، فلا يرد على أحد حتى ينتهي.
const express = require('express');const app = express(); app.get('/heavy', (req, res) => { const t0 = Date.now(); let x = 0; for (let i = 0; i < 3e7; i++) x += i % 7; // حساب متزامن يحجز الخيط console.log('انتهى الحساب الثقيل بعد', Date.now() - t0, 'ms'); res.json({ x });}); app.get('/ping', (req, res) => res.send('pong')); app.listen(3000);
في خادم حقيقي، لو وصل طلب /ping أثناء حساب /heavy لانتظر حتى ينتهي. الحل: تجنّب الحسابات الثقيلة في المعالج، أو انقلها إلى Worker Threads أو خدمة منفصلة أو مهمة في الخلفية.
| يحجب الخادم ✗ | لا يحجبه ✓ |
|---|---|
fs.readFileSync داخل معالج | await fs.promises.readFile |
| حلقة ضخمة أو JSON.parse لملف ضخم | تقسيم العمل، أو Worker Threads |
bcrypt.hashSync(p, 14) | await bcrypt.hash(p, 10) |
٢التخزين المؤقت في الذاكرة
أسرع استعلام هو الذي لا يُنفَّذ. إن كانت البيانات تُطلب كثيرًا وتتغير قليلًا (قائمة المقررات، إحصاءات الصفحة الرئيسية)، نحفظ النتيجة في الذاكرة مدة محددة TTL، ونردها مباشرة:
const express = require('express');const app = express(); // محاكاة استعلام بطيء (300 ملّي ثانية)const slowDb = () => new Promise(r => setTimeout(() => r(['CS241', 'CS242', 'CS243']), 300)); const cache = new Map(); // المفتاح ← { value, expires }async function cached(key, ttlMs, load) { const hit = cache.get(key); if (hit && hit.expires > Date.now()) return { value: hit.value, hit: true }; const value = await load(); cache.set(key, { value, expires: Date.now() + ttlMs }); return { value, hit: false };} app.get('/api/courses', async (req, res) => { const t0 = Date.now(); const { value, hit } = await cached('courses', 60_000, slowDb); res.set('X-Cache', hit ? 'HIT' : 'MISS'); console.log(hit ? 'من الذاكرة' : 'من قاعدة البيانات', Date.now() - t0, 'ms'); res.json(value);}); app.post('/api/courses', (req, res) => { cache.delete('courses'); // أي تعديل يُبطل المخزّن res.status(201).json({ ok: true });}); app.listen(3000);
الطلب الأول MISS (300 ms)، والثاني HIT فوري. وبعد الإضافة أُبطل المخزّن فعاد الطلب التالي إلى قاعدة البيانات. الترويسة X-Cache تساعدك على المراقبة.
٣تخزين المتصفح: ETag و 304
المتصفح يستطيع أن يسأل: «عندي نسخة بصمتها كذا، هل تغيّرت؟» بالترويسة If-None-Match. Express يضيف بصمة ETag لكل رد تلقائيًّا، فإن طابقت يرد 304 Not Modified بلا جسم: توفير في الشبكة. ومع Cache-Control: max-age لا يسأل المتصفح أصلًا خلال المدة:
const express = require('express');const app = express(); app.get('/api/menu', (req, res) => { res.set('Cache-Control', 'public, max-age=300'); // 5 دقائق بلا سؤال res.json({ items: ['كبسة', 'سليق', 'جريش'], day: 'الأحد' });}); app.listen(3000);
الطلب الثاني أرسل ETag الذي عاد في الأول، فرد الخادم 304 بلا محتوى، واستخدم المتصفح نسخته المحفوظة.
٤الفهارس: من مسح الجدول إلى البحث المباشر
بلا فهرس Index، البحث عن طالب برقم هويته يعني أن قاعدة البيانات تقرأ كل الصفوف (Full scan). الفهرس بنية مرتبة (كفهرس الكتاب) توصل للصف مباشرة. EXPLAIN QUERY PLAN يكشف ما تفعله قاعدة البيانات:
const express = require('express');const { Sequelize, DataTypes } = require('sequelize');const db = new Sequelize({ dialect: 'sqlite', storage: ':memory:', logging: false }); const Student = db.define('Student', { nationalId: DataTypes.STRING, name: DataTypes.STRING, major: DataTypes.STRING,}, { indexes: [{ unique: true, fields: ['nationalId'] }], // ← فهرس}); const app = express();const plan = async (sql) => (await db.query('EXPLAIN QUERY PLAN ' + sql, { type: 'SELECT' })).map(r => r.detail); app.get('/plan', async (req, res) => { res.json({ byNationalId: await plan("SELECT * FROM Students WHERE nationalId = '1100'"), byMajor: await plan("SELECT * FROM Students WHERE major = 'CS'"), });}); db.sync().then(() => app.listen(3000));
البحث بـ nationalId يستخدم الفهرس (SEARCH … USING INDEX)، أما البحث بـ major فيمسح الجدول كله (SCAN). مع مليون طالب يكون الفرق بين ملّي ثانية وثوانٍ.
| ضع فهرسًا على | ولا تبالغ لأن |
|---|---|
| الأعمدة المستخدمة في WHERE كثيرًا | كل فهرس يبطّئ الإضافة والتعديل قليلًا |
| المفاتيح الأجنبية (AuthorId…) المستخدمة في JOIN | ويأخذ مساحة تخزين |
| أعمدة الفرز ORDER BY في القوائم الكبيرة | الجداول الصغيرة لا تحتاجه |
٥أرسل أقل
| التقنية | في Sequelize/Express |
|---|---|
| الأعمدة المطلوبة فقط | attributes: ['id', 'title'] |
| الترقيم دائمًا | limit و offset مع حد أعلى |
JOIN بدل N+1 | include |
| ضغط الردود | وسيط compression() (gzip) أو من الخادم الأمامي |
| عدّ بلا جلب | Model.count() بدل findAll().length |
توقّع وتحقّق
لماذا يجمّد حساب متزامن ثقيل خادم Node كله؟
Event loop.ما معنى الرد 304؟
Not Modified.ماذا يعني SCAN في خطة الاستعلام؟
Full scan.متى يجب إبطال المخزّن المؤقت؟
Invalidation.الخلاصة
- خيط واحد: لا حسابات متزامنة ثقيلة في المعالجات.
CacheبـTTL، وأبطله عند التعديل؛Redisللأنظمة الكبيرة.ETag + If-None-Match← 304 بلا جسم؛max-ageيمنع السؤال.- الفهرس يحوّل
SCANإلىSEARCH؛ للأعمدة المستخدمة فيWHEREوJOIN. attributesوlimitوincludeوcount: أرسل أقل.
تحدٍّ برمجي: ذاكرة مؤقتة لإحصاءات الصفحة الرئيسية
متقدمالمسار /api/stats يحسب إحصاءات من قاعدة بيانات بطيئة في كل طلب. أضف تخزينًا مؤقتًا.
- أول طلب يحسب من
slowStats()ويرد بالترويسةX-Cache: MISS. - الطلبات التالية خلال 30 ثانية ترد من الذاكرة بـ
X-Cache: HITدون استدعاء slowStats (يطبع «حساب»). POST /api/ordersيضيف طلبًا ويُبطل المخزّن، فيعود الطلب التالي MISS بالأرقام الجديدة.
👀 اعرض الاستجابات المطلوبة
GET /api/statsGET /api/statsGET /api/statsPOST /api/ordersGET /api/stats
- الخادم يعمل ويرد على كل الطلبات بلا أخطاء غير معالَجة
- الأول MISS
- الثاني HIT
- والقيم نفسها
- الثالث HIT
- بعد الإضافة MISS
- … بالأرقام الجديدة
(121) - الحساب البطيء نُفّذ مرتين فقط (الطرفية)
💡 تلميح 1
cache = { value, expires } خارج المعالج.💡 تلميح 2
cache = null.حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.
const express = require('express');const app = express();let orders = 120;let cache = null; const slowStats = () => new Promise(r => setTimeout(() => { console.log('حساب'); r({ orders, revenue: orders * 50 });}, 200)); app.get('/api/stats', async (req, res) => { if (cache && cache.expires > Date.now()) { res.set('X-Cache', 'HIT'); return res.json(cache.value); } const value = await slowStats(); cache = { value, expires: Date.now() + 30_000 }; res.set('X-Cache', 'MISS'); res.json(value);}); app.post('/api/orders', (req, res) => { orders++; cache = null; res.status(201).json({ orders });}); app.listen(3000);