رفتن به محتوای اصلی

Pedram Sarani

یادداشت‌های SRE و DevOps

بازگشت به مقاله‌ها

تفاوت OOMKilled و Evicted در Kubernetes؛ از cgroup تا kubelet

تفاوت OOMKilled و Evicted در Kubernetes؛ اینکه چه زمانی مشکل از عبور container از memory limit است و چه زمانی kubelet به‌خاطر فشار روی node یک Pod را حذف می‌کند.

یک سرویس عملیاتی یهو قاطی میکنه. طبق عادت اول می‌رویم سراغ kubectl get pods و همان‌جا دو چیز توی چشم می‌زند: یک Pod هی restart می‌شود، یکی دیگر هم با وضعیت Evicted مانده. واکنش سریع و خیلی رایج این است که بگوییم: «خب، حافظه کم آورده؛ limit را بزرگ‌تر کنیم یا دلیت کنیم تا دوباره پاد رو بسازه.»

گاهی واقعاً همین جواب می‌دهد. ولی دردسر دقیقاً از همین «گاهی» شروع می‌شود. اگر تشخیص اول اشتباه باشد، ممکن است چند ساعت تمام دنبال memory leak، heap dump، memory limit و نمودارهای container بگردیم؛ در حالی که اصل اتفاق اصلاً داخل آن container نبوده و node زیر فشار رفته است.

OOMKilled و Evicted از بیرون شبیه‌اند، چون هر دو سرویس را از حالت سالم خارج می‌کنند. اما پشت صحنه‌شان دو دنیای جداست. OOMKilled بیشتر به kernel لینوکس، cgroup و محدودیت حافظه‌ی container مربوط است. یک واکنش سریع و خشن. اما Evicted تصمیم kubelet است؛ تصمیمی در سطح node برای اینکه خود ماشین و کامپوننت های اصلی تا اخرین لحظه سالم بمانند.

همین فرق ظاهراً کوچک، مسیر incident response را عوض می‌کند. alertها را عوض می‌کند. حتی روی capacity planning هم اثر می‌گذارد.

خیلی جاها این موضوع را با یک جدول دو ستونه تمام می‌کنند. بد هم نیست، اما کافی نیست. بهتر است یک لایه پایین‌تر برویم: ببینیم memory controller در cgroup چه می‌کند، kernel چطور قربانی را انتخاب می‌کند، چرا یک Pod با QoS برابر Guaranteed هنوز هم ممکن است OOMKilled شود، و در نسخه‌های جدیدتر Kubernetes، مخصوصاً v1.35 و v1.36، چند ابزار مهم‌تر برای کنترل و مشاهده این وضعیت‌ها داریم؛ از In-Place Pod Resize گرفته تا PSI metrics و بهبودهای مربوط به memory management روی nodeهای Linux.


بخش اول — اول تصویر node را درست ببینیم

قبل از اینکه دنبال مقصر بگردیم، باید بدانیم حافظه‌ی یک node دقیقاً چطور مصرف می‌شود. تمام RAM فیزیکی node قرار نیست بی‌واسطه در اختیار Podها باشد. kubelet ظرفیت node را چند قسمت میکند: سیستم‌عامل، اجزای Kubernetes، حاشیه‌ی امن eviction، و باقی‌مانده همان چیزی است که به workloadها می‌رسد و معمولاً آن را Allocatable می‌بینیم.

node-memory

شکل ۱. تقسیم حافظه‌ی node: Capacity = system-reserved + kube-reserved + eviction-threshold + Allocatable

این تصویر بیشتر مدل حسابداری ظرفیت است، نه یک دیوار فیزیکی خودکار. اگر reservationها، cgroupهای مربوط به آن‌ها، و enforceNodeAllocatable درست تنظیم نشده باشند، system-reserved و kube-reserved الزاماً به‌عنوان مرز سخت enforce نمی‌شوند و workload می‌تواند node را به فشار واقعی نزدیک کند.

دو شکست اصلی هم در دو مرز متفاوت رخ می‌دهند: OOMKilled داخل مرز container/cgroup، و Evicted در سطح کل node.

این تصویر دو نکته‌ی قابل توجه دارد که پرداختن بهشون شاید لازم باشد:

اول: یک container می‌تواند OOMKilled شود، حتی وقتی node هنوز حافظه‌ی آزاد قابل توجهی دارد. کافی است همان container از limit خودش عبور کند.

دوم: یک Pod ممکن است Evicted شود، حتی اگر خودش هیچ limit مشخصی را رد نکرده باشد. وقتی فشار کلی node زیاد شود، kubelet ممکن است برای نجات node یک یا چند Pod را حذف کند.

برای همین است که نسخه‌ی ساده‌ی «limit را زیاد کن» بعضی وقت‌ها درمان است، بعضی وقت‌ها فقط حواس‌پرتی.

اگه بخوایم ساده تر بگیم:

OOMKilled یعنی: پروسس داخل container به‌خاطر OOM با SIGKILL مرده است؛ در بیشتر incidentهای اپلیکیشنی، اولین سرنخ عبور از memory limit همان کانتینر است، اما برای تشخیص قطعی باید dmesg، وضعیت cgroup و eventهای Pod را کنار هم ببینیم.
Evicted یعنی: این node زیر فشار است و kubelet مجبور شده چیزی را از روی آن بردارد.


بخش دوم — تفاوت سریع، قبل از رفتن سراغ جزئیات

این جدول را می‌شود مثل نقشه‌ی راه نگه داشت. سمت OOMKilled، تصمیم بیشتر دست kernel است. سمت Evicted، پای kubelet و eviction manager وسط است.

محورOOMKilled — kernelEvicted — kubelet
تصمیم‌گیرندهkernel لینوکس؛ runtime و kubelet بیشتر گزارش می‌کنندkubelet از طریق eviction manager
سطح اتفاقداخل یک container یا cgroupکل node
دلیل اصلیعبور از memory limitفشار memory، disk، inode، PID یا ephemeral-storage
سرعت واکنشتقریباً لحظه‌ایدوره‌ای؛ معمولاً بر اساس polling حدوداً هر ۱۰ ثانیه
graceful shutdownندارد؛ SIGKILL مستقیمدر soft eviction ممکن است مهلت بدهد، در hard eviction عملاً نه
سرنخ اصلیreason: OOMKilled همراه با exitCode: 137status: Failed همراه با reason: Evicted
سرنوشت Podcontainer معمولاً روی همان node restart می‌شودPod به phase برابر Failed می‌رود و terminate می‌شود؛ controller معمولاً Pod جایگزین می‌سازد
ارتباط با PDBمستقیم نیستبه PDB احترام نمی‌گذارد؛ برخلاف drain
شروع بررسیرفتار application و مصرف containerوضعیت node، QoS، requestها و ظرفیت

ردیف‌های آخر جدول از همه کاربردی‌ترند. در زمان incident دقیقاً همان‌ها مسیر بررسی را جدا می‌کنند. اگر پاد فقط restart می‌شود، معمولاً باید از همان کانتینر و اپلیکیشن شروع کنیم. اما وقتی پاد با Evicted تمام شده، اولین سؤال باید این باشد: روی node چه خبر بوده؟(حالا واقعا خبری بوده؟)

