SSD Nodes Learn Hosting plans →
راهنماها Matt Connorتوسط Matt Connor · به‌روزرسانی شده 2026-08-27

محدود کردن مصرف CPU و RAM پردازش‌ها با systemd

با تنظیم MemoryMax و CPUQuota در فایل drop-in سرویس‌های systemd، منابع سرور را مدیریت کنید. این راهنما نحوه جلوگیری از خطای Unit configuration و مدیریت OOM kill را توضیح می‌دهد.

محدود کردن حافظه و پردازنده پردازش با استفاده از drop-in در systemd

شما می‌توانید حافظه و پردازنده یک پردازش را در VPS لینوکسی خود با افزودن چند خط به unit مربوط به آن پردازش محدود کنید. MemoryMax= سقف سخت (hard ceiling) برای حافظه است. CPUQuota= سقف زمانی برای پردازنده است. هر دو مورد توسط cgroup v2 (گروه‌های کنترلی نسخه 2) اعمال می‌شوند؛ قابلیتی در هسته لینوکس که systemd از آن برای مدیریت منابع هر سرویس روی سرور استفاده می‌کند.

sudo systemctl edit myapp.service

این دستور یک فایل drop-in را باز می‌کند که شامل دستورالعمل‌هایی در قالب کامنت است. موارد زیر را در بالای آن‌ها اضافه کنید:

[Service]
MemoryHigh=512M
MemoryMax=768M
MemorySwapMax=0
CPUQuota=80%
TasksMax=128
sudo systemctl daemon-reload
sudo systemctl restart myapp.service
systemctl show myapp.service -p MemoryHigh -p MemoryMax -p CPUQuotaPerSecUSec -p TasksMax

systemctl show باید اعداد شما را با واحدهای استاندارد هسته بازگرداند: MemoryMax=805306368 و CPUQuotaPerSecUSec=800ms. اگر خروجی دستور MemoryMax=infinity باشد، یعنی فایل drop-in بارگذاری نشده است. بررسی کنید که فایل در مسیر /etc/systemd/system/myapp.service.d/override.conf قرار گرفته باشد و با هدر [Service] شروع شود؛ زیرا اگر یک خط تنظیمات بدون هدر بخش در بالای آن قرار گیرد، systemd خطای Assignment outside of section. Ignoring. را در لاگ ثبت کرده و سرویس را بدون هیچ محدودیتی اجرا می‌کند.

باقی این راهنما به نحوه انتخاب این اعداد و مشکلاتی که ممکن است پس از تنظیم آن‌ها رخ دهد، می‌پردازد.

چرا یک پردازش سرکش باعث فریز شدن VPS می‌شود، حتی اگر حافظه را پر نکند

پردازشی که به سقف سخت حافظه می‌رسد، در حدود یک ثانیه متوقف می‌شود و سرویس دوباره راه‌اندازی می‌گردد. این حالت مطلوب است. حالت نامطلوب زمانی است که هیچ‌چیز متوقف نمی‌شود: سرور به ping پاسخ می‌دهد، SSH اتصال را می‌پذیرد، اما prompt پوسته هرگز ظاهر نمی‌شود. ماشین زنده و مشغول است، اما هیچ‌کدام از این کارها مفید نیست.

مکانیسم این اتفاق به این شرح است، زیرا چندان بدیهی نیست. وقتی حافظه آزاد کم می‌شود، هسته (kernel) به‌جای تخصیص صفحات جدید، شروع به بازپس‌گیری صفحات موجود می‌کند. ارزان‌ترین صفحات برای بازپس‌گیری، صفحات مبتنی بر فایل (file-backed) هستند و page cache، کد اجرایی تمام برنامه‌های در حال اجرا را در خود نگه می‌دارد. بنابراین، هسته صفحات متنی sshd را تخلیه می‌کند و دستور بعدی که sshd اجرا می‌کند، یک page fault است که باید آن بایت‌ها را دوباره از حافظه ذخیره‌سازی بخواند. هر پردازش در نهایت به‌جای اجرا، منتظر دیسک می‌ماند. همان صفحات در یک حلقه خارج و وارد می‌شوند که به آن thrashing می‌گویند.

