اختبار الأداء والتحميل

k6 في التطبيق العملي

18 دقيقة الدرس 3 من 28

k6 في التطبيق العملي

أرسى الدرس الثاني نظرية اختبار التحميل — المستخدمون الافتراضيون، أشكال الارتفاع، رياضيات النسب المئوية. ينتقل هذا الدرس إلى الأداة التي ستقضي فيها معظم وقتك: k6. بُنيت أصلاً بواسطة Load Impact، وهي الآن مشروع تابع لـ Grafana Labs، وتُعدّ k6 المعيار الصناعي لاختبار التحميل الذي يمتلكه المطوّرون. مكتوبة بـ Go (تستطيع استيعاب مئات الآلاف من الـ VUs بذاكرة RAM متواضعة)، مع نصوص برمجية بـ JavaScript (ES2015+)، ومصمّمة من الأساس للعيش داخل خط أنابيب CI. في Grafana وShopify وفرق SRE من الدرجة الأولى، تُخزَّن نصوص k6 إلى جانب كود الخدمة — كل طلب سحب يتضمن اختبار دخان، وكل مرشح للإصدار يُشغّل اختبار تحمّل كاملاً.

k6 ليست أداة أتمتة متصفح. تولّد حركة HTTP/WebSocket/gRPC على مستوى البروتوكول. لا تُنفّذ JavaScript في متصفح. إذا احتجت اختبار تحميل بمتصفح حقيقي (لتطبيقات SPA ذات عرض ضخم على جانب العميل) استخدم k6 Browser (إضافة xk6-browser). لاختبار API والخلفية البحتة، المحرك الافتراضي هو ما تريد.

بنية النص البرمجي: تشريح اختبار k6

كل نص k6 يُصدّر دالة default وهي جسم الـ VU — الكود الذي يُنفّذه كل مستخدم افتراضي في حلقة. يمتلك النص أيضاً سياق init (كود على مستوى الوحدة) يعمل مرة واحدة لكل VU قبل بدء الاختبار، وخطافات دورة حياة اختيارية: setup() (يعمل مرة واحدة قبل بدء جميع الـ VUs) وteardown(data) (يعمل مرة واحدة بعد انتهاء جميع الـ VUs).

// checkout-flow.js — هيكل نص k6 احترافي import http from 'k6/http'; import { check, sleep } from 'k6'; import { Trend, Rate, Counter } from 'k6/metrics'; // --- مقاييس مخصصة (تُعرَّف في وقت init، مشتركة بين جميع الـ VUs) --- const checkoutLatency = new Trend('checkout_latency_ms', true); // true = دقة عالية const checkoutErrors = new Rate('checkout_error_rate'); const checkoutCount = new Counter('checkouts_attempted'); // --- حدود وأطوار (عقد الاختبار) --- export const options = { stages: [ { duration: '2m', target: 50 }, // ارتفاع تدريجي إلى 50 VU { duration: '5m', target: 50 }, // ثبات عند الحمل المعتدل { duration: '2m', target: 200 }, // ارتفاع مفاجئ إلى 200 VU { duration: '5m', target: 200 }, // ثبات عند الذروة { duration: '2m', target: 0 }, // انخفاض تدريجي ], thresholds: { http_req_duration: ['p(95)<500', 'p(99)<1500'], // بوابة SLO checkout_error_rate: ['rate<0.01'], // أقل من 1% أخطاء http_req_failed: ['rate<0.005'], // معدل فشل k6 المدمج }, }; // setup() تعمل مرة واحدة قبل الـ VUs؛ قيمة الإرجاع تُمرَّر إلى default() و teardown() export function setup() { const res = http.post('https://api.example.com/auth/token', JSON.stringify({ client_id: 'load-test-bot', client_secret: __ENV.API_SECRET, // حقن الأسرار عبر البيئة، لا تُضمَّن في الكود }), { headers: { 'Content-Type': 'application/json' } }); check(res, { 'auth OK': (r) => r.status === 200 }); return { token: res.json('access_token') }; } // default() هي حلقة الـ VU — تُستدعى بشكل متكرر لكل VU export default function (data) { const headers = { Authorization: `Bearer ${data.token}`, 'Content-Type': 'application/json', }; const start = Date.now(); const res = http.post('https://api.example.com/checkout', JSON.stringify({ cart_id: `cart-${__VU}-${__ITER}`, // __VU = رقم الـ VU, __ITER = عدد التكرار promo_code: 'LOAD_TEST', }), { headers }); checkoutLatency.add(Date.now() - start); checkoutCount.add(1); const ok = check(res, { 'status 200': (r) => r.status === 200, 'order_id present': (r) => r.json('order_id') !== undefined, }); checkoutErrors.add(!ok); sleep(1); // وقت التفكير بين التكرارات (محاكاة وتيرة المستخدم الحقيقي) } export function teardown(data) { // إلغاء رمز الاختبار لإبقاء نظام المصادقة نظيفاً http.del('https://api.example.com/auth/token', null, { headers: { Authorization: `Bearer ${data.token}` }, }); }