two-paths

شکل ۲. دو مسیر علت‌ومعلولی جدا.

مسیر kernel: اول مصرف کانتینر از memory.max عبور می‌کند، بعد memory controller مربوط به cgroup سقف را enforce می‌کند، بعد OOM killer با SIGKILL وارد می‌شود، خروجی معمولاً exit 137 و reason: OOMKilled است و در نهایت کانتینر، بسته به restartPolicy، معمولاً روی همان node دوباره start می‌شود.

مسیر kubelet: اول افت منابع node، بعد تشخیص در poll بعدی، بعد رتبه‌بندی Podها، بعد حذف Pod، در نهایت، اگر پاد زیر کنترل Deployment، StatefulSet یا controller مشابه باشد(که در کلاسترهای عملیاتی غیر این نیست)، controller معمولاً پاد جایگزین می‌سازد؛ اینکه Pod جدید روی همان node یا node دیگری برود، به وضعیت node، taintها، capacity و تصمیم scheduler بستگی دارد.


بخش سوم — OOMKilled؛ جایی که kernel بی‌تعارف عمل می‌کند

بیاییم از پایین‌ترین لایه شروع کنیم. وقتی برای کانتینر مقدار limits.memory می‌گذاریم، خود کوبرنتیز مستقیماً حافظه را «کنترل» نمی‌کند. این مقدار به container runtime، مثلاً containerd، داده می‌شود و runtime آن را روی memory controller مربوط به cgroup تنظیم می‌کند.

در cgroup v2، سقف حافظه معمولاً داخل فایل memory.max قرار می‌گیرد.مسیر دقیق این فایل به runtime، cgroup driver و QoS مربوط به Pod بستگی دارد. اما روی nodeهایی که از cgroup v2 و systemd cgroup driver استفاده می‌کنند، مسیر معمولاً شبیه این است:

root@pedram:~# cat /sys/fs/cgroup/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod<POD_UID>.slice/cri-containerd-<CONTAINER_ID>.scope/memory.max

برای Podهای Guaranteed معمولاً مسیر از شاخه‌ی مستقیم‌تری زیر kubepods.slice می‌آید. برای Podهای BestEffort هم معمولاً زیر kubepods-besteffort.slice قرار می‌گیرد. برای همین بهتر است مسیر را حدس نزنیم و از روی process واقعی داخل کانتینر پیدا کنیم.

یک روش عملی روی همان node این است:

root@pedram:~# PID=$(crictl inspect --output go-template --template '{{.info.pid}}' <CONTAINER_ID>)
root@pedram:~# cat /proc/$PID/cgroup

در cgroup v2 خروجی معمولاً شبیه این است:

0::/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod2f4c1b2a_1c3d_4e5f_9a10_123456789abc.slice/cri-containerd-9f8e7d6c5b4a.scope

حالا همان مسیر را زیر /sys/fs/cgroup قرار میدهیم و فایل memory.max را میشود خواند:

root@pedram:~# cat /sys/fs/cgroup/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod2f4c1b2a_1c3d_4e5f_9a10_123456789abc.slice/cri-containerd-9f8e7d6c5b4a.scope/memory.max

اگر برای کانتینر مقدار limits.memory تنظیم شده باشد، خروجی یک عدد بر حسب byte است. مثلاً برای limit برابر 128Mi خروجی تقریباً این می‌شود:

134217728

اگر خروجی max باشد، یعنی برای آن cgroup سقف limits تنظیم نشده است.

از این لحظه به بعد، نقش kubelet در تصمیم لحظه‌ای کم‌رنگ می‌شود. کرنل لینوکس تخصیص‌های حافظه را می‌بیند. تا وقتی مصرف داخل cgroup از سقف پایین‌تر باشد، همه‌چیز عادی است. اما اگر process بخواهد از این سقف عبور کند و kernel نتواند با آزاد کردن page cache یا reclaimهای دیگر فضا باز کند، OOM killer وارد می‌شود.(حالا باور نکن وقتی وارد شد یاد حرف من می افتی)
در cgroup v2 فقط memory.max مهم نیست. در کوبرنتیز، وقتی مموری QoS روی cgroup v2 فعال و قابل استفاده باشد، مسیر مدیریت حافظه می‌تواند از فایل‌هایی مثل memory.high هم استفاده کند تا قبل از رسیدن به kill سخت، فشار و reclaim را زودتر وارد بازی کند. این قابلیت بسته به نسخه، feature gate و policy فعال‌شده رفتار متفاوتی دارد و نباید آن را با خود limit یکی بگیریم. memory.max مرز سخت kill است، اما memory.high بیشتر نقش فشار و throttling/reclaim قبل از نابودی کامل را دارد.

OOM killer چه می‌کند؟

وقتی یک cgroup به حد حافظه‌اش می‌چسبد و راه مناسبی برای آزاد کردن حافظه باقی نمی‌ماند، kernel باید چیزی را کیل کند. برای این کار به processها score می‌دهد. این score به مصرف حافظه و چند عامل دیگر وابسته است. بعد processی که قربانی مناسب‌تری به نظر برسد، با SIGKILL کشته می‌شود.

چرا exit code معمولاً 137 است؟

در سیستم‌های Unix-like، وقتی process با signal کشته می‌شود، exit code معمولاً از این فرمول می‌آید:

128 + signal شماره‌ی

شماره‌ی SIGKILL برابر ۹ است. پس:

128 + 9 = 137

به همین دلیل 137 یعنی process با SIGKILL کشته شده. البته 137 به‌تنهایی کافی نیست که بگوییم OOM بوده. وقتی کنار آن Reason: OOMKilled را می‌بینیم، آن وقت تصویر کامل‌تر می‌شود.

برای مقایسه، 143 یعنی 128 + 15، یعنی process با SIGTERM تمام شده. این جنس پایان، معمولاً مؤدبانه‌تر است.

دو نوع OOM که نباید یکی فرض شوند

اینجا یکی از همان جاهایی است که خیلی ممکن است اشتباه شود. دو اتفاق متفاوت ممکن است در Kubernetes شبیه هم دیده شوند، اما درمانشان یکی نیست.

نوعOOM در سطح کانتینر / cgroupOOM در سطح سیستم / node
اصل ماجراcontainer از memory.max خودش رد شدهحافظه‌ی فیزیکی node کم آمده
سرنخ در dmesgMemory cgroup out of memoryOut of memory: Killed process بدون اشاره‌ی روشن به cgroup
قربانی معمولprocess یا در cgroup v2 معمولاً کل container cgroupprocessی با oom_score بالاتر در کل node
مسیر درمانبررسی limit، leak یا رفتار همان workloadبررسی ظرفیت node، thresholdها، scheduling و headroom

نوع دوم عملاً بوی node-level می‌دهد. در حالت ایده‌آل kubelet باید قبل از رسیدن node به OOM سیستمی، با eviction چند Pod را حذف کند. ولی همیشه به‌موقع نمی‌رسد؛ جلوتر می‌بینیم چرا.

پس اگر در dmesg دیدیم OOM از جنس سیستمی است، فقط زوم کردن روی همان application احتمالاً کافی نیست. شاید مشکل اصلی از capacity، چیدمان workloadها یا thresholdهای kubelet باشد.

