تحديد ذاكرة العملية والمعالج في systemd
تعلّم ضبط MemoryHigh وMemoryMax وCPUQuota وTasksMax عبر systemd، وتحقّق من الخطأ «Unit has a bad setting» بعد تجاوز الحد.
حدِّد ذاكرة العملية ووحدة المعالجة المركزية باستخدام 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 الصدفة أبداً. يكون الجهاز حياً ومشغولاً، ولا يكون أي من ذلك العمل مفيداً.
إليك الآلية، لأنها ليست واضحة. عندما تنخفض الذاكرة الحرة، تستعيد النواة الصفحات بدلاً من تخصيص صفحات جديدة. وأسهل الصفحات استعادةً هي الصفحات المدعومة بالملفات، بينما تحتوي page cache على الشيفرة التنفيذية لكل ما يعمل. لذلك تطرد النواة صفحات النص الخاصة بـsshd، ويكون التعليمة التالية التي ينفذها sshd عبارة عن page fault يجب أن يقرأ تلك البايتات مجدداً من التخزين. وينتهي بكل process الأمر إلى انتظار القرص بدلاً من التنفيذ. تغادر الصفحات نفسها ثم تعود في حلقة، ويُسمى ذلك thrashing.
هناك عاملان يجعلان هذه الحالة أسوأ على VPS مقارنةً بالحاسوب المحمول. غالباً ما يكون التخزين متصلاً عبر الشبكة أو مشتركاً، لذلك يستغرق كل page fault عدداً أكبر من الملليثواني مقارنةً بجهاز NVMe محلي. كما أن النواة لا تقيس الزمن، بل تقيس الفشل: ما دام reclaim يعيد صفحة، ولو ببطء، تعتقد النواة أنها تحرز تقدماً، ولا تستدعي OOM killer. وقد يبقى الخادم في هذه الحالة عدة دقائق قبل قتل أي process.
يمكنك مراقبة حدوث ذلك. توفّر النواة معلومات 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 أنه خلال آخر عشر ثوانٍ، كانت كل مهمة قابلة للتنفيذ على الخادم عالقة في انتظار أعمال الذاكرة لمدة 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 افتراضياً. أما الصورة القديمة، أو النواة التي أقلعت باستخدام systemd.unified_cgroup_hierarchy=0، فلا تستخدمه.
في نظام cgroup v2، يفعّل systemd محاسبة الذاكرة لكل وحدة افتراضياً، لذلك تكون القيم متاحة مسبقاً:
systemd-cgtop -mيسرد ذلك مجموعات cgroup مرتبة حسب استخدام الذاكرة. وهذه أسرع طريقة لمعرفة «ما الذي يستهلك موارد هذا الخادم» ما دام لا يزال يستجيب. إذا كان الخادم جديداً، فتنفيذ إعداد الحساب وجدار الحماية الوارد في الدقائق العشر الأولى على VPS جديد يسبق هذه الخطوة.
تؤدي MemoryHigh إلى تقييد الذاكرة. وتؤدي MemoryMax إلى إنهاء العملية.
يحدّد الفرق بين إعدادَي الذاكرة شكل الفشل.
MemoryHigh=حدّ ناعم. عند تجاوزه، يستعيد kernel الذاكرة بقوة من مجموعة cgroup المعنية، ويبطئ عمليات تخصيص الذاكرة فيها عمداً. يمكن أن يتجاوز الاستخدام هذه القيمة، ولا تُنهى أي عملية.MemoryMax=حدّ صارم. عندما يتعذر تلبية عملية تخصيص الذاكرة ضمن هذا الحد، يعمل قاتل نفاد الذاكرة OOM داخل مجموعة cgroup نفسها، وينهي إحدى عمليات الوحدة المعنية.
هذا الجزء الثاني هو السبب الفعلي لتعيين MemoryMax= لأي شيء لا تثق به بالكامل. من دون حد، تصبح حالة نفاد الذاكرة مشكلة على مستوى الخادم كله، ويختار قاتل نفاد الذاكرة العام العملية المستهدفة وفق oom_score، ما يعني غالباً اختيار أكبر عملية. وتكون أكبر عملية عادةً قاعدة البيانات، لا البرنامج النصي الذي تسبب في التسرب. عند تعيين حد، يحدث الإنهاء داخل الوحدة التي تسببت في المشكلة.
عيّن القيمتين معاً، واجعل MemoryHigh= أقل من MemoryMax= بنحو 20 إلى 30 بالمئة. تمثل الفجوة منطقة تحذير: يتجاوز التسرب البطيء High ويظهر على شكل خدمة أصبحت بطيئة، بينما يتجاوز الارتفاع المفاجئ Max مباشرةً فتُنهى العملية.
تُحتسب القيم المئوية من الذاكرة الفعلية المثبتة، لذلك تعادل MemoryMax=25% مقدار 1 GB في خطة بسعة 4 GB، وتبقى ربع الخادم بعد تغيير حجم الخطة. ويمنع MemorySwapMax=0 تلك الوحدة من استخدام swap تماماً، ما يحوّل التباطؤ الطويل إلى إنهاء سريع وواضح.
يحتاج الحد إلى سياسة إعادة تشغيل بجانبه، وإلا سيؤدي الإنهاء فقط إلى ترك الخدمة متوقفة.
[Unit]
StartLimitIntervalSec=300
StartLimitBurst=5
[Service]
Restart=on-failure
RestartSec=5sينتمي StartLimit* إلى [Unit]، وينتمي Restart= إلى [Service]. إذا وضعت أيّاً منهما في القسم الخطأ، فسيتجاهله systemd. تمثل 5 عمليات إعادة تشغيل خلال 5 دقائق تسرباً لا حالة عابرة، لذلك يتوقف systemd عن المحاولة بعد ذلك ويترك الوحدة في الحالة failed. وهذه هي الحالة التي تريد اكتشافها لاحقاً، بدلاً من حلقة أعطال وإعادة تشغيل تخفي المشكلة.
ضع حداً لاستخدام CPU باستخدام CPUQuota، أو شاركه باستخدام CPUWeight
CPUQuota= يحدد نسبة مئوية من الوقت المتاح على CPU واحد. CPUQuota=50% يعادل نصف نواة واحدة. CPUQuota=200% يعادل نواتين، ويمكن للوحدة توزيع هذا الاستخدام على أي عدد تريده من سلاسل التنفيذ. في خطة تحتوي على 2 vCPU، يعادل CPUQuota=200% كامل الجهاز.
يُعد CPUWeight= الخيار الافتراضي الأفضل لمعظم الخدمات. فهو يحدد حصة نسبية من 1 إلى 10000، والقيمة الافتراضية في kernel هي 100. ولا يظهر تأثيره إلا عند وجود تنافس: تتراجع مهمة نسخ احتياطي عند CPUWeight=20 أمام خادم ويب عند 100 أثناء الحمل، لكنها تظل تستخدم كامل الجهاز عندما يكون الجهاز خاملاً. أما الحصة الصارمة فتتخلص من هذه السعة الخاملة.
كن واضحاً بشأن ما يحققه حد CPU. نادراً ما تؤدي عملية مقيدة بـCPU إلى تجميد Linux، لأن المجدول يواصل منح الوقت لجميع العمليات. الذاكرة هي ما يتسبب في توقف الجهاز. استخدم CPUQuota= عندما تريد سقفاً متوقعاً، مثلاً في عملية build أو agent قد يعمل بكامل طاقته لمدة ساعة لولا ذلك. ويُعد تحديد حجم هذا النوع من أعباء العمل مسألة مستقلة، وقد غُطيت في مقدار RAM وCPU الذي يحتاج إليه coding agent على VPS.
إذا ظهر CPU مشغولاً بينما لا تنفذ عملياتك الكثير، فقد يكون السبب في الجانب الآخر من hypervisor. يُسمى ذلك وقت سرقة CPU بسبب جار مزعج، ولن يغيره أي حد تضعه للحصة.
يوقف TasksMax حلقة fork
TasksMax= هو عدد العمليات والخيوط التي يمكن للوحدة الاحتفاظ بها. تُحتسب الخيوط أيضاً، لذلك تحتاج خدمة Java أو Go إلى هامش أكبر مما توحي به قائمة العمليات. وهذه أرخص وسيلة لحماية النظام من برنامج نصي ينشئ عمليات في حلقة، لأن عملية 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، يصل تسرب الذاكرة إلى الحد الأقصى، وتتوقف إحدى العمليات خلال ثوانٍ. يكون الانقطاع واضحاً وقصيراً، ويسهل فهمه من السجل لاحقاً. مع swap، تكتب النواة الصفحات المجهولة الباردة إلى القرص، وتمنحك وقتاً إضافياً. إذا كانت العملية ستستقر عند مستوى معين، فإن swap ينقذك. أما إذا كانت العملية خارجة عن السيطرة، فإن swap يحوّل انقطاعاً مدته خمس ثوانٍ إلى توقف بطيء مدته عشرون دقيقة. ويكون هذا التوقف أسوأ، لأن العملية المتوقفة تترك لك على الأقل shell عاملاً، بينما الخادم الذي يعاني من thrashing لا يفعل ذلك.
swapon --show
free -hالحل العملي المتوسط على VPS صغير هو الاحتفاظ بملف swap بحجم معتدل للصفحات التي تُخصَّص مرة واحدة ولا تُستخدم مجدداً، وضبط MemorySwapMax=0 على الوحدات التي تقبل فقدانها. تحتفظ الخدمات المهمة بمساحة swap الخاصة بها. أما الخدمات غير المتوقعة فتصل سريعاً إلى الحد الأقصى ثم تعاد تشغيلها.
يُعد خفض vm.swappiness وسيلة محدودة التأثير، ومن المهم معرفة السبب. فهو يغيّر التوازن فقط بين إخلاء page cache ونقل الصفحات المجهولة إلى swap، وكلاهما يفرض قراءة من القرص لاحقاً. إنه يغيّر الصفحات التي تتعرض لـthrashing، لكنه لا يمنع الخادم من التعرض له.
يُنهي برنامج OOM المبكر العمليات قبل حدوث التوقف
ينتظر kernel حتى يفشل استرجاع الذاكرة بالكامل. وعلى 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 الخيار الآخر. تصفه صفحة الدليل بأنه "خدمة نظام تستخدم 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 الذاكرة التي كانت العملية تشغلها في RAM عند توقفها، وهي نحو 1.8 GB هنا. تعامل بحذر مع الاسم الموجود بين قوسين. هذه هي العملية التي اختارتها النواة لتقتلها. وتختار النواة العملية الأكبر، وهي ليست دائماً العملية التي تسببت في نقص الذاكرة.
تُضاف بادئة مختلفة إلى عملية القتل الناتجة عن حد 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 المصدر الثالث، وهي المصدر الوحيد الذي يسجّل الخنق، الذي لا ينتج أي سطر في السجل إطلاقاً:
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= وتعرضت للخنق. ويحصي 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 العام في النواة خادم SSH ليكون العملية التي ينهيها. وهذا هو الفارق بين إصلاح الخادم وإعادة تشغيله من لوحة التحكم. لا يغيّر ذلك إلا اختيار النواة للعملية التي ستُنهيها. ولا يقلل مدة التوقف.
تعمل الحاويات ضمن cgroups خاصة بها، ينشئها وقت تشغيل الحاويات بدلاً من ملفات الوحدات الخاصة بك. لذلك لا يتحول الحد المفروض على 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= فهو حد صارم: عندما يتعذر تلبية تخصيص ضمن هذا الحد، يستدعي kernel 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 في الذاكرة وفقدت الأدلة عند إعادة التشغيل، لذلك أنشئ هذا المجلد قبل وقوع الحادثة التالية.
هل ينبغي أن أضيف swap إلى VPS صغير؟
يساعد ملف swap صغير في التعامل مع الصفحات الباردة التي يتم تخصيصها مرة واحدة ولا يتم الوصول إليها مجدداً. لكنه لا يساعد في حالة العملية الخارجة عن السيطرة؛ إذ يؤخر الإنهاء ويستبدل انقطاعاً قصيراً بتوقف طويل لا يمكنك خلاله تسجيل الدخول لإصلاح المشكلة. أبقِ swap بحجم معتدل، واضبط MemorySwapMax=0 على الوحدات التي يمكنك تحمل فقدانها، حتى تصل هذه الوحدات إلى حدها الأقصى وتُعاد تشغيلها سريعاً، بينما تحتفظ الخدمات المهمة بمساحة swap الخاصة بها.
هل يمكنني تقييد أمر من دون كتابة ملف وحدة؟
نعم. يشغّل sudo systemd-run --scope -p MemoryMax=1G -p CPUQuota=50% ./script.sh الأمر في الطرفية داخل scope مؤقت مع تطبيق هذه الحدود، وتختفي الحدود عند انتهاء الأمر. تتوفر كل خاصية من systemd.resource-control بعد -p، لذلك تعمل MemorySwapMax= وTasksMax= وCPUWeight= هناك أيضاً. احذف --scope وأضف --unit=name لتشغيل المهمة في الخلفية مع تسجيل مخرجاتها في الـjournal.