دو عامل باعث می‌شود این وضعیت در یک VPS بدتر از لپ‌تاپ باشد. فضای ذخیره‌سازی اغلب به شبکه متصل است یا اشتراکی است، بنابراین هر fault میلی‌ثانیه‌های بیشتری نسبت به یک دستگاه NVMe محلی هزینه دارد. همچنین هسته زمان را اندازه‌گیری نمی‌کند، بلکه شکست را می‌سنجد: تا زمانی که بازپس‌گیری (reclaim) همچنان صفحه‌ای را تحویل می‌دهد، هرچند به‌کندی، هسته معتقد است که در حال پیشرفت است و OOM killer را فراخوانی نمی‌کند. یک سرور می‌تواند برای دقایق طولانی در این وضعیت بماند پیش از آنکه چیزی کشته شود.

شما می‌توانید این اتفاق را مشاهده کنید. هسته در لینوکس 4.20 و نسخه‌های جدیدتر، اطلاعات فشار (PSI) را ارائه می‌دهد:

cat /proc/pressure/memory
cat /proc/pressure/io
some avg10=63.72 avg60=41.02 avg300=12.33 total=13729481
full avg10=48.15 avg60=30.44 avg300=8.90 total=9114233

خط full همان چیزی است که اهمیت دارد. full avg10=48.15 به این معنی است که در ده ثانیه گذشته، 48 درصد از زمان، تمام وظایف قابل‌اجرا روی سرور به دلیل انتظار برای عملیات حافظه متوقف بوده‌اند، بنابراین هیچ‌چیز اجرا نشده است. یک سرور سالم در full عددی نزدیک به صفر را نشان می‌دهد. بالای 10 برای کاربر کند احساس می‌شود و 40 یا بیشتر، وضعیتی است که افراد آن را فریز شده توصیف می‌کنند.

به همین دلیل است که یک محدودیت به‌تنهایی یک تضمین نیست. واحدی که تحت MemoryHigh= نگه داشته می‌شود، به‌جای کشته شدن، محدود (throttle) می‌شود؛ بنابراین زنده و کند باقی می‌ماند و چون از دید systemd هرگز شکست نخورده است، هیچ‌چیز آن را دوباره راه‌اندازی نمی‌کند. یک واحد محدودشده که همچنان اجازه استفاده از swap را دارد، عملیات خواندن و نوشتنی ایجاد می‌کند که به حساب آن واحد گذاشته می‌شود اما توسط یک دستگاه مشترک سرویس‌دهی می‌شود، بنابراین می‌تواند /proc/pressure/io را برای سایر سرویس‌های روی سرور بالا ببرد. محدودیت‌ها تعیین می‌کنند چه کسی هزینه کمبود را می‌پردازد، اما نمی‌توانند ظرفیت ایجاد کنند.

بررسی اجرای cgroup v2 روی VPS

stat -fc %T /sys/fs/cgroup

cgroup2fs سلسله‌مراتب یکپارچه است که تمام تنظیمات زیر به آن نیاز دارند. tmpfs به این معناست که سرور با ساختار قدیمی v1 بوت شده است؛ ساختاری که در آن MemoryHigh= و MemorySwapMax= وجود ندارند و رفتار OOM در هر واحد متفاوت است. توزیع‌های Ubuntu 22.04 و نسخه‌های بعد از آن، و همچنین Debian 11 و نسخه‌های بعد، به‌صورت پیش‌فرض از v2 استفاده می‌کنند. ایمیج‌های قدیمی یا کرنلی که با systemd.unified_cgroup_hierarchy=0 بوت شده باشد، از این نسخه استفاده نمی‌کنند.

در cgroup v2، سیستم‌عامل systemd به‌صورت پیش‌فرض قابلیت memory accounting را برای هر واحد فعال می‌کند، بنابراین اعداد از قبل موجود هستند:

systemd-cgtop -m

این دستور cgroupها را بر اساس میزان مصرف حافظه مرتب می‌کند که سریع‌ترین راه برای پاسخ به این پرسش است که «چه چیزی در حال مصرف منابع این سرور است»، البته تا زمانی که سرور هنوز پاسخگو باشد. اگر سرور جدید است، کارهای مربوط به حساب کاربری و فایروال در ده دقیقه اول روی یک VPS جدید باید پیش از این مرحله انجام شود.