یک مثال کوچک برای خواندن خروجی OOMKilled

فرض کنیم Pod زیر را داریم:

# memory-test.yaml
apiVersion: v1
kind: Pod
metadata:
  name: memory-test
spec:
  containers:
    - name: pedram-app
      image: busybox:1.36.1
      command: ["sh", "-c", "sleep 3600"]
      resources:
        requests: { memory: "64Mi", cpu: "50m" }
        limits:   { memory: "128Mi", cpu: "200m" }   # memory.max = 128Mi

این manifest فقط نشان می‌دهد limit کجا تعریف می‌شود. خود این container با sleep 3600 قرار نیست 128Mi مصرف کند؛ اگر همین limit را روی یک workload حافظه‌ لازم دارد بگذاریم و process از آن عبور کند، OOM killer وارد می‌شود

در اینجا manifest فقط برای نشان دادن جای memory limit است؛ خروجی زیر نمونه‌ای از یک workload حافظه‌بر است که واقعاً از همین limit عبور کرده است:

root@pedram:~# kubectl describe pod memory-test
...
    Last State:     Terminated
      Reason:        OOMKilled
      Exit Code:     137
      Started:       Mon, 15 Jun 2026 11:04:18
      Finished:      Mon, 15 Jun 2026 11:09:02
    Restart Count:   7

Restart Count بالا می‌تواند نشانه‌ی leak باشد، یا شاید limit از اول کم انتخاب شده. اما describe همه‌ی ماجرا را نمی‌گوید. برای تشخیص دقیق‌تر، باید روی node سراغ لاگ kernel برویم:

root@pedram:~# dmesg -T | grep -iE 'oom|killed'
[Mon Jun  2 11:09:02 2026] pedram-app invoked oom-killer: gfp_mask=0x..., order=0, oom_score_adj=982
[Mon Jun  2 11:09:02 2026] Memory cgroup out of memory: Killed process 24871 (pedram-app)
total-vm:206156kB, anon-rss:131072kB, file-rss:0kB
[Mon Jun  2 11:09:02 2026] oom_reaper: reaped process 24871 (pedram-app), now anon-rss:0kB

عبارت Memory cgroup out of memory سرنخ اصلی است. یعنی OOM داخل محدوده‌ی همان کانتینر رخ داده، نه اینکه الزاماً کل node از حافظه خالی شده باشد.

anon-rss هم کمک می‌کند بفهمیم process تقریباً نزدیک همان limit کشته شده. عدد oom_score_adj=982 هم نشان می‌دهد این کانتینر از نگاه OOM killer خیلی محافظت‌شده نبوده. جلوتر، در بخش QoS، برمی‌گردیم سراغ همین عدد.


بخش چهارم — Evicted؛ وقتی kubelet به node کمک میکند

حالا میریم سمت دیگر ماجرا. در Evicted، معمولاً kernel بازیگر اصلی نیست. تصمیم اصلی را kubelet می‌گیرد. هدفش هم نجات یک application خاص نیست؛ هدفش این است که خود node از پا نیفتد.

اگر نود از memory، disk، inode یا PID خالی شود، مشکل فقط پادها نیستند. کیوبلت، container runtime و حتی OS هم ممکن است به‌هم بریزند. برای همین kubelet قبل از رسیدن به آن نقطه سعی می‌کند منابع آزاد کند. اگر نشد، پاد حذف می‌کند.

kubelet چه چیزهایی را مانیتور میکند؟

kubelet فقط memory را نگاه نمی‌کند. چند signal مختلف را پیگیری می‌کند و هرکدام می‌تواند نود را وارد وضعیت pressure کند:

memory.available — حافظه‌ی قابل استفاده‌ی node؛ معمولاً با MemoryPressure دیده می‌شود.
nodefs.available — فضای آزاد فایل‌سیستم اصلی kubelet؛ logها، volumeهای لوکال و موارد مشابه.
imagefs.available — فضای مربوط به imageها و layerهای کانتینر، اگر runtime جدا اعلام کند.
nodefs.inodesFree و imagefs.inodesFree — تعداد inode آزاد. گاهی حجم دیسک هنوز هست، اما inode تمام شده.
pid.available — تعداد PID قابل تخصیص؛ برای جلوگیری از قفل شدن node به‌خاطر process زیاد.
containerfs.available — فضای فایل‌سیستم مربوط به کانتینرهای در حال اجرا، وقتی runtime آن را جدا از nodefs و imagefs گزارش کند.
containerfs.inodesFree — تعداد inode آزاد روی همان containerfs؛ در کوبرنتیزهای جدیدتر می‌تواند در تصمیم‌های eviction مربوط به دیسک نقش داشته باشد.

سافت و هارد eviction

kubelet دو مدل threshold دارد: hard و soft.

Hard eviction یعنی وقتی signal از حد مشخص پایین‌تر رفت، kubelet باید سریع واکنش نشان دهد. در این حالت معمولاً graceful shutdown یا نداریم یا آن‌قدر کوتاه است که نباید رویش حساب کرد.

چند مقدار پیش‌فرض معروف:

memory.available<100Mi
nodefs.available<10%
imagefs.available<15%
nodefs.inodesFree<5%
imagefs.inodesFree<5%

Soft eviction یعنی فشار باید برای مدتی ادامه داشته باشد تا kubelet وارد عمل شود. این کمک می‌کند spikeهای کوتاه، بی‌دلیل باعث حذف Pod نشوند. در soft eviction ممکن است kubelet تا سقف eviction-max-pod-grace-period به پاد مهلت بدهد.
conditionهای فشار روی node برای جلوگیری از flapping فوراً بالا و پایین نمی‌شوند. kubelet از eviction-pressure-transition-period استفاده می‌کند و مقدار پیش‌فرض آن ۵ دقیقه است. پس ممکن است خود فشار لحظه‌ای تغییر کند، اما conditionهایی مثل MemoryPressure با تأخیر و hysteresis دیده شوند.

kubelet هر چند وقت یک بار می‌فهمد؟

برخلاف kernel که لحظه‌ای واکنش نشان می‌دهد، kubelet سیگنال ها را دوره‌ای می‌بیند. این بازه با housekeeping-interval کنترل می‌شود و مقدار پیش‌فرض آن معمولاً حدود ۱۰ ثانیه است.
همین ۱۰ ثانیه در incidentهای واقعی مهم می‌شود. چون memory spike ممکن است بین دو poll رخ دهد و قبل از اینکه kubelet فرصت eviction پیدا کند، kernel وارد عمل شود و بالاتر گفتیم که تعارف هم اصلا ندارد.

وقتی threshold رد می‌شود

وقتی kubelet ببیند یکی از thresholdها رد شده، اول تلاش می‌کند بدون حذف Pod منابع آزاد کند. مثلاً imageهای استفاده‌نشده را پاک کند. اگر این کار کافی نبود، سراغ eviction می‌رود.
همزمان condition مناسب هم روی node می‌نشیند، تا scheduler کمتر سراغ آن node برود.

نمونه خروجی:

