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

JMeter وأدوات الاختبار الأخرى

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

JMeter وأدوات الاختبار الأخرى

أرست الدرس السابق أسس k6 بوصفها مولّد الحمل الحديث الموجَّه للمطورين. لكن k6 ليست الأداة الوحيدة في الميدان — وعلى نطاق شركات التقنية الكبرى، يحمل اختيار الأداة تبعات حقيقية. كان Apache JMeter حصان العمل في الصناعة على مدى عشرين عاماً. يجلب Gatling لغة DSL مكتوبة بـ Scala وتقارير HTML فورية. يتيح Locust كتابة منطق الاختبار بلغة Python الخالصة. كل أداة تعكس فلسفة مختلفة حول حدود التفريق بين "تأليف الاختبار" و"تنفيذه". يرسم هذا الدرس المشهد بدقة ويمنحك إطار الحكم الذي يستخدمه مهندسو Amazon وNetflix وGoogle فعلياً عند اختيار أداة اختبار الحمل لسيناريو معين.

Apache JMeter: البنية والنموذج الذهني

JMeter تطبيق Java يُمثّل الحمل كشجرة من Thread Groups. كل Thread Group يحاكي N مستخدماً متزامناً؛ كل مستخدم محاكى يسير في سلسلة Sampler — HTTP Request وJDBC Request وgRPC Request — بالتسلسل، مُغلَّفة اختياريًا بـ Logic Controllers (If وWhile وLoop وTransaction) ومُعزَّزة بـ Pre/Post Processors وAssertions. تجمع Listeners النتائج وتعرضها.

القطعة الأساسية لخطة الاختبار هي ملف .jmx — مستند XML يُرمِّز الشجرة بأكملها. في مسارات CI، تتخلى عن واجهة المستخدم وتُشغّل بدون واجهة بالأمر jmeter -n -t plan.jmx -l results.jtl -e -o report/. تُولّد خيارا -e -o لوحة معلومات HTML مكتفية بذاتها من ملف JTL — أكثر تقرير جاهز جودةً في هذا النظام البيئي.

# تثبيت JMeter 5.6.3 (أحدث إصدار مستقر، يتطلب JDK 17+) wget https://downloads.apache.org/jmeter/binaries/apache-jmeter-5.6.3.tgz tar xzf apache-jmeter-5.6.3.tgz export PATH=$PATH:$(pwd)/apache-jmeter-5.6.3/bin # تشغيل خطة اختبار بدون واجهة، إخراج JTL + لوحة HTML jmeter -n \ -t checkout-flow.jmx \ -l results/run-$(date +%Y%m%d-%H%M%S).jtl \ -e -o results/html-report/ \ -Jthreads=200 \ -Jramp=60 \ -Jduration=300 # تجاوز خصائص الخطة وقت التشغيل بأعلام -J (يتجنب تحرير XML) # threads=200 → عدد المستخدمين الافتراضيين المتزامنين # ramp=60 → ثوانٍ للوصول إلى التزامن الكامل # duration=300 → المدة الكلية للاختبار بالثوانٍ # خصائص JMeter CLI مفيدة في CI: # -Jsummariser.interval=10 → طباعة ملخص كل 10 ثوانٍ # -Jjmeter.save.saveservice.output_format=csv → JTL مضغوط
الواجهة الرسومية للتأليف لا للتنفيذ. تستهلك واجهة JMeter الرسومية ذاكرة كبيرة وتُشغّل خيوطها الخاصة التي تُشوّه النتائج. كل فريق أجرى بالخطأ اختباراً بـ 500 VU داخل الواجهة الرسومية وتساءل لماذا الإنتاجية محدودة عند 200 RPS تعلّم هذا الدرس بالطريقة الصعبة. الواجهة الرسومية محرر خطط. التنفيذ على نطاق الإنتاج يكون دائماً بـ jmeter -n (بدون واجهة)، ومُوزَّعاً مثالياً عبر حاقنات متعددة.

الوضع الموزّع في JMeter

