مشروع: خدمة تعمل
درس 6 من 6
6مشروع: خدمة تعمل
الصورة الكبيرةفي هذا المشروع تشغّل نموذجك خلف نقطة نهاية حقيقية، بلوحة أرقام تعرض الطلبات والأخطاء والزمن، ثم تضغط عليها بحمل حقيقي حتى تعرف أين تنكسر. والتقرير النهائي يقول بالأرقام: كم طلبًا في الثانية تخدم الخدمة بزمن مقبول، وكم يكلّف ألف طلب، وماذا يحدث بعد ذلك.
ماذا تشاهد في الرسم: أربعة عمّال وكل طلب نحو 200 ملّي ثانية. حتى نصف السعة يكاد p95 لا يتحرّك، ثم يتضاعف عند 90% ويقارب ثلاث ثوانٍ عند 98%. لهذا تخطّط لتعمل الخدمة عند 60% إلى 70% في الذروة.
خارطة هذا الجزء
المفاهيم بالترتيب، اضغط أيًّا منها للقفز إليه:
طريقان إلى نفس الفكرة
تشغّل نموذجك كخدمة حقيقية: واجهة تستقبل الطلبات، ولوحة أرقام تعرض الزمن والأخطاء، ثم ترسل إليها مئات الطلبات دفعة واحدة لترى كيف تتحمل الضغط. التقرير يقول ما حدث فعلًا بالأرقام، لا ما تتوقع أن يحدث.
خدمة FastAPI في حاوية مع نقطة صحة ومقاييس بصيغة Prometheus ولوحة Grafana، واختبار حمل بأداة مثل locust أو k6. التقرير يذكر p50 وp95 والإنتاجية ونسبة الأخطاء ونقطة الانهيار.
قبل أن تبدأ
📦الحاويات والنشر
خدمتك في حاوية برقم نسخة وإعدادات من البيئة.
📡المراقبة والانجراف
عندك مقاييس الطلبات والأخطاء والزمن جاهزة للوحة.
الفكرة الجوهرية
في هذا المشروع تشغّل نموذجك خلف نقطة حقيقية وتدفع عليها حملًا حتى تعرف أين تنكسر. تبدأ بحساب السعة، ثم ترى الطابور يقترب منها، ثم تصمّم اختبار الحمل، ثم تكتب التقرير.
1السعة النظرية
السعة عدد العمّال مضروبًا في ما يخدمه كل عامل في الثانية. عامل يأخذ 200 ملّي ثانية لكل طلب يخدم 5 طلبات في الثانية.
انظر إلى الرسم: أربعة عمّال يعطون سعة 20 طلبًا في الثانية.
2الطابور قرب السعة
السعة النظرية ليست الحمل الذي تعمل عليه. كلما اقتربت منها انتظرت الطلبات دورها، فيرتفع الزمن دون أي زيادة في الحساب.
import heapq
import random
def simulate(rate_per_s, workers=4, service_ms=200, n=5000, seed=0):
rnd = random.Random(seed)
t, free_at, waits = 0.0, [0.0] * workers, []
heapq.heapify(free_at)
for _ in range(n):
t += rnd.expovariate(rate_per_s) # next request arrives
start = max(t, heapq.heappop(free_at)) # wait for a free worker
service = rnd.expovariate(1 / (service_ms / 1000)) # how long this one takes
heapq.heappush(free_at, start + service)
waits.append((start + service - t) * 1000)
waits.sort()
return waits[n // 2], waits[int(n * 0.95)]
capacity = 4 / 0.2 # 4 workers x 5 requests/s each
print(f"capacity: {capacity:.0f} requests/s")
for rate in [5, 10, 15, 18, 19.5]:
p50, p95 = simulate(rate)
print(f"{rate:5} req/s ({rate / capacity:4.0%} busy): p50 {p50:6.0f} ms p95 {p95:7.0f} ms")
حتى 50% من السعة، الزمن ثابت تقريبًا. عند 75% يبدأ الارتفاع، وعند 90% تضاعف p95، وعند 98% يقارب ثلاث ثوانٍ لطلب يستغرق 200 ملّي ثانية فعلًا. كل هذا الزمن الإضافي انتظار في الطابور لا حساب. ولهذا تخطّط السعة لتعمل الخدمة عند 60% إلى 70% في الذروة، لا عند 95%.
3اختبار الحمل
اختبار الحمل الجيد يشبه الاستخدام الحقيقي: مدخلات من توزيع الأطوال الحقيقي، وحمل يرتفع على مراحل، ومن جهاز غير الخادم.
انظر إلى الرسم: أربع مراحل، كل مرحلة دقيقتان، وعدد المستخدمين يتضاعف.
يحتاج pip install locust وخدمتك تعمل، فهو للقراءة هنا.
import json
import random
from locust import HttpUser, task, between, LoadTestShape
SAMPLES = [json.loads(line)["text"] for line in open("real_inputs.jsonl", encoding="utf-8")]
class ApiUser(HttpUser):
wait_time = between(0.5, 1.5)
@task
def predict(self):
self.client.post("/predict", json={"text": random.choice(SAMPLES)})
class Stages(LoadTestShape):
stages = [(120, 5), (240, 10), (360, 20), (480, 40)] # (end second, users)
def tick(self):
t = self.get_run_time()
for end, users in self.stages:
if t < end:
return users, users
return None
المدخلات من ملف رسائل حقيقية (منقّحة)، والحمل يتصاعد على أربع مراحل كل منها دقيقتان. شغّله بـlocust -f load.py --host http://server:8000 --headless --csv results واكتب لكل مرحلة: الطلبات في الثانية، وp50 وp95، ومعدّل الأخطاء، من اللوحة ومن ملف النتائج معًا.
4اللوحة والتقرير
أثناء الاختبار راقب اللوحة لا أداة الحمل وحدها: الطلبات في الثانية، ونسبة الأخطاء، و p50 و p95 لكل نسخة. فاللوحة هي التي ستخبرك بالمشاكل في الإنتاج.
انظر إلى الرسم: التقرير صفحة واحدة بأربعة أجزاء.
كيف يعمل، خطوة بخطوة: كيف تُجري اختبار الحمل
تكتب هدف p95 قبل الاختبار، مثل «تحت 500 ملّي ثانية».
تجهّز مدخلات حقيقية منقّحة بنفس توزيع الأطوال، وتشغّل أداة الحمل من جهاز غير الخادم.
ترفع الحمل على مراحل، دقيقتين لكل مرحلة، وتسجّل لكل مرحلة الطلبات في الثانية وp50 وp95 والأخطاء.
السعة هي آخر مرحلة قبل تجاوز الهدف أو ظهور الأخطاء، ومنها تحسب التكلفة لكل ألف طلب.
مثال محلول
قبل أي اختبار حمل، افهم لماذا ينهار الزمن قرب السعة القصوى. خدمة بأربعة عمّال، كل طلب يستغرق 200 ملّي ثانية في المتوسط، فسعتها النظرية 20 طلبًا في الثانية. لنرَ ما يحدث للزمن حين نقترب منها.
جرّب بلا كود
خرافة مقابل حقيقة
| الخرافة | الحقيقة |
|---|---|
| خادم سعته 20 طلبًا في الثانية يكفي لذروة 18. | عند 90% يتضاعف p95، وأي زيادة صغيرة تدفعه للانهيار، وخادم واحد يعني أن أي عطل يوقف الخدمة. |
| الزمن يرتفع قرب السعة لأن النموذج يبطؤ. | زمن المعالجة ثابت. الطلبات تصل عشوائيًّا فتتكدّس أحيانًا، وقرب السعة لا وقت فراغ لتصريفها، فيكبر الطابور. |
| جملة واحدة مكرّرة تكفي لاختبار الحمل. | لا تمثّل الأطوال الحقيقية، والذاكرة المؤقتة تجعل النتائج متفائلة. الطلبات الطويلة هي ما يصنع p95. |
جرّب بنفسك
لماذا يرتفع الزمن بشدّة قرب السعة القصوى رغم أن زمن المعالجة لكل طلب ثابت؟
الطلبات تصل عشوائيًّا لا بانتظام، فتتجمّع أحيانًا. قرب السعة لا يوجد وقت فراغ لتصريف التجمّع، فيطول الطابور.
اختبار حمل يرسل نفس الجملة القصيرة 10 آلاف مرة. ما المشكلة؟
الزمن يعتمد على طول المدخل، والطلبات الطويلة هي التي تصنع p95. ومدخل واحد مكرّر قد يُخدم من ذاكرة مؤقتة لن تساعدك في الإنتاج.
بطاقات مراجعة سريعة
تحقق من فهمك
خدمتك تتحمّل 20 طلبًا في الثانية نظريًّا، والذروة المتوقّعة 18. هل يكفي خادم واحد؟
لا. عند 90% من السعة يتضاعف p95 كما رأيت في المحاكاة، وأي ارتفاع صغير فوق التوقّع يدفعها للانهيار. وخادم واحد يعني أن أي عطل أو نشر يوقف الخدمة كلها. خطّط لتعمل عند 60% إلى 70% في الذروة، أي خادمين على الأقل هنا.
في المحاكاة، لماذا كان p95 عند 25% انشغالًا 585 ملّي ثانية مع أن متوسط الطلب 200؟
لأن زمن المعالجة نفسه عشوائي بتوزيع أُسّي، فبعض الطلبات تستغرق أضعاف المتوسط حتى بلا طابور. الطابور يضيف فوق ذلك كلما اقتربت من السعة.
مصادر هذا الدرس
مرتّبة بالنوع. ابدأ بالتوثيق الرسمي إذا تبي المرجع الدقيق، وبالمقال أو الفيديو إذا تبي الشرح أولًا.
توثيق رسمي1
خلّصت الدرس؟
سجّل الدخول ليُحفظ تقدّمك.