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

مشروع: اختبار تحميل خدمة

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

مشروع: اختبار تحميل خدمة

أعطتك الدروس التسعة السابقة المفردات والأدوات والتقنيات. هذا الدرس يُغلق الدائرة: ستخطط وتكتب السكريبت وتُنفّذ وتُقدّم تقريرًا عن اختبار تحميل كامل لواجهة HTTP واقعية. كل خطوة تعكس بدقة كيف يُجري مهندسو Google SRE ومهندسو الأداء في Netflix مراجعات الجاهزية للإنتاج — بدءًا من كتابة خطة اختبار يوافق عليها أصحاب المصلحة، وصولًا إلى فتح تذكرة Jira بسبب جذر التراجع في الأداء.

الخطوة الأولى — اكتب خطة الاختبار قبل لمس الطرفية

اختبار التحميل بدون خطة هو تجربة بدون فرضية. تجيب خطة الاختبار على خمسة أسئلة قبل إرسال أي حزمة:

  1. الخدمة قيد الاختبار (SUT): أي نقاط نهاية، وأي بيئة (staging أو مرآة الإنتاج)، وأي مجموعة بيانات؟
  2. معايير النجاح (SLOs): كيف يبدو "النجاح"؟ عبّر عنه بأهداف P99 للكمون ومعدل الخطأ والإنتاجية — لا بصفات مبهمة.
  3. ملف التحميل: طلبات في الثانية في الحالة الثابتة، مدة الرفع، مدة النقع، شكل الارتفاع المفاجئ. استند إلى percentiles حركة الإنتاج من Grafana/Datadog لتكون الأرقام مستندة للواقع.
  4. نطاق المراقبة: أي لوحات تحكم وسجلات وعيّنات من أداة الاستبيان ستكون نشطة خلال التشغيل؟
  5. حدود التراجع وتأثير الاختبار: إذا تدهورت الخدمة إلى أقل من 20 % من طاقتها، من يقرر الإيقاف؟ ما قواطع الدوائر أو رؤوس تحديد المعدل التي تمنع الحادث العرضي على التبعيات المشتركة؟
مشاركة خطة اختبار من صفحة واحدة مع فريق الباك-إند ومدير قاعدة البيانات والمهندس المناوب قبل تشغيل الاختبار تمنع السؤال الشائع في ما بعد: "لماذا ضربت قاعدة بيانات الإنتاج النسخية الساعة الثانية ظهرًا يوم الثلاثاء؟"

الخطوة الثانية — تحضير البيئة

استخدم بيئة staging تطابق الإنتاج بمقياس واقعي: نفس أنواع الأجهزة، نفس حجم قاعدة البيانات (أو نسخة إنتاجية معقّمة)، بنفس قواعد CDN معطّلة، ونفس feature flags. اختبار على مجموعة staging منخفضة التوفير لا يخبرك بشيء مفيد عن سعة الإنتاج.

أنشئ منفّذ k6 على جهاز (أو k6 Cloud / مشغّل k6 OSS الموزّع من Grafana) ليس على المضيف ذاته للخدمة قيد الاختبار. يجب أن يكون RTT الشبكة بين مُولّد الحمل والخدمة واقعيًا — نفس منطقة AWS، نفس VPC، مماثل لجغرافيا العميل الحقيقي. صدّر مقاييس البداية من Prometheus/Datadog إلى لقطة حتى تتمكن من مقارنة القبل والبعد.

# تحقق أن staging تطابق أنواع أجهزة prod kubectl get nodes -o wide --context=staging kubectl get nodes -o wide --context=prod # تأكد أن التطبيق على نفس وسم الصورة kubectl get deployment api-server -n production --context=staging \ -o jsonpath='{.spec.template.spec.containers[0].image}' # لقط مجموعة مقاييس Prometheus الحالية كخط أساس curl -s http://prometheus.internal:9090/api/v1/query \ --data-urlencode 'query=histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket[5m])) by (le, handler))' \ | jq '.data.result' > baseline_p99.json