يمكن لعقدة متحكمة JMeter واحدة أن تُشغّل نحو 1000–1500 خيط متزامن قبل أن يُصبح ضغط GC وإدخال/إخراج الشبكة هو العنق الزجاجي. للتزامن الأعلى، يستخدم JMeter بنية المتحكم/العامل: متحكم واحد (الجهاز المُشغّل لخطة الاختبار) يُنسّق N عقدة عاملة (حاقنات). تستلم العُمّال خطة الاختبار عند بدء الاختبار وتُنفّذ بالتوازي وتُدفق النتائج للمتحكم.

# على كل عقدة عاملة — تشغيل عملية JMeter server jmeter-server -Djava.rmi.server.hostname=<WORKER_IP> # على المتحكم — تشغيل اختبار موزّع jmeter -n \ -t plan.jmx \ -R 10.0.0.11,10.0.0.12,10.0.0.13 \ -l results/distributed.jtl \ -e -o results/html-report/ \ -Gthreads=500 \ -Gramp=120 \ -Gduration=600 # -R → قائمة عناوين IP للعُمّال مفصولة بفواصل # -G → تعيين خاصية على جميع العُمّال البُعديين (مثل -J لكن مُذاع) # قواعد الجدار الناري المطلوبة: # المتحكم <- العُمّال: TCP 1099 (سجل RMI) + TCP 4000-4002 (دفق النتائج) # العُمّال <- المتحكم: TCP 60000 (منفذ النتائج الافتراضي، قابل للتهيئة في jmeter.properties)

أنماط JMeter الجاهزة للإنتاج