محدودیت‌های MemoryHigh و MemoryMax

تفاوت بین این دو تنظیم حافظه، تعیین‌کننده نحوه بروز خطا است.

  • MemoryHigh= یک سقف نرم (soft cap) است. بالاتر از این مقدار، هسته سیستم‌عامل به‌طور تهاجمی حافظه را از آن cgroup بازپس می‌گیرد و تخصیص‌های حافظه را عمداً کند می‌کند. میزان مصرف می‌تواند از این عدد فراتر رود و هیچ پردازشی کشته نمی‌شود.
  • MemoryMax= یک سقف سخت (hard cap) است. هنگامی که یک درخواست تخصیص حافظه نتواند در محدوده این سقف برآورده شود، OOM killer درون همان cgroup اجرا شده و یکی از پردازش‌های خود آن واحد را می‌کشد.

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

هر دو را تنظیم کنید، به‌طوری که MemoryHigh= حدود 20 تا 30 درصد پایین‌تر از MemoryMax= باشد. این فاصله یک منطقه هشدار است: یک نشت حافظه تدریجی از High عبور می‌کند و به‌صورت کند شدن سرویس نمایان می‌شود، در حالی که یک جهش ناگهانی مستقیماً از Max عبور کرده و منجر به توقف سرویس می‌شود.

مقادیر درصدی نسبت به حافظه فیزیکی نصب‌شده محاسبه می‌شوند، بنابراین MemoryMax=25% در یک پلن 4 GB برابر با 1 GB است و پس از ارتقای پلن، همچنان یک‌چهارم ظرفیت سرور باقی می‌ماند. MemorySwapMax=0 آن واحد را به‌طور کامل از swap دور نگه می‌دارد، که باعث می‌شود یک کندی طولانی به یک توقف سریع و مشخص تبدیل شود.

برخی سرویس‌ها به شما اجازه می‌دهند به‌جای اندازه‌گیری، میزان مصرف حافظه را از قبل تعیین کنید: یک واحد Ollama حافظه کش KV خود را بر اساس context window که به آن می‌دهید تنظیم می‌کند، بنابراین پیش از انتخاب سقف برای آن، هزینه افزایش num_ctx در RAM را مطالعه کنید.

یک سقف حافظه در کنار خود به یک سیاست راه‌اندازی مجدد (restart policy) نیاز دارد، وگرنه پس از کشته شدن پردازش، تنها با یک سرویس متوقف‌شده مواجه خواهید شد.

[Unit]
StartLimitIntervalSec=300
StartLimitBurst=5

[Service]
Restart=on-failure
RestartSec=5s

StartLimit* باید در [Unit] و Restart= در [Service] قرار گیرد. اگر هر کدام را در بخش اشتباه بگذارید، systemd آن را نادیده می‌گیرد. پنج بار راه‌اندازی مجدد در پنج دقیقه نشان‌دهنده نشت حافظه است، نه یک نوسان گذرا؛ بنابراین پس از آن، systemd تلاش مجدد را متوقف کرده و واحد را در وضعیت failed باقی می‌گذارد. این همان وضعیتی است که می‌خواهید بعداً مشاهده کنید تا به‌جای گیر افتادن در یک crash loop، مشکل را ریشه‌یابی کنید.

محدود کردن CPU با CPUQuota یا اشتراک‌گذاری آن با CPUWeight

CPUQuota= درصدی از زمان در دسترس روی یک هسته CPU را اختصاص می‌دهد. CPUQuota=50% معادل نیمی از یک هسته است. CPUQuota=200% معادل دو هسته کامل است که واحد می‌تواند آن را بین هر تعداد ترد که نیاز دارد، توزیع کند. در یک پلن با 2 vCPU، مقدار CPUQuota=200% کل توان دستگاه است.

