SSD Nodes Learn 🎉 VPS از $5.50/ماه
راهنماها Matt Connorتوسط Matt Connor · به‌روزرسانی شده 2026-08-13

محدودکردن حافظه و CPU فرایند با systemd

با MemoryHigh، MemoryMax، CPUQuota و TasksMax منابع سرویس را در systemd محدود کنید؛ سپس خطای OOM kill و علت از کار افتادن VPS را دقیق بررسی کنید.

محدودکردن حافظه و CPU فرایند با drop-in در systemd

برای محدودکردن حافظه و CPU فرایند در یک Linux VPS، چند خط به unitای اضافه کنید که فرایند را اجرا می‌کند. MemoryMax= سقف سخت حافظه است. CPUQuota= سقف زمان پردازنده است. هر دو محدودیت را cgroup v2 (control groups، نسخه 2) اعمال می‌کند؛ این قابلیت kernel را systemd از قبل برای محاسبه مصرف هر سرویس در سرور استفاده می‌کند.

sudo systemctl edit myapp.service

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

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

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

چرا یک process سرکش، VPS را بدون پر کردن آن از کار می‌اندازد

processی که به hard memory cap می‌رسد، تقریباً پس از 1 ثانیه متوقف می‌شود و سرویس دوباره راه‌اندازی می‌شود. این حالت خوب است. حالت بد زمانی است که هیچ‌چیز متوقف نمی‌شود: سرور به ping پاسخ می‌دهد، SSH اتصال را می‌پذیرد، اما prompt شل هرگز نمایش داده نمی‌شود. ماشین زنده و مشغول است، اما هیچ‌کدام از این فعالیت‌ها مفید نیست.

سازوکار آن چنین است، چون در نگاه اول بدیهی نیست. وقتی حافظه آزاد کم می‌شود، kernel به‌جای اختصاص حافظه جدید، pageها را reclaim می‌کند. ارزان‌ترین pageها برای reclaim، pageهای file-backed هستند و page cache، کد اجرایی همه برنامه‌های در حال اجرا را نگه می‌دارد. بنابراین kernel، pageهای متن sshd را evict می‌کند و دستور بعدی که sshd اجرا می‌کند، page faultی است که باید آن بایت‌ها را دوباره از storage بخواند. در نتیجه، هر process به‌جای اجرا منتظر disk می‌ماند. همین pageها به‌صورت چرخه‌ای خارج و دوباره وارد می‌شوند؛ به این وضعیت thrashing گفته می‌شود.

دو عامل باعث می‌شوند این وضعیت در VPS از laptop بدتر باشد. storage اغلب به‌صورت network-attached یا shared است؛ بنابراین هر fault نسبت به یک دستگاه NVMe محلی، چند millisecond هزینه بیشتری دارد. همچنین kernel زمان را اندازه‌گیری نمی‌کند، بلکه failure را می‌سنجد: تا زمانی که reclaim بتواند، هرچند با سرعت کم، یک page را پس بگیرد، kernel تصور می‌کند در حال پیشرفت است و OOM killer را فعال نمی‌کند. یک سرور ممکن است چندین دقیقه در این وضعیت بماند، پیش از آن‌که هر چیزی kill شود.

می‌توانید این وضعیت را هنگام وقوع monitor کنید. kernel، اطلاعات pressure stall (PSI) را در Linux 4.20 و نسخه‌های جدیدتر export می‌کند:

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 یعنی در 10 ثانیه گذشته، در 48% از زمان، همه taskهای قابل‌اجرای سرور به‌دلیل عملیات حافظه stalled بوده‌اند؛ بنابراین هیچ‌چیز اجرا نشده است. در یک سرور سالم، مقدار full نزدیک به صفر است. مقدار بالاتر از 10 برای کاربر کند احساس می‌شود و مقدار 40 یا بیشتر همان وضعیتی است که افراد آن را frozen توصیف می‌کنند.