Stages مقابل Scenarios: اختيار الشكل الصحيح

مصفوفة stages هي الطريقة السريعة لتعريف ملف ارتفاع VU واحد. لكن حركة الإنتاج الحقيقية ليست مجموعة واحدة من المستخدمين المتطابقين. تمنحك واجهة برمجة scenarios مجمّعات منفّذة مستقلة، لكل منها عدد VUs خاص وشكل ارتفاع ومعدل وصول ودالة نص برمجي — قابلة للتركيب في نموذج تحميل واقعي.

المنفّذون الرئيسيون ومتى تستخدم كلاً منهم:

  • ramping-vus — الارتفاع الكلاسيكي. تتحكم في عدد VU بمرور الوقت. جيد لاختبارات التحمّل وتمارين الارتفاع المفاجئ. الافتراضي عند كتابة stages.
  • constant-arrival-rate — تحدد طلبات في الثانية، لا عدد VU. يدير k6 أكبر عدد ممكن من الـ VUs حسب الحاجة. استخدم هذا لمحاكاة معدل طلب ثابت (مثل 500 RPS من موازن التحميل) بشكل مستقل عن سرعة استجابة خدمتك. هذا هو المنفّذ الصحيح لاختبارات بوابة SLO.
  • ramping-arrival-rate — مثل ramping-vus لكن بـ RPS. جيد للعثور على حافة الإنتاجية.
  • per-vu-iterations — يُنفّذ كل VU بالضبط N تكراراً. مفيد للاختبارات المدفوعة بالبيانات حيث يحتاج كل VU صفاً فريداً من مجموعة البيانات.
// multi-scenario.js — تركيب حركة القراءة + حركة الكتابة + حركة الإدارة export const options = { scenarios: { // السيناريو 1: حركة قراءة كثيفة بمعدل وصول ثابت browse_products: { executor: 'constant-arrival-rate', rate: 300, // 300 RPS timeUnit: '1s', duration: '10m', preAllocatedVUs: 50, // تخصيص مسبق لتجنب زمن الإطلاق البارد maxVUs: 200, // السماح لـ k6 بالتوسع التلقائي عند الحاجة exec: 'browseFlow', // يشير إلى دالة مُصدَّرة في هذا الملف }, // السيناريو 2: حركة كتابة بحجم أقل create_orders: { executor: 'ramping-arrival-rate', startRate: 10, timeUnit: '1s', stages: [ { target: 10, duration: '2m' }, { target: 50, duration: '5m' }, { target: 10, duration: '2m' }, ], preAllocatedVUs: 20, maxVUs: 100, exec: 'checkoutFlow', }, // السيناريو 3: استطلاع إداري بمعدل ثابت منخفض admin_reports: { executor: 'constant-vus', vus: 5, duration: '10m', exec: 'adminFlow', startTime: '30s', // يبدأ بعد 30 ثانية للسماح بانتهاء الإطلاق }, }, thresholds: { 'http_req_duration{scenario:browse_products}': ['p(95)<200'], 'http_req_duration{scenario:create_orders}': ['p(95)<500'], 'http_req_failed': ['rate<0.005'], }, }; export function browseFlow() { /* ... */ } export function checkoutFlow() { /* ... */ } export function adminFlow() { /* ... */ }
k6 Scenarios: Composing Multiple Executor Pools 0 2m 5m 8m 10m time → browse_products constant-arrival-rate 300 RPS (up to 200 VUs auto-scaled) create_orders ramping-arrival-rate 10→50→10 RPS (up to 100 VUs) admin_reports constant-vus 5 VUs (startTime 30s) +30s Per-scenario thresholds p95 < 200ms (browse) · p95 < 500ms (orders)
ثلاثة منفّذي سيناريو k6 مستقلين يُركّبون مزيجاً واقعياً من حركة الإنتاج: قراءات كثيفة بـ RPS ثابت، كتابات متصاعدة، واستطلاع إداري منخفض المعدل مع تأخير في البداية.

