بلاگ/خطای OOM Killer در لینوکس: چرا سرویس‌ها روی سرور شما ناگهان کشته می‌شوند و چطور رفعش کنیم؟
به‌روز شده

خطای OOM Killer در لینوکس: چرا سرویس‌ها روی سرور شما ناگهان کشته می‌شوند و چطور رفعش کنیم؟

1405/07/240 بازدید
خطای OOM Killer در لینوکس: چرا سرویس‌ها روی سرور شما ناگهان کشته می‌شوند و چطور رفعش کنیم؟

خطای OOM Killer در لینوکس: چرا سرویس‌ها روی سرور شما ناگهان کشته می‌شوند و چطور رفعش کنیم؟

نمایی مفهومی از کرنل لینوکس که یک پردازش پرمصرف حافظه را متوقف می‌کند

یک وب‌سرور یا دیتابیس بدون هیچ خطای مشخصی از کار می‌افتد، در لاگ خود اپلیکیشن هم چیزی دیده نمی‌شود، و فقط یک پیام Killed در ترمینال یا یک خروج ناگهانی باقی می‌ماند. در بسیاری از این موارد عامل واقعی خودِ اپلیکیشن نیست؛ خطای OOM Killer در لینوکس است — تصمیم کرنل برای کشتن یک پردازش تا سرور از کار نیفتد.

این رفتار یک باگ نیست، بخشی از طراحی مدیریت حافظه در لینوکس است. اما وقتی سرویس اصلی سرور بدون هشدار قبلی کشته شود، نتیجه برای کاربر نهایی دقیقاً شبیه یک کرش واقعی است. در این مقاله می‌بینیم OOM Killer چیست، چرا فعال می‌شود، چطور آن را در لاگ‌ها تشخیص دهیم، و چه راه‌حل‌هایی واقعاً مشکل را حل می‌کنند.

خطای OOM Killer دقیقاً چیست؟

لینوکس به برنامه‌ها اجازه می‌دهد بیشتر از حافظه‌ای که فعلاً آزاد است درخواست کنند؛ به این رفتار memory overcommit گفته می‌شود. دلیلش این است که بسیاری از برنامه‌ها حافظه را رزرو می‌کنند ولی همهٔ آن را واقعاً استفاده نمی‌کنند، و رد کردن هر درخواست حافظه بزرگ باعث می‌شود خیلی از نرم‌افزارهای عادی اجرا نشوند.

مشکل وقتی شروع می‌شود که مصرف واقعی حافظه به مرز ظرفیت فیزیکی (و Swap، اگر وجود داشته باشد) برسد. در این لحظه کرنل دیگر نمی‌تواند حافظهٔ درخواستی را تأمین کند. به‌جای این‌که کل سیستم فریز شود یا کرش کند، ماژول OOM Killer وارد عمل می‌شود و یکی از پردازش‌ها را انتخاب و با سیگنال SIGKILL متوقف می‌کند تا حافظه آزاد شود.

انتخاب پردازش قربانی تصادفی نیست. کرنل برای هر پردازش یک oom_score حساب می‌کند که عمدتاً بر اساس میزان حافظهٔ مصرفی آن پردازش است — هرچه پردازشی حافظهٔ بیشتری گرفته باشد، نمرهٔ بالاتری می‌گیرد و شانس بیشتری برای کشته شدن دارد. به همین دلیل معمولاً دیتابیس یا سرویس اصلی اپلیکیشن، نه یک پردازش کوچک پس‌زمینه، قربانی اول می‌شود؛ چون معمولاً بیشترین رم را مصرف می‌کند.

چرا این اتفاق می‌افتد؟ دلایل رایج

نشت حافظه در اپلیکیشن

یک سرویس که به‌مرور حافظهٔ بیشتری نگه می‌دارد و هیچ‌وقت آن را آزاد نمی‌کند، دیر یا زود کل رم سرور را پر می‌کند. این رفتار معمولاً بعد از چند ساعت یا چند روز uptime خودش را نشان می‌دهد، نه بلافاصله بعد از استارت سرویس.

نبود یا کمبود Swap