الخطوة الثالثة — كتابة سكريبت k6

يجب أن يُنمذج سكريبتك رحلات المستخدم الحقيقية، لا ضرب نقاط نهاية عشوائية. بالنسبة لـ API سلة التسوق، الرحلات المحورية هي: تصفّح الكتالوج، عرض تفاصيل المنتج، إضافة إلى السلة، إتمام الشراء. رجّحها حسب بيانات التحليلات — في معظم الشركات حجم التصفّح أكبر 10 مرات من حجم الشراء.

استخدم k6 scenarios لترميز أشكال تنفيذ متعددة في ملف واحد، حتى يشترك اختبار CI، واختبار النقع، واختبار الارتفاع في نفس السكريبت دون تكرار.

// load-test.js — سكريبت k6 بجودة الإنتاج import http from 'k6/http'; import { check, sleep, group } from 'k6'; import { Rate, Trend } from 'k6/metrics'; const errorRate = new Rate('errors'); const checkoutDur = new Trend('checkout_duration', true); // --- عتبات SLO (تفشل الاختبار عند الخرق) --- export const options = { scenarios: { // 1. الحالة الثابتة: رفع إلى ذروة prod، تثبيت 20 دقيقة steady_state: { executor: 'ramping-vus', startVUs: 0, stages: [ { duration: '3m', target: 200 }, { duration: '20m', target: 200 }, { duration: '2m', target: 0 }, ], gracefulRampDown: '30s', }, // 2. الارتفاع المفاجئ: 5x حركة لمدة 60 ثانية ثم العودة spike: { executor: 'ramping-vus', startTime: '25m', startVUs: 0, stages: [ { duration: '10s', target: 1000 }, { duration: '60s', target: 1000 }, { duration: '10s', target: 0 }, ], }, }, thresholds: { http_req_duration: ['p(99)<500', 'p(95)<250'], http_req_failed: ['rate<0.01'], checkout_duration: ['p(99)<800'], errors: ['rate<0.005'], }, }; const BASE = __ENV.BASE_URL || 'https://api-staging.example.com'; import { SharedArray } from 'k6/data'; const users = new SharedArray('users', function () { return JSON.parse(open('./test-users.json')); }); export default function () { const user = users[__VU % users.length]; const headers = { 'Authorization': `Bearer ${user.token}`, 'Content-Type': 'application/json', }; group('browse', function () { const res = http.get(`${BASE}/v1/products?page=1&limit=20`, { headers }); check(res, { 'browse 200': (r) => r.status === 200, 'browse latency ok': (r) => r.timings.duration < 300, }) || errorRate.add(1); }); sleep(Math.random() * 2 + 0.5); group('checkout', function () { const start = Date.now(); let res = http.post(`${BASE}/v1/cart/items`, JSON.stringify({ product_id: 'sku-42', qty: 1 }), { headers }); check(res, { 'cart 201': (r) => r.status === 201 }) || errorRate.add(1); res = http.post(`${BASE}/v1/orders`, JSON.stringify({ payment_method: 'test_card' }), { headers }); check(res, { 'order 201': (r) => r.status === 201 }) || errorRate.add(1); checkoutDur.add(Date.now() - start); }); sleep(1); }
ملئ بيانات الاختبار مسبقًا (حسابات المستخدمين، كتالوج المنتجات، سلات موجودة) في قاعدة البيانات قبل تشغيل الاختبار. توليد البيانات داخل حلقة VU يُشوّه قياسات الكمون ويُنتج أنماط وصول غير واقعية لقاعدة البيانات. على مستوى Google، إعداد البيانات هو pipeline منفصل يعمل قبل ساعات من نافذة الاختبار.

الخطوة الرابعة — التشغيل مع تفعيل المراقبة الكاملة