CPUWeight= برای اکثر سرویس‌ها گزینه پیش‌فرض بهتری است. این یک سهم نسبی از 1 تا 10000 است و مقدار پیش‌فرض هسته (kernel) برابر با 100 می‌باشد. این محدودیت تنها زمانی اعمال می‌شود که رقابتی برای منابع وجود داشته باشد: یک job پشتیبان‌گیری با مقدار CPUWeight=20 در زمان فشار کاری، منابع را به یک وب‌سرور با مقدار 100 واگذار می‌کند، اما در زمان بیکاری سیستم، همچنان می‌تواند از تمام توان دستگاه استفاده کند. یک سهمیه سخت (hard quota) این ظرفیت خالی را هدر می‌دهد.

در مورد مزایای محدودیت CPU واقع‌بین باشید. یک پردازش CPU-bound به‌ندرت باعث فریز شدن لینوکس می‌شود، زیرا زمان‌بند (scheduler) سیستم‌عامل همچنان زمان پردازنده را بین همه تقسیم می‌کند. حافظه (RAM) همان چیزی است که باعث از کار افتادن دستگاه می‌شود. زمانی که به یک سقف پیش‌بینی‌پذیر نیاز دارید، از CPUQuota= استفاده کنید؛ مثلاً برای یک فرآیند build یا عاملی (agent) که در غیر این صورت ممکن است یک ساعت با تمام توان اجرا شود. تعیین اندازه برای این نوع workload موضوعی جداگانه است که در میزان RAM و CPU مورد نیاز برای یک VPS عامل کدنویسی به آن پرداخته شده است.

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

پارامتر TasksMax از ایجاد حلقه fork جلوگیری می‌کند

TasksMax= تعداد پردازش‌ها و ترد‌هایی است که یک واحد (unit) می‌تواند در خود نگه دارد. تردها نیز در این شمارش لحاظ می‌شوند، بنابراین سرویس‌های Java یا Go به فضای بیشتری نسبت به آنچه لیست پردازش‌ها نشان می‌دهد، نیاز دارند. این ارزان‌ترین روش محافظت در برابر اسکریپتی است که در یک حلقه، fork ایجاد می‌کند؛ زیرا عملیات fork درون همان واحد با شکست مواجه می‌شود و کل سیستم با کمبود شناسه پردازش (PID) مواجه نخواهد شد.

TasksMax=128

هنگامی که یک واحد به این محدودیت می‌رسد، هسته سیستم‌عامل خطی را در لاگ ثبت می‌کند که نام cgroup مربوطه را نشان می‌دهد:

cgroup: fork rejected by pids controller in /system.slice/myapp.service

خود برنامه معمولاً خطای fork: retry: Resource temporarily unavailable را گزارش می‌دهد. برای بررسی مقداری که مدیر سیستم به‌صورت پیش‌فرض اعمال می‌کند، از systemctl show -p DefaultTasksMax استفاده کنید.

محدود کردن یک وظیفه موقت با systemd-run

برای استفاده از این قابلیت، نیازی به فایل unit ندارید. systemd-run یک unit گذرا (transient) پیرامون یک دستور واحد ایجاد می‌کند.

sudo systemd-run --scope -p MemoryMax=1G -p MemorySwapMax=0 -p CPUQuota=50% -p TasksMax=64 ./import-data.sh

--scope دستور را در ترمینال شما اجرا می‌کند، پس از آنکه Running scope as unit: run-r7c1a....scope را چاپ کرد. خروجی روی صفحه باقی می‌ماند و محدودیت‌ها پس از خروج دستور از بین می‌روند. هر ویژگی از systemd.resource-control پس از -p کار می‌کند.

برای یک وظیفه طولانی‌مدت، --scope را حذف کرده و به آن یک نام بدهید. سپس این وظیفه در پس‌زمینه به عنوان یک سرویس گذرا اجرا شده و در journal لاگ می‌شود:

sudo systemd-run --unit=nightly-import -p MemoryMax=1G -p CPUWeight=20 ./import-data.sh
journalctl -u nightly-import -f

همین گزینه‌ها با --user نیز کار می‌کنند، حتی زمانی که root نیستید؛ اگرچه مدیر سرویس کاربر شما فقط کنترلرهایی را دارد که به آن تفویض شده‌اند، بنابراین ممکن است یک ویژگی در آنجا رد شود. در صورت بروز این مشکل، آن را با sudo اجرا کنید. هنگامی که یک وظیفه به یک جایگاه دائمی نیاز پیدا کرد، تنظیمات بدون تغییر به یک unit واقعی منتقل می‌شوند: به اجرای یک اسکریپت به عنوان سرویس و تایمر systemd مراجعه کنید.

