SSD Nodes Learn Hosting plans →
गाइड Matt Connorलेखक: Matt Connor

systemd unit में CPU और Memory limit कैसे सेट करें

Linux VPS पर systemd के जरिए MemoryMax और CPUQuota सेट करके प्रोसेस को नियंत्रित करें। जानें कि कैसे OOM kill के बाद भी प्रोसेस सिस्टम को फ्रीज कर सकती है और उसे कैसे रोकें।

systemd drop-in के साथ process memory और CPU को सीमित करना

आप Linux VPS पर process की memory और CPU को उस unit में कुछ lines जोड़कर सीमित कर सकते हैं जो process को चलाती है। MemoryMax= memory पर एक hard ceiling है। CPUQuota= processor time पर एक ceiling है। दोनों को cgroup v2 (control groups, version 2) द्वारा लागू किया जाता है, जो कि kernel का वह feature है जिसे systemd पहले से ही box पर मौजूद हर service का हिसाब रखने के लिए उपयोग करता है।

sudo systemctl edit myapp.service

यह comments में निर्देशों के साथ एक drop-in file खोलता है। उनके ऊपर यह जोड़ें:

[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 को आपके numbers को kernel की अपनी units में वापस दोहराना चाहिए: MemoryMax=805306368 और CPUQuotaPerSecUSec=800ms। यदि यह MemoryMax=infinity print करता है, तो drop-in कभी load नहीं हुआ। जाँचें कि file /etc/systemd/system/myapp.service.d/override.conf पर स्थित है, और यह कि यह [Service] header के साथ शुरू होती है, क्योंकि बिना किसी section वाली settings line systemd को Assignment outside of section. Ignoring. log करने के लिए मजबूर करती है और service बिना किसी limit के start हो जाती है।

इस guide का बाकी हिस्सा यह है कि उन numbers को कैसे चुनें, और set होने के बाद भी क्या गलत हो सकता है।

रनअवे प्रोसेस VPS को फ्रीज क्यों कर देती है जबकि वह उसे भरती नहीं है

जब कोई प्रोसेस मेमोरी की हार्ड लिमिट तक पहुँचती है, तो वह लगभग एक सेकंड में समाप्त हो जाती है और सर्विस रीस्टार्ट हो जाती है। यह अच्छी स्थिति है। बुरी स्थिति वह है जहाँ कुछ भी समाप्त नहीं होता: मशीन ping का जवाब देती है, SSH कनेक्शन स्वीकार करता है, लेकिन shell prompt कभी नहीं आता। मशीन चालू और व्यस्त है, लेकिन वह कोई उपयोगी काम नहीं कर रही है।

इसका कारण यह है, क्योंकि यह स्पष्ट नहीं है। जब free memory कम हो जाती है, तो kernel नई pages देने के बजाय पुरानी pages को reclaim करने लगता है। सबसे सस्ती pages वे होती हैं जो file-backed होती हैं, और page cache में चल रहे हर प्रोग्राम का executable code होता है। इसलिए kernel sshd के text pages को हटा देता है, और अगली बार जब sshd कोई instruction चलाता है, तो एक page fault होता है जिसे उन bytes को storage से वापस पढ़ना पड़ता है। हर प्रोसेस चलने के बजाय disk पर प्रतीक्षा करने लगती है। वही pages एक लूप में बाहर जाती हैं और वापस आती हैं, जिसे thrashing कहा जाता है।

VPS पर लैपटॉप की तुलना में दो चीजें इसे और खराब बना देती हैं। Storage अक्सर network attached या shared होती है, इसलिए प्रत्येक fault में local NVMe डिवाइस की तुलना में अधिक milliseconds लगते हैं। और kernel समय नहीं मापता, वह विफलता मापता है: जब तक reclaim एक page वापस देता रहता है, चाहे वह कितना भी धीमा क्यों न हो, kernel को लगता है कि वह प्रगति कर रहा है और वह out of memory (OOM) killer को कॉल नहीं करता है। कोई भी चीज kill होने से पहले एक मशीन कई मिनटों तक उस स्थिति में रह सकती है।

आप इसे होते हुए देख सकते हैं। Linux 4.20 और उसके बाद के वर्ज़न पर kernel pressure stall information (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 का मतलब है कि पिछले दस सेकंड में, मशीन पर हर runnable task 48% समय मेमोरी कार्य की प्रतीक्षा में रुका हुआ था, इसलिए कुछ भी नहीं चला। एक स्वस्थ सर्वर full पर शून्य के करीब रहता है। 10 से ऊपर यह इंसान को धीमा महसूस होता है, और 40 या उससे अधिक वह स्थिति है जिसे लोग फ्रीज होना कहते हैं।

यही कारण है कि केवल एक limit लगा देना ही काफी नहीं है। MemoryHigh= के तहत रखी गई unit को kill करने के बजाय throttled कर दिया जाता है, इसलिए वह जीवित रहती है और धीमी बनी रहती है, और कोई भी उसे रीस्टार्ट नहीं करता क्योंकि systemd के नजरिए से वह कभी विफल नहीं हुई। एक capped unit जिसे swap करने की अनुमति है, वह ऐसे reads और writes उत्पन्न करती है जो उस unit के खाते में जाते हैं लेकिन एक shared डिवाइस द्वारा पूरे किए जाते हैं, इसलिए यह बॉक्स पर मौजूद हर दूसरी सर्विस के लिए /proc/pressure/io को बढ़ा सकती है। Limits यह तय करती हैं कि कमी की कीमत कौन चुकाएगा, वे क्षमता (capacity) पैदा नहीं कर सकतीं।

जाँचें कि आपका VPS cgroup v2 चला रहा है

stat -fc %T /sys/fs/cgroup

cgroup2fs एक एकीकृत पदानुक्रम (unified hierarchy) है, जिसकी आवश्यकता नीचे दी गई प्रत्येक सेटिंग के लिए होती है। tmpfs का अर्थ है कि सिस्टम पुराने v1 लेआउट के साथ बूट हुआ है, जहाँ MemoryHigh= और MemorySwapMax= मौजूद नहीं होते हैं और प्रति-यूनिट OOM व्यवहार अलग होता है। Ubuntu 22.04 और उसके बाद के संस्करण, तथा Debian 11 और उसके बाद के संस्करण, डिफ़ॉल्ट रूप से v2 का उपयोग करते हैं। एक पुरानी इमेज, या systemd.unified_cgroup_hierarchy=0 के साथ बूट किया गया कर्नेल, इसका उपयोग नहीं करता है।

cgroup v2 पर systemd डिफ़ॉल्ट रूप से प्रत्येक यूनिट के लिए मेमोरी अकाउंटिंग चालू कर देता है, इसलिए आंकड़े पहले से ही वहां मौजूद होते हैं:

systemd-cgtop -m

यह मेमोरी उपयोग के आधार पर cgroups को क्रमबद्ध करता है, जो यह जानने का सबसे तेज़ तरीका है कि "इस सर्वर की मेमोरी क्या खा रहा है" जबकि सिस्टम अभी भी प्रतिक्रिया दे रहा है। यदि सर्वर नया है, तो नए VPS पर पहले दस मिनट में किए जाने वाले अकाउंट और फायरवाल संबंधी कार्य इससे पहले आते हैं।

MemoryHigh थ्रॉटलिंग करता है। MemoryMax प्रोसेस को समाप्त (kill) कर देता है।

इन दो मेमोरी सेटिंग्स के बीच का अंतर यह तय करता है कि विफलता (failure) किस तरह की होगी।

  • MemoryHigh= एक सॉफ्ट कैप है। इसके ऊपर जाने पर कर्नल उस cgroup से आक्रामक रूप से मेमोरी वापस लेता है और जानबूझकर उसके एलोकेशन को धीमा कर देता है। उपयोग इस संख्या से आगे जा सकता है, और कोई भी प्रोसेस किल नहीं होती है।
  • MemoryMax= एक हार्ड कैप है। जब इसके अंतर्गत एलोकेशन पूरा नहीं हो पाता है, तो OOM killer उस cgroup के अंदर चलता है और उस यूनिट की अपनी किसी एक प्रोसेस को किल कर देता है।

यह दूसरा भाग ही वह मुख्य कारण है कि आपको उन सभी चीजों पर MemoryMax= सेट करना चाहिए जिन पर आप पूरी तरह भरोसा नहीं करते हैं। बिना किसी कैप के, मेमोरी की कमी पूरी मशीन की समस्या बन जाती है, और ग्लोबल OOM killer अपना शिकार oom_score के आधार पर चुनता है, जिसका अर्थ आमतौर पर सबसे बड़ी प्रोसेस होता है। सबसे बड़ी प्रोसेस अक्सर आपका डेटाबेस होता है, न कि वह स्क्रिप्ट जिसमें मेमोरी लीक हुई है। कैप के साथ, किल उसी यूनिट के अंदर होती है जिसने समस्या पैदा की है।

दोनों को सेट करें, जिसमें MemoryHigh= को MemoryMax= से लगभग 20 से 30 प्रतिशत नीचे रखें। यह अंतर एक चेतावनी क्षेत्र है: एक धीमा लीक High को पार करता है और एक ऐसी सर्विस के रूप में दिखाई देता है जो धीमी हो गई है, जबकि अचानक आया स्पाइक सीधे Max को पार कर जाता है और सर्विस बंद हो जाती है।

प्रतिशत मानों को इंस्टॉल की गई फिजिकल मेमोरी के आधार पर पढ़ा जाता है, इसलिए 4 GB के प्लान पर MemoryMax=25% का मतलब 1 GB है और प्लान का आकार बदलने के बाद भी यह बॉक्स का एक चौथाई ही रहता है। MemorySwapMax=0 उस यूनिट को पूरी तरह से स्वैप (swap) से बाहर रखता है, जो एक लंबी सुस्ती को एक तेज़ और स्पष्ट किल में बदल देता है।

कुछ सर्विसेज आपको मापने के बजाय पहले से ही उनकी खपत तय करने देती हैं: एक Ollama यूनिट आपके द्वारा दिए गए कॉन्टेक्स्ट विंडो से अपना KV कैश आकार निर्धारित करती है, इसलिए RAM में num_ctx बढ़ाने की लागत क्या है पढ़ें, इससे पहले कि आप किसी एक के लिए अधिकतम सीमा चुनें।

एक कैप के साथ रिस्टार्ट पॉलिसी का होना आवश्यक है, अन्यथा किल के बाद आपके पास केवल एक बंद सर्विस रह जाएगी।

[Unit]
StartLimitIntervalSec=300
StartLimitBurst=5

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

StartLimit* को [Unit] में और Restart= को [Service] में होना चाहिए। यदि आप किसी को गलत सेक्शन में रखते हैं, तो systemd उसे अनदेखा कर देता है। पांच मिनट में पांच रिस्टार्ट एक छोटी समस्या के बजाय एक लीक है, इसलिए उसके बाद systemd प्रयास करना छोड़ देता है और यूनिट को failed स्थिति में छोड़ देता है। यह वही स्थिति है जिसे आप बाद में देखना चाहेंगे, बजाय इसके कि एक क्रैश लूप समस्या को छिपाए रखे।

CPUQuota के साथ CPU को सीमित करें, या CPUWeight के साथ उसे साझा करें

CPUQuota= एक CPU पर उपलब्ध समय का प्रतिशत लेता है। CPUQuota=50% एक कोर का आधा हिस्सा है। CPUQuota=200% दो कोर के बराबर है, जिसे यूनिट अपनी इच्छा अनुसार कई थ्रेड्स में फैला सकती है। 2 vCPU वाले प्लान पर, CPUQuota=200% पूरी मशीन के बराबर है।

CPUWeight= अधिकांश सेवाओं के लिए बेहतर डिफ़ॉल्ट है। यह 1 से 10000 तक का एक सापेक्ष हिस्सा (relative share) है, और कर्नल का डिफ़ॉल्ट मान 100 है। यह केवल तब प्रभावी होता है जब संसाधन के लिए प्रतिस्पर्धा हो: लोड होने पर CPUWeight=20 पर चल रहा बैकअप जॉब 100 पर चल रहे वेब सर्वर को प्राथमिकता देता है, और जब सर्वर खाली (idle) होता है तो यह पूरे बॉक्स का उपयोग कर सकता है। एक हार्ड कोटा उस खाली क्षमता को व्यर्थ कर देता है।

ईमानदार रहें कि CPU लिमिट आपको क्या देती है। एक CPU-बाउंड प्रोसेस शायद ही कभी Linux को फ्रीज करती है, क्योंकि शेड्यूलर सभी को समय देता रहता है। मेमोरी वह चीज है जो बॉक्स को डाउन कर देती है। जब आप एक अनुमानित सीमा (predictable ceiling) चाहते हैं, तो CPUQuota= का उपयोग करें, उदाहरण के लिए किसी ऐसे बिल्ड या एजेंट पर जो अन्यथा एक घंटे तक पूरी क्षमता पर चलेगा। उस प्रकार के वर्कलोड का आकार निर्धारित करना अपने आप में एक अलग प्रश्न है, जिसे coding agent VPS को कितनी RAM और CPU की आवश्यकता होती है में कवर किया गया है।

यदि CPU व्यस्त दिखाई देता है जबकि आपकी कोई भी प्रोसेस ज्यादा काम नहीं कर रही है, तो इसका कारण हाइपरवाइजर के दूसरी तरफ हो सकता है। यह noisy neighbour से CPU steal time है, और आपके द्वारा सेट किया गया कोई भी कोटा इसे नहीं बदलेगा।

TasksMax एक fork loop को रोकता है

TasksMax= वह संख्या है जो एक unit में processes और threads की हो सकती है। Threads भी इसमें गिने जाते हैं, इसलिए Java या Go service को process list के अनुमान से अधिक headroom की आवश्यकता होती है। यह एक ऐसी script के खिलाफ सबसे सस्ता बचाव है जो loop में fork करती है, क्योंकि fork पूरी machine के process IDs खत्म होने के बजाय unit के भीतर ही विफल हो जाता है।

TasksMax=128

जब कोई unit इस limit तक पहुँचती है, तो kernel cgroup का नाम बताते हुए एक line log करता है:

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

Program स्वयं आमतौर पर fork: retry: Resource temporarily unavailable की रिपोर्ट करता है। यह जाँचें कि manager डिफ़ॉल्ट रूप से क्या लागू करता है, इसके लिए systemctl show -p DefaultTasksMax का उपयोग करें।

systemd-run के साथ एक बार चलने वाले जॉब को सीमित करना

इसके लिए आपको किसी unit file की आवश्यकता नहीं है। systemd-run एक कमांड के चारों ओर एक अस्थायी (transient) unit बनाता है।

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 प्रिंट करता है। आउटपुट आपकी स्क्रीन पर बना रहता है और कमांड के समाप्त होते ही सीमाएं (limits) हट जाती हैं। 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

जब आप root नहीं होते हैं, तब भी यही विकल्प --user के साथ काम करते हैं, हालाँकि आपके user manager के पास केवल वही controllers होते हैं जो उसे सौंपे (delegate) गए हैं, इसलिए वहाँ कोई प्रॉपर्टी अस्वीकार की जा सकती है। यदि ऐसा होता है, तो इसे sudo के साथ चलाएँ। जब किसी जॉब को स्थायी स्थान मिल जाता है, तो सेटिंग्स बिना किसी बदलाव के एक वास्तविक unit में चली जाती हैं: देखें running a script as a systemd service and timer।

Swap का सवाल, ईमानदारी से जवाब

Swap विफलता को रोकने के बजाय उसके स्वरूप को बदल देता है।

बिना swap के, memory leak होने पर सीमा तक पहुँचते ही कोई process तुरंत बंद हो जाती है। यह outage स्पष्ट, कम समय का होता है और बाद में journal में इसे पढ़ना आसान होता है। Swap के साथ, kernel कम इस्तेमाल होने वाले anonymous pages को disk पर लिख देता है और समय खरीद लेता है। यदि process का memory usage स्थिर होने वाला था, तो swap आपको बचा लेता है। यदि यह runaway process है, तो swap पांच सेकंड के outage को बीस मिनट के धीमेपन (stall) में बदल देता है। यह stall अधिक हानिकारक है, क्योंकि एक मृत process के बाद भी आपको working shell मिल जाता है, जबकि thrashing कर रहा server पूरी तरह अनुत्तरदायी हो जाता है।

swapon --show
free -h

एक छोटे VPS पर व्यावहारिक समाधान यह है: उन pages के लिए एक छोटा swap file रखें जो एक बार allocate होने के बाद दोबारा इस्तेमाल नहीं होते, और जिन units को आप खोने के लिए तैयार हैं उन पर MemorySwapMax=0 सेट करें। महत्वपूर्ण services अपना swap बनाए रखती हैं। अनिश्चित services जल्दी ही सीमा तक पहुँच कर restart हो जाती हैं।

vm.swappiness को कम करना एक कमजोर उपाय है, और यह जानना जरूरी है कि क्यों। यह केवल page cache को हटाने और anonymous pages को swap करने के बीच के संतुलन को बदलता है, और दोनों ही स्थितियों में बाद में disk read की लागत चुकानी पड़ती है। यह केवल यह बदलता है कि कौन से pages thrash होंगे, यह नहीं कि server thrash होगा या नहीं।

एक शुरुआती OOM डेमन स्टॉल से पहले ही प्रोसेस को समाप्त कर देता है

कर्नेल मेमोरी रिक्लेम (reclaim) के पूरी तरह विफल होने का इंतज़ार करता है, और एक छोटे VPS पर वह इंतज़ार ही वह सटीक समय है जब आप मशीन का एक्सेस खो देते हैं। दो यूजरस्पेस डेमन खुद मेमोरी पर नज़र रखकर और जल्दी प्रोसेस को समाप्त करके इस समस्या को हल करते हैं।

earlyoom उपलब्ध मेमोरी और फ्री स्वैप की प्रति सेकंड दस बार तक जाँच करता है, और केवल तभी सबसे अधिक oom_score वाली प्रोसेस को समाप्त करता है जब दोनों अपने थ्रेशोल्ड से नीचे गिर जाते हैं। बिना स्वैप वाले बॉक्स पर, केवल मेमोरी थ्रेशोल्ड ही निर्णय लेता है।

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 फ्री स्वैप का न्यूनतम स्तर, दोनों डिफ़ॉल्ट रूप से 10 प्रतिशत पर होते हैं। प्रत्येक जोड़ी में दूसरी संख्या SIGKILL पॉइंट है: earlyoom तब SIGTERM भेजता है जब मेमोरी और स्वैप दोनों अपने पहले मानों से नीचे गिर जाते हैं, फिर SIGKILL तब भेजता है जब दोनों अपने दूसरे मान से नीचे होते हैं, जो डिफ़ॉल्ट रूप से पहले का आधा होता है। --avoid और --prefer पैटर्न को बिना कोट्स और बिना स्पेस के रखें, क्योंकि systemd कोट्स को अक्षरशः पास करता है और एक कोटेड पैटर्न कभी मैच नहीं होता। sudo systemctl restart earlyoom के साथ बदलाव लागू करें, और यह देखने के लिए journalctl -u earlyoom पढ़ें कि इसने किस प्रोसेस को समाप्त किया और वह प्रोसेस कितनी मेमोरी ले रही थी।

systemd-oomd दूसरा विकल्प है। इसका मैनुअल पेज इसे "एक सिस्टम सर्विस के रूप में वर्णित करता है जो cgroups-v2 और प्रेशर स्टॉल इंफॉर्मेशन (PSI) का उपयोग करके कर्नेल स्पेस में OOM होने से पहले निगरानी करती है और सुधारात्मक कार्रवाई करती है"। यह एकल प्रोसेस के बजाय पूरे cgroups पर काम करता है, इसलिए यह एक यूनिट को समाप्त करता है, न कि किसी एक चाइल्ड प्रोसेस को। यूनिट्स ManagedOOMMemoryPressure=kill या ManagedOOMSwap=kill के साथ इसमें शामिल (opt-in) होती हैं, और थ्रेशोल्ड /etc/systemd/oomd.conf में स्थित होते हैं।

systemctl status systemd-oomd
oomctl

oomctl प्रिंट करता है कि यह वर्तमान में किसकी निगरानी कर रहा है, जो अक्सर सर्वर इमेज पर कुछ भी नहीं होता है, क्योंकि यह सेटिंग प्रति यूनिट ऑप्ट-इन है। एक डेमन चुनें और वहीं रुक जाएं। दोनों को एक साथ चलाने का मतलब है कि दो चीजें एक विक्टिम चुनने की होड़ में हैं, और किसी भी किल (kill) का कारण पता लगाना कठिन हो जाता है।

कौन सी unit जिम्मेदार थी?

kernel से शुरुआत करें, क्योंकि यह अपनी हर kill का रिकॉर्ड रखता है।

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

global OOM killer द्वारा की गई kill कुछ इस तरह दिखती है:

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 वह memory है जो process के मरने के समय RAM में थी, यहाँ लगभग 1.8 GB है। कोष्ठक में दिए गए नाम को संदेह की दृष्टि से देखें। यह वह victim है जिसे kernel ने चुना है, और kernel सबसे बड़ी process को चुनता है, जो हमेशा वह नहीं होती जिसने कमी पैदा की हो।

cgroup limit से होने वाली kill का prefix अलग होता है, और उसके ऊपर छपी रिपोर्ट उस 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

वह prefix ही निदान का मुख्य हिस्सा है। Memory cgroup out of memory का मतलब है कि एक unit अपनी MemoryMax= सीमा तक पहुँच गई थी और बाकी मशीन ठीक थी। एक साधारण Out of memory का मतलब है कि पूरी मशीन की 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 counters तीसरा स्रोत हैं, और एकमात्र ऐसा स्रोत जो throttling को रिकॉर्ड करता है, जिसके लिए कभी कोई log line नहीं बनती:

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 उन processes को गिनता है जो वास्तव में kill की गईं। oom_kill 0 के साथ एक बड़ा high पहले बताए गए silent case का उदाहरण है: service चल रही है, बहुत धीमी हो गई है, और उसने किसी को कोई failure रिपोर्ट नहीं किया है। memory.peak (Linux 5.19 और उससे नया) उस उच्चतम usage को दर्शाता है जिस तक cgroup पहुँचा था, और इसी संख्या के आधार पर MemoryMax= का आकार तय करना चाहिए। unit के restart होने पर दोनों files reset हो जाती हैं, क्योंकि systemd cgroup को फिर से बनाता है।

इन सबके नीचे एक पूर्व-शर्त है। यदि /var/log/journal मौजूद नहीं है, तो journal RAM में रहता है, और मशीन को recover करने के लिए किए गए reboot के बाद हर line मिट जाती है।

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 आपको उस boot के kernel messages दिखा सकता है जिसमें system crash हुआ था।

छोटे VPS के लिए एक शुरुआती बिंदु

2 GB के प्लान पर, kernel और page cache के लिए 300 से 400 MB जगह छोड़ें। सुनिश्चित करें कि सभी caps का योग कुल 2 GB से कम हो, क्योंकि हर unit का उपयोग एक ही समय पर चरम (peak) पर पहुँच सकता है। सबसे महत्वपूर्ण service को सबसे बड़ा हिस्सा दें, और उसके आसपास की अन्य speculative सेवाओं को सीमित रखें।

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

सर्वर का एक्सेस बनाए रखना एक अतिरिक्त सेटिंग के लायक है। ssh.service के लिए एक drop-in में OOMScoreAdjust=-500 सेट करने से global OOM killer द्वारा आपके SSH daemon को victim के रूप में चुने जाने की संभावना काफी कम हो जाती है। यही वह अंतर है जो सर्वर को ठीक करने और उसे control panel से reboot करने के बीच होता है। यह केवल kernel द्वारा victim चुनने की प्राथमिकता को बदलता है। यह stall की अवधि को कम नहीं करता है।

Containers अपने स्वयं के cgroups में चलते हैं, जिन्हें आपके unit files के बजाय container runtime द्वारा बनाया जाता है, इसलिए docker.service पर लगाई गई सीमा एक container की सीमा नहीं बनती है। MemoryMax= और CPUQuota= के प्रति-container विकल्प Docker Compose में memory और CPU सीमाएं सेट करना में बताए गए हैं।

FAQ

मेरा VPS runaway process को kill करने के बजाय freeze क्यों हो गया?

क्योंकि kernel यह तय करता है कि progress हो रही है या नहीं, यह देखकर कि क्या reclaim pages वापस कर रहा है, न कि इसमें कितना समय लग रहा है। जब memory कम होती है, तो यह page cache को हटा देता है, जिसमें चल रहे programs के executable pages भी शामिल होते हैं, और फिर अगली instruction पर उन्हें वापस read करता है। सब कुछ storage पर निर्भर हो जाता है और तकनीकी रूप से कोई allocation fail नहीं होता, इसलिए OOM killer कभी call नहीं होता। जब ऐसा हो रहा हो तो /proc/pressure/memory की जाँच करें: 40 से ऊपर का full avg10 मतलब है कि पिछले दस सेकंड में लगभग कोई task run नहीं हो पाया। earlyoom जैसा userspace daemon box के उस स्थिति में पहुँचने से पहले ही process को kill कर देता है।

MemoryHigh और MemoryMax के बीच क्या अंतर है?

MemoryHigh= एक soft cap है जो throttle करता है। kernel unit से सख्ती से memory reclaim करता है और उसके allocations को धीमा कर देता है, लेकिन usage उस संख्या से अधिक हो सकता है और कुछ भी kill नहीं होता। MemoryMax= एक hard cap है: इसके तहत जो allocation पूरा नहीं हो पाता, वह उस unit के अपने cgroup के अंदर OOM killer को invoke करता है, इसलिए जो process समस्या का कारण बनी, वही बंद होती है, न कि box पर चल रही सबसे बड़ी process। MemoryHigh= को MemoryMax= से नीचे सेट करें और उनके बीच के अंतर को warning zone के रूप में मानें।

मुझे कैसे पता चलेगा कि OOM killer ने किस service को hit किया?

journalctl -k --grep "Killed process" --since "2 hours ago" चलाएँ। Memory cgroup out of memory से शुरू होने वाली line का मतलब है कि एक unit ने अपना MemoryMax= hit किया, जबकि साधारण Out of memory का मतलब है कि पूरी machine की memory खत्म हो गई थी। फिर journalctl -u <unit> -n 50 चलाएँ और Failed with result 'oom-kill' खोजें। यदि आपके server पर /var/log/journal मौजूद नहीं है, तो journal RAM में था और reboot के साथ सबूत मिट गए, इसलिए अगली घटना से पहले वह directory बना लें।

क्या मुझे छोटे VPS में swap जोड़ना चाहिए?

एक छोटा swap file उन cold pages के लिए मददगार होता है जो एक बार allocate होते हैं और फिर कभी इस्तेमाल नहीं होते। यह runaway process के मामले में मदद नहीं करता: यह kill करने में देरी करता है और एक छोटे outage को एक लंबे stall में बदल देता है जिसे आप login करके ठीक भी नहीं कर सकते। swap को सीमित रखें, और जिन units को आप खोने के लिए तैयार हैं उन पर MemorySwapMax=0 सेट करें, ताकि वे अपनी सीमा तक पहुँचें और जल्दी restart हो जाएँ, जबकि महत्वपूर्ण services अपना swap बनाए रखें।

क्या मैं unit file लिखे बिना किसी command को limit कर सकता हूँ?

हाँ। sudo systemd-run --scope -p MemoryMax=1G -p CPUQuota=50% ./script.sh उस command को आपके terminal में उन limits के साथ एक transient scope के अंदर run करता है, और exit होने पर वे limits खत्म हो जाती हैं। systemd.resource-control की हर property -p के बाद उपलब्ध होती है, इसलिए MemorySwapMax=, TasksMax= और CPUWeight= वहाँ भी काम करते हैं। --scope छोड़ें और job को background में run करने के लिए --unit=name जोड़ें, जिसका output journal में दिखाई देगा।