الملفات
ghaymah-exam-OmarHussein-SRE/q2-postmortem/postmortem-report.md
2026-07-27 00:05:12 +03:00

181 أسطر
17 KiB
Markdown
خام الرابط الدائم اللوم التاريخ

هذا الملف يحتوي على أحرف Unicode غامضة

هذا الملف يحتوي على أحرف Unicode قد تُخلط مع أحرف أخرى. إذا كنت تعتقد أن هذا مقصود، يمكنك تجاهل هذا التحذير بأمان. استخدم زر الهروب للكشف عنها.

# تقرير ما بعد الحادثة (Postmortem)
## انقطاع خدمة sample-api لمدة 45 دقيقة بسبب OOMKilled متكرر
| الحقل | القيمة |
|---|---|
| **معرّف الحادثة** | INC-2026-0726-01 |
| **الخدمة المتأثرة** | `sample-api` (Ghaymah Containers) |
| **تاريخ الحادثة** | 26 يوليو 2026 |
| **مدة الانقطاع** | 45 دقيقة (14:05 14:50 UTC) |
| **الخطورة (Severity)** | SEV-2 — تعطّل كامل للخدمة، بدون فقدان بيانات |
| **الحالة** | مغلق — الإجراءات التصحيحية قيد التنفيذ |
| **معدّ التقرير** | فريق SRE |
| **نوع التقرير** | Blameless Postmortem |
---
## 1. الملخص التنفيذي
توقفت خدمة `sample-api` عن الاستجابة لمدة 45 دقيقة نتيجة دخولها في حلقة إعادة تشغيل متكررة (**CrashLoop**) بسبب قتل الحاوية من قبل نظام إدارة الذاكرة في المضيف (**OOMKilled**، exit code 137). السبب الجذري هو عدم تطابق بين حد الذاكرة (memory limit) المضبوط للحاوية وحجم الذاكرة الفعلي الذي يحتاجه التطبيق تحت الحمل، مع غياب أي تنبيه استباقي (proactive alert) قبل وصول الاستهلاك للحد الأقصى. تمت استعادة الخدمة برفع حد الذاكرة يدويًا وإعادة نشر الخدمة. لا يوجد فقدان بيانات، والتأثير كان محصورًا في عدم توفر الـ API (Full Outage) للمستخدمين خلال فترة الحادثة.
---
## 2. الأثر (Impact)
- **التوفر:** 0% استجابة ناجحة من `/health` و `/metrics` خلال 45 دقيقة.
- **المستخدمون المتأثرون:** جميع الطلبات الواردة على النطاق العام للخدمة (HTTP 502/504 من طبقة الـ ingress بعد فشل كل محاولات إعادة التشغيل المتتالية).
- **البيانات:** لا يوجد فقدان بيانات (الخدمة عديمة الحالة / stateless).
- **السمعة/SLA:** تجاوز الحادث ميزانية الخطأ الشهرية (error budget) لهذه الخدمة بنسبة تقديرية 60%.
---
## 3. الجدول الزمني (Timeline) — بتوقيت UTC
| الوقت | الحدث |
|---|---|
| 13:42 | ارتفاع تدريجي وغير ملحوظ في استهلاك الذاكرة بعد نشر تحديث يضيف معالجة استجابات أكبر (JSON payloads) دون تحرير كائنات مؤقتة بشكل كافٍ. |
| 14:05 | استهلاك الذاكرة يصل لحد الحاوية (512MB) → **OOMKilled** الأولى، exit code 137. المنصة تعيد تشغيل الحاوية تلقائيًا (restart policy: Always). |
| 14:0614:20 | حلقة CrashLoopBackOff: الحاوية تُقتل مجددًا خلال ثوانٍ من كل إعادة تشغيل بسبب تحميل الحالة القديمة/التخزين المؤقت الذي يستهلك الذاكرة بسرعة من جديد. عدد مرات إعادة التشغيل: 9 خلال 14 دقيقة. |
| 14:22 | فحص `health-check.py` الخارجي يسجّل 3 فشل متتالي على `/health` → تسجيل `ALERT` في `monitor.log` (لا يوجد تنبيه فوري لفريق العمل لأن السكربت كان يعمل يدويًا بدون تكامل مع قناة إشعارات). |
| 14:31 | أحد المهندسين يلاحظ الانقطاع أثناء فحص روتيني (لم يكن هناك تنبيه تلقائي يوقظ الفريق — **فجوة كشف أولى**). |
| 14:33 | بدء التحقيق: مراجعة سجلات الحاوية على console غيمة، ملاحظة أحداث `OOMKilled` المتكررة. |
| 14:40 | تحديد السبب المباشر: حد الذاكرة المضبوط (512MB) غير كافٍ لحجم البيانات المعالجة بعد آخر تحديث. |
| 14:45 | رفع حد الذاكرة إلى 1024MB مؤقتًا وإعادة نشر الخدمة (rolling restart). |
| 14:50 | الخدمة تستقر، `/health` يعيد 200 باستمرار لمدة 10 دقائق متتالية. **إغلاق الحادثة.** |
| 15:30 | مراجعة ما بعد الحادثة (هذا التقرير) وفتح بنود الإجراءات التصحيحية. |
---
## 4. تحليل السبب الجذري (Root Cause Analysis — 5 Whys)
1. **لماذا توقفت الخدمة؟**
لأن الحاوية كانت تُقتل بشكل متكرر من قبل نواة النظام (OOM Killer) — exit code 137.
2. **لماذا قُتلت الحاوية؟**
لأن استهلاك الذاكرة الفعلي تجاوز حد الذاكرة (memory limit) المضبوط للحاوية على منصة غيمة.
3. **لماذا تجاوز الاستهلاك الحد؟**
لأن آخر تحديث للتطبيق أضاف معالجة لحمولات JSON أكبر مع احتفاظ بكائنات في الذاكرة (buffers/caches) دون تحرير كافٍ، ولم يُختبر تحت حمل واقعي قبل النشر.
4. **لماذا لم يُكتشف هذا قبل النشر أو فور حدوثه؟**
لأنه لا يوجد اختبار حمل (load/soak test) ضمن خط النشر، ولا يوجد تنبيه استباقي على نسبة استهلاك الذاكرة (كان الاعتماد فقط على فحص `/health` بعد فشل الخدمة فعليًا، وهو كشف "بعد الحقيقة" لا استباقي).
5. **لماذا لم تتعافَ الخدمة تلقائيًا رغم إعادة التشغيل التلقائي؟**
لأن سياسة إعادة التشغيل لم تكن مقترنة بأي تعديل ذاتي في الموارد (auto-scaling/right-sizing)، فكانت الحاوية تدخل في نفس ظرف نفاد الذاكرة عند كل إعادة تشغيل (Crash Loop) بدل التعافي.
**السبب الجذري النهائي:**
غياب سياسة توسّع تلقائي مرتبطة باستهلاك الذاكرة الفعلي، مع تحديد ثابت وغير كافٍ لحدود الموارد (static under-provisioned memory limit)، وغياب مراقبة استباقية لاستهلاك الذاكرة قبل الوصول لنقطة القتل.
---
## 5. ما الذي سار بشكل جيد / ما الذي لم يسر بشكل جيد
**سار بشكل جيد:**
- إعادة التشغيل التلقائي للحاوية منع الحاجة لتدخل يدوي لإعادة تشغيلها.
- التطبيق عديم الحالة، فلم يحدث فقدان أو تلف بيانات.
- سكربت `health-check.py` سجّل الحادثة بدقة زمنية (توقيت أول فشل متتالي) وساعد في بناء الجدول الزمني.
**لم يسر بشكل جيد:**
- لا يوجد تنبيه فوري (Slack/Email/SMS) عند فشل الفحوصات — الاعتماد على ملاحظة بشرية عرضية أخّر الاستجابة حوالي 9 دقائق.
- لا توجد مراقبة لاستهلاك الذاكرة كنسبة مئوية من الحد (memory usage %) قبل وقوع OOM.
- حدود الموارد (requests/limits) لم تُراجَع بعد آخر تحديث للكود.
- لا يوجد اختبار حمل ضمن CI/CD قبل النشر للإنتاج.
---
## 6. الإجراءات التصحيحية (Action Items)
| # | الإجراء | الأولوية | المسؤول | الموعد المستهدف |
|---|---|---|---|---|
| 1 | ضبط تنبيهات فورية (Alerting) عند تجاوز استهلاك الذاكرة 80% من الحد لمدة > 3 دقائق | عالية | SRE | خلال 3 أيام |
| 2 | تطبيق سياسة auto-scaling أفقي/عمودي مبنية على الذاكرة (تفاصيل في القسم 7) | عالية | SRE / Platform | خلال أسبوع |
| 3 | إضافة اختبار حمل (load test) إلى خط CI/CD قبل أي نشر للإنتاج | متوسطة | فريق التطوير | خلال أسبوعين |
| 4 | ربط `health-check.py` بقناة تنبيه فعلية (Webhook/Slack) بدل الاكتفاء بملف سجل محلي | عالية | SRE | خلال 3 أيام |
| 5 | مراجعة الكود المسؤول عن معالجة الحمولات الكبيرة وتحرير الذاكرة (buffer/cache) بشكل صريح | عالية | فريق التطوير | خلال أسبوع |
| 6 | إضافة مقياس `memory_usage_percent` إلى `/metrics` ولوحة المراقبة | متوسطة | فريق التطوير | خلال أسبوعين |
| 7 | توثيق حادثة OOMKilled ضمن Runbook التشغيل مع خطوات الاستجابة القياسية | منخفضة | SRE | خلال أسبوعين |
---
## 7. سياسة Auto-Scaling المقترحة لمنصة غيمة (منع التكرار)
الهدف: منع دخول الخدمة في CrashLoop بسبب نفاد الذاكرة، عبر ثلاث طبقات حماية مكمّلة لبعضها: **تحديد صحيح للموارد**، **توسّع أفقي عند الضغط**، و**حواجز أمان تمنع القتل المفاجئ**.
### 7.1 تصحيح حدود الموارد (Right-Sizing) — الأساس قبل أي auto-scaling
```yaml
resources:
requests:
memory: "512Mi" # الحد الأدنى المضمون للحاوية
cpu: "250m"
limits:
memory: "768Mi" # هامش أمان 50% فوق الاستهلاك الطبيعي المُقاس
cpu: "500m"
```
> القاعدة: `limit` يجب أن يكون أعلى من أعلى استهلاك ملاحظ فعليًا (p99) بهامش لا يقل عن 30-50%، وليس رقمًا تقديريًا ثابتًا. يُعاد قياس هذه الأرقام بعد كل تغيير جوهري في الكود.
### 7.2 التوسّع الأفقي التلقائي (Horizontal Auto-scaling) حسب الذاكرة
```yaml
autoscaling:
min_replicas: 2 # لا تقل عن نسختين لتفادي نقطة فشل واحدة
max_replicas: 6
metrics:
- type: memory
target_utilization_percent: 65 # التوسّع قبل الوصول لحد الخطر بكثير
- type: cpu
target_utilization_percent: 70
scale_up:
cooldown_seconds: 60 # استجابة سريعة عند الضغط
scale_down:
cooldown_seconds: 300 # تهدئة أبطأ لتفادي "التذبذب" (flapping)
```
- التوسّع يعتمد على **متوسط استهلاك الذاكرة عبر جميع النسخ**، وليس نسخة واحدة، لتفادي توسّع كاذب بسبب Memory Leak في نسخة واحدة فقط.
- `min_replicas: 2` يضمن استمرار الخدمة أثناء إعادة تشغيل أي نسخة.
### 7.3 حواجز أمان تمنع الدخول في CrashLoop
| الآلية | الإعداد المقترح |
|---|---|
| **Restart Policy** | `Always` مع `backoff` تصاعدي (5s → 10s → 20s ... حتى 5 دقائق) لمنع استهلاك الموارد بمحاولات إعادة تشغيل سريعة متتالية |
| **Startup/Readiness Probe** | فحص `/health` مع `initial_delay=10s`، `period=10s`، `failure_threshold=3` — لا تُدخل النسخة حركة مرور إلا بعد استقرارها |
| **Liveness Probe منفصل عن Readiness** | لتفادي إعادة تشغيل نسخة تعاني بطئًا مؤقتًا وليس تعطلًا كاملًا |
| **Pod Disruption Budget المكافئ** | الحفاظ على نسخة واحدة صحية على الأقل أثناء أي تحديث أو إعادة توسّع |
| **Circuit breaker على مستوى التطبيق** | رفض الطلبات الكبيرة (حد أقصى لحجم body) بدل معالجتها ثم الانهيار |
### 7.4 لماذا هذا يمنع تكرار هذا الحادث تحديدًا
- **التوسّع عند 65%** يعطي وقتًا كافيًا لإضافة نسخ جديدة *قبل* وصول أي نسخة لحد الذاكرة الفعلي (768Mi)، بدل انتظار القتل ثم إعادة التشغيل في نفس الظرف.
- **الحد الأعلى المرفوع مع هامش الأمان** يمنع التصادم مع نفس نقطة الفشل التي وقعت سابقًا (512Mi غير كافٍ).
- **`min_replicas: 2`** يعني أن فشل نسخة واحدة (حتى لو حدث OOM لأي سبب مستقبلي) لا يعني انقطاع الخدمة بالكامل — النسخة الأخرى تستمر بخدمة الطلبات أثناء إعادة التشغيل.
---
## 8. الكشف المبكر باستخدام أدوات مراقبة غيمة
الهدف: الانتقال من **كشف تفاعلي** (بعد توقف الخدمة فعليًا) إلى **كشف استباقي** (قبل وصول الاستهلاك لنقطة الخطر بدقائق).
### 8.1 المقاييس (Metrics) الواجب مراقبتها
| المقياس | مصدره | لماذا يهم |
|---|---|---|
| نسبة استهلاك الذاكرة من الحد (`memory_usage / memory_limit`) | Container Metrics في console غيمة | المؤشر الأهم — يكشف الاتجاه التصاعدي قبل القتل بدقائق |
| عدد مرات إعادة التشغيل (`restart_count`) خلال نافذة زمنية | Container Events | ارتفاع مفاجئ = علامة CrashLoop مبكرة |
| رمز الخروج (`exit_code = 137`) | Container Logs/Events | توقيع مباشر لحدث OOMKilled تحديدًا (يميّزه عن أعطال أخرى) |
| زمن استجابة `/health` و `/metrics` | Application-level (سكربت `health-check.py` + لوحة المراقبة) | يكشف التدهور التدريجي (latency يرتفع قبل الانهيار الكامل غالبًا) |
| معدل الأخطاء 5xx من الـ ingress | منصة غيمة (Edge/Load Balancer metrics) | يعكس الأثر الفعلي على المستخدمين |
### 8.2 التنبيهات المقترحة (Alert Rules)
| التنبيه | الشرط | الخطورة | القناة |
|---|---|---|---|
| اقتراب من حد الذاكرة | `memory_usage_percent > 75%` لمدة 5 دقائق متواصلة | تحذير (Warning) | Slack/Email |
| خطر وشيك | `memory_usage_percent > 90%` لمدة 2 دقيقة | حرج (Critical) | Slack + استدعاء (Page) |
| حدث OOMKilled | ظهور `exit_code=137` مرة واحدة | حرج فوري | Page مباشر |
| CrashLoop | `restart_count >= 3` خلال 10 دقائق | حرج | Page مباشر |
| تدهور زمن الاستجابة | متوسط زمن استجابة `/health` يرتفع 3 أضعاف عن خط الأساس | تحذير | Slack |
### 8.3 كيف يمنع هذا تكرار الحادث تحديدًا
في حادثة اليوم، كان الاستهلاك يتصاعد تدريجيًا بين 13:42 و14:05 (23 دقيقة) قبل أول عملية قتل — وهي نافذة زمنية كانت كافية تمامًا لإطلاق تنبيه "اقتراب من حد الذاكرة" عند تجاوز 75%، مما كان يسمح بالتدخل (زيادة الحد أو التوسّع اليدوي) قبل وصول الخدمة لنقطة الانهيار الكامل بدقائق طويلة، بدل اكتشاف الحادثة بعد 26 دقيقة من توقف الخدمة فعليًا.
### 8.4 تكامل عملي مع أدوات المشروع الحالية
- تحديث `main.py` لإضافة حقل `memory_usage_percent` إلى استجابة `/metrics` (عبر قراءة `/sys/fs/cgroup/memory.current` و`memory.max` داخل الحاوية).
- ربط `health-check.py` بقناة Webhook (Slack Incoming Webhook) بدلاً من الاكتفاء بـ `monitor.log`، بحيث يُرسل تنبيهًا فوريًا بدل انتظار مراجعة يدوية للسجل.
- عرض `memory_usage_percent` كبطاقة إضافية في `dashboard.html` مع تلوين تحذيري (أصفر عند 75%+، أحمر عند 90%+) لإعطاء رؤية بصرية مباشرة لفريق التشغيل.
---
## الخلاصة
الحادثة نتجت عن تحديد ثابت وغير كافٍ لحدود الذاكرة تصادم مع زيادة حقيقية في استهلاك التطبيق، وتفاقم أثرها بسبب غياب تنبيه استباقي وغياب توسّع تلقائي يمتص الضغط قبل الوصول لنقطة القتل. الإجراءات التصحيحية في هذا التقرير (right-sizing + auto-scaling أفقي + تنبيهات استباقية) تعالج السبب الجذري مباشرة، وتقلّل احتمال تكرار حادثة مشابهة من "شبه مؤكد" إلى "نادر جدًا" إذا نُفّذت جميعها خلال الجدول الزمني المحدد أعلاه.