به همین دلیل، یک limit به‌تنهایی تضمین ایجاد نمی‌کند. یک unit که تحت MemoryHigh= قرار گرفته باشد، به‌جای kill شدن throttle می‌شود؛ بنابراین زنده اما کند باقی می‌ماند و چیزی آن را restart نمی‌کند، چون از دید systemd هرگز fail نشده است. یک unit محدودشده که همچنان اجازه swap دارد، read و writeهایی ایجاد می‌کند که به همان unit نسبت داده می‌شوند، اما یک device مشترک آن‌ها را سرویس می‌دهد؛ بنابراین می‌تواند مقدار /proc/pressure/io را برای همه سرویس‌های دیگر روی سرور افزایش دهد. limitها تعیین می‌کنند هزینه کمبود را چه کسی بپردازد، اما ظرفیت ایجاد نمی‌کنند.

بررسی کنید که VPS شما از cgroup v2 استفاده می‌کند

stat -fc %T /sys/fs/cgroup

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

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

systemd-cgtop -m

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

MemoryHigh باعث throttling می‌شود. MemoryMax باعث kill می‌شود.

تفاوت بین این دو تنظیم memory تعیین می‌کند failure چه شکلی داشته باشد.

  • MemoryHigh= یک soft cap است. پس از عبور مصرف از این مقدار، kernel از آن cgroup با شدت بیشتری حافظه reclaim می‌کند و عمداً allocationهای آن را کند می‌کند. مصرف همچنان می‌تواند از این مقدار بیشتر شود و هیچ فرایندی kill نمی‌شود.
  • MemoryMax= یک hard cap است. وقتی allocation دیگر نتواند در محدوده این مقدار تأمین شود، OOM killer در همان cgroup اجرا می‌شود و یکی از processهای متعلق به همان unit را kill می‌کند.

همین بخش دوم دلیل اصلی تنظیم MemoryMax= برای هر چیزی است که کاملاً به آن اعتماد ندارید. بدون cap، کمبود حافظه به مشکلی در کل سیستم تبدیل می‌شود و OOM killer سراسری قربانی را بر اساس oom_score انتخاب می‌کند؛ این معمولاً یعنی بزرگ‌ترین process. بزرگ‌ترین process معمولاً database شماست، نه scriptی که memory leak ایجاد کرده است. با وجود cap، kill داخل همان unitی رخ می‌دهد که مشکل را ایجاد کرده است.

هر دو مقدار را تنظیم کنید و MemoryHigh= را حدود 20 تا 30 درصد کمتر از MemoryMax= قرار دهید. این فاصله یک ناحیه هشدار است: یک leak آهسته از High عبور می‌کند و خود را به شکل کندشدن service نشان می‌دهد، اما یک spike ناگهانی مستقیماً از Max عبور می‌کند و باعث kill شدن service می‌شود.

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

یک cap باید در کنار آن restart policy نیز داشته باشد؛ در غیر این صورت kill فقط service را متوقف می‌کند.

[Unit]
StartLimitIntervalSec=300
StartLimitBurst=5

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

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

سقف مصرف CPU با CPUQuota یا اشتراک‌گذاری آن با CPUWeight

CPUQuota= درصدی از زمان در دسترس روی یک CPU را مصرف می‌کند. CPUQuota=50% معادل نصف یک core است. CPUQuota=200% معادل دو core است و unit می‌تواند آن را در هر تعداد thread که لازم است توزیع کند. در یک پلن 2 vCPU، CPUQuota=200% معادل کل ماشین است.

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

درباره مزیتی که محدودکردن CPU ایجاد می‌کند، واقع‌بین باشید. یک process محدودشده توسط CPU معمولاً Linux را متوقف نمی‌کند، چون scheduler همچنان زمان اجرا را در اختیار همه قرار می‌دهد. این memory است که باعث از کار افتادن ماشین می‌شود. زمانی از CPUQuota= استفاده کنید که سقفی قابل‌پیش‌بینی می‌خواهید؛ برای مثال، برای یک build یا agent که در غیر این صورت به‌مدت یک ساعت با تمام توان اجرا می‌شود. تعیین اندازه مناسب برای چنین workloadهایی موضوع جداگانه‌ای است که در مقدار RAM و CPU موردنیاز coding agent در VPS بررسی شده است.

اگر CPU در وضعیت busy نمایش داده می‌شود، درحالی‌که هیچ‌یک از processهای شما فعالیت زیادی ندارند، علت ممکن است در سمت دیگر hypervisor باشد. این وضعیت CPU steal time ناشی از noisy neighbour است و هیچ quotaای که تنظیم کنید آن را تغییر نمی‌دهد.

