الملفات
ghaymah-exam--Yousifhamdyam…/q2-postmortem/q3-cicd/postmortem-report.md

8.0 KiB
خام اللوم التاريخ

تقرير Postmortem — انقطاع 45 دقيقة بسبب OOMKilled متكرر

الحالة: تم الحل | الخطورة: SEV-2 | مدة التأثير: 45 دقيقة | النطاق: تطبيق واحد على ghaymah.systems

1. الملخص التنفيذي

تعطّل تطبيق الإنتاج لمدة 45 دقيقة بسبب دخول الحاوية في حلقة تعطّل متكررة (CrashLoopBackOff) ناتجة عن تجاوز حد الذاكرة (OOMKilled). كل إعادة تشغيل تلقائية للحاوية كانت تصل لنفس حد الذاكرة خلال ثوانٍ من التشغيل، فيُعاد قتلها، دون أن يتدخل أي نظام auto-scaling لتوزيع الحمل أو رفع الحد. تم الحل برفع حد الذاكرة المخصص للحاوية مؤقتاً وإصلاح تسريب الذاكرة (memory leak) في الكود لاحقاً.

2. التأثير

  • التطبيق كان يعيد HTTP 502/503 لكل الطلبات الواردة خلال فترة الانقطاع.
  • عدد الطلبات المتأثرة: ~ (يُقدَّر بناءً على متوسط الحمل × 45 دقيقة).
  • لا فقدان بيانات (التطبيق stateless، وقاعدة البيانات المُدارة لم تتأثر).

3. الجدول الزمني (Timeline)

الوقت (UTC) الحدث
T+0:00 ارتفاع تدريجي في استهلاك الذاكرة يبدأ بعد نشر إصدار جديد يحتوي memory leak (كائنات غير مُحرَّرة في كاش داخلي للطلبات).
T+12:00 استهلاك الذاكرة يتجاوز حد الحاوية المخصص (512Mi). Kernel OOM-killer يقتل العملية.
T+12:05 منصة غيمة تعيد تشغيل الحاوية تلقائياً (restart policy).
T+12:05 T+45:00 الحاوية تصل لنفس حد الذاكرة خلال ~90 ثانية من كل إعادة تشغيل (بسبب سرعة تراكم التسريب تحت الحمل)، فتدخل في CrashLoopBackOff. لا توجد نسخة (replica) ثانية تستقبل الحمل أثناء ذلك → انقطاع كامل للخدمة.
T+20:00 تنبيه أول يصل عبر health-check الخارجي (فشل 3 فحوصات متتالية = تنبيه).
T+25:00 فريق SRE يبدأ التحقيق، يفحص kubectl describe pod / سجلات المنصة، يجد Reason: OOMKilled.
T+35:00 إجراء مؤقت: رفع حد الذاكرة من 512Mi إلى 1Gi عبر إعدادات الخطة (g4.medium) لإعطاء هامش أثناء التحقيق.
T+40:00 التطبيق يستقر، يبدأ باستقبال الطلبات بنجاح.
T+45:00 تأكيد الاستقرار عبر health-check لمدة 5 دقائق متتالية → إغلاق الحادثة.
T+1 يوم تحديد السبب الجذري بدقة: كائن cache من نوع dict يكبر بلا حد أقصى (no eviction policy) في مسار معالجة الطلبات. تم إصلاحه وإضافة حد أقصى (LRU cache بحجم محدود).

4. السبب الجذري (Root Cause)

السبب المباشر: تسريب ذاكرة في كود التطبيق (كاش داخلي بلا حد أقصى وبلا آلية إخلاء/TTL) أدى لنمو استهلاك الذاكرة باستمرار حتى تجاوز حد الحاوية (512Mi)، فقتل نظام التشغيل العملية (OOM Kill).

