تقييد ذاكرة العملية ووحدة المعالجة في systemd
اضبط MemoryHigh وMemoryMax وCPUQuota وTasksMax في وحدة systemd، وتعلّم لماذا قد يتجمّد VPS رغم بلوغ العملية حد الذاكرة، وكيف تقرأ OOM kill بعدها.
حدِّد ذاكرة العمليات ووحدة المعالجة المركزية باستخدام drop-in في systemd
تحدِّد ذاكرة العمليات ووحدة المعالجة المركزية على Linux VPS بإضافة بضعة أسطر إلى الوحدة التي تشغّل العملية. يحدّد MemoryMax= الحد الأقصى الصارم للذاكرة. ويحدّد CPUQuota= الحد الأقصى لوقت المعالج. يفرض cgroup v2 (مجموعات التحكم، الإصدار 2) هذين الحدّين. وهذه ميزة في النواة يستخدمها systemd مسبقاً لحساب موارد كل خدمة على الخادم.
sudo systemctl edit myapp.serviceيفتح ذلك ملف drop-in يحتوي على تعليمات داخل التعليقات. أضف ما يلي قبلها:
[Service]
MemoryHigh=512M
MemoryMax=768M
MemorySwapMax=0
CPUQuota=80%
TasksMax=128sudo 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. وتشغيل الخدمة من دون أي حدود.
يشرح باقي هذا الدليل كيفية اختيار هذه الأرقام، وما الذي قد يستمر في التعطل بعد تعيينها.
لماذا يجمّد process منفلت VPS من دون أن يستهلكه بالكامل
يموت process الذي يصل إلى حد صارم للذاكرة خلال نحو ثانية، ثم تعيد الخدمة تشغيل نفسها. هذه هي الحالة الجيدة. أما الحالة السيئة فهي التي لا يموت فيها شيء: يستجيب الخادم لـping، ويقبل SSH الاتصال، لكن لا يظهر prompt الصدفة أبداً. يكون الجهاز حياً ومشغولاً، ولا يكون أي من ذلك العمل مفيداً.
إليك الآلية، لأنها غير واضحة. عندما تنخفض الذاكرة الحرة، تستعيد kernel الصفحات بدلاً من تخصيص صفحات جديدة. وأسهل الصفحات استعادةً هي الصفحات المدعومة بالملفات، بينما يحتفظ page cache بشيفرة executable لكل ما هو قيد التشغيل. لذلك تطرد kernel صفحات النص الخاصة بـsshd، وتكون التعليمة التالية التي ينفذها sshd هي page fault يجب أن تقرأ تلك البايتات مجدداً من التخزين. وينتهي الأمر بكل process إلى انتظار القرص بدلاً من التنفيذ. تغادر الصفحات نفسها ثم تعود في حلقة، وتُسمى هذه الحالة thrashing.
هناك سببان يجعلان ذلك أسوأ على VPS منه على laptop. غالباً ما يكون التخزين متصلاً عبر الشبكة أو مشتركاً، لذلك يستغرق كل fault عدداً أكبر من الملليثواني مقارنةً بجهاز NVMe محلي. كما أن kernel لا تقيس الوقت، بل تقيس الفشل: ما دامت عملية reclaim تستعيد صفحة، مهما كان ذلك بطيئاً، تعتقد kernel أنها تحرز تقدماً، ولا تستدعي OOM killer. وقد يبقى الخادم في هذه الحالة دقائق كثيرة قبل قتل أي process.
يمكنك مراقبة حدوث ذلك. توفّر kernel معلومات pressure stall (PSI) على Linux 4.20 والإصدارات الأحدث:
cat /proc/pressure/memory
cat /proc/pressure/iosome 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 أنه خلال آخر عشر ثوانٍ، كانت كل task قابلة للتنفيذ على الخادم متوقفة بانتظار عمل متعلق بالذاكرة لمدة 48% من الوقت، ولذلك لم يُنفَّذ شيء. تكون قراءة الخادم السليم قريبة من الصفر في full. وعندما تتجاوز 10، يشعر الإنسان بأن الخادم بطيء، أما 40 أو أكثر فهي الحالة التي يصفها الناس بأنها متجمّدة.
ولهذا السبب لا يشكّل الحد وحده ضماناً. يجري تقييد الوحدة الواقعة تحت MemoryHigh= بدلاً من قتلها، فتظل حية وبطيئة، ولا يعيد systemd تشغيلها لأنه لا يعتبرها فاشلة. وإذا سُمح لوحدة محدودة باستخدام swap، فإنها تولّد عمليات قراءة وكتابة تُحتسب على تلك الوحدة، لكن جهازاً واحداً مشتركاً يخدمها، لذلك يمكنها رفع /proc/pressure/io لكل خدمة أخرى على الخادم. تحدد الحدود من يتحمل تكلفة النقص، لكنها لا تنشئ سعة.
تحقق من أن VPS يستخدم cgroup v2
stat -fc %T /sys/fs/cgroupcgroup2fs هو التسلسل الهرمي الموحّد، وهو ما تحتاج إليه كل الإعدادات أدناه. ويعني tmpfs أن الخادم أقلع باستخدام تخطيط v1 الأقدم، حيث لا يوجد MemoryHigh= ولا MemorySwapMax=، كما يختلف سلوك OOM لكل وحدة. تستخدم Ubuntu 22.04 والإصدارات الأحدث، وDebian 11 والإصدارات الأحدث، v2 افتراضياً. أما image قديمة أو kernel أقلع باستخدام systemd.unified_cgroup_hierarchy=0 فلا تستخدمه.
في cgroup v2، يفعّل systemd احتساب الذاكرة لكل وحدة افتراضياً، لذلك تكون القيم متاحة مسبقاً:
systemd-cgtop -mيعرض ذلك cgroups مرتبة حسب استخدام الذاكرة. وهذه أسرع طريقة لمعرفة «ما الذي يستهلك هذا الخادم» ما دام لا يزال قادراً على الاستجابة. إذا كان الخادم جديداً، فتنفيذ خطوات الدقائق العشر الأولى على VPS جديد يسبق هذه الخطوة.
MemoryHigh يحدّ من الاستخدام. وMemoryMax يقتل العملية.
يحدد الفرق بين إعدادَي الذاكرة شكل الفشل.
MemoryHigh=حدّ مرن. عند تجاوزه، يستعيد kernel الذاكرة بقوة من cgroup المعنية ويبطئ تخصيصاتها عمداً. يمكن أن يتجاوز الاستخدام هذه القيمة، ولا تُقتل أي عملية.MemoryMax=حدّ صارم. عندما يتعذر تلبية تخصيص للذاكرة ضمن هذا الحد، يعمل OOM killer داخل cgroup المعنية ويقتل إحدى عمليات تلك الوحدة.
هذا الجزء الثاني هو السبب الفعلي لتعيين MemoryMax= لأي شيء لا تثق به بالكامل. من دون حد، تصبح حالة نفاد الذاكرة مشكلة على مستوى الخادم كله، ويختار OOM killer العام ضحيته وفق oom_score، ما يعني غالباً اختيار العملية الأكبر. وعادةً تكون العملية الأكبر هي قاعدة البيانات، لا البرنامج النصي الذي تسبب في تسرب الذاكرة. مع وجود حد، تحدث عملية القتل داخل الوحدة التي تسببت في المشكلة.
عيّن القيمتين معاً، واجعل MemoryHigh= أقل من MemoryMax= بنحو 20 إلى 30 بالمئة. تمثل الفجوة منطقة تحذير: يتجاوز تسرب بطيء High ويظهر على شكل خدمة أصبحت بطيئة، بينما يتجاوز ارتفاع مفاجئ Max مباشرةً وتتوقف الخدمة.
تُقرأ قيم النسب المئوية استناداً إلى الذاكرة الفعلية المثبتة، لذلك تكون قيمة MemoryMax=25% على خطة بسعة 4 GB هي 1 GB، وتبقى ربع الخادم بعد تغيير حجم الخطة. ويمنع MemorySwapMax=0 هذه الوحدة من استخدام swap تماماً، ما يحوّل التباطؤ الطويل إلى عملية قتل سريعة وواضحة.
تتيح لك بعض الخدمات تحديد استهلاكها مسبقاً بدلاً من قياسه. تضبط وحدة Ollama حجم ذاكرة KV cache استناداً إلى نافذة السياق التي تحددها لها، لذلك اقرأ ما تكلفة رفع num_ctx على ذاكرة RAM قبل اختيار حد لها.
يحتاج الحد إلى سياسة إعادة تشغيل بجانبه، وإلا سيؤدي القتل إلى ترك الخدمة متوقفة فقط.
[Unit]
StartLimitIntervalSec=300
StartLimitBurst=5
[Service]
Restart=on-failure
RestartSec=5sينتمي StartLimit* إلى [Unit]، وينتمي Restart= إلى [Service]. إذا وضعت أيّاً منهما في القسم الخطأ، فسيتجاهله systemd. تمثل 5 عمليات إعادة تشغيل خلال 5 دقائق تسرباً لا حالة مؤقتة، لذلك يتوقف systemd عن المحاولة بعد ذلك ويترك الوحدة في حالة فشل. هذه هي الحالة التي تريد اكتشافها لاحقاً، بدلاً من حلقة أعطال متكررة تخفي المشكلة.
حدّد CPU باستخدام CPUQuota أو شاركه باستخدام CPUWeight
CPUQuota= يستهلك نسبة مئوية من الوقت المتاح على CPU واحد. CPUQuota=50% يساوي نصف نواة واحدة. CPUQuota=200% يعادل نواتين، ويمكن للوحدة توزيع الاستخدام عليهما عبر أي عدد تريده من سلاسل التنفيذ. في خطة تحتوي على 2 vCPU، يمثّل CPUQuota=200% كامل الجهاز.
يُعد CPUWeight= الخيار الافتراضي الأفضل لمعظم الخدمات. وهو حصة نسبية من 1 إلى 10000، والقيمة الافتراضية في النواة هي 100. لا يظهر أثره إلا عند تنافس العمليات: إذ تتراجع مهمة نسخ احتياطي عند CPUWeight=20 أمام خادم ويب بقيمة 100 تحت الحمل، لكنها تظل تستخدم الجهاز بالكامل عندما يكون الجهاز خاملاً. أما الحصة الصارمة فتتخلص من هذه السعة الخاملة.
كن دقيقاً بشأن الفائدة التي يحققها حد CPU. نادراً ما تؤدي العملية المقيدة بـCPU إلى تجميد Linux، لأن المجدول يواصل منح الوقت لجميع العمليات. الذاكرة هي ما قد يؤدي إلى توقف الجهاز. استخدم CPUQuota= عندما تريد سقفاً متوقعاً، مثلاً في عملية بناء أو وكيل قد يعمل بأقصى سرعة لمدة ساعة لولا ذلك. ويُعد تحديد موارد هذا النوع من أحمال العمل مسألة مستقلة، وقد غُطيت في مقدار RAM وCPU الذي يحتاج إليه coding agent على VPS.
إذا ظهر CPU مشغولاً بينما لا تنفذ أي من عملياتك عملاً كبيراً، فقد يكون السبب في الجانب الآخر من hypervisor. يُسمى ذلك وقت سرقة CPU بسبب جار صاخب، ولن يغيره أي حد للحصة تحدده.
يمنع TasksMax حلقة fork
TasksMax= هو عدد العمليات والخيوط التي يمكن للوحدة الاحتفاظ بها. تُحتسب الخيوط أيضاً، لذلك تحتاج خدمة Java أو Go إلى هامش أكبر مما توحي به قائمة العمليات. هذه أرخص وسيلة للحماية من script ينفّذ fork داخل حلقة، لأن fork يفشل داخل الوحدة بدلاً من نفاد معرّفات العمليات في الخادم.
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
لا تحتاج إلى ملف وحدة لاستخدام أي من ذلك. ينشئ systemd-run وحدة مؤقتة حول أمر واحد.
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 إذا حدث ذلك. عندما تحتاج المهمة إلى تشغيل دائم، انقل الإعدادات كما هي إلى وحدة فعلية: راجع تشغيل برنامج نصي كخدمة ومؤقت systemd.
الإجابة الصريحة عن swap
يغيّر swap شكل الفشل بدلاً من منعه.
عند عدم وجود swap، يصل تسرّب الذاكرة إلى الحد الأقصى، وتتوقف عملية ما خلال ثوانٍ. يكون الانقطاع واضحاً وقصيراً، ويسهل فهمه من journal لاحقاً. عند وجود swap، تكتب النواة الصفحات المجهولة غير النشطة إلى القرص، وتكسب وقتاً إضافياً. إذا كانت العملية ستستقر عند مستوى معين، فإن swap ينقذك. أما إذا كانت العملية خارجة عن السيطرة، فيحوّل swap انقطاعاً مدته خمس ثوانٍ إلى توقف بطيء مدته عشرون دقيقة. ويكون هذا التوقف أسوأ، لأن العملية المتوقفة تترك لك shell يعمل، بينما الجهاز الذي يعاني من thrashing لا يفعل ذلك.
swapon --show
free -hالحل العملي الأوسط على VPS صغير هو الاحتفاظ بملف swap متوسط الحجم للصفحات التي تُخصَّص مرة واحدة ولا تُستخدم بعد ذلك، وضبط MemorySwapMax=0 على الوحدات التي تقبل فقدانها. تحتفظ الخدمات المهمة بإمكانية استخدام swap. أما الخدمات غير المتوقعة فتصل سريعاً إلى الحد الأقصى ثم تُعاد تشغيلها.
يُعد خفض vm.swappiness أداة محدودة التأثير، ومن المفيد معرفة السبب. فهو يغيّر التوازن فقط بين إخراج page cache واستبدال الصفحات المجهولة، وكلا الخيارين يفرض قراءة من القرص لاحقاً. إنه يغيّر الصفحات التي تعاني من thrashing، لكنه لا يمنع الجهاز من معاناة thrashing.
برنامج OOM مبكر يقتل العمليات قبل حدوث التوقف
ينتظر kernel فشل عمليات استعادة الذاكرة بالكامل. وعلى VPS صغير، تمثل فترة الانتظار هذه النافذة التي تفقد فيها الخادم. يغلقها برنامجان يعملان في مساحة المستخدم، إذ يراقبان الذاكرة بنفسيهما ويقتلان العمليات في وقت أبكر.
يراقب 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 الخيار الآخر. تصفه صفحة الدليل بأنه "خدمة نظام تستخدم cgroups-v2 ومعلومات ضغط الانتظار (PSI) لمراقبة النظام واتخاذ إجراء تصحيحي قبل حدوث OOM في مساحة kernel". يعمل على cgroups كاملة بدلاً من العمليات المنفردة، ولذلك يقتل وحدة لا عملية فرعية عشوائية. يجب أن تشترك الوحدات صراحةً باستخدام ManagedOOMMemoryPressure=kill أو ManagedOOMSwap=kill، وتوجد الحدود في /etc/systemd/oomd.conf.
systemctl status systemd-oomd
oomctlيعرض oomctl ما يراقبه حالياً. وغالباً لا يراقب شيئاً في صورة الخادم، لأن الإعداد اختياري لكل وحدة. اختر برنامجاً واحداً وتوقف عنده. يؤدي تشغيل البرنامجين معاً إلى تنافسهما على اختيار الضحية، ويجعل تحديد سبب أي عملية قتل أكثر صعوبة.
أي وحدة كانت مسؤولة؟
ابدأ بالنواة، لأنها تسجّل كل عملية قتل تنفذها.
journalctl -k --grep "Killed process" --since "2 hours ago"تظهر عملية القتل التي ينفذها قاتل OOM العام بهذا الشكل:
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 هنا. اقرأ الاسم الموجود بين القوسين بحذر. فهذا هو الضحية التي اختارتها النواة، وتختار النواة أكبر process، وليس بالضرورة process الذي تسبب في النقص.
تُسبق عملية القتل الناتجة عن حد 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 50myapp.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.peaklow 0
high 4213
max 118
oom 12
oom_kill 12يحصي high عدد المرات التي تجاوزت فيها الوحدة MemoryHigh= وتعرضت لـthrottling. ويحصي max عدد المرات التي بلغت فيها الحد الصارم، بينما يحصي oom_kill عدد العمليات التي قُتلت فعلياً. تشير قيمة high الكبيرة مع oom_kill 0 إلى الحالة الصامتة المذكورة سابقاً: الخدمة تعمل، لكن أداءها تباطأ بشدة، ولم تبلغ أي جهة بحدوث فشل. يحتوي memory.peak (Linux 5.19 والإصدارات الأحدث) على أعلى استخدام بلغته cgroup، وهو الرقم الذي ينبغي أن تعتمد عليه في تحديد حجم MemoryMax=. يُعاد ضبط الملفين عند إعادة تشغيل الوحدة، لأن systemd ينشئ cgroup من جديد.
هناك متطلب أساسي يعتمد عليه كل ما سبق. إذا لم يكن /var/log/journal موجوداً، فسيكون journal في RAM، وستختفي كل الأسطر بعد إعادة التشغيل التي احتجت إليها لاستعادة الخادم.
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 بأكثر من قيمة الإقلاع الحالية أن السجل التاريخي أصبح محفوظاً، ولذلك يمكن لـjournalctl -k -b -1 عرض رسائل النواة من الإقلاع الذي توقف.
نقطة بداية لـ VPS صغير
في خطة بسعة 2 GB، اترك 300 إلى 400 MB للنواة وذاكرة الصفحات المؤقتة، ولا تجعل مجموع الحدود يصل إلى 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 العام في النواة خدمة SSH ضحيةً له، وهذا هو الفرق بين إصلاح الخادم وإعادة تشغيله من لوحة التحكم. لا يغيّر ذلك إلا اختيار النواة للعملية التي ستُنهى. ولا يقلل مدة التوقف.
تعمل الحاويات ضمن cgroups خاصة بها، ينشئها runtime الخاص بالحاويات بدلاً من ملفات الوحدات لديك، لذلك لا يتحول الحد المفروض على docker.service إلى حد لحاوية واحدة. وتغطي ضبط حدود الذاكرة ووحدة المعالجة المركزية في Docker Compose النظائر الخاصة بكل حاوية من MemoryMax= وCPUQuota=.
FAQ
لماذا تجمّد VPS بدلاً من إنهاء العملية الخارجة عن السيطرة؟
لأن kernel يقيّم التقدم وفقاً لما إذا كانت عملية الاسترداد تعيد الصفحات، وليس وفقاً للمدة التي تستغرقها. عندما تكون الذاكرة شحيحة، يطرد kernel ذاكرة التخزين المؤقت للصفحات، بما في ذلك صفحات الملفات التنفيذية للبرامج قيد التشغيل، ثم يقرأها مجدداً عند تنفيذ التعليمة التالية. ينتظر كل شيء عمليات التخزين، ولم يفشل أي تخصيص من الناحية التقنية، لذلك لا يُستدعى OOM killer أبداً. تحقّق من /proc/pressure/memory أثناء حدوث ذلك: تعني قيمة full avg10 التي تتجاوز 40 أن المهمة لم تحصل تقريباً على فرصة للتشغيل خلال الثواني العشر الأخيرة. ويمكن لخدمة userspace مثل earlyoom إنهاء العملية قبل وصول الخادم إلى هذه الحالة.
ما الفرق بين MemoryHigh وMemoryMax؟
MemoryHigh= هو حدّ لين يفرض الاختناق. يستعيد kernel الذاكرة بقوة من الوحدة ويبطئ عمليات التخصيص فيها، لكن الاستخدام قد يتجاوز الرقم ولا تُنهى أي عملية. أما MemoryMax= فهو حدّ صارم: إذا تعذر تلبية تخصيص ضمن هذا الحد، يُستدعى 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 موجوداً على خادمك، فقد احتُفظ بالسجل في الذاكرة RAM وفُقد الدليل مع إعادة التشغيل، لذلك أنشئ هذا الدليل قبل الحادثة التالية.
هل ينبغي أن أضيف swap إلى VPS صغير؟
يساعد ملف swap صغير في الصفحات غير النشطة التي تُخصَّص مرة واحدة ولا يُعاد الوصول إليها. لكنه لا يساعد في حالة عملية خارجة عن السيطرة؛ إذ يؤخر الإنهاء ويحوّل انقطاعاً قصيراً إلى توقف طويل لا يمكنك تسجيل الدخول أثناءه لإصلاحه. اجعل swap محدوداً، واضبط MemorySwapMax=0 على الوحدات التي يمكنك الاستغناء عنها، حتى تصل هذه الوحدات إلى سقفها وتُعاد تشغيلها بسرعة، بينما تحتفظ الخدمات المهمة بذاكرة swap الخاصة بها.
هل يمكنني تقييد أمر من دون كتابة ملف وحدة؟
نعم. يشغّل sudo systemd-run --scope -p MemoryMax=1G -p CPUQuota=50% ./script.sh الأمر في الطرفية داخل نطاق مؤقت بهذه الحدود، وتختفي الحدود عند انتهاء الأمر. تتوفر كل خاصية من systemd.resource-control بعد -p، ولذلك تعمل MemorySwapMax= وTasksMax= وCPUWeight= هناك أيضاً. احذف --scope وأضف --unit=name لتشغيل المهمة في الخلفية مع تسجيل مخرجاتها في السجل.