root@pedram:~# kubectl get pods
NAME             READY   STATUS     RESTARTS   AGE
pedram-app-6f8c9d7c9b   0/1     Evicted    0          12m
root@pedram:~# kubectl describe pod pedram-app-6f8c9d7c9b
Status:    Failed
Reason:    Evicted
Message:   The node was low on resource: memory. Threshold quantity: 100Mi,
           available: 87Mi. Container app was using 1567892Ki, request is 0.
root@pedram:~# kubectl describe node ip-10-0-0-1
Conditions:
  Type             Status
  MemoryPressure   True    KubeletHasInsufficientMemory
Events:
  Warning  EvictionThresholdMet   Attempting to reclaim memory
  Warning  NodeHasMemoryPressure  Node ip-10-0-0-1 status is now: NodeHasMemoryPressure

چند چیز مهم:

The node was low on resource: memory می‌گوید مشکل از سطح نود بوده. request is 0 یعنی برای همان resource تحت فشار، این کانتینر درخواست مشخصی نداشته است. اما این به‌تنهایی ثابت نمی‌کند پاد از نوع BestEffort بوده. برای BestEffort بودن، پاد نباید برای CPU یا memory هیچ request یا limitای داشته باشد. اگر پاد برای CPU یا memory در هر سطحی request یا limit داشته باشد، ممکن است همچنان Burstable باشد. پس این پیام را باید کنار QoS واقعی پاد بخوانیم، نه جدا از آن.
از آن طرف، MemoryPressure: True روی node نشان می‌دهد ما با ظرفیت و فشار کلی node طرفیم، نه صرفاً یک bug داخل اپلیکیشن.

چرا Podهای Evicted باقی می‌مانند؟

وقتی کانتینرOOMKilled می‌شود، معمولاً همان پاد دوباره کانتینر را بالا می‌آورد. اما وقتی پاد Evicted می‌شود، phase آن Failed می‌ماند. بعد از یک موج فشار، ممکن است خروجی kubectl get pods پر از پادهای Evicted شود.

این‌ها معمولاً اثر باقی‌مانده‌ی incident هستند. controllerها پادهای جدید را ساخته‌اند.
برای تمیز کردن خروجی می‌توانیم این دستور را بزنیم:

root@pedram:~# kubectl delete pod --field-selector status.phase=Failed

یک نمونه تنظیم kubelet با soft و hard:

kind: KubeletConfiguration
apiVersion: kubelet.config.k8s.io/v1beta1
evictionHard:
  memory.available:  "500Mi"
  nodefs.available:  "10%"
  imagefs.available: "15%"
evictionSoft:
  memory.available:  "1Gi"
  nodefs.available:  "12%"
evictionSoftGracePeriod:
  memory.available:  "90s"
evictionMaxPodGracePeriod: 60
mergeDefaultEvictionSettings: true

بخش پنجم — مسابقه‌: kernel یا kubelet، کدام زودتر می‌رسد؟

روی کاغذ، kubelet قرار است قبل از OOM شدن کامل node وارد عمل شود. thresholdهای eviction دقیقاً برای همین‌اند؛ یک فاصله‌ی امن، قبل از اینکه node به نقطه‌ی خطر برسد.
اما production همیشه تمیز و کتابی رفتار نمی‌کند.
در حالت پیش‌فرض مبتنی بر polling، دلیلش ساده است: kubelet با فاصله‌های زمانی وضعیت را می‌بیند. مثلاً حدود هر ۱۰ ثانیه. kernel اما پیوسته درگیر تخصیص حافظه است و همان لحظه واکنش نشان می‌دهد.
اگر مصرف حافظه آرام‌آرام بالا برود، kubelet احتمالاً در یکی از همان pollها وضعیت را می‌فهمد و eviction انجام می‌دهد. اگر kernelMemcgNotification فعال باشد، kubelet می‌تواند برای مموری thresholdها زودتر از طریق memcg notification خبردار شود. اما اگر مصرف حافظه ناگهانی بالا بپرد و کانتینر از memory.max رد شود، kernel هنوز می‌تواند قبل از eviction وارد عمل شود و پروسس را بکشد.
اگر مصرف حافظه آرام‌آرام بالا برود، kubelet احتمالاً در یکی از همان pollها وضعیت را می‌فهمد و eviction انجام می‌دهد. اما اگر مصرف حافظه ناگهانی بین دو poll بپرد بالا، ممکن است قبل از اینکه kubelet متوجه شود، container از memory.max رد شود و کرنل پروسس را بکشد.

در حالت اول، احتمالاً Evicted می‌بینیم.
در حالت دوم، احتمالاً OOMKilled.

timing-race

شکل ۳. kubelet وضعیت node را در بازه‌های زمانی مشخص بررسی می‌کند، اما kernel لحظه‌ای واکنش دارد.

اگر مصرف ناگهان بالا برود و قبل از poll بعدی از limit رد شود، کرنل پروسس را می‌کشد. اگر فشار تدریجی‌تر باشد، kubelet معمولاً فرصت eviction پیدا می‌کند.

پس جمله‌ی درست این است:


بخش ششم — QoS، oom_score_adj و اینکه چه چیزی قربانی می‌شود

در هر دو مسیر، بالاخره باید یک قربانی انتخاب شود. اما kernel و kubelet مثل هم فکر نمی‌کنند.
kubelet برای eviction منطق خودش را دارد؛ مصرف نسبت به request، Priority و نوع منبع تحت فشار را وارد تصمیم می‌کند. QoS برای memory pressure یک تخمین مفید است، اما قانون مستقیم رتبه‌بندی نیست.

سه کلاس QoS

Kubernetes از روی request و limitهای Pod، کلاس QoS را تعیین می‌کند:

کلاسشرطریسک eviction
Guaranteedبرای همه containerها، CPU و memory هم request دارند هم limit، و مقدارشان برابر استکمترین
Burstableحداقل یک request یا limit وجود دارد، اما شرایط Guaranteed کامل نیستمتوسط
BestEffortهیچ request و limitای تعریف نشدهبیشترین

برای دیدن QoS یک Pod:

root@pedram:~# kubectl get pod pedram-test -o jsonpath='{.status.qosClass}'
Burstable

منطق oom_score_adj در کرنل

kubelet برای containerها مقدار oom_score_adj تنظیم می‌کند. این عدد به kernel می‌گوید کدام process راحت‌تر قربانی شود.
هرچه عدد بالاتر باشد، ریسک کیل یا کشته شدن بیشتر است. هرچه پایین‌تر باشد، پروسس محافظت بیشتری دارد.

qos-ladder

شکل ۴. kubelet کلاس QoS را به oom_score_adj ترجمه می‌کند. BestEffort معمولاً نزدیک +1000 است. Guaranteed معمولاً نزدیک -997 است. Burstable بین این دو قرار می‌گیرد و مقدار دقیق آن به request حافظه نسبت به ظرفیت node وابسته است.

منطق kubelet: رتبه‌بندی برای eviction

در eviction، کیوبلت دقیقاً مثل کرنل عمل نمی‌کند و انتخاب قربانی را فقط با QoS انجام نمی‌دهد. منطق اصلی این است:
۱. آیا مصرف Pod از request همان منبع تحت فشار بالاتر رفته یا نه.
۲. Priority آن Pod چقدر است.
۳. مصرف آن Pod نسبت به request خودش چقدر بیشتر شده است.