Thresholds: جعل الاختبارات تفرض نفسها ذاتياً

اختبار تحميل بلا حدود مجرد جمع بيانات. الحدود هي SLO القابلة للتنفيذ: يخرج k6 بكود حالة غير صفري إذا خُرق أي حدّ، مما يعني فشل خط أنابيب CI وحجب الإصدار. هذه أهم ميزة يقدمها k6 — تحوّل اختبار الأداء إلى بوابة صحة.

تعبيرات الحدود تدعم أي مقياس مدمج أو مخصص مع المشغّلات p(N) وavg وmin وmax وrate وcount:

  • 'p(95)<500' — زمن الاستجابة عند النسيل 95 أقل من 500ms
  • 'p(99)<2000' — النسيل 99 أقل من 2 ثانية (SLO الذيل الطويل)
  • 'rate<0.01' — أقل من 1% معدل خطأ على مقياس Rate
  • 'count>1000' — ما لا يقل عن 1,000 إتمام ناجح (مفيد لتأكيدات تغطية البيانات)

يمكنك إرفاق علامة abortOnFail: true ومدة delayAbortEval بأي حدّ لجعل k6 يوقف الاختبار مبكراً حين تعلم أنه فاشل بالفعل — تجنباً لإهدار الحمل على نظام ساقط.