پاسخ صادقانه به پرسش swap

استفاده از swap ماهیت خرابی را تغییر می‌دهد، نه اینکه از آن جلوگیری کند.

بدون swap، نشت حافظه به سقف می‌رسد و سرویس در عرض چند ثانیه از کار می‌افتد. این قطعی سریع، پرصدا و در لاگ‌های سیستم به‌راحتی قابل‌تشخیص است. با وجود swap، هسته سیستم‌عامل صفحات anonymous غیرفعال را به دیسک منتقل می‌کند و زمان می‌خرد. اگر مصرف حافظهٔ پردازش قرار بود تثبیت شود، swap شما را نجات می‌دهد. اما اگر پردازش دچار نشت حافظه باشد، swap یک قطعی 5 ثانیه‌ای را به یک وقفه 20 دقیقه‌ای تبدیل می‌کند؛ این وقفه بدتر است، زیرا یک پردازش مرده همچنان به شما اجازه می‌دهد از shell استفاده کنید، اما سیستمی که در حال thrashing (جابه‌جایی مداوم داده بین RAM و دیسک) است، پاسخگو نخواهد بود.

swapon --show
free -h

یک راهکار میانی مناسب برای VPSهای کوچک: یک فایل swap کوچک برای صفحاتی که یک‌بار تخصیص یافته و دیگر استفاده نمی‌شوند نگه دارید و برای سرویس‌هایی که حاضرید در صورت کمبود منابع از دست بدهید، MemorySwapMax=0 را تنظیم کنید. سرویس‌های حیاتی swap خود را حفظ می‌کنند و سرویس‌های غیرقابل‌پیش‌بینی به‌سرعت به محدودیت می‌خورند و دوباره راه‌اندازی می‌شوند.

کاهش vm.swappiness اهرم ضعیفی است و دانستن دلیل آن اهمیت دارد. این کار فقط تعادل بین تخلیه page cache و swap کردن صفحات anonymous را تغییر می‌دهد و هر دو در نهایت هزینه خواندن از دیسک را به همراه دارند. این تنظیم فقط تعیین می‌کند کدام صفحات دچار thrashing شوند، نه اینکه آیا کل سیستم دچار این وضعیت بشود یا خیر.

یک دیمون OOM زودهنگام پیش از وقوع توقف، فرآیندها را متوقف می‌کند

هسته سیستم‌عامل تا زمانی که عملیات بازیابی حافظه (reclaim) به‌طور کامل شکست نخورد منتظر می‌ماند؛ در یک VPS کوچک، همین زمان انتظار دقیقاً همان لحظه‌ای است که دسترسی به سرور را از دست می‌دهید. دو دیمون فضای کاربری (userspace) با نظارت مستقیم بر حافظه و متوقف‌سازی سریع‌تر فرآیندها، این مشکل را حل می‌کنند.

earlyoom میزان حافظه در دسترس و swap آزاد را پایش می‌کند و هنگامی که هر یک از آن‌ها به زیر حد آستانه برسد، فرآیندی که بالاترین امتیاز را دارد متوقف می‌کند.

sudo apt install earlyoom
systemctl status earlyoom

بسته‌های Debian و Ubuntu این سرویس را بلافاصله پس از نصب اجرا می‌کنند. گزینه‌های پیکربندی آن در /etc/default/earlyoom قرار دارند:

EARLYOOM_ARGS="-m 5,2 -s 5,2 --avoid '^(sshd|systemd)$' --prefer '^(node|python3)$'"

-m PERCENT حداقل حافظه در دسترس و -s PERCENT حداقل swap آزاد را تعیین می‌کند که هر دو به‌صورت پیش‌فرض روی 10 درصد تنظیم شده‌اند. عدد دوم در هر جفت، نقطه ارسال SIGKILL است: earlyoom پس از رسیدن به زیر مقدار اول، سیگنال SIGTERM و پس از رسیدن به زیر مقدار دوم، سیگنال SIGKILL ارسال می‌کند که مقدار پیش‌فرض آن نصف مقدار اول است. تغییرات را با sudo systemctl restart earlyoom اعمال کنید و برای مشاهده اینکه کدام فرآیند متوقف شده و چه مقدار حافظه اشغال کرده بود، journalctl -u earlyoom را بخوانید.

