diff --git a/q2-postmortem/q3-cicd/postmortem-report.md b/q2-postmortem/q3-cicd/postmortem-report.md new file mode 100644 index 0000000..52fa7d3 --- /dev/null +++ b/q2-postmortem/q3-cicd/postmortem-report.md @@ -0,0 +1,91 @@ +# تقرير 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 المقترحة لمنصة غيمة (لمنع التكرار) + +```yaml +# مثال توضيحي لسياسة 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 |