وقتی سرور اصلاً Swap ندارد، به محض پر شدن رم فیزیکی کرنل بلافاصله به OOM Killer می‌رسد. Swap یک حاشیهٔ امن ایجاد می‌کند، هرچند راه‌حل اصلی نیست — در بخش بعد به این تفاوت برمی‌گردیم.

چند سرویس هم‌زمان روی یک رم محدود

اجرای وب‌سرور، دیتابیس و صف پردازش روی یک VPS با رم کم معمولاً تا یک حجم ترافیک مشخص کار می‌کند، ولی هر افزایش ناگهانی بار (مثلاً یک کمپین تبلیغاتی یا crawl ناگهانی) جمع مصرف حافظهٔ آن سه سرویس را از ظرفیت سرور بالاتر می‌برد. اگر دیتابیس بخش اصلی این فشار است، مقایسهٔ MySQL مدیریت‌شده و خودمیزبان می‌تواند در تصمیم نگه داشتن یا جدا کردن آن از بقیهٔ سرویس‌ها کمک کند.

محدودیت‌های cgroup یا کانتینر

اگر یک کانتینر Docker یا سرویس systemd با MemoryMax محدود شده باشد، حتی وقتی رم کل سرور آزاد است، همان پردازش می‌تواند صرفاً به‌خاطر رسیدن به سقف داخلی خودش توسط OOM Killer کشته شود. این حالت را باید جدا از OOM Killer کل سیستم بررسی کرد.

چطور تشخیص بدهیم سرویس توسط OOM Killer کشته شده؟

اولین جایی که باید نگاه کرد لاگ کرنل است، نه لاگ اپلیکیشن — چون تصمیم را خود کرنل گرفته، نه برنامه.

dmesg -T | grep -i -E "out of memory|oom_kill|killed process"

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

[Thu Oct 16 09:12:04 2026] Out of memory: Killed process 18422 (mysqld) total-vm:4123456kB, anon-rss:3980112kB

روی سیستم‌هایی که لاگ dmesg بعد از ریبوت پاک شده، همان رویداد را می‌توان از journalctl هم دید:

journalctl -k --since "1 hour ago" | grep -i "out of memory"

نمونه خروجی journalctl که رویداد OOM Killer و نام پردازش کشته‌شده را نشان می‌دهد

اگر پردازش قربانی یک سرویس systemd باشد (مثل mysqld.service)، معمولاً در journalctl -u <service> هم یک خط Main process exited, code=killed, status=9/KILL دیده می‌شود — همین عدد ۹ (SIGKILL) سرنخ اول است که باید OOM Killer را بررسی کرد، نه یک کرش معمولی اپلیکیشن.

به‌جای چک کردن دستی لاگ بعد از هر حادثه، راه‌اندازی مانیتورینگ سرور با Prometheus و Grafana روند مصرف رم را از قبل زیر نظر می‌گیرد و معمولاً کمبود حافظه را پیش از رسیدن به OOM Killer نشان می‌دهد.

راه‌حل‌های رفع خطای OOM Killer

افزودن یا افزایش Swap

Swap فوراً فضای مانور بیشتری به کرنل می‌دهد و از کشته شدن سرویس در نوسانات کوتاه‌مدت مصرف حافظه جلوگیری می‌کند:

fallocate -l 2G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab

Swap یک راه‌حل موقت برای نوسان است، نه برای یک کسری ساختاری رم. اگر سرویس واقعاً به‌طور پیوسته بیشتر از رم فیزیکی نیاز دارد، همان مصرف فقط به دیسک منتقل می‌شود که معمولاً چند برابر کندتر از رم است و خودش می‌تواند باعث timeout در سرویس‌هایی مثل دیتابیس شود.

تنظیم oom_score_adj برای محافظت از سرویس‌های حیاتی

می‌توان به کرنل گفت کدام سرویس را آخرین گزینه برای کشتن در نظر بگیرد. مقدار oom_score_adj بین ۱۰۰۰- (تقریباً معاف) تا ۱۰۰۰ (اولین قربانی) است:

echo -500 > /proc/$(pidof mysqld)/oom_score_adj