برای QoS، memory pressure کمک می‌کند حدس بزنیم چه Podهایی بیشتر در خطرند، اما معیار مستقیم و کامل eviction order نیست. برای فشارهایی مثل inode و PID هم چون request مشخصی وجود ندارد، kubelet بیشتر از Priority برای رتبه‌بندی استفاده می‌کند.

eviction-ranking

شکل ۵. kubelet برای eviction به مصرف نسبت به request، Priority و نوع منبع تحت فشار نگاه می‌کند. QoS برای memory pressure تخمین خوبی می‌دهد، اما قانون مستقیم رتبه‌بندی نیست.

این تفاوت عملی است. ممکن است kubelet به‌خاطر Priority بالاتر، یک Pod را مهم‌تر بداند. اما اگر kernel در OOM سیستمی مجبور به کشتن process شود، الزاماً همان منطق kubelet را ندارد.\


بخش هفتم — تله‌ی PodDisruptionBudget

یک جمله‌ی خطرناک که عموما زیاد شنیدیم این است: «برای دیتابیس PDB داریم، پس موقع فشار حافظه quorum حفظ می‌شود.»
نه، لزوماً نه.

Node-pressure eviction وقتی رخ می‌دهد که kubelet روی node خودش می‌بیند منابع کم آمده و برای نجات نود Pod را حذف می‌کند. این مسیر به PodDisruptionBudget احترام نمی‌گذارد. در hard eviction حتی روی terminationGracePeriodSeconds هم نباید حساب کرد.
اما API-initiated eviction چیز دیگری است. مثلاً kubectl drain، descheduler یا scale-down در Cluster Autoscaler معمولاً از مسیر API می‌آیند. این مسیر PDB را بررسی می‌کند و اگر حذف Pod باعث خرابی یا شکست Budget شود، جلو آن را می‌گیرد.

محورNode-pressure eviction توسط kubeletAPI-initiated eviction مثل drain/autoscaler
شروع‌کنندهkubelet زیر فشار منابعکاربر یا کنترلر از طریق API
احترام به PDBخیربله
احترام به terminationGracePeriodSecondshard: نه، soft: محدودبله
مثالکمبود memory/disk/PID روی nodeنگه‌داری node، drain، scale-down، جابه‌جایی برنامه‌ریزی‌شده

پس PDB ابزار خوبی برای اختلال‌های داوطلبانه و قابل برنامه‌ریزی است. اما در برابر resource pressure روی node، سپر مطمئنی نیست.
اگر workload حساسی داریم و می‌خواهیم زیر فشار node شانس بیشتری برای زنده ماندن داشته باشد، باید request درست، QoS مناسب، PriorityClass و headroom واقعی داشته باشیم.
این اشتباه است که برای StatefulSet دیتابیس فقط PDB بگذاریم و خیالمان راحت شود. در یک موج MemoryPressure، kubelet می‌تواند Pod را حذف کند، بدون اینکه PDB نجاتش دهد.


بخش هشتم — آزمایشگاه کوچک؛ هر دو رفتار

تا اینجا توضیح نظری بود. اما خوبتر میشه اگه هر دو رفتار را در محیط تست ببینیم.

مخصوصاً آزمایش eviction عمداً node را تحت فشار می‌گذارد و می‌تواند Podهای دیگر را هم درگیر کند.

برای این کار kind، minikube یا یک کلاستر آزمایشی یک‌بارمصرف مناسب‌تر است.

تست اول — ساختن OOMKilled

از image آماده‌ی polinux/stress استفاده می‌کنیم. limit را 128Mi می‌گذاریم، اما به stress می‌گوییم 250M حافظه بگیرد. یعنی عمداً از limit رد می‌شویم. به طور مثال فایل oom-lab:

apiVersion: v1
kind: Pod
metadata:
  name: oom-lab
spec:
  restartPolicy: Never
  containers:
    - name: stress
      image: polinux/stress
      resources:
        requests: { memory: "64Mi" }
        limits:   { memory: "128Mi" }
      command: ["stress"]
      args: ["--vm", "1", "--vm-bytes", "250M", "--vm-hang", "1"]

اجرا:

root@pedram:~# kubectl apply -f oom-lab.yaml
root@pedram:~# kubectl get pod oom-lab -w
# میشود OOMKilled احتمالاً Status بعد از چند ثانیه 

حالا میشه exit code رو هم ببینیم:

root@pedram:~# kubectl get pod oom-lab \
    -o jsonpath='{.status.containerStatuses[0].state.terminated.exitCode}'
137

روی همان node:

root@pedram:~# dmesg -T | grep -i oom
... Memory cgroup out of memory: Killed process ... (stress)

اینجا ترکیب OOMKilled، exit 137 و عبارت Memory cgroup out of memory نشان می‌دهد مشکل در سطح container بوده.

تست دوم — دیدن Evicted

eviction را همیشه به‌صورت تمیز و قابل تکرار نمی‌شود ساخت، چون به ظرفیت نود، thresholdها و زمان‌بندی بستگی دارد. ولی ایده این است: روی node کوچک چند Pod بدون request و limit بسازیم که حافظه مصرف کنند. این‌ها BestEffort می‌شوند و معمولاً در فشار node زودتر قربانی‌اند.

root@pedram:~# kubectl create deployment havij --image=polinux/stress --replicas=6 -- \
    stress --vm 1 --vm-bytes 200M --vm-hang 1

بعد وضعیت را دنبال کنیم:

root@pedram:~# kubectl get pods -o wide -w
root@pedram:~# kubectl describe node <node>

اگر فشار کافی ایجاد شود، پادهایی با status برابر Evicted می‌بینیم و روی node هم conditionهایی مثل MemoryPressure: True ظاهر می‌شود.


بخش نهم — تشخیص با Prometheus و PSI

بررسی دستی در incident لازم است، اما کافی نیست. بهتر است قبل از اینکه سرویس بیفتد alert داشته باشیم. برای همین هم alertهای OOMKilled و Evicted را نباید با هم یکی کنیم.

برای کانتینر Working Set را ببینم

برای نزدیک شدن کانتینر به limit، معمولاً container_memory_working_set_bytes معیار خوبی میتونه باشه. یک alert ساده می‌تونه نسبت working set به limit را بسنجد:

container_memory_working_set_bytes{container!="", container!="POD"}
  / on(pod, container, namespace)
  kube_pod_container_resource_limits{resource="memory", unit="byte"}
  > 0.9

اگر این نسبت چند دقیقه بالای ۰.۹ بماند، خطر OOMKilled جدی‌تر می‌شود.البته این یک heuristic برای alert است، نه فرمول دقیق تصمیم کرنل. memory limit در نهایت توسط kernel enforce می‌شود و kill شدن می‌تواند به زمان تشخیص memory pressure، نوع مصرف حافظه و رفتار cgroup بستگی داشته باشد.

چند query زیر برای triage و dashboard مفیدند؛ برای alert نهایی، بهتر است OOMKilled را با timestamp آخرین termination محدود کنیم و Evicted را صرفاً به‌عنوان وضعیت باقی‌مانده‌ی Pod ببینیم، نه event لحظه‌ای:

(
  kube_pod_container_status_last_terminated_reason{reason="OOMKilled"} == 1
)
and on(namespace, pod, container, uid)
(
  time() - kube_pod_container_status_last_terminated_timestamp < 300
)

