بتذكرها زي كأنه مبارح. كنا قاعدين، فريق التطوير كله، قبل إطلاق ميزة جديدة كبيرة في واحد من المشاريع. الأجواء كانت حلوة، فنجان القهوة الصباحي، والكل مبسوط. الكود نظيف، الاختبارات الوحدوية (Unit Tests) كلها ناجحة، واختبارات الأداء الأولية اللي عملناها كانت بتعطينا نتائج بتفرّح القلب: “1000 طلب في الثانية؟ ولا أسهل منها!”.
أطلقنا الميزة… وبعدها بساعات قليلة، بلش الجحيم. رسائل Slack بتولّع، تنبيهات المراقبة (Monitoring) بتصرخ، والمستخدمين بشتكوا من بطء شديد وأخطاء “503 Service Unavailable”. الموقع كان فعليًا بينهار تحت ضغط ما كان مفروض يكون “ضغط” أصلًا حسب اختباراتنا.
وقفنا كل اشي وعملنا اجتماع طارئ. السؤال اللي كان يدور في بال الكل: “كيف كل اختباراتنا نجحت، والواقع كان كارثي؟ شو اللي صار؟”. هاي القصة يا جماعة الخير هي قصة “الأداء الوهمي”، وكيف تعلمنا بالطريقة الصعبة إنه اختبار الأداء مش مجرد قصف الخادم (Server) بطلبات HTTP بسيطة.
لماذا كذبت علينا اختباراتنا؟ لعنة الأداء الوهمي
المشكلة ما كانت في الكود نفسه بالضرورة، ولا في قوة الخوادم. المشكلة كانت في عقليتنا وطريقتنا في الاختبار. كنا نرتكب خطأ شائعًا جدًا: اختبار نقاط النهاية (Endpoints) بشكل معزول، وليس محاكاة سلوك المستخدم الحقيقي.
اختباراتنا الأولية كانت عبارة عن سكريبت بسيط يرسل آلاف طلبات GET إلى الصفحة الرئيسية، وطلبات POST لإنشاء مستخدم وهمي. كانت النتائج رائعة لأن هذه العمليات كانت سريعة جدًا ومعظمها يتم عبر الذاكرة المؤقتة (Cache). لقد قمنا ببناء سيارة سباق قادرة على الانطلاق بسرعة 200 كم/ساعة في خط مستقيم، لكننا نسينا أن الطريق الحقيقي مليء بالمنعطفات والإشارات الضوئية والمشاة.
باختصار، اختباراتنا كانت تقيس أداء النظام في ظل ظروف مثالية وغير واقعية، وهذا ما أسميه “الأداء الوهمي”.
ما هو سلوك المستخدم الحقيقي؟
المستخدم الحقيقي لا يقصف الموقع بالطلبات. سلوكه أكثر تعقيدًا:
- يدخل إلى الصفحة الرئيسية.
- يتوقف ليفكر لعدة ثوانٍ (Think Time) وهو يقرأ المحتوى.
- ينقر على صفحة “المنتجات”.
- يتوقف مرة أخرى ليتصفح المنتجات.
- يستخدم شريط البحث للعثور على منتج معين.
- يضيف المنتج إلى عربة التسوق.
- يذهب إلى صفحة الدفع، ويملأ بياناته.
- يكمل عملية الشراء.
هذا “السيناريو” أو “رحلة المستخدم” (User Journey) يشمل عمليات قراءة وكتابة متعددة، ويضع ضغطًا مختلفًا تمامًا على قواعد البيانات، والخدمات الخارجية، والمنطق البرمجي المعقد، وليس فقط على الـ Cache.
k6: السكين السويسري لاختبارات الأداء الحديثة
هنا قررنا أن نغير أدواتنا وعقليتنا. بعد بحث وتقييم، استقر رأينا على k6. ليش k6 بالذات؟ لعدة أسباب وجيهة:
- صديقة للمطورين (Developer-Friendly): تكتب اختباراتك بلغة جافاسكربت (ES6)، اللغة اللي معظم مطوري الويب بيعرفوها وبيحبوها. ما في داعي تتعلم لغة جديدة أو تستخدم واجهات رسومية معقدة.
- أداء عالي: مكتوبة بلغة Go، مما يجعلها قادرة على توليد ضغط هائل من جهاز واحد.
- مرنة وقابلة للتوسيع: تدعم البروتوكولات المختلفة (HTTP/1.1, HTTP/2, WebSockets, gRPC) ويمكن توسيعها بسهولة.
- موجهة للسيناريوهات: مصممة من الألف إلى الياء لتسهيل كتابة سيناريوهات المستخدم المعقدة.
التحول في العقلية: من اختبار نقاط النهاية إلى محاكاة المستخدمين
الخطوة الأولى ما كانت كتابة الكود، بل كانت جلسة عصف ذهني رسمنا فيها “رحلات المستخدم” الأكثر شيوعًا وأهمية في نظامنا. حددنا 3 سيناريوهات رئيسية:
- المستخدم المتصفح: يدخل، يتصفح عدة صفحات، ثم يغادر.
- المستخدم المشتري: يبحث، يضيف للسلة، ويقوم بعملية شراء كاملة.
- المستخدم النشط: يسجل الدخول، يتفقد لوحة التحكم الخاصة به، ويقوم بتعديل بعض البيانات.
الآن، ومع هذه السيناريوهات الواضحة، حان وقت ترجمتها إلى كود k6.
بناء سيناريو واقعي مع k6: مثال عملي
لنفترض أننا نريد محاكاة سيناريو “المستخدم المشتري” على متجر إلكتروني. السكريبت لن يكون مجرد طلب واحد، بل سلسلة من الطلبات مع فترات توقف منطقية.
الخطوة 1: تعريف رحلة المستخدم في الكود
سنستخدم ميزة group في k6 لتنظيم الكود وجعل التقارير أكثر وضوحًا، و sleep لمحاكاة “وقت التفكير” للمستخدم.
import http from 'k6/http';
import { sleep, check, group } from 'k6';
import { randomIntBetween } from 'k6/utils/random';
// إعدادات الاختبار: زيادة تدريجية في عدد المستخدمين
export const options = {
stages: [
{ duration: '1m', target: 20 }, // زيادة إلى 20 مستخدم خلال دقيقة
{ duration: '3m', target: 50 }, // زيادة إلى 50 مستخدم خلال 3 دقائق
{ duration: '1m', target: 0 }, // تقليل عدد المستخدمين إلى صفر
],
thresholds: {
'http_req_failed': ['rate<0.01'], // أقل من 1% من الطلبات يجب أن تفشل
'http_req_duration': ['p(95)<800'], // 95% من الطلبات يجب أن تتم في أقل من 800ms
},
};
export default function () {
const baseUrl = 'https://your-awesome-store.com';
group('User Journey: E-commerce Purchase', function () {
// الخطوة 1: زيارة الصفحة الرئيسية
group('Step 1: Visit Homepage', function () {
const res = http.get(baseUrl + '/');
check(res, { 'status was 200': (r) => r.status === 200 });
});
// محاكاة وقت التفكير: المستخدم يتصفح الصفحة الرئيسية
sleep(randomIntBetween(3, 7)); // توقف عشوائي بين 3 و 7 ثوانٍ
// الخطوة 2: البحث عن منتج
group('Step 2: Search for a Product', function () {
const res = http.get(baseUrl + '/search?q=laptop');
check(res, { 'search successful': (r) => r.status === 200 });
});
sleep(randomIntBetween(2, 5));
// الخطوة 3: عرض تفاصيل المنتج
group('Step 3: View Product Details', function () {
// في سيناريو حقيقي، ستقوم باستخلاص رابط المنتج من نتيجة البحث
const productId = 'product-123';
const res = http.get(`${baseUrl}/products/${productId}`);
check(res, { 'product page loaded': (r) => r.status === 200 });
});
sleep(randomIntBetween(3, 6));
// الخطوة 4: إضافة المنتج إلى السلة
group('Step 4: Add to Cart', function () {
const payload = JSON.stringify({ productId: 'product-123', quantity: 1 });
const params = { headers: { 'Content-Type': 'application/json' } };
const res = http.post(baseUrl + '/api/cart', payload, params);
check(res, {
'item added to cart': (r) => r.status === 200,
'cart contains item': (r) => r.json('itemCount') > 0,
});
});
// الخطوة 5: الانتقال إلى الدفع
group('Step 5: Checkout', function () {
const res = http.get(baseUrl + '/checkout');
check(res, { 'checkout page loaded': (r) => r.status === 200 });
});
});
}
ماذا كشف لنا هذا الاختبار؟
عندما قمنا بتشغيل هذا النوع من الاختبارات، ظهرت المشاكل الحقيقية:
- بطء في عملية “إضافة إلى السلة”: اكتشفنا أن هذه العملية كانت تقوم بتحديثات كثيرة ومتزامنة في قاعدة البيانات، مما سبب عنق زجاجة (Bottleneck) مع زيادة عدد المستخدمين.
- أخطاء في صفحة الدفع: كانت خدمة الدفع الخارجية لها حد أقصى من الطلبات في الدقيقة، وهو ما لم نكن نختبره من قبل.
- استهلاك عالي للذاكرة: مع كل مستخدم يضيف شيئًا للسلة، كانت جلسات المستخدم (User Sessions) على الخادم تستهلك ذاكرة أكثر من المتوقع.
هذه هي المشاكل التي لم يكن من الممكن اكتشافها أبدًا عبر إرسال طلبات GET بسيطة. السيناريو الواقعي هو الذي كشفها.
نصائح من العبد لله (خبرة عملية)
بعد ما مرينا فيه، خلوني أعطيكم شوية نصائح عملية من القلب:
1. لا تختبر المسار السعيد فقط (Happy Path)
ماذا لو أضاف المستخدم منتجًا ثم حذفه؟ ماذا لو حاول الدفع ببطاقة مرفوضة؟ قم ببناء سيناريوهات لهذه الحالات أيضًا، فهي تضع ضغطًا مختلفًا على النظام.
2. استخدم بيانات ديناميكية
لا تجعل كل المستخدمين الافتراضيين يبحثون عن كلمة “laptop” ويشترون “product-123”. هذا يؤدي إلى نتائج مضللة بسبب الـ Caching. استخدم ملفات CSV أو قم بتوليد بيانات عشوائية داخل السكريبت لجعل كل مستخدم فريدًا.
3. حدد عتبات الفشل (Thresholds)
من أجمل ميزات k6 هي الـ thresholds. يمكنك أن تقول للاختبار أن يفشل تلقائيًا إذا زادت نسبة الأخطاء عن 0.1%، أو إذا أصبح 95% من الطلبات أبطأ من 500ms. هذا يجعل النتائج واضحة وقابلة للأتمتة في بيئات CI/CD.
export const options = {
thresholds: {
// 99% من الطلبات يجب أن تكون أسرع من 1.5 ثانية
'http_req_duration': ['p(99)<1500'],
// مدة فحص تفاصيل المنتج يجب أن تكون أسرع من 600ms لـ 95% من الطلبات
'http_req_duration{group:::Step 3: View Product Details}': ['p(95)<600'],
},
};
4. ابدأ صغيرًا ثم تصاعد (Ramp Up)
لا تبدأ الاختبار بـ 1000 مستخدم دفعة واحدة. استخدم الـ stages لزيادة الحمل تدريجيًا. هذا يساعدك على تحديد النقطة الدقيقة التي يبدأ عندها النظام في التدهور.
الخلاصة: من الأداء الوهمي إلى الموثوقية الحقيقية 👍
الحكي اللي بدي أوصلكم إياه يا جماعة الخير بسيط: اختبارات الأداء ليست مجرد أرقام ورسوم بيانية خضراء. هي فن محاكاة الواقع بكل تعقيداته. الانتقال من اختبارات نقاط النهاية البسيطة إلى اختبارات السيناريوهات الواقعية باستخدام أدوات مثل k6 كان نقطة تحول في مشاريعنا.
صحيح، الأمر يتطلب جهدًا أكبر في التفكير والتخطيط، لكنه الجهد الذي يفرق بين إطلاق كارثي وإطلاق ناجح وموثوق. لا تثقوا بالأداء الوهمي، بل ابحثوا عن الحقيقة في سلوك المستخدمين. والآن، تفضلوا اشربوا فنجان قهوة وفكروا في سيناريوهات المستخدمين الخاصة بكم.
الله يعطيكم العافية.