برای این‌که این تنظیم بعد از هر ریستارت سرویس هم باقی بماند، بهتر است داخل unit فایل systemd قرار بگیرد:

[Service]
OOMScoreAdjust=-500

این تنظیم فقط ترتیب انتخاب قربانی را تغییر می‌دهد، حافظهٔ جدیدی به سرور اضافه نمی‌کند — اگر همهٔ سرویس‌های سرور همین مقدار را بگیرند، مشکل دوباره به یک تصمیم تصادفی برمی‌گردد.

محدود کردن مصرف حافظهٔ هر سرویس

به‌جای اجازه دادن به یک سرویس برای مصرف بی‌حد رم، می‌توان برای آن سقف تعریف کرد تا قبل از درگیر کردن کل سرور، فقط همان سرویس با خطای حافظه مواجه شود:

[Service]
MemoryMax=1536M

در Docker معادل آن --memory="1.5g" در زمان اجرای کانتینر است. این روش باعث می‌شود اثر یک نشت حافظه یا باگ به همان سرویس محدود بماند، نه کل سرور. جزئیات بیشتر دربارهٔ تعیین سقف CPU و RAM برای سرویس‌های systemd را می‌توانید در محدود کردن مصرف CPU و RAM سرویس‌ها با systemd و cgroups ببینید.

جدول زیر این راه‌حل‌ها را در یک نگاه مقایسه می‌کند:

روشچه چیزی را حل می‌کندزمان اجرامحدودیت اصلی
افزودن Swapنوسان کوتاه‌مدت مصرف حافظهچند دقیقهراه‌حل موقت، کسری ساختاری رم را حل نمی‌کند
oom_score_adjترتیب انتخاب قربانی توسط کرنلچند دقیقهحافظهٔ جدید اضافه نمی‌کند
MemoryMax / cgroupsجلوگیری از سرایت یک سرویس پرمصرف به بقیهچند دقیقه تا نیم ساعتباید سقف درست را بر اساس مصرف واقعی تعیین کرد
ارتقای RAM سرورکسری واقعی و پیوسته حافظهچند دقیقه (با تغییر پلن)هزینهٔ ماهانهٔ بیشتر

چه زمانی واقعاً باید رم سرور را ارتقا دهیم؟

اگر بعد از افزودن Swap و تنظیم محدودیت‌ها، سرور همچنان هر چند روز یک‌بار Swap را هم پر می‌کند و دوباره OOM Killer فعال می‌شود، نشانهٔ این است که مصرف واقعی سرویس‌ها از ظرفیت فیزیکی رم بیشتر شده — نه یک نشتی موقت. در این حالت تنظیمات بیشتر فقط فاصلهٔ بین دو حادثه را کمی زیاد می‌کند، مشکل را حل نمی‌کند.

برای این سناریو، ساده‌ترین راه بررسی پلن فعلی سرور و ارتقای رم است. اگر سرور شما روی سرور مجازی آریانت اجرا می‌شود، تغییر پلن به یک تیر با رم بیشتر معمولاً بدون نیاز به مهاجرت کامل داده‌ها انجام می‌شود و سریع‌تر از هر تنظیم دیگری ریسک تکرار این خطا را از بین می‌برد. اگر این کمبود مشخصاً از سمت دیتابیس است، راهنمای تعیین رم و دیسک مناسب برای سرور دیتابیس به تخمین دقیق‌تر ظرفیت لازم کمک می‌کند.

جمع‌بندی

خطای OOM Killer در لینوکس همیشه به معنی خرابی اپلیکیشن نیست؛ در بیشتر موارد یک تصمیم عمدی کرنل برای جلوگیری از کرش کامل سرور است. اولین قدم همیشه تشخیص دقیق در dmesg یا journalctl است، نه حدس زدن بر اساس لاگ اپلیکیشن.

از آن نقطه، Swap و oom_score_adj می‌توانند فاصلهٔ بین حوادث را زیاد کنند و MemoryMax می‌تواند اثر یک سرویس خراب را محدود کند. اما وقتی مصرف واقعی رم به‌طور پیوسته از ظرفیت سرور بیشتر است، تنها راه‌حل پایدار افزایش رم واقعی سرور است.