TasksMax حلقهٔ fork را متوقف می‌کند

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

TasksMax=128

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

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

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

محدود کردن یک job یک‌باره با systemd-run

برای استفاده از این قابلیت‌ها به فایل unit نیاز ندارید. systemd-run یک unit موقت را برای یک command منفرد ایجاد می‌کند.

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، command را در terminal شما اجرا می‌کند. خروجی روی صفحه باقی می‌ماند و محدودیت‌ها پس از پایان command از بین می‌روند. هر property از systemd.resource-control پس از -p قابل استفاده است.

برای یک job طولانی، --scope را حذف کنید و برای آن نام تعیین کنید. در این حالت job در پس‌زمینه و به‌صورت یک service موقت اجرا می‌شود و logها در journal ثبت می‌شوند:

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

همین optionها را می‌توان با --user نیز زمانی که root نیستید استفاده کرد؛ بااین‌حال، user manager شما فقط به controllerهایی دسترسی دارد که به آن واگذار شده‌اند، بنابراین ممکن است یک property در آنجا رد شود. اگر چنین شد، آن را با sudo اجرا کنید. وقتی یک job به محل دائمی خود منتقل شد، تنظیمات بدون تغییر به یک unit واقعی منتقل می‌شوند: اجرای یک script به‌عنوان service و timer در systemd مراجعه کنید.

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

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

بدون swap، نشت حافظه به سقف می‌رسد و ظرف چند ثانیه یک فرایند از کار می‌افتد. قطعی سرویس شدید و کوتاه است و بررسی آن در journal بعداً آسان خواهد بود. با swap، kernel صفحه‌های anonymous کم‌استفاده را روی دیسک می‌نویسد و زمان بیشتری فراهم می‌کند. اگر فرایند قرار باشد در یک سطح ثابت متوقف شود، swap شما را نجات می‌دهد. اما اگر فرایند runaway باشد، swap یک قطعی 5 ثانیه‌ای را به توقفی 20 دقیقه‌ای تبدیل می‌کند. این توقف بدتر است، چون یک فرایند ازکارافتاده همچنان shell قابل‌استفاده را برای شما باقی می‌گذارد، اما یک سیستم درگیر thrashing چنین امکانی نمی‌دهد.

swapon --show
free -h

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

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

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

kernel منتظر می‌ماند تا reclaim کاملاً شکست بخورد. در یک VPS کوچک، همین زمان دقیقاً همان فرصتی است که ممکن است در آن دسترسی به ماشین را از دست بدهید. دو daemon در فضای کاربر با پایش مستقیم memory و توقف زودتر فرایندها، این فاصله را کاهش می‌دهند.

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

sudo apt install earlyoom
systemctl status earlyoom

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

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

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

systemd-oomd گزینهٔ دیگر است. صفحهٔ راهنمای آن را چنین توصیف می‌کند: «یک system service که از cgroups-v2 و pressure stall information (PSI) برای پایش و انجام اقدام اصلاحی پیش از رخ‌دادن OOM در kernel space استفاده می‌کند.» این ابزار به‌جای فرایندهای منفرد، روی cgroupهای کامل عمل می‌کند؛ بنابراین یک unit را متوقف می‌کند، نه یک child سرگردان را. Unitها با ManagedOOMMemoryPressure=kill یا ManagedOOMSwap=kill به‌صورت opt-in فعال می‌شوند و thresholdها در /etc/systemd/oomd.conf قرار دارند.

systemctl status systemd-oomd
oomctl

oomctl مواردی را که اکنون پایش می‌کند نمایش می‌دهد. در یک server image معمولاً چیزی نمایش داده نمی‌شود، زیرا این تنظیم برای هر unit به‌صورت opt-in انجام می‌شود. فقط یکی از این daemonها را انتخاب کنید و به همان اکتفا کنید. اجرای هر دو باعث می‌شود دو ابزار هم‌زمان برای انتخاب قربانی رقابت کنند و بازسازی علت هر kill دشوارتر شود.

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

از kernel شروع کنید، چون تمام killهایی را که انجام می‌دهد ثبت می‌کند.

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

