8.9 KiB
8.9 KiB
تقرير حادثة (Postmortem) — انقطاع خدمة بسبب OOMKilled متكرر
الحالة: مغلق (Resolved) التأثير: انقطاع كامل للخدمة لمدة 45 دقيقة الخطورة: SEV-2
1. الملخص التنفيذي
تعطّل التطبيق المستضاف على منصة غيمة عن العمل لمدة 45 دقيقة. السبب المباشر هو دخول الحاوية (Container) في حلقة تعطّل متكررة (CrashLoop) ناتجة عن OOMKilled — أي أن نظام التشغيل أنهى العملية (Process) بالقوة لأنها تجاوزت حد الذاكرة (Memory Limit) المخصص لها. كل مرة كانت المنصة تعيد تشغيل الحاوية تلقائيًا، كانت تعود لتستهلك نفس القدر من الذاكرة خلال دقائق وتتعطل من جديد، وهو ما جعل الخدمة غير متاحة بشكل شبه مستمر طوال فترة الحادثة.
2. الجدول الزمني (Timeline)
| الوقت (تقريبي) | الحدث |
|---|---|
| T+0 دقيقة | بدء ارتفاع غير طبيعي في استهلاك الذاكرة (Memory) للحاوية، غير ملحوظ بعد من الفريق |
| T+3 دقيقة | استهلاك الذاكرة يتجاوز الحد المخصص (Memory Limit) → kernel OOM Killer يوقف العملية |
| T+3 دقيقة | منصة غيمة تكتشف توقف الحاوية وتعيد تشغيلها تلقائيًا (Auto-restart) |
| T+3 إلى T+40 دقيقة | حلقة متكررة: الحاوية تبدأ، تستهلك الذاكرة بسرعة، تُقتل (OOMKilled)، تُعاد تلقائيًا — والتطبيق غير مستجيب لمعظم الطلبات خلال هذه الفترة |
| T+12 دقيقة | أول تنبيه (Alert) يصل لفريق العمليات بسبب فشل متكرر في health check |
| T+15 دقيقة | بدء التحقيق: مراجعة السجلات (Logs) ولوحة المراقبة على غيمة |
| T+25 دقيقة | تحديد السبب: تسريب ذاكرة (Memory Leak) تراكمي في مسار معالجة الطلبات الكبيرة (لم يكن هناك تحرير صحيح للـ cache بعد كل طلب) |
| T+35 دقيقة | إجراء مؤقت (Mitigation): رفع حد الذاكرة (Memory Limit) للحاوية مؤقتًا + إعادة نشر (Redeploy) نسخة سابقة مستقرة (Rollback) |
| T+45 دقيقة | استقرار الخدمة، عودة معدل الاستجابة الطبيعي، إغلاق الحادثة (Incident Resolved) |
| T+1 يوم | إصلاح جذري: تعديل الكود لتحرير الـ cache بشكل صحيح، ونشره بعد اختبار في staging |
3. السبب الجذري (Root Cause)
- كان هناك تسريب ذاكرة تدريجي (gradual memory leak) في التطبيق: كائنات (objects) خاصة بمعالجة الطلبات الكبيرة كانت تبقى محتفظًا بها في الذاكرة (cache/in-memory buffer) دون تحرير بعد انتهاء الطلب.
- مع تزايد عدد الطلبات، تراكم استهلاك الذاكرة تدريجيًا حتى تجاوز الحد الأقصى المحدد للحاوية (Memory Limit)، فقام نظام Linux (OOM Killer) بإنهاء العملية بالقوة — وهو ما يظهر في منصة غيمة كحالة OOMKilled.
- العامل المُضاعِف (contributing factor): لم يكن هناك auto-scaling أو حد أدنى/أقصى مضبوط بذكاء (فقط نسخة واحدة Single Replica)، لذلك أي إعادة تشغيل كانت تعني توقف الخدمة بالكامل بدلاً من أن تستوعبها نسخة أخرى سليمة.
- عامل مُضاعِف آخر: غياب تنبيه مبكر على اتجاه استهلاك الذاكرة (Memory Trend Alert) — التنبيه الوحيد كان بعد فشل health check، أي بعد وقوع المشكلة فعليًا وليس قبلها.
4. الإجراءات التصحيحية (Action Items)
| # | الإجراء | الأولوية | المسؤول |
|---|---|---|---|
| 1 | إصلاح تسريب الذاكرة في كود معالجة الطلبات (تحرير الـ cache/buffers بعد كل طلب) | عالية | فريق التطوير |
| 2 | ضبط resources.requests و resources.limits بشكل واقعي بناءً على قياس فعلي لاستهلاك التطبيق تحت حمل حقيقي (Load Test) |
عالية | SRE |
| 3 | تفعيل Horizontal Auto-scaling (2 نسخ كحد أدنى) حتى لا تعتمد الخدمة على نسخة واحدة (انظر السياسة في القسم 5) | عالية | SRE |
| 4 | إضافة تنبيه استباقي عند وصول استهلاك الذاكرة إلى 80% من الحد المخصص (قبل الوصول لـ OOM) | متوسطة | SRE |
| 5 | إضافة اختبار حمل (Load/Soak Test) دوري في CI قبل كل نشر يفحص استقرار استهلاك الذاكرة مع الوقت | متوسطة | فريق التطوير |
| 6 | توثيق runbook لحالة OOMKilled ليستخدمه أي مهندس مناوب مستقبلًا لتقليل زمن التحقيق (MTTR) | منخفضة | SRE |
5. سياسة Auto-Scaling المقترحة لمنصة غيمة (لمنع التكرار)
الهدف: ألا يعتمد استقرار الخدمة على حاوية واحدة، وأن يتم استيعاب أي ضغط ذاكرة/معالج تلقائيًا قبل أن يتحول لانقطاع كامل.
# مثال توضيحي لسياسة auto-scaling على منصة غيمة
scaling_policy:
min_replicas: 2 # لا تقل عن نسختين دائمًا (لا يوجد Single Point of Failure)
max_replicas: 6
metrics:
- type: memory
target_utilization: 70% # التوسع عند وصول استهلاك الذاكرة لـ 70% من الحد المخصص
- type: cpu
target_utilization: 75%
scale_up:
cooldown_seconds: 60 # استجابة سريعة عند الضغط
scale_down:
cooldown_seconds: 300 # تقليل النسخ بحذر لتفادي "flapping"
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi" # حد واقعي مبني على قياس فعلي، وليس تخمينًا
cpu: "500m"
restart_policy:
max_restarts_before_alert: 3 # تنبيه فوري (لا انتظار) بعد 3 إعادة تشغيل متتالية لنفس الحاوية
لماذا هذا يمنع تكرار الحادثة:
min_replicas: 2يعني أن فشل حاوية واحدة بـ OOMKilled لا يوقف الخدمة بالكامل — الحاويات الأخرى تستمر في خدمة الطلبات.- التوسع على أساس استهلاك الذاكرة (وليس فقط CPU) يستوعب زيادة الحمل قبل أن تصل أي حاوية لحد OOM.
max_restarts_before_alertيحوّل "إعادة التشغيل الصامتة والمتكررة" (اللي حصلت في هذا الحادث) إلى تنبيه فوري بدل ما تستمر لمدة 45 دقيقة قبل ما حد يلاحظ.
6. كيفية اكتشاف هذه المشكلة مبكرًا باستخدام أدوات مراقبة غيمة
- Memory Trend Alerting: ضبط تنبيه على لوحة مراقبة غيمة عند وصول استهلاك الذاكرة لنسبة 80% من الـ limit لمدة تزيد عن دقيقتين متتاليتين (وليس فقط عند الفشل الفعلي). هذا كان سيعطي إنذارًا مبكرًا قبل أول OOMKilled بعدة دقائق.
- Restart Count Alerting: تنبيه فوري عند تجاوز عدد إعادة التشغيل التلقائي (auto-restart) لحاوية واحدة أكثر من مرتين خلال 10 دقائق — بدل الاعتماد فقط على فشل health check.
- Container Event Logs: مراجعة دورية لسجل أحداث المنصة (Container Events) اللي يسجل صراحة سبب إعادة التشغيل (OOMKilled / Crash / Manual)، بدل الاعتماد فقط على سجل التطبيق نفسه.
- Dashboard واحد يجمع: Memory usage % + Restart count + Response time + Error rate في نفس الشاشة، حتى يقدر المهندس المناوب يربط بين الأعراض بسرعة (استجابة بطيئة + ارتفاع ذاكرة + إعادة تشغيل = إشارة قوية على OOM قادم).
- Synthetic health checks خارجية (مثل سكريبت
health-check.shفي السؤال الأول) تعمل من خارج المنصة، لأن أدوات المراقبة الداخلية أحيانًا لا تكتشف أن الخدمة "بطيئة جدًا" حتى لو لم تكن "متوقفة تمامًا".