لا تُشغّل اختبار تحميل في الظلام أبدًا. قبل التنفيذ، تأكد أن فترات scrape في Prometheus عند 15 ثانية أو أقل، وأن التتبع الموزع يأخذ عينات بنسبة 100 % (أو 10 % على الأقل مع tail-based sampling على التتبعات البطيئة)، وأن محلل الأداء (pprof, async-profiler, py-spy) جاهز للتشغيل عند الطلب.

# تصدير النتائج إلى InfluxDB للوحة تحكم Grafana k6 run \ --out influxdb=http://influxdb.internal:8086/k6 \ --out json=results/run-$(date +%Y%m%d-%H%M).json \ -e BASE_URL=https://api-staging.example.com \ load-test.js # في طرفية ثانية — مراقبة معدل الخطأ مباشرة watch -n5 'curl -s "http://influxdb.internal:8086/query" \ --data-urlencode "db=k6" \ --data-urlencode "q=SELECT last(\"value\") FROM \"http_req_failed\" WHERE time > now()-1m" \ | jq .results[0].series[0].values'

أثناء تشغيل الاختبار، راقب أربع إشارات في آنٍ واحد: CPU وذاكرة الخدمة (هل نحن مقيّدون بالمعالج أم بالذاكرة؟)، استخدام مجمع اتصالات قاعدة البيانات (استنزاف المجمع هو السبب الأول لتدهور الكمون المفاجئ عند 500+ RPS)، اتجاه كمون P99 (هل مستقر أم يرتفع تدريجيًا؟)، وتكرار توقفات GC لخدمات JVM/Go.

End-to-end load test pipeline: generator, SUT, observability stack k6 / JMeter Load Generator 200 VUs HTTP API Service (SUT) K8s Deployment 3 replicas SQL PostgreSQL Primary + Replica pool: 100 conns Observability Stack Prometheus Jaeger / Tempo InfluxDB + Grafana pprof / py-spy End-to-End Load Test Pipeline Load Generator → SUT → Downstream Dependencies → Observability
خط أنابيب اختبار التحميل الشامل: k6 يُولّد الحمل، الخدمة تتواصل مع تبعياتها، وتلتقط منظومة المراقبة الكاملة المقاييس والتتبعات وعينات المحلل في آنٍ واحد.

الخطوة الخامسة — تحليل النتائج: من الأرقام إلى الجذر

المخرجات الخام من k6 أو InfluxDB هي بيانات، ليست رؤية. مهمتك الانتقال من "ارتفع P99 إلى 1.2 ثانية عند 180 VU" إلى "استنزاف مجمع الاتصالات على النسخة القرائية عند ~175 استعلامًا متزامنًا." هذه السلسلة من الاستدلال تتطلب ربط ثلاث طبقات:

  1. جانب العميل (k6): متى بالضبط تدهور الكمون؟ ما RPS وعدد VU المرتبطان؟ هل قفز معدل الخطأ في نفس الوقت أم تأخر عن تسلق الكمون؟
  2. جانب الخدمة (APM / traces): أي span استحوذ على الكمون المضاف — كود التطبيق أم استعلام قاعدة البيانات أم الشبكة؟ استخدم Jaeger أو Tempo للعثور على أبطأ التتبعات خلال نافذة التدهور.
  3. البنية التحتية (Prometheus): هل وُصل لأي حد من حدود الموارد؟ الإشارات الكلاسيكية: تقييد CPU (container_cpu_cfs_throttled_seconds_total)، أحداث OOM، وقت انتظار مجمع اتصالات قاعدة البيانات (pgbouncer_client_wait_seconds)، توقفات GC (jvm_gc_pause_seconds).