خروجی یک kill از global 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 مقدار حافظه‌ای است که آن process هنگام مرگ در RAM در اختیار داشت؛ در اینجا حدود 1.8 GB. نام داخل براکت را با احتیاط تفسیر کنید. این همان victimای است که kernel انتخاب کرده است. kernel بزرگ‌ترین process را انتخاب می‌کند، اما این process همیشه عامل ایجاد کمبود نیست.

خروجی یک kill ناشی از محدودیت 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 یعنی یک unit به MemoryMax=ای که برای آن تعیین کرده‌اید رسیده و وضعیت بقیه سیستم عادی بوده است. Out of memory ساده یعنی کل machine با کمبود حافظه مواجه شده است؛ بنابراین capها یا تعریف نشده‌اند یا در مجموع بیش از حد بزرگ‌اند.

سپس از 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) نشان می‌دهد.

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

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 تعداد دفعاتی را می‌شمارد که unit از MemoryHigh= عبور کرده و throttled شده است. max تعداد دفعات رسیدن به hard cap را می‌شمارد و oom_kill تعداد processهایی را می‌شمارد که واقعاً kill شده‌اند. مقدار زیاد high همراه با oom_kill 0 همان حالت بی‌صدای بخش قبل است: service در حال اجراست، سرعت آن به‌شدت کاهش یافته و هیچ failureای را به کسی گزارش نکرده است. memory.peak (در Linux 5.19 و جدیدتر) بیشترین مصرفی را نگه می‌دارد که cgroup به آن رسیده است؛ این همان عددی است که باید MemoryMax= را بر اساس آن تعیین کنید. هر دو file هنگام restart شدن unit reset می‌شوند، چون systemd cgroup را دوباره ایجاد می‌کند.

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

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 فعلی یعنی history اکنون باقی می‌ماند؛ بنابراین journalctl -k -b -1 می‌تواند messageهای kernel را از bootای که با failure مواجه شده است نشان دهد.

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

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

[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 کردن آن از control panel است. این تنظیم فقط انتخاب قربانی توسط kernel را تغییر می‌دهد و مدت stall را کوتاه نمی‌کند.

Containerها در cgroupهای مستقل خود اجرا می‌شوند. این cgroupها را container runtime ایجاد می‌کند، نه فایل‌های unit شما. بنابراین، محدودیت روی docker.service به محدودیت برای یک container تبدیل نمی‌شود. معادل‌های per-container برای MemoryMax= و CPUQuota= در تنظیم محدودیت حافظه و CPU در Docker Compose توضیح داده شده‌اند.

FAQ

چرا VPS من به‌جای متوقف‌کردن فرایند خارج از کنترل، هنگ کرد؟

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

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

MemoryHigh= یک سقف نرم است که باعث throttling می‌شود. kernel از unit به‌صورت اجباری reclaim انجام می‌دهد و allocationهای آن را کند می‌کند؛ اما مصرف می‌تواند از این مقدار بیشتر شود و هیچ فرایندی نیز متوقف نمی‌شود. MemoryMax= یک سقف سخت است: اگر allocation در محدوده آن تأمین نشود، OOM killer در cgroup خود unit فراخوانی می‌شود. در نتیجه، فرایندی که مشکل را ایجاد کرده است متوقف می‌شود، نه بزرگ‌ترین فرایند روی سیستم. مقدار MemoryHigh= را کمتر از MemoryMax= تنظیم کنید و فاصله میان این دو را ناحیه هشدار در نظر بگیرید.

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

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

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

یک فایل swap کوچک برای pageهای سردی مفید است که یک‌بار allocation می‌شوند و دیگر مورد استفاده قرار نمی‌گیرند. برای فرایند خارج از کنترل مفید نیست. swap فقط توقف فرایند را به تأخیر می‌اندازد و یک قطعی کوتاه را به stall طولانی تبدیل می‌کند؛ در این مدت نمی‌توانید برای رفع مشکل login کنید. مقدار swap را محدود نگه دارید و MemorySwapMax=0 را روی unitهایی تنظیم کنید که از دست‌دادن آن‌ها قابل‌قبول است. به این ترتیب، این unitها به سقف خود می‌رسند و سریع restart می‌شوند، در حالی که سرویس‌های مهم swap خود را حفظ می‌کنند.

آیا می‌توانم یک command را بدون نوشتن unit file محدود کنم؟

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