export const options = { thresholds: { // بوابات SLO القياسية (فشل CI عند الخرق) http_req_duration: [ { threshold: 'p(95)<500', abortOnFail: true, delayAbortEval: '1m' }, { threshold: 'p(99)<2000' }, ], http_req_failed: [ { threshold: 'rate<0.005', abortOnFail: true, delayAbortEval: '30s' }, ], // حدود المقاييس المخصصة — تفصيل لكل نقطة نهاية 'http_req_duration{url:https://api.example.com/checkout}': ['p(95)<600'], 'http_req_duration{url:https://api.example.com/catalog}': ['p(95)<150'], // مقياس أعمال مخصص checkout_error_rate: ['rate<0.01'], }, };
ضع وسماً لطلبات HTTP باسم، لا بعنوان URL. عندما يحتوي عنوان URL على معرّفات ديناميكية مثل /orders/12345، ينشئ k6 مقياساً منفصلاً لكل URL فريد. تتحوّل لوحة القيادة إلى ضجيج. استخدم الخيار { tags: { name: 'GET /orders/:id' } } على كل طلب. هذا ضروري لكي تكون لوحات Grafana والحدود ذات معنى.

تشغيل k6: محلي وموزع وفي CI

للتطوير المحلي والتصحيح، يكفي التشغيل على جهاز واحد. لمستويات تحميل تتجاوز تقريباً 2,000-5,000 VU (السقف النموذجي لجهاز واحد)، تُوزّع عبر عقد متعددة باستخدام k6 run --execution-segment أو مشغّل Kubernetes.

# --- تشغيل محلي --- k6 run --vus 50 --duration 5m checkout-flow.js # تمرير الأسرار عبر البيئة (لا تُضمَّن في النص) k6 run -e API_SECRET=$API_SECRET checkout-flow.js # --- الإخراج إلى InfluxDB + لوحة Grafana (إعداد SRE القياسي) --- k6 run --out influxdb=http://influxdb:8086/k6 checkout-flow.js # --- الإخراج إلى Prometheus remote-write (المجموعة الحديثة) --- K6_PROMETHEUS_RW_SERVER_URL=http://prometheus:9090/api/v1/write \ k6 run --out experimental-prometheus-rw checkout-flow.js # --- تشغيل موزع على 3 عقد (كل منها يتعامل مع ثلث الـ VUs) --- # العقدة 1: k6 run --execution-segment "0:1/3" --execution-segment-sequence "0,1/3,2/3,1" checkout-flow.js # العقدة 2: k6 run --execution-segment "1/3:2/3" --execution-segment-sequence "0,1/3,2/3,1" checkout-flow.js # العقدة 3: k6 run --execution-segment "2/3:1" --execution-segment-sequence "0,1/3,2/3,1" checkout-flow.js
لا تختبر تحميل الإنتاج من حاسوب محمول واحد. إذا كانت سرعة رفع اتصالك 50 Mbps واستجابات الـ API بحجم 10 كيلوبايت، تبلغ سقف الشبكة عند ~500 RPS قبل أن يتعرض الخادم للضغط. شغّل مولدات الحمل من داخل نفس VPC المستهدف، على أجهزة بعرض نطاق ترددي كافٍ. نتائج اختبار مقيّد بالشبكة لا معنى لها — تقيس اتصالك، لا خدمتك.

بيانات واقعية: تجنب فخ تسخين الكاش

اختبار تحميل يقصف معرّف منتج واحد سيسخّن كاش Redis في الطلب الأول ويقيس زمن استجابة الإصابة بالكاش لـ 99.9% من بقية التكرارات. هذا لا يخبرك شيئاً عن أداء المسار غير المُخزَّن. حركة الإنتاج تضرب آلاف المعرّفات المختلفة. استخدم SharedArray لتحميل مجموعة بيانات واقعية مرة واحدة (لا لكل VU) وتوزيع الحمل عبر جميع المعرّفات.

import { SharedArray } from 'k6/data'; import { randomItem } from 'https://jslib.k6.io/k6-utils/1.4.0/index.js'; // SharedArray تُحمَّل مرة واحدة في وقت init ومشتركة للقراءة فقط عبر جميع الـ VUs // لا استهلاك ذاكرة لكل VU — حرج عند تشغيل +10k VU const products = new SharedArray('products', function () { return JSON.parse(open('./data/products.json')); // 10,000 معرّف منتج }); const users = new SharedArray('users', function () { return JSON.parse(open('./data/users.json')); // 5,000 حساب مستخدم اختبار }); export default function () { const product = randomItem(products); const user = randomItem(users); const res = http.get(`https://api.example.com/products/${product.id}`, { tags: { name: 'GET /products/:id' }, // تجميع المقياس بغض النظر عن المعرّف }); check(res, { 'status 200': (r) => r.status === 200 }); sleep(Math.random() * 2 + 0.5); // وقت تفكير عشوائي 0.5-2.5 ثانية }

أنماط الإخفاق الشائعة في الإنتاج

معرفة أنماط الإخفاق ستوفّر عليك ساعات في نتائج اختبار غير صالحة:

  • الحذف المنسّق (Coordinated Omission): إذا نام الـ VU أثناء انتظار استجابة بطيئة، يبدأ التكرار التالي لاحقاً — وتظهر الاستجابات البطيئة بنسبة أقل في النسيلات. استخدم منفّذ constant-arrival-rate لفصل معدل الوصول عن زمن استجابة الخدمة وقياس سلوك قائمة الانتظار الحقيقي.
  • هيمنة عبء مصافحة TLS: اختبارات قصيرة المدة (أقل من دقيقتين) بعدد كبير من الـ VUs قد تُظهر زمن استجابة مرتفعاً اصطناعياً لأن مصافحات TLS تهيمن. شغّل الاختبارات لفترة كافية لاستقرار تجمّعات الاتصال.
  • عنق زجاجة في حل DNS: عندما يحلّ كل VU DNS بشكل مستقل، يمكن لألف VU إنهاك خادم DNS الداخلي. استخدم --dns ttl=60s لتخزين DNS مؤقتاً طوال مدة الاختبار.
  • تسرّب ذاكرة يكشفه اختبار التحمّل الطويل: اختبار إجهاد لمدة 5 دقائق ينجح؛ اختبار تحمّل لمدة ساعتين يكشف تسرّب ذاكرة بطيئاً في تجمّع اتصالات خدمتك. نفّذ دائماً اختبار تحمّل قبل أي إصدار كبير.