الفرق التي تُشغّل JMeter على النطاق الواسع طوّرت مجموعة من الممارسات غير القابلة للتفاوض:

  • اجعل كل شيء قابلاً للمعاملات. لا تضع عناوين URL الأساسية أو بيانات الاعتماد أو قيم التزامن كثوابت داخل ملف .jmx. استخدم خصائص JMeter (${__P(baseUrl,http://localhost)}) مُحقونة وقت التشغيل بأعلام -J. هذا يجعل نفس الخطة قابلة لإعادة الاستخدام عبر بيئات dev وstaging والإنتاج دون تعديلات XML.
  • استخدم CSV Data Set Config لبيانات مستخدم واقعية. تقديم نفس بيانات الاعتماد لـ 500 VU سيضرب بجدار تعارض الجلسات في أي نظام ذو حالة لكل مستخدم. يتيح عنصر CSV Data Set Config لكل VU قراءة صف مميز من CSV — مستخدمون ورموز ومعرفات طلبات فريدة.
  • أضف Response Assertion أو Duration Assertion لكل Sampler حرج. بدون assertions، يَعدّ JMeter كل HTTP 500 "نجاحاً" ما لم تُخبره بغير ذلك. أكِّد على كود الاستجابة وسلسلة نص الاستجابة وحد الكمون.
  • اضبط حجم JVM heap لعقد العُمّال. heap JMeter الافتراضي 1 غيغابايت. عند 500 خيط مع تخزين أجساد استجابة كبيرة، ستُصاب بـ OutOfMemoryError. عيّن JVM_ARGS="-Xms2g -Xmx4g -XX:+UseG1GC" في سكريبت shell الخاص بـ jmeter.
  • دفق النتائج إلى InfluxDB + Grafana في الوقت الفعلي. تقرير HTML يأتي بعد الحدث. للمراقبة الحية أثناء الاختبار، هيّئ Backend Listener بـ InfluxdbBackendListenerClient وأشر إلى InfluxDB، ثم استورد لوحة JMeter القياسية في Grafana (ID 5496).

Gatling: المنافس ذو DSL الـ Scala

يُترجم Gatling سيناريو الحمل كـ Scala (أو DSL Java/Kotlin الأحدث) ويُشغّله على Akka Netty — حلقة I/O غير محجوبة. الفرق المعماري عن JMeter جوهري: JMeter يُشغّل خيط نظام تشغيل واحد لكل مستخدم افتراضي؛ Gatling يُشغّل آلاف المستخدمين الافتراضيين مُتعدَّدة على مجموعة خيوط ثابتة. هذا يعني أن Gatling يستطيع الحفاظ على 10000+ اتصال متزامن من حاقن واحد قد يُصيب JMeter بـ OOM.

// محاكاة Gatling 3.10 — مشروع Maven/Gradle، ضع في src/gatling/simulations/ import io.gatling.javaapi.core.* import io.gatling.javaapi.core.CoreDsl.* import io.gatling.javaapi.http.* import io.gatling.javaapi.http.HttpDsl.* import java.time.Duration class CheckoutSimulation : Simulation() { val httpProtocol = http .baseUrl("https://api-staging.example.com") .acceptHeader("application/json") .contentTypeHeader("application/json") .header("X-Load-Test", "true") val login = exec( http("POST /auth/login") .post("/auth/login") .body(StringBody("""{"email":"user_#{userId}@example.com","password":"Passw0rd!"}""")) .check(status().`is`(200)) .check(jsonPath("$.token").saveAs("authToken")) ) val checkout = exec( http("POST /orders") .post("/orders") .header("Authorization", "Bearer #{authToken}") .body(StringBody("""{"productId":42,"qty":1}""")) .check(status().`is`(201)) .check(jsonPath("$.orderId").saveAs("orderId")) ) val scn = scenario("Checkout Flow") .feed(csv("users.csv").circular()) .exec(login) .pause(Duration.ofMillis(500), Duration.ofMillis(1500)) .exec(checkout) init { setUp( scn.injectOpen( nothingFor(Duration.ofSeconds(5)), rampUsers(500).during(Duration.ofSeconds(60)), constantUsersPerSec(100.0).during(Duration.ofSeconds(300)) ) ).protocols(httpProtocol) .assertions( global().responseTime().percentile3().lte(2000), // P99 <= 2s global().failedRequests().percent().lte(1.0) ) } } // التشغيل من جذر المشروع: // ./gradlew gatlingRun // التقرير في: build/reports/gatling/checkoutsimulation-<timestamp>/index.html

ميزة Gatling المدمجة في تقارير HTML هي الأكثر تفصيلاً في فئتها: توزيعات زمن الاستجابة لكل طلب، والمستخدمون النشطون عبر الزمن، ومخططات الطلبات/الثانية، وملخصات الأخطاء — كلها في ملف واحد مكتفٍ بذاته.

Locust: اختبار الحمل بلغة Python الأصيلة

يُعرّف Locust سلوك المستخدم الافتراضي كفئة Python ترث من HttpUser. المهام مُزيَّنة بـ @task(weight)؛ يُوزّع مُجدوِل Locust المهام نسبياً حسب الوزن. بيئة التشغيل هي حلقة تعاونية متعددة المهام قائمة على gevent — مثل Gatling، لا تُخصّص خيط نظام تشغيل واحد لكل VU.

# locustfile.py — Locust 2.x from locust import HttpUser, task, between, events import json, random class CheckoutUser(HttpUser): wait_time = between(0.5, 2.0) # وقت تفكير عشوائي بين المهام host = "https://api-staging.example.com" def on_start(self): """يعمل مرة واحدة عند إنشاء مستخدم محاكى.""" resp = self.client.post( "/auth/login", json={"email": f"user{random.randint(1,10000)}@example.com", "password": "Passw0rd!"}, name="/auth/login" ) self.token = resp.json()["token"] self.client.headers.update({"Authorization": f"Bearer {self.token}"}) @task(5) def browse_catalog(self): self.client.get(f"/products?page={random.randint(1,20)}", name="/products") @task(2) def view_product(self): pid = random.randint(1, 500) self.client.get(f"/products/{pid}", name="/products/[id]") @task(1) def checkout(self): self.client.post( "/orders", json={"productId": random.randint(1, 500), "qty": 1}, name="/orders" ) # تشغيل بدون واجهة (وضع CI): # locust -f locustfile.py \ # --headless \ # --users 500 \ # --spawn-rate 50 \ # --run-time 5m \ # --csv results/locust \ # --html results/locust-report.html # الوضع الموزّع (1 master + N workers): # locust -f locustfile.py --master # locust -f locustfile.py --worker --master-host=<MASTER_IP>

الميزة القاتلة لـ Locust لفرق المنصة هي واجهة الويب وAPI الحية. يمكنك بدء/إيقاف/تعديل أعداد المستخدمين في منتصف الاختبار عبر المتصفح أو curl http://localhost:8089/swarm. هذا يجعل الاختبار الاستكشافي وتجارب التصعيد التدريجي أسرع بكثير من إعادة تشغيل خطة JMeter.

Load testing tool landscape: JMeter vs Gatling vs Locust vs k6 Load Testing Tool Landscape JMeter Language: XML (GUI) Runtime: Java threads Concurrency: ~1k / node Protocols: HTTP/JDBC/ JMS/LDAP/gRPC+plugin Report: HTML dashboard Best: legacy stacks, non-HTTP protocols Gatling Language: Scala/Java Runtime: Akka Netty NIO Concurrency: 10k+ / node Protocols: HTTP/WS/ gRPC/JMS Report: Built-in HTML Best: high concurrency, JVM-native shops Locust Language: Python Runtime: gevent coroutines Concurrency: ~5k / node Protocols: HTTP (custom clients possible) Report: CSV + HTML Best: Python teams, interactive exploration k6 Language: JavaScript Runtime: Go + goja VM Concurrency: 10k+ / node Protocols: HTTP/WS/ gRPC/browser Report: Prometheus/Grafana Best: CI pipelines, developer-centric teams Tool Selection Heuristics Non-HTTP protocol (JDBC, JMS, LDAP)? → JMeter (broadest protocol plugin ecosystem) Python shop, need live UI, or exploratory testing? → Locust JVM shop, need best-in-class report, high concurrency? → Gatling CI-first, Grafana stack, developer-written tests? → k6 Massive scale (100k+ VUs) without managing injectors? → k6 Cloud / Grafana Cloud k6 | Gatling Enterprise | BlazeMeter (JMeter SaaS) | Artillery Cloud
إطار اختيار الأداة: JMeter يتصدر اتساع البروتوكولات، Gatling وk6 يتصدران كفاءة التزامن، Locust يتصدر بيئة Python الأصيلة، وكل أداة رئيسية لديها طبقة SaaS مستضافة سحابياً للحقن على نطاق كوكبي.

اختيار الأداة: إطار الحكم

المهندسون الكبار في شركات التقنية الكبرى لا يملكون ولاءً دينياً لأداة واحدة. يُقيّمون على أربعة محاور:

1. ملاءمة البروتوكول. إذا كان نظامك تحت الاختبار خدمة REST/gRPC HTTP، كل أداة في هذا الدرس تعمل. إذا احتجت اختبار حمل وسيط رسائل (ActiveMQ، Kafka)، أو قاعدة بيانات علائقية (استنفاد connection pool)، أو خادم LDAP، فنظام إضافات JMeter لا يُضاهى. لتطبيقات WebSocket المكثفة، كل من Gatling وk6 لديهما دعم أصلي من الدرجة الأولى؛ إضافة WebSocket لـ JMeter من طرف ثالث وهشة.

2. سقف التزامن لكل عقدة. نماذج thread-per-VU (JMeter) تصطدم بقيود نظام التشغيل عند نحو 1000–2000 خيط لكل JVM قبل أن تُهيمن توقفات GC. نماذج حلقة الأحداث (Gatling وk6 وLocust-gevent) تحافظ على 5000–20000 اتصال متزامن لكل عقدة. لسيناريوهات تتطلب 50000+ VU، ستُوزّع أفقياً بغض النظر عن الأداة؛ لكن أدوات حلقة الأحداث توزّع بتكلفة أقل.

3. تكامل النظام البيئي. إذا كان فريقك يُشحن بالفعل سكريبتات k6 في مسار CI (الدرس السابق)، فإضافة JMeter لمجموعة اختبار جديدة تُدخل سلسلتَي أدوات. تكلفة التوحيد حقيقية. في المقابل، إذا كان فريق ضمان الجودة يملك اختبار الحمل ويعيش في JMeter، المطالبة بإعادة كتابتهم بـ k6 لبوابة CI واحدة غير معقول. التقِ الفريق حيث هو — ما لم يُجبرك السقف البروتوكولي أو سقف التزامن على التغيير.

4. أمانة الاختبار كرمز. ملفات .jmx في JMeter هي XML. من المعروف أنها صعبة التمييز في pull requests والصيانة في نظام التحكم بالإصدار بدون واجهة رسومية. محاكاة Gatling وسكريبتات k6 وملفات Locust هي كود مصدر حقيقي: قابلة للمراجعة والتكوين والتحسين. للفرق التي تُشغّل اختبارات الحمل على كل pull request، الأدوات المبنية على الكود مُفضَّلة بشدة.

تجنب تشتت الأدوات؛ وحِّد لكل طبقة. فريق هندسة المنصة الناضج يُوحِّد عادةً على أداة واحدة للمعايير الدقيقة التي يكتبها المطورون (غالباً k6 في CI)، وأداة واحدة لمجموعات الانحدار التي يملكها ضمان الجودة (غالباً JMeter أو Gatling في مسار مجدوَل)، وطبقة مستضافة سحابياً لتشغيلات تخطيط السعة على النطاق الكامل للإنتاج. هذه ثلاث طبقات بحدود ملكية واضحة — لا خمس أدوات يملكها أحد تحديداً.
تسجيل وإعادة التشغيل فخ للتدفقات المعقدة. يُنتج مُسجّل JMeter و Recorder Gatling سكريبتات هشة تُثبّت ملفات تعريف الارتباط ورموز CSRF ومقاطع المسار الديناميكية كسلاسل نصية ثابتة. سكريبت مُسجَّل يوم الاثنين يفشل يوم الثلاثاء حين يتغير تنسيق رمز الجلسة. استخدم التسجيل فقط كهيكل مبدئي، ثم فوراً اجعل كل قيمة ديناميكية معاملة. الفرق التي تتعامل مع السكريبتات المُسجَّلة كجاهزة للإنتاج تدفع ضريبة الصيانة لسنوات.

Artillery وأدوات النظام البيئي الأخرى

المشهد يضم مزيداً من الداخلين الجديرين بالمعرفة بالاسم. Artillery (مبني على Node.js، سيناريوهات YAML أو JS) اكتسب قبولاً واسعاً بسبب DSL YAML البسيط ومُشغّل AWS Lambda الأصلي — يمكنك الوصول إلى 50000 VU بصورة لا خوادم كاملة دون إدارة عقد حاقن. NBomber (NET.) هو الإجابة الصحيحة حين يُشحن الفريق بـ C# ويريد اختبارات حمل آمنة النوع. Vegeta (Go CLI) هو أسرع طريقة للإجابة على سؤال مرجعي واحد: "ما أقصى RPS مستدامة لهذه النقطة الطرفية قبل أن يتجاوز P99 SLO؟" — إنه ليس مُشغّل سيناريوهات بل مُرسِل معدل HTTP مع إخراج إحصائي ممتاز. لاختبار الحمل على مستوى المتصفح (تنفيذ JavaScript والعرض الفعلي)، يحل k6 browser وسيناريوهات Artillery المبنية على Playwright محل مناهج شبكة Selenium القديمة.

القاسم المشترك عبر كل هذه الأدوات: إنها مدخلات لنفس مسار التحليل. سواء كان الإخراج الخام ملف .jtl أو تقرير Gatling JSON أو CSV من Locust، الخطوة التالية دائماً واحدة — استخراج كمون P50/P95/P99، ومعدل الخطأ، والإنتاجية، ومقارنتها بـ SLOs الخاصة بك، وقيادة قرار. الأداة تُولّد البيانات؛ المهندس يُفسّرها. هذا التفسير موضوع الدرس التاسع.