systemd-oomd گزینه دیگر است. صفحه راهنمای (manual page) آن، این ابزار را به عنوان «یک سرویس سیستمی که از cgroups-v2 و اطلاعات فشار توقف (PSI) برای نظارت و انجام اقدامات اصلاحی پیش از وقوع OOM در فضای هسته استفاده می‌کند» توصیف می‌کند. این ابزار به‌جای فرآیندهای تکی، روی کل cgroupها عمل می‌کند؛ بنابراین به‌جای یک زیرفرآیند سرگردان، کل یک واحد (unit) را متوقف می‌کند. واحدها با ManagedOOMMemoryPressure=kill یا ManagedOOMSwap=kill در این سیستم عضو می‌شوند و حد آستانه‌ها در /etc/systemd/oomd.conf تنظیم می‌گردند.

systemctl status systemd-oomd
oomctl

oomctl آنچه را که در حال حاضر تحت نظارت است چاپ می‌کند؛ این خروجی در ایمیج‌های سرور اغلب خالی است، زیرا تنظیمات باید به‌صورت دستی برای هر واحد فعال شوند. یک دیمون را انتخاب کنید و به همان بسنده کنید. اجرای همزمان هر دو به این معناست که دو ابزار برای انتخاب قربانی با هم رقابت می‌کنند و بازسازی دلیل هر توقف بسیار دشوارتر خواهد شد.

کدام واحد مسئول بود؟

با هسته (kernel) شروع کنید، زیرا هر فرآیندی را که خاتمه می‌دهد، ثبت می‌کند.

journalctl -k --grep "Killed process" --since "2 hours ago"

یک خاتمه توسط OOM killer سراسری به این شکل دیده می‌شود:

Out of memory: Killed process 4127 (node) total-vm:2731084kB, anon-rss:1874232kB, file-rss:0kB, shmem-rss:0kB, UID:1000 pgtables:4212kB oom_score_adj:0

anon-rss حافظه‌ای است که آن فرآیند در زمان مرگ در RAM اشغال کرده بود، که در اینجا حدود 1.8 گیگابایت است. نام داخل کروشه را با شک و تردید بخوانید. این قربانی‌ای است که هسته انتخاب کرده است؛ هسته معمولاً بزرگ‌ترین فرآیند را انتخاب می‌کند که لزوماً همان فرآیندی نیست که باعث کمبود حافظه شده است.

خاتمه ناشی از محدودیت cgroup پیشوند متفاوتی دارد و گزارشی که بالای آن چاپ می‌شود، نام cgroupای که به سقف محدودیت خود رسیده است را ذکر می‌کند:

Memory cgroup out of memory: Killed process 8811 (python3) total-vm:1044320kB, anon-rss:769112kB, file-rss:0kB, shmem-rss:0kB, UID:998 pgtables:1720kB oom_score_adj:0

آن پیشوند بخش اصلی تشخیص است. Memory cgroup out of memory به این معنی است که یک واحد به MemoryMax= تعیین‌شده توسط شما رسیده و بقیه سیستم در وضعیت مناسبی بوده است. یک Out of memory ساده به این معنی است که کل ماشین با کمبود حافظه مواجه شده، بنابراین محدودیت‌های شما یا وجود نداشته‌اند و یا بیش از حد سخاوتمندانه تنظیم شده‌اند.

سپس از systemd بپرسید چه چیزی مشاهده کرده است:

systemctl status myapp.service
journalctl -u myapp.service -n 50
myapp.service: A process of this unit has been killed by the OOM killer.
myapp.service: Main process exited, code=killed, status=9/KILL
myapp.service: Failed with result 'oom-kill'.

systemctl status همان موضوع را در یک خط و به عنوان Active: failed (Result: oom-kill) بیان می‌کند.