# PromQL: إيجاد تشبع مجمع قاعدة البيانات خلال نافذة الاختبار sum(pgbouncer_client_wait_seconds) by (database) [30m:15s] # PromQL: تقييد CPU لكل حاوية sum(rate(container_cpu_cfs_throttled_seconds_total[1m])) by (container) # ملخص k6 JSON — استخراج P99 ومعدل الخطأ cat results/run-20250612-1430.json | jq ' .metrics | { p99_ms: .http_req_duration."p(99)", p95_ms: .http_req_duration."p(95)", error_rate: .http_req_failed.rate, rps: .http_reqs.rate }'
أخطر نتيجة لاختبار التحميل هي النتيجة الناجحة التي تُخفي وضع فشل صامتًا. تحقق دائمًا أن معدل نجاح check() هو 100 % — معدل خطأ 0.5 % على 500 RPS يعني 2.5 خطأ/ثانية، وهو ما يُفشل مئات الآلاف من المعاملات يوميًا في الإنتاج بصمت. العتبات التي تُبوّب فقط على كمون P99 ستتجاهل هذا.

الخطوة السادسة — كتابة تقرير الأداء

تقرير الأداء هو عقد بين الهندسة والأعمال. يجب أن يحتوي على: (1) ملخص تنفيذي بفقرة واحدة بحكم نجاح/فشل واضح مقابل كل SLO؛ (2) مخطط سلاسل زمنية يُظهر كمون P50/P95/P99 ومعدل RPS على مدار الاختبار؛ (3) الاختناقات المُحدّدة مرتّبة حسب الأثر؛ (4) توصيات محددة وقابلة للتنفيذ (لا "حسّن استعلامات قاعدة البيانات" — بل "أضف مؤشرًا مركّبًا على (user_id, created_at DESC) في جدول orders، الانخفاض المُقدّر لوقت الاستعلام من 180 مللي ثانية إلى 12 مللي ثانية بناءً على EXPLAIN ANALYZE")؛ و(5) قسم مخاطر التراجع يُوضّح أي التغييرات آمنة للشحن وأيها يحتاج إعادة اختبار.

في شركات مثل LinkedIn وStripe، تصبح تقارير الأداء وثائق حية تُتبّع في نفس نظام إدارة المشاريع مثل تذاكر الهندسة. التراجع المكتشف في CI يُشير إلى تقرير الخط الأساسي الأصلي، مما يجعل الفارق لا يُنكر والإصلاح محسوبًا على PR محدد.

أتمتة توليد التقرير. علم --out json في k6 مع سكريبت Python أو Go قصير يستطيع إنتاج تقرير Markdown، وحفظه كمنتج CI، ونشر ملخص Slack بحالة النجاح/الفشل في أقل من 30 ثانية. هكذا تجعل الأداء بوابة من الدرجة الأولى لا طقسًا دوريًا.

تجميع الكل: قائمة التحقق الجاهزة للإنتاج

  • مراجعة خطة الاختبار من قِبَل الباك-إند ومدير قاعدة البيانات والمهندس المناوب قبل التنفيذ
  • البيانات مُعبّأة مسبقًا؛ لا توليد بيانات داخل حلقة k6 VU
  • بيئة staging مُتحقق منها لتطابق أنواع أجهزة الإنتاج وعدد النسخ
  • المراقبة نشطة: Prometheus والتتبع والمحلل كلها تعمل
  • العتبات تُرمّز SLOs، لا فقط percentiles الكمون — أدرج معدل الخطأ
  • سيناريوهات الارتفاع والنقع تُشغَّل بالإضافة إلى الحالة الثابتة
  • السبب الجذري مُؤكّد على طبقة البنية التحتية قبل الإعلان عن اختناق
  • التقرير يتضمن تذاكر قابلة للتنفيذ، لا نصائح عامة
  • بوابة تكامل CI تُشغّل سيناريو الحالة الثابتة على كل PR على المسار الحرج

هذا الانضباط الشامل هو ما يُفرّق بين مهندس الأداء الذي يشحن الثقة ومن يشحن الشك. كل درس في هذه الدورة — من قانون Little إلى استبيان flamegraphs وحدة المعالج — يُغذّي هذه العملية الواحدة. المخرج ليس رقمًا؛ إنه قرار هندسي موقّع بشأن ما إذا كانت الخدمة جاهزة لحمل إنتاجي حقيقي.