این query به metricهای kube-state-metrics برای آخرین termination وابسته است. در kube-state-metrics این خانواده metricها ممکن است بسته به نسخه و configuration در دسترس نباشند یا experimental باشند، پس قبل از اعمال production باید وجود kube_pod_container_status_last_terminated_reason و kube_pod_container_status_last_terminated_timestamp را در Prometheus خودمان چک کنیم.

increase(kube_pod_container_status_restarts_total[15m]) > 0
kube_node_status_condition{condition="MemoryPressure", status="true"} == 1
kube_pod_status_reason{reason="Evicted"} == 1

PSI؛ فشار را قبل از شکست ببینیم

PSI یا Pressure Stall Information فقط نمی‌گوید چقدر resource آزاد است. می‌گوید processها چه بخشی از زمان را منتظر memory، CPU یا IO مانده‌اند.
این خیلی مهم است. چون ممکن است قبل از اینکه eviction یا OOM اتفاق بیفتد، فشار را در PSI ببینیم. یعنی به‌جای واکنش بعد از incident، کمی زودتر شیب خطر را تشخیص می‌دهیم.

در کوبرنتز PSI metrics، v1.36 هابه وضعیت stable رسیده‌اند و kubelet می‌تواند فشار CPU، memory و I/O را در سطح node، pod و container جمع‌آوری کند. این داده‌ها از دو مسیر دیده می‌شوند: Summary API و endpoint مربوط به /metrics/cadvisor. در خروجی Prometheus می‌توان metricهایی مثل container_pressure_memory_waiting_seconds_total، container_pressure_memory_stalled_seconds_total، container_pressure_cpu_waiting_seconds_total و container_pressure_cpu_stalled_seconds_total را دید. ارزش این metricها این است که کندی و انتظار ناشی از فشار resource را قبل از رسیدن به OOMKilled یا Evicted نشان می‌دهند. البته این قابلیت روی nodeهای Linux به kernel 4.20+، فعال بودن PSI در kernel و استفاده از cgroup v2 وابسته است.

نمونه‌ی PrometheusRule:

apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: memory-failures
spec:
  groups:
    - name: memory.rules
      rules:
        - alert: ContainerOOMKilled
          expr: |
            (
              kube_pod_container_status_last_terminated_reason{reason="OOMKilled"} == 1
            )
            and on(namespace, pod, container, uid)
            (
              time() - kube_pod_container_status_last_terminated_timestamp < 300
            )
          for: 0m
          labels: { severity: warning }
          annotations:
            summary: "container {{ $labels.container }} was OOMKilled"

        - alert: ContainerNearMemoryLimit
          expr: |
            container_memory_working_set_bytes{container!="", container!="POD"}
              / on(pod, container, namespace)
              kube_pod_container_resource_limits{resource="memory", unit="byte"} > 0.9
          for: 5m
          labels: { severity: warning }
          annotations:
            summary: "{{ $labels.container }} is above 90% of memory limit"

        - alert: NodeMemoryPressure
          expr: kube_node_status_condition{condition="MemoryPressure", status="true"} == 1
          for: 2m
          labels: { severity: critical }
          annotations:
            summary: "node {{ $labels.node }} is under memory pressure; eviction is possible"

        - alert: ContainerMemoryPSIStall
          expr: |
            rate(container_pressure_memory_stalled_seconds_total{container!="", container!="POD"}[5m]) > 0.1
          for: 10m
          labels: { severity: warning }
          annotations:
            summary: "{{ $labels.container }} has memory PSI stall"

ایده اصلی این است: alert سطح container و alert سطح node را یکی نکنیم.

ContainerNearMemoryLimit بیشتر به سمت OOMKilled اشاره می‌کند؛ یعنی مشکل احتمالاً در workload است.
NodeMemoryPressure بیشتر بوی eviction و capacity می‌دهد.

همان دو دنیا، در alerting هم باید جدا بمانند.


بخش دهم — مسیر درست troubleshooting

وقتی pager صدا می‌کند، از حدس شروع نکنیم. اول status و reason را ببینیم، بعد وارد مسیر درست شویم.

troubleshooting-tree

شکل ۶. درخت تصمیم: اول STATUS و Reason را بخوانیم. اگر OOMKilled بود، مسیر container و kernel را برویم. اگر Evicted بود، مسیر node و pressure را دنبال کنیم.

قدم ۱: اول دسته‌بندی کنیم

شروع ساده:

root@pedram:~# kubectl get pods
root@pedram:~# kubectl describe pod <pod>

دنبال یکی از این دو الگو بگردیم:

reason: OOMKilled
exitCode: 137

یا:

status: Failed
reason: Evicted

همین جدا کردن اولیه، خیلی از مسیرهای اشتباه را حذف می‌کند.

قدم ۲: اگر OOMKilled بود، داخل مسیر container برویم

روی node لاگ kernel را ببینیم:

root@pedram:~# dmesg -T | grep -i oom

اگر دیدیم:

Memory cgroup out of memory

به احتمال زیاد container-level OOM است. یعنی همان workload از limit خودش رد شده. اینجا باید دنبال limit، memory leak کم یا رفتار غیرعادی application باشیم.
اما اگر دیدیم:

Out of memory: Killed process

و اشاره‌ی واضحی به cgroup نبود، احتمال OOM سیستمی بیشتر می‌شود. این دیگر فقط مشکل همان application نیست. باید ظرفیت node، headroom، eviction thresholdها و scheduling را هم بررسی کنیم.
بعد در monitoring نسبت working_set / limit را در طول زمان ببینیم. اگر مصرف آرام‌آرام بالا می‌رود، leak محتمل است. اگر همیشه نزدیک سقف است، شاید limit از اول کم بوده.

قدم ۳: اگر Evicted بود، node را چک کنیم

در kubectl describe pod معمولاً پیام واضحی هست:

The node was low on resource: memory

بعد node را بررسی کنیم:

root@pedram:~# kubectl describe node <node>

دنبال conditionها بگردیم:

MemoryPressure
DiskPressure
PIDPressure

اگر مشکل disk، inode یا PID باشد، زیاد کردن memory limit هیچ کاری نمی‌کند. الکی وقت خودمون و بقیه رو هدر ندیم.
اگر request is 0 دیدیم، فقط یعنی برای همان resource تحت فشار request تعریف نشده است. برای تشخیص QoS باید کل resourceهای پاد و containerها را بررسی کنیم؛ چون Pod ممکن است با وجود memory request صفر، به‌خاطر CPU request یا memory limit همچنان Burstable باشد.

قدم ۴: سنگ‌قبرها را پاک کنیم

بعد از موج eviction، پادهای Failed خروجی را شلوغ می‌کنند:

root@pedram:~# kubectl delete pod --field-selector status.phase=Failed

این کار معمولاً برای تمیز کردن رکوردهای باقی‌مانده مفید است، اما در production بهتر است آن را با namespace یا label محدود کنیم و قبل از حذف، اگر incident investigation لازم است، اطلاعات Podهای Failed را نگه داریم.


بخش یازدهم — چند اشتباه که بهتره انجام ندیم

