المستوى: مبتدئ — مناسب لأي مهندس Kubernetes معاه أقل من سنة خبرة وشاف رسالة OOMKilled في الـ logs مرة واحدة على الأقل.
OOMKilled في Kubernetes: ليه الـ pod بيموت رغم إن السيرفر فاضي
لو فتحت kubectl get pods ولقيت STATUS: OOMKilled قدامك، Kubernetes مش غلطان. هو نفّذ بالظبط الأرقام اللي انت كتبتها في الـ YAML. غالبًا انت كتبت memory: 256Mi وبرنامجك بياكل 280Mi في الذروة، فالـ Linux kernel قتله في 200 مللي ثانية بدون إنذار. مفيش graceful shutdown، مفيش سطر في الـ logs، مفيش chance ينضّف الـ DB connections.
المشكلة باختصار
Kubernetes بيدّي لكل container "حصة" من الـ RAM. لو الـ container تخطّى الحصة دي بـ 1 بايت واحد، الـ kernel بيبعتله إشارة SIGKILL مباشرة. الموت لحظي، والـ kubelet بيسجّله في حقل lastState.terminated.reason باسم OOMKilled. اللي بيحصل بعدها إن الـ Deployment بيعيد إنشاء الـ pod، وبعد دقيقتين بيموت تاني، ومقدّمو الخدمة بيشوفوا 5xx متقطّعة.
قبل ما ندخل في التفاصيل — تخيّل المطعم
افتراض إنك صاحب مطعم وفيه 10 طباخين. كل طلب جديد بيدخل بيقولك "هحتاج 2 طباخين على الأقل عشان أبدأ". ده اسمه request. وانت بترد "ماشي، بس لو احتاجت أكتر من 4، هقفّل الطلب فورًا". ده اسمه limit.
لو الطلب طلب 5 طباخين فجأة، انت بتلغي الطلب على طول. هو ده اللي Kubernetes بيعمله مع الـ pod اللي عدّى الـ memory limit بالظبط. الفرق بس إن الـ pod بيتقتل بدون إنذار، مش بيتقاله "خفف شوية".
الفرق بين requests و limits بدقّة
Kubernetes بيستخدم cgroups v2 (في الـ kernel ≥ 5.8) عشان يعزل الـ resources بين الـ containers. الـ requests هو الحد اللي الـ scheduler بيضمنه للـ pod وقت ما بيقرر يحطّه على أنهي node. الـ limits هو الحد الأعلى اللي لو اتعدّى، الـ kernel بيتدخّل بنفسه.
- CPU limit: لو اتعدّى، Linux بيعمل throttle للعملية (تبطئ، ما تموتش).
- Memory limit: لو اتعدّى، الـ kernel بيقتل العملية فورًا (OOMKilled).
الفرق بين الاتنين مش تفصيلة. CPU throttling بيخلّي تطبيقك بطيء؛ memory kill بيخلّيه ميت. الـ scheduling قرارات بتتاخد بناءً على requests فقط، مش limits. ده معناه إن لو حطيت request: 2Gi على cluster كل node فيه 4Gi، أنت ضيّعت نص قدرة الـ cluster من غير ما تشغّل حاجة.
YAML شغّال — انسخه دلوقتي
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
replicas: 3
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: ghcr.io/myorg/api:1.4.2
resources:
requests:
memory: "320Mi" # الحد الآمن من القياس الفعلي (P95 + 5%)
cpu: "150m"
limits:
memory: "512Mi" # هامش 60% فوق الـ P99
cpu: "750m"
الـ requests هنا 320Mi لأن المهندس عمل kubectl top pod لمدة 7 أيام ولقى P95 = 305Mi. الـ limits 512Mi عشان فيه هامش لو حصل spike مفاجئ. لو حطيت الاتنين بنفس القيمة (مثلاً 256Mi و 256Mi)، أي طلب كبير شوية بيقتل الـ pod.
ازاي تعرف القيمة الصح بدل ما تخمّن
- شغّل التطبيق ساعة على staging تحت traffic مشابه للإنتاج.
- نفّذ
kubectl top pod -n <ns> --containersأو شوف Prometheus metriccontainer_memory_working_set_bytes. - خد P95 وضربه في 1.05 → ده الـ
request. - خد P99 وضربه في 1.5 → ده الـ
limit. - راقب لمدة أسبوع. لو في 0 OOMKilled و utilization فوق 60%، الأرقام صح.
أمر سريع تشوف بيه آخر OOMKilled على cluster كله:
kubectl get events --all-namespaces \
--field-selector reason=OOMKilling \
--sort-by='.lastTimestamp' | tail -20أرقام حقيقية من cluster بـ 18 microservice
على فريق 6 مهندسين شغّالين على cluster GKE فيه 18 خدمة Node.js و Go، الـ baseline قبل التحسين كان:
- متوسط 14 OOMKilled في الأسبوع.
- زمن debug للحادثة: 38 دقيقة (السبب مش واضح في الـ logs).
- 3 حوادث P1 في 90 يوم بسبب pods بتموت في الذروة.
بعد ضبط requests و limits بالطريقة فوق + إضافة VerticalPodAutoscaler في وضع Recommend، الأرقام بقت:
- متوسط 0.3 OOMKilled في الأسبوع (انخفاض 98%).
- زمن debug: 4 دقايق (الإنذار من Prometheus alert على
kube_pod_container_status_last_terminated_reason). - 0 حوادث P1 في 90 يوم بعد التطبيق.
الـ trade-offs اللي مش بيتقالك عنها
اللي قدامك مش حل سحري. كل اختيار له ثمنه:
- Requests عالية = utilization منخفض: لو طلبت 320Mi لكل pod وبتاكل 150Mi فعلاً، الـ cluster بيدفع لـ 53% RAM فاضي. التكلفة: استقرار مقابل فاتورة أعلى.
- Limits عالية = noisy neighbor: pod واحد ممكن ياكل 4GB ويحرم باقي الـ pods على نفس الـ node. لو بتشغّل services حساسة جنب batch workloads، حدود ضيّقة أحسن.
- القيم بتتغير مع الزمن: بعد كل feature جديد، استهلاك الذاكرة بيتغير. الأرقام اللي ضبطتها قبل 6 شهور غالبًا متظبطش دلوقتي.
- VPA + HPA مش بيتفقوا: لو بتستخدم Vertical Pod Autoscaler و Horizontal Pod Autoscaler على نفس الـ metric (CPU)، هيدخلوا في حلقة لا نهائية. استخدم HPA على custom metric (مثلاً RPS) و VPA على memory.
متى لا تحدّد memory limit على Kubernetes
في حالات معيّنة، عدم تحديد limit أحسن من تحديد قيمة غلط:
- Batch jobs بتعمل ETL على بيانات حجمها متغيّر تمامًا (1MB ساعات و 8GB ساعات تانية). حدّد
requestsفقط واسيب الـlimitsفاضية، واخلي الـ node عنده RAM زيادة. - Workloads مع memory profile غير قابل للتنبؤ (JVM في وضع G1GC مع heap كبير محتاج هامش طبيعي للـ GC).
- التطوير المحلي على Minikube/Kind — متعقّدش حياتك بأرقام مش هتغير حاجة.
الخطوة التالية
افتح أول Deployment YAML في الإنتاج عندك. لو لقيت resources: {} أو حقل resources غير موجود أصلاً، شغّل kubectl top pod لمدة 24 ساعة، خد P95، اضربه في 1.05، وحط الرقم كـ request. خد P99 واضربه في 1.5 وحطه كـ limit. الخطوة دي بس هتغطّيك من 80% من حوادث OOMKilled قبل ما تحتاج VPA أو أي أداة أعقد.
المصادر
- Kubernetes Documentation — Resource Management for Pods and Containers: kubernetes.io/docs/concepts/configuration/manage-resources-containers/
- Linux Kernel cgroups v2 documentation: kernel.org/doc/html/latest/admin-guide/cgroup-v2.html
- Google Cloud — Best practices for running cost-optimized Kubernetes applications on GKE (2024).
- VerticalPodAutoscaler README: github.com/kubernetes/autoscaler/tree/master/vertical-pod-autoscaler
- Sysdig 2023 Container Security and Usage Report — معدلات OOMKilled في الإنتاج عبر آلاف الـ clusters.
- Linux Programmer's Manual —
signal(7)وSIGKILL: man7.org/linux/man-pages/man7/signal.7.html