איך להגביל זיכרון ומעבד לתהליך באמצעות systemd
תהליך מוגבל עדיין עלול לתקוע VPS. הגדירו MemoryHigh, MemoryMax, CPUQuota ו־TasksMax ביחידת systemd, ובדקו את הודעת 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 TasksMaxsystemctl 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 מקבל את החיבור, אך שורת הפקודה אינה מופיעה. המכונה פעילה ועסוקה, אך אף אחת מהפעולות האלה אינה מועילה.
כך המנגנון פועל, והוא אינו מובן מאליו. כאשר הזיכרון הפנוי הולך ואוזל, הליבה משחררת דפים במקום להקצות חדשים. הדפים הזולים ביותר לשחרור הם דפים מגובי־קבצים, ומטמון הדפים מחזיק את קוד ההרצה של כל התהליכים הפעילים. לכן הליבה מפנה את דפי הקוד של sshd, וההוראה הבאה ש־sshd מריץ גורמת ל־page fault, שמחייב לקרוא את הבתים האלה מחדש מהאחסון. בסופו של דבר, כל התהליכים ממתינים לדיסק במקום לרוץ. אותם דפים יוצאים וחוזרים שוב ושוב בלולאה, ותופעה זו נקראת thrashing.
שני גורמים מחמירים את המצב ב־VPS יותר מאשר במחשב נייד. האחסון מחובר לעיתים קרובות דרך הרשת או משותף, ולכן כל תקלה כזו עולה יותר אלפיות השנייה מאשר בהתקן NVMe מקומי. נוסף על כך, הליבה אינה מודדת זמן אלא כשל: כל עוד מנגנון שחרור הדפים מצליח להחזיר דף כלשהו, גם אם באיטיות, הליבה מניחה שיש התקדמות ואינה מפעילה את מחסל הזיכרון, כלומר את out of memory (OOM) killer. שרת יכול להישאר במצב הזה דקות רבות לפני שתהליך כלשהו מסתיים.
אפשר לצפות בתופעה בזמן שהיא מתרחשת. הליבה מייצאת מידע pressure stall information (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= עוברת throttling במקום להסתיים, ולכן היא נשארת פעילה ואיטית. שום דבר אינו מפעיל אותה מחדש, משום שמנקודת המבט של 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 ממוינות לפי שימוש בזיכרון. זו הדרך המהירה ביותר להבין "מה צורך את השרת הזה" בזמן שהוא עדיין מגיב. אם השרת חדש, יצירת החשבון והגדרת ה־firewall המפורטות ב־עשר הדקות הראשונות ב־VPS חדש קודמות לשלב הזה.
MemoryHigh מגביל קצב. MemoryMax הורג.
ההבדל בין שתי הגדרות הזיכרון קובע כיצד נראה כשל.
MemoryHigh=הוא גבול רך. מעליו, הליבה מפנה זיכרון באגרסיביות מתוך אותוcgroupומאטה בכוונה את הקצאות הזיכרון שלו. השימוש עדיין יכול לחרוג מהערך, ושום תהליך אינו נהרג.MemoryMax=הוא גבול קשיח. כאשר אי־אפשר לספק הקצאה במסגרת הגבול, מפעיל ה־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 שלה לפי חלון ההקשר שהגדרתם. לכן קראו את העלות של הגדלת num_ctx בזיכרון RAM לפני שתבחרו עבורה תקרת זיכרון.
לצד מגבלת זיכרון יש להגדיר גם מדיניות אתחול מחדש. אחרת ההרג ישאיר אתכם עם שירות שהופסק.
[Unit]
StartLimitIntervalSec=300
StartLimitBurst=5
[Service]
Restart=on-failure
RestartSec=5sStartLimit* שייך ל־[Unit], ו־Restart= שייך ל־[Service]. אם תציבו אחד מהם בחלק הלא נכון, systemd יתעלם ממנו. חמש הפעלות מחדש בתוך חמש דקות מעידות על דליפה ולא על תקלה רגעית. לכן לאחר מכן systemd מוותר ומשאיר את היחידה במצב כשל. זהו המצב שתרצו לאתר מאוחר יותר, במקום לולאת קריסות שמסתירה את הבעיה.
הגבלת CPU באמצעות CPUQuota, או חלוקת זמן באמצעות CPUWeight
CPUQuota= מקצה לתהליך אחוז מזמן המעבד הזמין ב־CPU אחד. CPUQuota=50% שווה למחצית מליבה אחת. CPUQuota=200% שקול לשתי ליבות, והיחידה יכולה לפזר את העומס ביניהן על פני מספר threads כרצונה. בתוכנית עם 2 vCPU, CPUQuota=200% מייצג את מלוא משאבי המכונה.
CPUWeight= הוא ברירת המחדל המתאימה יותר לרוב השירותים. זהו חלק יחסי בטווח שבין 1 ל־10000, וברירת המחדל של ה־kernel היא 100. ההגדרה משפיעה רק כאשר יש תחרות על המשאב: עבודת גיבוי עם CPUWeight=20 תפנה מקום ל־web server עם 100 תחת עומס, ועדיין תשתמש במלוא המכונה כאשר היא אינה עסוקה. מכסה קשיחה מבזבזת את הקיבולת הפנויה הזאת.
חשוב להגדיר ציפיות נכונות לגבי התועלת מהגבלת CPU. תהליך שתלוי ב־CPU כמעט לעולם אינו מקפיא את Linux, משום שה־scheduler ממשיך להקצות זמן לכל התהליכים. הזיכרון הוא הגורם שבדרך כלל משבית מכונה. השתמשו ב־CPUQuota= כאשר נדרשת תקרה צפויה, למשל ב־build או ב־agent שאחרת היה פועל בעומס מלא במשך שעה. התאמת המשאבים לעומס עבודה כזה היא שאלה נפרדת, והיא מוסברת ב־כמה RAM ו־CPU דרושים ל־VPS שמריץ coding agent.
אם ה־CPU נראה עסוק אף שאף אחד מהתהליכים שלכם אינו עושה עבודה רבה, ייתכן שהגורם נמצא בצד השני של ה־hypervisor. זהו זמן גניבת CPU משכן רועש, ושום quota שתגדירו לא ישנה זאת.
TasksMax מונע לולאת fork
TasksMax= הוא מספר התהליכים וה־threads שיחידה יכולה להחזיק. גם threads נספרים, לכן שירות Java או Go זקוק למרווח גדול יותר מכפי שרשימת התהליכים מרמזת. זו ההגנה הזולה ביותר מפני סקריפט שמבצע fork בלולאה, משום שה־fork נכשל בתוך היחידה במקום שהשרת יאזל ממזהי תהליכים.
TasksMax=128כאשר יחידה מגיעה למגבלה, ה־kernel רושם שורה שמציינת את ה־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, ה־kernel כותב לדיסק דפים אנונימיים שלא היו בשימוש לאחרונה, וקונה זמן. אם התהליך היה עומד להתייצב, ה־swap מציל את המצב. אם מדובר בתהליך שצורך זיכרון ללא הגבלה, ה־swap הופך השבתה של חמש שניות לקיפאון של עשרים דקות. הקיפאון גרוע יותר, משום שתהליך שקרס עדיין משאיר לכם shell פעיל, בעוד שמערכת שנמצאת ב־thrashing אינה עושה זאת.
swapon --show
free -hפתרון ביניים מעשי ב־VPS קטן הוא להשאיר קובץ swap בגודל מתון עבור דפים שהוקצו פעם אחת ולא ייעשה בהם שימוש נוסף, ולהגדיר MemorySwapMax=0 ביחידות שאפשר לאבד. השירותים החשובים שומרים על ה־swap שלהם. השירותים הבלתי צפויים מגיעים במהירות לתקרה ומופעלים מחדש.
הפחתת vm.swappiness היא מנוף חלש, וחשוב להבין מדוע. היא רק משנה את האיזון בין פינוי מטמון הדפים לבין העברת דפים אנונימיים ל־swap, ושתי הפעולות מחייבות קריאת דיסק בהמשך. היא משנה אילו דפים יגרמו ל־thrashing, אך לא אם המערכת תסבול מ־thrashing.
דמון OOM מוקדם מסיים תהליכים לפני התקיעה
ה־kernel ממתין עד שכשל reclaim מתרחש לחלוטין. ב־VPS קטן, ההמתנה הזאת היא בדיוק חלון הזמן שבו המכונה נעשית בלתי זמינה. שני daemons במרחב המשתמשים סוגרים את החלון הזה באמצעות ניטור עצמאי של הזיכרון וסיום תהליכים מוקדם יותר.
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
oomctloomctl מציג מה הוא מנטר כעת. לעיתים קרובות הוא אינו מנטר דבר ב־server image, משום שההגדרה מופעלת בנפרד לכל יחידה. בחרו daemon אחד והסתפקו בו. הפעלת שניהם גורמת לשני תהליכים להתחרות על בחירת הקורבן, ומקשה לשחזר את הסיבה לכל סיום תהליך.
איזו יחידה הייתה אחראית?
התחילו ב־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:0anon-rss הוא נפח הזיכרון שהתהליך החזיק ב־RAM כאשר הסתיים, כ־1.8 GB במקרה זה. קראו את השם שבסוגריים בזהירות. זהו התהליך שה־kernel בחר לסיים, אך ה־kernel בוחר את התהליך הגדול ביותר, ולא תמיד את התהליך שגרם למחסור.
הריגה עקב מגבלת 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, שאינו מפיק שורת log כלל:
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 12high סופר כמה פעמים היחידה חרגה מ־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 יכול להציג את הודעות ה־kernel מהאתחול שבו המערכת קרסה.
נקודת פתיחה עבור VPS קטן
בתוכנית עם 2 GB, השאירו 300 עד 400 MB עבור הליבה ו־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 הגלובלי יבחר ב־SSH daemon כקורבן שלו. זה ההבדל בין תיקון השרת לבין אתחול שלו מלוח הבקרה. ההגדרה משנה רק את בחירת הקורבן של הליבה. היא אינה מקצרת את זמן התקיעה.
מכולות פועלות ב־cgroups משלהן, שנוצרו על ידי runtime של המכולות ולא על ידי קובצי היחידות שלכם. לכן, מגבלה על docker.service אינה הופכת למגבלה על מכולה אחת. המקבילות לכל מכולה של MemoryMax= ושל CPUQuota= מוסברות ב־הגדרת מגבלות זיכרון ומעבד ב־Docker Compose.
FAQ
מדוע ה־VPS שלי קפא במקום לסיים את התהליך שיצא משליטה?
משום שהקרנל קובע אם יש התקדמות לפי השאלה אם פעולת reclaim מחזירה דפים, ולא לפי משך הזמן שהיא נמשכת. כאשר הזיכרון הזמין נמוך, הקרנל מפנה את page cache, כולל את דפי קובץ ההרצה של תוכניות פעילות, ולאחר מכן קורא אותם שוב בעת ביצוע ההוראה הבאה. הכול ממתין לאחסון, ואף הקצאת זיכרון לא נכשלה מבחינה טכנית. לכן ה־OOM killer אינו מופעל. בדקו את /proc/pressure/memory בזמן שהבעיה מתרחשת: ערך full avg10 הגבוה מ־40 מציין שכמעט אף משימה לא קיבלה זמן ריצה בעשר השניות האחרונות. daemon במרחב המשתמש, כגון earlyoom, מסיים תהליכים לפני שהשרת מגיע למצב הזה.
מה ההבדל בין MemoryHigh לבין MemoryMax?
MemoryHigh= הוא soft cap שמגביל את קצב העבודה. הקרנל מבצע reclaim אגרסיבי מה־unit ומאט את הקצאות הזיכרון שלו, אך השימוש יכול לחרוג מהערך ושום תהליך אינו מסיים. MemoryMax= הוא hard cap: הקצאה שלא ניתן לספק במסגרת המגבלה מפעילה את ה־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 והמידע אבד בעת האתחול. לכן צרו את התיקייה לפני התקלה הבאה.
האם כדאי להוסיף swap ל־VPS קטן?
קובץ swap קטן מסייע עבור דפים קרים שמוקצים פעם אחת ואינם נגישים שוב. הוא אינו מסייע לתהליך שיצא משליטה: הוא דוחה את סיום התהליך ומחליף השבתה קצרה בקיפאון ממושך, שבמהלכו אי־אפשר להתחבר כדי לתקן את הבעיה. השאירו את ה־swap בגודל מתון, והגדירו MemorySwapMax=0 ב־units שאפשר לאבד. כך הם יגיעו לתקרה שלהם ויופעלו מחדש במהירות, בעוד שהשירותים החשובים ימשיכו להשתמש ב־swap שלהם.
האם אפשר להגביל פקודה בלי לכתוב קובץ unit?
כן. sudo systemd-run --scope -p MemoryMax=1G -p CPUQuota=50% ./script.sh מריץ את הפקודה בטרמינל שלכם בתוך scope זמני עם המגבלות האלה, והמגבלות נעלמות כשהפקודה מסתיימת. כל property מתוך systemd.resource-control זמינה לאחר -p, ולכן גם MemorySwapMax=, TasksMax= ו־CPUWeight= פועלות שם. הסירו את --scope והוסיפו את --unit=name כדי להריץ את המשימה ברקע, כאשר הפלט שלה נכתב ל־journal.