شمارنده‌های cgroup سومین منبع هستند و تنها منبعی‌اند که throttling را ثبت می‌کنند؛ موردی که هرگز خط لاگی تولید نمی‌کند:

cat /sys/fs/cgroup/system.slice/myapp.service/memory.events
cat /sys/fs/cgroup/system.slice/myapp.service/memory.peak
low 0
high 4213
max 118
oom 12
oom_kill 12

high تعداد دفعاتی را می‌شمارد که واحد از MemoryHigh= عبور کرده و throttled شده است. max تعداد دفعاتی را می‌شمارد که به سقف سخت (hard cap) رسیده و oom_kill تعداد فرآیندهایی را می‌شمارد که واقعاً کشته شده‌اند. یک high بزرگ همراه با oom_kill 0 همان مورد خاموش (silent case) ذکر شده در قبل است: سرویس در حال اجراست، سرعت آن به شدت کاهش یافته و هیچ خطایی به هیچ‌کس گزارش نکرده است. memory.peak (در لینوکس 5.19 و جدیدتر) بالاترین میزان مصرفی که cgroup به آن رسیده را نگه می‌دارد؛ این عددی است که باید برای تنظیم MemoryMax= در نظر بگیرید. هر دو فایل هنگام restart شدن واحد بازنشانی می‌شوند، زیرا systemd مجدداً cgroup را ایجاد می‌کند.

یک پیش‌نیاز در زیر تمام این موارد وجود دارد. اگر /var/log/journal وجود نداشته باشد، journal در RAM باقی می‌ماند و پس از reboot که برای بازیابی سیستم به آن نیاز داشتید، تمام خطوط از بین می‌روند.

sudo mkdir -p /var/log/journal
sudo systemd-tmpfiles --create --prefix /var/log/journal
sudo systemctl restart systemd-journald
journalctl --list-boots

journalctl --list-boots که مقداری بیش از boot فعلی را نشان می‌دهد، به این معنی است که تاریخچه اکنون باقی می‌ماند، بنابراین journalctl -k -b -1 می‌تواند پیام‌های هسته از bootای که منجر به خرابی شده را به شما نشان دهد.

نقطه شروع برای یک VPS کوچک

در پلن‌های 2 GB، حدود 300 تا 400 MB را برای هسته (kernel) و page cache کنار بگذارید. اجازه ندهید مجموع سقف‌های تعیین‌شده به 2 GB کامل برسد، زیرا ممکن است تمام واحدها در یک لحظه به اوج مصرف خود برسند. بیشترین سهم را به سرویس حیاتی اختصاص دهید و سپس برای سایر سرویس‌های غیرضروری محدودیت اعمال کنید.

[Service]
MemoryHigh=256M
MemoryMax=384M
MemorySwapMax=0
CPUWeight=20
TasksMax=64
Restart=on-failure
RestartSec=5s

حفظ راه دسترسی به سرور ارزش یک تنظیم اضافه را دارد. استفاده از OOMScoreAdjust=-500 در یک فایل drop-in برای ssh.service باعث می‌شود OOM killer سراسری، کمتر احتمال داشته باشد که daemon مربوط به SSH شما را به عنوان قربانی انتخاب کند؛ این موضوع تفاوت بین رفع مشکل سرور و reboot کردن آن از طریق کنترل پنل است. این تنظیم فقط انتخاب هسته برای قربانی را تغییر می‌دهد و باعث کاهش زمان توقف (stall) نمی‌شود.

کانتینرها در cgroupهای مخصوص خود اجرا می‌شوند که توسط container runtime ایجاد شده‌اند، نه توسط unit fileهای شما؛ بنابراین محدودیت روی docker.service به محدودیت برای یک کانتینر تبدیل نمی‌شود. معادل‌های هر کانتینر برای MemoryMax= و CPUQuota= در تنظیم محدودیت حافظه و CPU در Docker Compose پوشش داده شده‌اند.

FAQ

چرا VPS من به‌جای کشتن پردازش سرکش، فریز شد؟