فقط زیاد کردن limit وقتی leak داریم
اگر مصرف حافظه دائم بالا می‌رود، limit بزرگ‌تر فقط زمان بین دو OOM را بیشتر می‌کند.

مقصر دانستن application برای هر eviction
وقتی پاد با reason: Evicted حذف شده و node هم MemoryPressure یا DiskPressure دارد، اول ظرفیت و چیدمان node را بررسی کنیم. شاید application بی‌گناه نباشد، اما نقطه شروع آن نیست.

نادیده گرفتن سرعت رشد مصرف حافظه
فقط عدد مصرف مهم نیست، شیب هم مهم است. spike سریع می‌تواند قبل از poll بعدی kubelet به OOMKilled برسد. رشد آرام‌تر ممکن است به eviction ختم شود.بعضی اپلیکیشن ها واقعا رم را ازاد نمیکنند و این میتواند باعث دردسر شود.(این مورد در محیط پروداکشن بارها برای ما پیش آمده است)

یکی گرفتن OOM سیستمی و OOM container
هر دو ممکن است exit 137 نشان بدهند، اما یکی در cgroup است و دیگری در سطح node. بدون dmesg تشخیص قطعی سخت است.

اعتماد بیش از حد به PDB یا Guaranteed
PDB جلوی node-pressure eviction را نمی‌گیرد. Guaranteed هم اگر از memory limit رد شویم، جلوی OOMKilled را نمی‌گیرد.

مانیتور کردن RSS به‌جای working set
RSS همیشه همان چیزی نیست که برای تصمیم OOM مهم است. برای هشدار نزدیک شدن به Working set، memory limit معمولاً بهتر جواب می‌دهد.

دست‌کاری eviction threshold بدون merge پیش‌فرض‌ها
اگر یک threshold را تغییر بدهیم و mergeDefaultEvictionSettings: true نگذاریم، ممکن است بقیه محافظت‌های پیش‌فرض مثل disk و inode را هم خراب کنیم.


بخش دوازدهم — از خاموش کردن آتش تا پیشگیری

هدف فقط این نیست که بعد از OOM یا eviction سرویس را برگردانیم. تنظیمات و monitoring باید طوری باشند که کمتر به آن نقطه برسیم.

۱) request و limit را با داده واقعی تنظیم کنیم

request و limit را حدسی انتخاب نکنیم. working_set را در یک بازه معنی‌دار ببینیم؛ مثلاً p95 یا p99 چند روز گذشته. بعد بر اساس همان memory request و limit را تنظیم کنیم.

VPA در حالت recommendation می‌تواند کمک کند. لازم نیست حتماً اجازه بدهیم خودش Podها را recreate کند؛ گاهی فقط recommendation آن برای تصمیم‌گیری کافی است.

۲) با In-Place Pod Resize تغییر را کم‌دردتر کنیم

در نسخه‌های جدید کوبرنتیز، امکان تغییر resourceهای Pod بدون recreate کامل فراهم شده است. این برای CPU و memory مفید است، مخصوصاً وقتی نمی‌خواهیم برای اصلاح request یا limit کل Pod را از نو بسازیم.

نمونه:

root@pedram:~# kubectl patch pod pedram-app --subresource resize --patch \
  '{"spec":{"containers":[{"name":"app","resources":{"limits":{"memory":"512Mi"},"requests":{"memory":"512Mi"}}}]}}'

این روش می‌تواند تغییر منابع در production را ساده‌تر کند، اما جای تحلیل رفتار workload را نمی‌گیرد. هنوز باید با داده تصمیم بگیریم.

قابلیت In-Place Pod Resize برای تغییر CPU و memory مربوط به containerها در کوبرنتیز v1.35 به وضعیت stable رسیده است. این قابلیت کمک می‌کند در بعضی سناریوها مقدار request یا limit را بدون حذف و ساخت دوباره Pod تغییر بدهیم، اما راه‌حل جادویی نیست. QoS class بعد از ساخته‌شدن Pod تغییر نمی‌کند؛ یعنی با resize نمی‌توان یک Pod را از Burstable به Guaranteed تبدیل کرد. فقط CPU و memory قابل resize هستند، نه همه resourceها. بعضی containerها مثل non-restartable init containerها و ephemeral containerها قابل resize نیستند. روی Windows پشتیبانی نمی‌شود. همچنین Podهایی که تحت static CPU Manager یا static Memory Manager policy مدیریت می‌شوند، قابل resize in-place نیستند. در مورد swap هم باید دقیق‌تر حرف بزنیم: پادهایی که از swap memory استفاده می‌کنند، نمی‌توانند memory request را resize کنند مگر اینکه resizePolicy مربوط به memory روی RestartContainer باشد. پس این قابلیت برای همه نوع Pod، همه نوع نود policy و همه نوع تغییر memory، مسیر ساده بدون restart نیست. این قابلیت ابزار کم‌کردن درد است، نه جایگزین طراحی درست request، limit و capacity planning.

۳) Swap را با احتیاط وارد بازی کنیم

در کوبرنتیز جدید، swap برای workloadها فقط وقتی وارد بازی می‌شود که kubelet با failSwapOn: false اجرا شود و memorySwap.swapBehavior روی LimitedSwap تنظیم شده باشد. حالت پیش‌فرض NoSwap است؛ یعنی پادها از swap استفاده نمی‌کنند، هرچند processهای خارج از scope کوبرنتیز مثل systemd serviceها ممکن است swap مصرف کنند.

در حالت LimitedSwap، مقدار swap برای پادها دستی و آزادانه تعیین نمی‌شود؛ kubelet و runtime آن را به‌صورت محدود و متناسب با memory request تنظیم می‌کنند. پادهای BestEffort و Guaranteed در این حالت از swap استفاده نمی‌کنند، و swap عملاً برای Podهای Burstable معنا پیدا می‌کند. بنابراین swap می‌تواند برای spikeهای کوتاه کمک کند، اما latency می‌آورد و جایگزین request/limit درست، capacity planning و رفع memory leak نیست

۴) برای workload حیاتی، QoS و Priority را کنار هم بچینیم

برای سرویس مهم، فقط PriorityClass کافی نیست. request و limit حافظه باید درست باشند.اگر می‌خواهیم Pod در کلاس Guaranteed قرار بگیرد، فقط برابر کردن request و limit حافظه کافی نیست؛ برای همه containerها باید CPU و memory هم request و limit داشته باشند و مقدار request و limit برای هرکدام برابر باشد. اگر از Pod-level resources استفاده می‌کنیم، پادلول request و limit مربوط به CPU و memory هم باید تعریف شده و برابر باشند. ضمن اینکه QoS class بعد از ساخته‌شدن Pod تغییر نمی‌کند؛ پس با resize نمی‌توان یک Pod را از Burstable به Guaranteed تبدیل کرد.
PriorityClass هم در eviction به kubelet کمک می‌کند تصمیم بهتری بگیرد. ترکیب QoS درست، Priority مناسب و ظرفیت کافی خیلی مطمئن‌تر از تکیه به یکی از آن‌هاست.

۵) headroom واقعی نگه داریم

اگر nodeها همیشه لب مرز پر باشند، یک spike کوچک می‌تواند به eviction یا OOM سیستمی ختم شود. workloadهای bursty حاشیه امن می‌خواهند.