أسباب مساهمة (Contributing Factors):

  1. لا توجد نسخة ثانية (replica) — عند فشل النسخة الوحيدة، انقطعت الخدمة بالكامل بدلاً من أن تستمر نسخة أخرى بخدمة الطلبات.
  2. لا توجد سياسة auto-scaling أو حتى Horizontal scaling أساسي لتوزيع الحمل عند ارتفاع استهلاك الموارد.
  3. غياب تنبيه مبكر على استهلاك الذاكرة (memory usage trend) — التنبيه الوحيد كان على توفر الخدمة (uptime)، وليس على مؤشرات تنذر مسبقاً.
  4. لم يُختبر الإصدار الجديد تحت حمل واقعي (load test) قبل النشر، فلم يظهر التسريب في بيئة staging.

5. سياسة Auto-Scaling المقترحة لمنصة غيمة (لمنع التكرار)

# مثال توضيحي لسياسة auto-scaling على منصة الحاويات في غيمة
autoscaling:
  min_replicas: 2          # لا تقل عن نسختين دائماً — يمنع انقطاعاً كاملاً عند فشل نسخة واحدة
  max_replicas: 6
  metrics:
    - type: memory
      target_utilization_percent: 70   # التوسع عند وصول الذاكرة لـ 70% من الحد المخصص
    - type: cpu
      target_utilization_percent: 75
  scale_up:
    cooldown_seconds: 60
  scale_down:
    cooldown_seconds: 300   # هبوط أبطأ لتفادي "flapping"
  restart_policy:
    max_restarts_within: 300s
    max_restarts_count: 3
    action_after_limit: page_oncall   # بدل الاستمرار في إعادة التشغيل بلا نهاية، أبلغ الفريق فوراً
resources:
  requests:
    memory: 512Mi
    cpu: 250m
  limits:
    memory: 1Gi            # هامش أعلى من الـ request لتفادي OOM عند spikes مؤقتة
    cpu: 500m

لماذا هذا يمنع التكرار:

  • min_replicas: 2 يضمن استمرار الخدمة حتى لو دخلت نسخة واحدة في OOMKilled.
  • الحد limits.memory أعلى من requests.memory يعطي هامش أمان قبل الوصول لحد القتل الصارم.
  • max_restarts_count يحوّل مشكلة "restart loop صامتة" إلى تنبيه فوري لمهندس النوبة (on-call) بدل انتظار اكتشافها يدوياً.
  • التوسع الأفقي عند 70% استهلاك ذاكرة يوزّع الحمل قبل الوصول لحد الخطر.

6. الكشف المبكر باستخدام أدوات مراقبة غيمة

  1. مراقبة الموارد (Resource Metrics): تفعيل تنبيه على غيمة عند تجاوز استهلاك الذاكرة 80% من الحد المخصص لمدة أكثر من دقيقتين متتاليتين (وليس فقط عند الفشل الكامل).
  2. تتبع اتجاه الاستهلاك (Trend Alerting): تنبيه عند نمو خطي مستمر في استهلاك الذاكرة عبر آخر 15 دقيقة (مؤشر تسريب) حتى قبل الوصول للحد.
  3. سجلات الحاوية (Container Events): ربط تنبيه فوري بحدث OOMKilled أو CrashLoopBackOff من سجل أحداث المنصة مباشرة لقناة on-call (Slack/Telegram/Webhook)، بدل الاعتماد فقط على health-check خارجي.
  4. اختبار الحمل قبل النشر (Pre-deploy Load Test): تشغيل اختبار حمل تلقائي في CI/CD (راجع q3) على بيئة staging قبل الموافقة على نشر production، لرصد تسريبات الذاكرة قبل وصولها للمستخدمين.
  5. Canary Deployment: نشر الإصدار الجديد على نسخة واحدة فقط أولاً (10% من الحمل) لمدة 15-30 دقيقة، ومراقبة الذاكرة قبل الانتشار الكامل.

7. عناصر إجراءات المتابعة (Action Items)

المهمة الأولوية المسؤول
إصلاح تسريب الذاكرة (LRU cache بحد أقصى) عالية فريق التطبيق
رفع min_replicas إلى 2 لكل تطبيقات production عالية SRE
إضافة تنبيه على اتجاه استهلاك الذاكرة (trend-based) عالية SRE
إضافة اختبار حمل (load test) كخطوة إجبارية في CI/CD قبل production متوسطة DevOps
توثيق حد أقصى لعدد إعادة التشغيل قبل تصعيد تلقائي لـ on-call متوسطة SRE