زیرا هسته (kernel) پیشرفت را بر اساس بازگشت صفحات (pages) توسط reclaim می‌سنجد، نه بر اساس مدت‌زمانی که این کار طول می‌کشد. هنگامی که حافظه کم است، هسته page cache را تخلیه می‌کند، که شامل صفحات اجرایی برنامه‌های در حال اجرا نیز می‌شود، و سپس آن‌ها را در دستور بعدی دوباره می‌خواند. همه چیز منتظر ذخیره‌سازی می‌ماند و از نظر فنی هیچ تخصیص حافظه‌ای شکست نخورده است، بنابراین OOM killer هرگز فراخوانی نمی‌شود. در حین وقوع این وضعیت، /proc/pressure/memory را بررسی کنید: مقدار full avg10 بالای 40 به این معنی است که تقریباً هیچ وظیفه‌ای در ده ثانیه گذشته فرصت اجرا پیدا نکرده است. یک دیمون فضای کاربری مانند earlyoom، پیش از آنکه سیستم به این وضعیت برسد، پردازش را می‌کشد.

تفاوت MemoryHigh و MemoryMax چیست؟

MemoryHigh= یک سقف نرم (soft cap) است که باعث محدودسازی (throttle) می‌شود. هسته به‌شدت از واحد مربوطه reclaim می‌کند و تخصیص‌های آن را کند می‌سازد، اما میزان مصرف می‌تواند از آن عدد فراتر رود و هیچ چیزی کشته نمی‌شود. MemoryMax= یک سقف سخت (hard cap) است: تخصیصی که نتواند تحت این سقف تأمین شود، OOM killer را در داخل همان cgroup واحد فراخوانی می‌کند، بنابراین پردازشی که مشکل ایجاد کرده است کشته می‌شود، نه بزرگ‌ترین پردازش روی کل سیستم. مقدار MemoryHigh= را کمتر از MemoryMax= تنظیم کنید و فاصله بین آن‌ها را به عنوان یک منطقه هشدار در نظر بگیرید.

چگونه بفهمم OOM killer کدام سرویس را هدف قرار داده است؟

دستور journalctl -k --grep "Killed process" --since "2 hours ago" را اجرا کنید. خطی که با Memory cgroup out of memory شروع می‌شود به این معنی است که یک واحد به MemoryMax= خود رسیده است، در حالی که یک Out of memory ساده به این معنی است که کل ماشین با کمبود حافظه مواجه شده است. سپس journalctl -u <unit> -n 50 را اجرا کرده و به دنبال Failed with result 'oom-kill' بگردید. اگر /var/log/journal روی سرور شما وجود ندارد، journal در RAM نگهداری می‌شده و شواهد با reboot از بین رفته‌اند؛ بنابراین پیش از حادثه بعدی، آن دایرکتوری را ایجاد کنید.

آیا باید به یک VPS کوچک swap اضافه کنم؟

یک فایل swap کوچک برای صفحات سرد (cold pages) که یک بار تخصیص داده شده و دیگر به آن‌ها دسترسی نمی‌شود، مفید است. این کار برای پردازش سرکش کمکی نمی‌کند: فقط کشتن پردازش را به تأخیر می‌اندازد و یک قطعی کوتاه را با یک توقف طولانی جایگزین می‌کند که حتی نمی‌توانید برای رفع آن وارد سیستم شوید. swap را در حد متوسط نگه دارید و MemorySwapMax=0 را روی واحدهایی که مایل به از دست دادنشان هستید تنظیم کنید تا آن‌ها به سقف خود برسند و سریعاً restart شوند، در حالی که سرویس‌های مهم swap خود را حفظ می‌کنند.

آیا می‌توانم یک دستور را بدون نوشتن فایل unit محدود کنم؟

بله. sudo systemd-run --scope -p MemoryMax=1G -p CPUQuota=50% ./script.sh دستور را در ترمینال شما درون یک scope موقت با همان محدودیت‌ها اجرا می‌کند و با خروج از آن، محدودیت‌ها از بین می‌روند. تمام ویژگی‌های systemd.resource-control پس از -p در دسترس هستند، بنابراین MemorySwapMax=، TasksMax= و CPUWeight= نیز در آنجا کار می‌کنند. برای اجرای کار در پس‌زمینه و ثبت خروجی در journal، --scope را حذف کرده و --unit=name را اضافه کنید.