Cluster Autoscaler یا Karpenter هم باید طوری تنظیم شود که قبل از شروع بحران ظرفیت اضافه کند، نه وقتی incident کاملاً شعله‌ور شده.

۶) PSI را به dashboard و alertها اضافه کنیم

PSI فقط نمی‌گوید چقدر حافظه آزاد است؛ نشان می‌دهد processها چقدر منتظر resource مانده‌اند. این برای تشخیص فشار قبل از OOM و eviction خیلی به درد می‌خورد.
برای production، اضافه کردن PSI به dashboardها می‌تواند کمک کند از واکنش دیرهنگام به سمت پیشگیری برویم.


۱۳ — جمع‌بندی و چیت‌شیت

اگر بخواهیم کل بحث را کوتاه کنیم:

برای OOMKilled از container شروع کنیم.
برای Evicted از node شروع کنیم.

اولی یعنی workload از سهم خودش عبور کرده. دومی یعنی node زیر فشار رفته و kubelet برای زنده نگه داشتن node چیزی را حذف کرده است.

این دو از بیرون شبیه‌اند، اما علت، لاگ، metric و درمانشان فرق دارد. وقتی تفاوت را در سطح cgroup، kernel، QoS و eviction manager بفهمیم، دیگر همینجوری limit را زیاد نمی‌کنیم. می‌فهمیم چه زمانی این کار درست است و چه زمانی فقط صورت مسئله را عقب می‌اندازد.

چیت‌شیت سریع

محورOOMKilledEvicted
تصمیم‌گیرندهkernelkubelet
نشانهreason: OOMKilled + exit 137status: Failed + reason: Evicted
اولین بررسیkubectl describe podkubectl describe pod
سرنخ روی nodedmesg -T | grep -i oomkubectl describe node
تشخیص مهموجود cgroup در dmesg یعنی OOM در سطح containerconditionهایی مثل MemoryPressure یا DiskPressure
علت رایجlimit کم، memory leak یا مصرف غیرمنتظره workloadکمبود ظرفیت نود، request اشتباه یا چیدمان نامناسب
درماناصلاح limit، بررسی leak، ریسایز منابعتنظیم request/QoS، Priority، headroom، autoscaling و thresholdها
متریک مهمcontainer_memory_working_set_byteskube_node_status_condition{condition="MemoryPressure"}

منابع و مطالعهٔ بیشتر

  1. Node-pressure Evictionمستندات رسمی

    Kubernetes Documentationkubernetes.ioبازبینی: ۲۶ خرداد ۱۴۰۵

    برای بخش Evicted، eviction signals، hard/soft eviction، housekeeping-interval، eviction-pressure-transition-period، PDB و منطق kubelet زیر فشار node.

  2. Kubernetes Documentationkubernetes.ioبازبینی: ۲۶ خرداد ۱۴۰۵

    برای بخش request، limit، memory limit، cgroup memory enforcement و فعال شدن Linux OOM subsystem وقتی container از memory limit عبور می‌کند.

  3. Pod Quality of Service Classesمستندات رسمی

    Kubernetes Documentationkubernetes.ioبازبینی: ۲۶ خرداد ۱۴۰۵

    برای توضیح QoS classهای Guaranteed، Burstable و BestEffort و اینکه Kubernetes چطور از این طبقه‌بندی در تصمیم‌های eviction استفاده می‌کند.

  4. About cgroup v2مستندات رسمی

    Kubernetes Documentationkubernetes.ioبازبینی: ۲۶ خرداد ۱۴۰۵

    برای توضیح cgroup v2 و اینکه kubelet و container runtime برای enforce کردن resource management مربوط به Podها و containerها با cgroup کار می‌کنند.

  5. Control Group v2مستندات رسمی

    Linux Kernel Documentationdocs.kernel.orgبازبینی: ۲۶ خرداد ۱۴۰۵

    برای جزئیات سطح kernel درباره cgroup v2، memory controller و رفتارهای مربوط به group-based OOM در سطح cgroup.

  6. Kubernetes Documentationkubernetes.ioبازبینی: ۲۶ خرداد ۱۴۰۵

    برای بخش PSI metrics، stable شدن در Kubernetes v1.36، پیش‌نیازهای Linux، cgroup v2، Summary API و endpoint مربوط به metrics/cadvisor.

  7. Kubernetes Blogkubernetes.ioبازبینی: ۲۶ خرداد ۱۴۰۵

    برای ادعای نسخه‌ای PSI metrics در Kubernetes v1.36 و تغییرات GA مربوط به تشخیص پشتیبانی OS و cgroup توسط kubelet.

  8. Kubernetes Blogkubernetes.ioبازبینی: ۲۶ خرداد ۱۴۰۵

    برای بخش In-Place Pod Resize، stable شدن در Kubernetes v1.35، قابل تغییر شدن CPU و memory request/limit و محدودیت‌های عملیاتی این قابلیت.

  9. Kubernetes Documentationkubernetes.ioبازبینی: ۲۶ خرداد ۱۴۰۵

    برای نحوه resize کردن CPU و memory containerها، resizePolicy، محدودیت‌های مربوط به QoS class، swap، static CPU Manager و static Memory Manager.

  10. Swap memory managementمستندات رسمی

    Kubernetes Documentationkubernetes.ioبازبینی: ۲۶ خرداد ۱۴۰۵

    برای بخش LimitedSwap، failSwapOn، NoSwap، رفتار swap برای Podهای Burstable و ریسک‌های production مربوط به swap در Kubernetes.

  11. Kubernetes Documentationkubernetes.ioبازبینی: ۲۶ خرداد ۱۴۰۵

    برای بخش system-reserved، kube-reserved، Node Allocatable، enforceNodeAllocatable و اینکه reservationها بدون enforcement درست فقط مدل حسابداری هستند.

  12. kube-state-metrics / Kubernetes SIG Instrumentationgithub.comبازبینی: ۲۶ خرداد ۱۴۰۵

    برای queryهای Prometheus مربوط به OOMKilled، metricهای kube_pod_container_status_last_terminated_reason، kube_pod_container_status_last_terminated_timestamp، kube_pod_status_reason و experimental بودن بعضی metricها.

  13. Netdata Academynetdata.cloudبازبینی: ۲۶ خرداد ۱۴۰۵

    مطالعه تکمیلی برای درک cgroup v2، memory.max، memory.high، memory pressure و PSI از زاویه Linux/container observability. این منبع مکمل است، نه جایگزین مستند رسمی Kubernetes یا Linux Kernel.

  14. Learnkubelearnkube.comبازبینی: ۲۶ خرداد ۱۴۰۵

    مطالعه تکمیلی برای فهم عملی request و limit در Kubernetes و اثر memory limit روی OOMKilled. این منبع برای درک شهودی بهتر است و جایگزین مستند رسمی Kubernetes نیست.

  15. Kubelet Configuration v1beta1مستندات رسمی

    Kubernetes Documentationkubernetes.ioبازبینی: ۲۶ خرداد ۱۴۰۵

    برای توضیح گزینه kernelMemcgNotification و اینکه kubelet می‌تواند برای تشخیص عبور از memory eviction thresholds از kernel memcg notification به‌جای polling استفاده کند.