วิธีจำกัดการใช้ CPU และ RAM ของ Service ด้วย systemd
เรียนรู้วิธีตั้งค่า MemoryHigh, MemoryMax, CPUQuota และ TasksMax ใน systemd unit เพื่อป้องกันโพรเซสใช้ทรัพยากรเกินกำหนด พร้อมวิธีตรวจสอบการตั้งค่าผ่าน cgroup v2 อย่างถูกต้อง
การจำกัดหน่วยความจำและ CPU ของโพรเซสด้วย systemd drop-in
คุณสามารถจำกัดหน่วยความจำและ CPU ของโพรเซสบน Linux VPS ได้โดยการเพิ่มบรรทัดคำสั่งลงใน unit ที่รันโพรเซสนั้น MemoryMax= คือเพดานสูงสุดของหน่วยความจำ ส่วน CPUQuota= คือเพดานของเวลาประมวลผล ทั้งสองค่าถูกบังคับใช้โดย cgroup v2 (control groups เวอร์ชัน 2) ซึ่งเป็นฟีเจอร์ของ kernel ที่ systemd ใช้งานอยู่แล้วเพื่อจัดการทรัพยากรของทุก service บนเครื่อง
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 จะต้องแสดงตัวเลขที่คุณตั้งค่าไว้กลับมาในหน่วยของ kernel เอง นั่นคือ MemoryMax=805306368 และ CPUQuotaPerSecUSec=800ms หากระบบแสดงผลเป็น MemoryMax=infinity แสดงว่า drop-in ไม่ได้ถูกโหลด ให้ตรวจสอบว่าไฟล์ถูกสร้างไว้ที่ /etc/systemd/system/myapp.service.d/override.conf และขึ้นต้นด้วย header [Service] เนื่องจากหากมีบรรทัดการตั้งค่าโดยไม่มีส่วนหัวกำกับ systemd จะบันทึก log เป็น Assignment outside of section. Ignoring. และเริ่ม service โดยไม่มีการจำกัดใดๆ เลย
เนื้อหาที่เหลือของคู่มือนี้จะอธิบายวิธีการเลือกตัวเลขเหล่านี้ และปัญหาที่อาจเกิดขึ้นได้หลังจากตั้งค่าเสร็จสิ้นแล้ว
เหตุใดกระบวนการที่ทำงานค้างจึงทำให้ VPS หยุดชะงักแม้หน่วยความจำจะไม่เต็ม
กระบวนการที่ใช้หน่วยความจำถึงขีดจำกัดสูงสุดมักจะหยุดทำงานภายในเวลาประมาณหนึ่งวินาทีและบริการจะเริ่มทำงานใหม่ นี่คือกรณีปกติ แต่กรณีที่เลวร้ายคือสถานการณ์ที่ไม่มีอะไรหยุดทำงาน: เครื่องตอบสนองต่อ ping, SSH ยอมรับการเชื่อมต่อ แต่กลับไม่แสดง shell prompt เครื่องยังคงทำงานอยู่แต่ไม่มีงานใดที่เป็นประโยชน์เกิดขึ้น
กลไกที่เกิดขึ้นมีดังนี้ เนื่องจากไม่ใช่เรื่องที่สังเกตได้ชัดเจน เมื่อหน่วยความจำว่างเหลือน้อย เคอร์เนลจะเรียกคืนหน้าหน่วยความจำ (page) แทนการจัดสรรหน้าใหม่ หน้าหน่วยความจำที่เรียกคืนได้ง่ายที่สุดคือหน้าที่มีไฟล์รองรับ (file-backed) และ page cache จะเก็บรหัสคำสั่งของทุกโปรแกรมที่กำลังทำงานอยู่ ดังนั้นเคอร์เนลจึงขับหน้าหน่วยความจำส่วนที่เป็นรหัสคำสั่งของ sshd ออกไป และคำสั่งถัดไปที่ sshd จะเรียกใช้จะทำให้เกิด page fault ซึ่งต้องอ่านข้อมูลเหล่านั้นกลับมาจากหน่วยเก็บข้อมูล ทุกกระบวนการจึงต้องรอการทำงานของดิสก์แทนที่จะได้ประมวลผล หน้าหน่วยความจำเหล่านี้ถูกขับออกและอ่านกลับเข้ามาซ้ำๆ ซึ่งเรียกว่าภาวะ thrashing
มีสองปัจจัยที่ทำให้สถานการณ์นี้บน VPS แย่กว่าบนแล็ปท็อป: หน่วยเก็บข้อมูลมักเป็นแบบ network attached หรือใช้งานร่วมกัน ดังนั้นแต่ละ fault จึงใช้เวลาหลายมิลลิวินาทีนานกว่าอุปกรณ์ 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% ของเวลาทั้งหมด งานที่พร้อมทำงาน (runnable task) ทุกงานบนเครื่องต้องหยุดชะงักเพื่อรอการจัดการหน่วยความจำ ทำให้ไม่มีงานใดได้ประมวลผล เซิร์ฟเวอร์ที่มีสถานะปกติจะมีค่าบน full ใกล้เคียงศูนย์ หากค่าสูงกว่า 10 ผู้ใช้งานจะรู้สึกว่าระบบช้า และหากค่าเป็น 40 หรือมากกว่า จะเป็นสถานะที่ผู้ใช้มักเรียกว่าเครื่องค้าง
นี่คือเหตุผลว่าทำไมการจำกัดทรัพยากรเพียงอย่างเดียวจึงไม่ใช่การรับประกันเสมอไป หน่วยที่ถูกจำกัดภายใต้ MemoryHigh= จะถูกลดความเร็วลงแทนที่จะถูกสั่งหยุดทำงาน ดังนั้นมันจึงยังคงทำงานอยู่แต่ช้าลง และไม่มีอะไรสั่งให้มันเริ่มทำงานใหม่เพราะในมุมมองของ systemd มันไม่เคยล้มเหลว หน่วยที่ถูกจำกัดซึ่งยังได้รับอนุญาตให้ใช้ swap จะสร้างการอ่านและเขียนข้อมูลที่ถูกนับรวมในหน่วยนั้น แต่ใช้ทรัพยากรจากอุปกรณ์ที่แชร์ร่วมกัน จึงสามารถผลักดันค่า /proc/pressure/io ให้สูงขึ้นจนส่งผลกระทบต่อบริการอื่นทั้งหมดบนเครื่อง การจำกัดทรัพยากรเป็นเพียงการตัดสินว่าใครจะเป็นผู้รับภาระเมื่อเกิดการขาดแคลน แต่ไม่สามารถสร้างความจุเพิ่มขึ้นได้
ตรวจสอบว่า VPS ของคุณรัน cgroup v2
stat -fc %T /sys/fs/cgroupcgroup2fs คือลำดับชั้นแบบรวม (unified hierarchy) ซึ่งเป็นสิ่งที่การตั้งค่าทุกอย่างด้านล่างนี้จำเป็นต้องใช้ tmpfs หมายความว่าเครื่องบูตด้วยโครงสร้างแบบ v1 รุ่นเก่า ซึ่งไม่มี MemoryHigh= และ MemorySwapMax= อยู่ และพฤติกรรม OOM ของแต่ละ unit จะแตกต่างออกไป Ubuntu 22.04 ขึ้นไป และ Debian 11 ขึ้นไป ใช้ v2 เป็นค่าเริ่มต้น หากเป็นอิมเมจเก่าหรือเคอร์เนลที่บูตด้วย systemd.unified_cgroup_hierarchy=0 จะไม่ใช้ v2
บน cgroup v2 ระบบ systemd จะเปิดใช้งานการนับหน่วยความจำ (memory accounting) สำหรับทุก unit โดยอัตโนมัติ ดังนั้นตัวเลขต่างๆ จึงมีอยู่แล้ว:
systemd-cgtop -mคำสั่งนี้จะแสดงรายการ cgroups โดยเรียงลำดับตามการใช้งานหน่วยความจำ ซึ่งเป็นวิธีที่เร็วที่สุดในการตอบคำถามว่า "อะไรกำลังกินทรัพยากรเครื่องนี้อยู่" ในขณะที่ระบบยังคงตอบสนองได้ หากเป็นเซิร์ฟเวอร์ใหม่ งานด้านบัญชีผู้ใช้และไฟร์วอลล์ใน สิบนาทีแรกบน VPS ใหม่ ควรทำก่อนขั้นตอนนี้
ความแตกต่างระหว่าง MemoryHigh และ MemoryMax
ความแตกต่างระหว่างการตั้งค่าหน่วยความจำทั้งสองแบบนี้เป็นตัวกำหนดลักษณะของความล้มเหลวที่เกิดขึ้น
MemoryHigh=เป็นขีดจำกัดแบบผ่อนปรน (soft cap) เมื่อการใช้งานเกินค่านี้ เคอร์เนลจะพยายามเรียกคืนหน่วยความจำจาก cgroup นั้นอย่างจริงจังและจงใจทำให้การจัดสรรหน่วยความจำช้าลง การใช้งานจริงยังคงเกินค่านี้ได้และไม่มีกระบวนการใดถูกสั่งยุติการทำงานMemoryMax=เป็นขีดจำกัดแบบเข้มงวด (hard cap) เมื่อไม่สามารถจัดสรรหน่วยความจำภายใต้ขีดจำกัดนี้ได้ 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 ตาม context window ที่คุณระบุ ดังนั้นโปรดอ่าน ต้นทุนการเพิ่ม num_ctx ต่อการใช้ RAM ก่อนที่คุณจะเลือกเพดานหน่วยความจำสำหรับบริการนั้น
การกำหนดขีดจำกัดจำเป็นต้องมีนโยบายการรีสตาร์ทควบคู่กันไป มิฉะนั้นเมื่อบริการถูกสั่งยุติการทำงาน คุณจะเหลือเพียงบริการที่หยุดทำงานไปเฉยๆ
[Unit]
StartLimitIntervalSec=300
StartLimitBurst=5
[Service]
Restart=on-failure
RestartSec=5sStartLimit* ควรอยู่ในส่วน [Unit] และ Restart= ควรอยู่ในส่วน [Service] หากคุณใส่ค่าใดค่าหนึ่งผิดส่วน systemd จะเพิกเฉยต่อการตั้งค่านั้น การรีสตาร์ท 5 ครั้งภายใน 5 นาทีถือเป็นสัญญาณของการรั่วไหลมากกว่าจะเป็นเพียงความผิดพลาดชั่วคราว ดังนั้นหลังจากนั้น systemd จะหยุดพยายามและปล่อยให้ยูนิตอยู่ในสถานะ failed ซึ่งเป็นสถานะที่คุณควรพบเมื่อเข้ามาตรวจสอบในภายหลัง แทนที่จะปล่อยให้เกิด crash loop ที่คอยปกปิดปัญหาที่แท้จริงไว้
จำกัดการใช้งาน CPU ด้วย CPUQuota หรือจัดสรรด้วย CPUWeight
CPUQuota= จะกำหนดเปอร์เซ็นต์ของเวลาที่ใช้งานได้บน CPU หนึ่งคอร์ CPUQuota=50% คือครึ่งหนึ่งของหนึ่งคอร์ CPUQuota=200% เทียบเท่ากับการใช้งานสองคอร์ ซึ่งยูนิตนั้นอาจกระจายภาระงานไปยังเธรดจำนวนเท่าใดก็ได้ตามต้องการ บนแผนบริการที่มี 2 vCPU นั้น CPUQuota=200% คือการใช้งานเครื่องทั้งหมด
CPUWeight= เป็นค่าเริ่มต้นที่เหมาะสมกว่าสำหรับบริการส่วนใหญ่ โดยเป็นค่าสัดส่วนสัมพัทธ์ตั้งแต่ 1 ถึง 10000 ซึ่งค่าเริ่มต้นของเคอร์เนลคือ 100 ค่านี้จะทำงานเฉพาะเมื่อมีการแย่งชิงทรัพยากรกันเท่านั้น เช่น งานสำรองข้อมูลที่ตั้งค่าไว้ที่ CPUWeight=20 จะหลีกทางให้กับเว็บเซิร์ฟเวอร์ที่ตั้งค่าไว้ที่ 100 เมื่อมีภาระงานสูง และยังคงสามารถใช้งานทรัพยากรทั้งเครื่องได้ในขณะที่เครื่องว่างงาน การใช้โควตาแบบจำกัดตายตัว (hard quota) จะทำให้เสียความสามารถในการใช้ทรัพยากรส่วนที่ว่างอยู่นี้ไปโดยเปล่าประโยชน์
ควรพิจารณาให้ชัดเจนว่าการจำกัด CPU ให้ผลลัพธ์อย่างไร กระบวนการที่เน้นการคำนวณ CPU (CPU-bound) แทบจะไม่ทำให้ Linux ค้าง เพราะตัวจัดตารางเวลา (scheduler) จะคอยจัดสรรเวลาให้กับทุกกระบวนการอยู่เสมอ หน่วยความจำต่างหากที่เป็นสาเหตุให้เครื่องหยุดทำงาน ให้เลือกใช้ CPUQuota= เมื่อคุณต้องการเพดานการใช้งานที่คาดการณ์ได้ เช่น ในงาน build หรือ agent ที่อาจทำงานเต็มกำลังต่อเนื่องเป็นชั่วโมง การกำหนดขนาดสำหรับภาระงานประเภทนี้เป็นหัวข้อเฉพาะ ซึ่งครอบคลุมอยู่ใน จำนวน RAM และ CPU ที่ VPS สำหรับ coding agent ต้องการ
หากค่า CPU แสดงว่ามีการใช้งานสูงในขณะที่ไม่มีกระบวนการใดของคุณทำงานหนัก สาเหตุอาจมาจากฝั่งตรงข้ามของ hypervisor ซึ่งนั่นคือ CPU steal time จากเพื่อนบ้านที่ใช้ทรัพยากรมากเกินไป และไม่มีโควตาใดที่คุณตั้งค่าไว้จะสามารถแก้ไขปัญหานี้ได้
TasksMax ช่วยหยุดการทำงานของ fork loop
TasksMax= คือจำนวนของ process และ thread ที่ unit หนึ่งสามารถถือครองได้ เนื่องจาก thread ก็นับรวมด้วย บริการที่เขียนด้วย Java หรือ Go จึงต้องการพื้นที่รองรับมากกว่าที่รายการ process จะแสดงให้เห็น นี่คือการป้องกันที่ประหยัดทรัพยากรที่สุดสำหรับสคริปต์ที่ทำ fork ใน loop เพราะการ fork จะล้มเหลวภายใน unit นั้น แทนที่จะทำให้เครื่องแม่ข่ายขาดแคลน process ID จนใช้งานไม่ได้
TasksMax=128เมื่อ unit ถึงขีดจำกัด kernel จะบันทึก log ระบุชื่อ cgroup ไว้ดังนี้:
cgroup: fork rejected by pids controller in /system.slice/myapp.serviceโดยปกติโปรแกรมจะรายงานข้อผิดพลาด fork: retry: Resource temporarily unavailable ตรวจสอบค่าที่ตัวจัดการระบบกำหนดไว้เป็นค่าเริ่มต้นได้ด้วยคำสั่ง systemctl show -p DefaultTasksMax
จำกัดการทำงานของงานแบบครั้งเดียวด้วย systemd-run
คุณไม่จำเป็นต้องมีไฟล์ unit เพื่อใช้งานสิ่งเหล่านี้ systemd-run จะสร้าง unit ชั่วคราวขึ้นมาล้อมรอบคำสั่งเดียว
sudo systemd-run --scope -p MemoryMax=1G -p MemorySwapMax=0 -p CPUQuota=50% -p TasksMax=64 ./import-data.sh--scope จะรันคำสั่งใน terminal ของคุณหลังจากแสดง Running scope as unit: run-r7c1a....scope ผลลัพธ์จะแสดงบนหน้าจอของคุณและข้อจำกัดต่างๆ จะหายไปเมื่อคำสั่งทำงานเสร็จสิ้น คุณสมบัติใดๆ จาก systemd.resource-control สามารถใช้งานได้หลังจาก -p
สำหรับงานที่ใช้เวลานาน ให้ละ --scope ออกและตั้งชื่อให้กับงานนั้น งานจะทำงานในเบื้องหลังในฐานะ transient service และบันทึก log ลงใน journal:
sudo systemd-run --unit=nightly-import -p MemoryMax=1G -p CPUWeight=20 ./import-data.sh
journalctl -u nightly-import -fตัวเลือกเดียวกันนี้สามารถใช้กับ --user ได้ในกรณีที่คุณไม่ได้เป็น root แม้ว่า user manager ของคุณจะมีเฉพาะ controller ที่ถูกมอบหมายให้เท่านั้น ซึ่งอาจทำให้คุณสมบัติบางอย่างถูกปฏิเสธ ให้รันด้วย sudo หากเกิดกรณีดังกล่าว เมื่อต้องการให้งานนั้นมีสถานะถาวร คุณสามารถย้ายการตั้งค่าไปไว้ใน unit จริงได้โดยไม่ต้องเปลี่ยนแปลงค่าใดๆ: ดูที่ การรันสคริปต์ในฐานะ systemd service และ timer
คำถามเรื่อง swap ตอบตามตรง
Swap เปลี่ยนรูปแบบของความล้มเหลวแทนที่จะป้องกันมัน
หากไม่มี swap เมื่อหน่วยความจำเต็มถึงขีดจำกัด กระบวนการทำงานจะหยุดลงภายในไม่กี่วินาที ความเสียหายที่เกิดขึ้นจะชัดเจน รวดเร็ว และตรวจสอบสาเหตุได้ง่ายจาก log ในภายหลัง แต่ถ้ามี swap เคอร์เนลจะเขียน anonymous pages ที่ไม่ได้ใช้งานออกไปยังดิสก์เพื่อซื้อเวลา หากกระบวนการนั้นมีการใช้หน่วยความจำที่คงที่ swap จะช่วยคุณได้ แต่หากเป็นกระบวนการที่ทำงานผิดปกติ (runaway) swap จะเปลี่ยนความเสียหายที่ใช้เวลาเพียง 5 วินาทีให้กลายเป็นอาการค้างนาน 20 นาที ซึ่งอาการค้างนั้นแย่กว่า เพราะหากกระบวนการหยุดทำงานไปเลย คุณยังสามารถเข้าใช้งาน shell ได้ แต่หากเครื่องอยู่ในสภาวะ thrashing คุณจะไม่สามารถทำอะไรได้เลย
swapon --show
free -hทางเลือกที่เหมาะสมสำหรับ VPS ขนาดเล็กคือ การสร้าง swap file ขนาดพอเหมาะไว้สำหรับเก็บข้อมูลที่ถูกจองไว้ครั้งเดียวแต่ไม่มีการเรียกใช้งานอีก และตั้งค่า MemorySwapMax=0 บนหน่วยบริการที่คุณยอมรับได้หากมันจะหยุดทำงาน บริการที่สำคัญจะยังคงใช้ swap ต่อไป ส่วนบริการที่คาดเดาไม่ได้จะหยุดทำงานและเริ่มใหม่ทันทีเมื่อหน่วยความจำเต็ม
การลดค่า vm.swappiness เป็นวิธีแก้ปัญหาที่ไม่ค่อยได้ผลนัก และควรทราบเหตุผลว่าทำไม เพราะมันทำได้เพียงปรับสมดุลระหว่างการลบ page cache กับการย้าย anonymous pages ไปยัง swap ซึ่งทั้งสองอย่างล้วนต้องใช้การอ่านดิสก์ในภายหลัง มันเพียงแค่เปลี่ยนว่าข้อมูลส่วนไหนที่จะทำให้เกิดอาการ thrashing ไม่ใช่การป้องกันไม่ให้เครื่องเกิดอาการ thrashing
Daemon สำหรับจัดการ OOM ล่วงหน้าจะทำการยุติกระบวนการก่อนเกิดอาการค้าง
Kernel จะรอจนกว่าการเรียกคืนหน่วยความจำจะล้มเหลวโดยสมบูรณ์ ซึ่งบน VPS ขนาดเล็ก ช่วงเวลารอนี้คือจังหวะที่คุณจะสูญเสียการควบคุมเครื่องไปโดยตรง มี daemon ในระดับ userspace สองตัวที่ช่วยปิดช่องว่างนี้ได้โดยการเฝ้าสังเกตหน่วยความจำด้วยตนเองและทำการยุติกระบวนการให้เร็วขึ้น
earlyoom จะคอยเฝ้าสังเกตหน่วยความจำที่ใช้งานได้และ swap ที่ว่างอยู่ และจะยุติกระบวนการที่มีคะแนนสูงสุดเมื่อค่าใดค่าหนึ่งลดลงต่ำกว่าเกณฑ์ที่กำหนด
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 ใช้กำหนดค่าขั้นต่ำของหน่วยความจำที่ใช้งานได้ และ -s PERCENT ใช้กำหนดค่าขั้นต่ำของ swap ที่ว่างอยู่ โดยทั้งสองค่ามีค่าเริ่มต้นอยู่ที่ 10 เปอร์เซ็นต์ ตัวเลขตัวที่สองในแต่ละคู่คือจุดที่ใช้ส่ง SIGKILL โดย earlyoom จะส่ง SIGTERM เมื่อค่าลดลงต่ำกว่าค่าแรก และส่ง SIGKILL เมื่อต่ำกว่าค่าที่สอง ซึ่งโดยปกติจะมีค่าเป็นครึ่งหนึ่งของค่าแรก ให้ใช้ sudo systemctl restart earlyoom เพื่อนำการเปลี่ยนแปลงไปใช้ และอ่าน journalctl -u earlyoom เพื่อดูว่ากระบวนการใดถูกยุติไปและกระบวนการนั้นใช้หน่วยความจำไปเท่าใด
systemd-oomd เป็นอีกทางเลือกหนึ่ง หน้าคู่มืออธิบายไว้ว่าเป็น "system service ที่ใช้ cgroups-v2 และ pressure stall information (PSI) เพื่อเฝ้าสังเกตและดำเนินการแก้ไขก่อนที่ OOM จะเกิดขึ้นในระดับ kernel space" มันจะทำงานกับ cgroups ทั้งกลุ่มแทนที่จะเป็นกระบวนการเดี่ยวๆ ดังนั้นมันจึงยุติการทำงานของ unit ทั้งหน่วย ไม่ใช่แค่ child process ที่หลงเหลืออยู่ โดย unit จะต้องเลือกเข้าร่วมผ่าน ManagedOOMMemoryPressure=kill หรือ ManagedOOMSwap=kill และเกณฑ์การตั้งค่าจะอยู่ใน /etc/systemd/oomd.conf
systemctl status systemd-oomd
oomctloomctl จะแสดงรายการสิ่งที่กำลังเฝ้าสังเกตอยู่ ซึ่งบ่อยครั้งบน image ของเซิร์ฟเวอร์จะไม่มีอะไรแสดง เนื่องจากต้องตั้งค่า opt-in แยกในแต่ละ unit ให้เลือกใช้เพียง daemon ตัวเดียวก็เพียงพอ การรันทั้งสองตัวพร้อมกันหมายความว่าจะมีสองสิ่งที่แข่งกันเลือกเหยื่อ และจะทำให้การหาสาเหตุว่าทำไมกระบวนการใดกระบวนการหนึ่งถึงถูกยุติทำได้ยากขึ้น
หน่วยใดเป็นผู้รับผิดชอบ?
ให้เริ่มจาก kernel เพราะมันจะบันทึกทุกการสั่ง kill ที่เกิดขึ้น
journalctl -k --grep "Killed process" --since "2 hours ago"การสั่ง kill จาก OOM killer ระดับ global จะมีลักษณะดังนี้:
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 คือหน่วยความจำที่ process นั้นถือครองอยู่ใน RAM ขณะที่ถูกสั่งปิด ซึ่งในที่นี้คือประมาณ 1.8 GB ให้สังเกตชื่อที่อยู่ในวงเล็บด้วยความสงสัย นั่นคือเหยื่อที่ kernel เลือก ซึ่ง kernel มักเลือก process ที่ใช้หน่วยความจำมากที่สุดเสมอ แต่นั่นไม่ได้หมายความว่า process นั้นคือสาเหตุที่ทำให้หน่วยความจำขาดแคลน
การสั่ง kill จากการจำกัด cgroup จะมี 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:0prefix ดังกล่าวคือหัวใจสำคัญของการวินิจฉัย 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= และถูกทำ throttling max นับจำนวนครั้งที่หน่วยนั้นชน hard cap และ oom_kill นับจำนวน process ที่ถูกสั่ง kill จริงๆ หาก high มีค่าสูงในขณะที่ oom_kill 0 เป็นศูนย์ นั่นคือกรณีเงียบที่กล่าวถึงก่อนหน้านี้ คือ service ยังทำงานอยู่แต่ช้าจนแทบขยับไม่ได้ และไม่ได้รายงานความผิดพลาดใดๆ ให้ใครทราบ 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 สำหรับ kernel และ 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 ของคุณเป็นเป้าหมาย ซึ่งเป็นจุดตัดสินว่าคุณจะสามารถแก้ไขเซิร์ฟเวอร์ได้โดยตรงหรือต้องรีบูตผ่าน control panel การตั้งค่านี้เพียงแค่เปลี่ยนเกณฑ์การเลือกเป้าหมายของ kernel เท่านั้น ไม่ได้ช่วยลดระยะเวลาที่ระบบค้างแต่อย่างใด
คอนเทนเนอร์ทำงานอยู่ใน cgroups ของตนเอง ซึ่งถูกสร้างโดย container runtime ไม่ใช่โดย unit files ของคุณ ดังนั้นการจำกัดค่าที่ docker.service จึงไม่มีผลจำกัดต่อคอนเทนเนอร์โดยตรง สำหรับการตั้งค่าที่เทียบเท่ากันในระดับคอนเทนเนอร์ของ MemoryMax= และ CPUQuota= สามารถดูได้ที่ การตั้งค่าขีดจำกัดหน่วยความจำและ CPU ใน Docker Compose
FAQ
ทำไม VPS ของฉันถึงค้างแทนที่จะสั่งยุติกระบวนการที่ทำงานผิดปกติ?
เพราะเคอร์เนลตัดสินความคืบหน้าจากว่าการเรียกคืนหน่วยความจำ (reclaim) คืนหน้าหน่วยความจำได้หรือไม่ ไม่ใช่ตัดสินจากระยะเวลาที่ใช้ ในขณะที่หน่วยความจำเหลือน้อย เคอร์เนลจะไล่หน้าหน่วยความจำในแคชออก รวมถึงหน้าหน่วยความจำที่เป็นไฟล์สั่งการของโปรแกรมที่กำลังทำงานอยู่ แล้วค่อยอ่านกลับเข้ามาใหม่เมื่อถึงคำสั่งถัดไป ทุกอย่างจึงต้องรอการอ่านเขียนจากหน่วยเก็บข้อมูล และในทางเทคนิคยังไม่มีการจัดสรรหน่วยความจำใดที่ล้มเหลว ดังนั้น OOM killer จึงไม่ถูกเรียกใช้งาน ให้ตรวจสอบ /proc/pressure/memory ในขณะที่เกิดปัญหา หากค่า full avg10 สูงกว่า 40 หมายความว่าแทบไม่มีงานใดได้ประมวลผลเลยในช่วงสิบวินาทีที่ผ่านมา การใช้ daemon ในระดับ userspace เช่น earlyoom จะช่วยยุติกระบวนการก่อนที่เครื่องจะเข้าสู่สภาวะดังกล่าว
MemoryHigh และ MemoryMax ต่างกันอย่างไร?
MemoryHigh= คือขีดจำกัดแบบผ่อนปรน (soft cap) ที่ทำหน้าที่หน่วงการทำงาน เคอร์เนลจะพยายามเรียกคืนหน่วยความจำจากหน่วยนั้นอย่างหนักและทำให้การจัดสรรหน่วยความจำช้าลง แต่การใช้งานจริงสามารถเกินตัวเลขนี้ได้และไม่มีกระบวนการใดถูกสั่งยุติ ส่วน MemoryMax= คือขีดจำกัดแบบเด็ดขาด (hard cap): หากมีการจัดสรรหน่วยความจำที่ไม่สามารถทำได้ภายใต้ขีดจำกัดนี้ ระบบจะเรียก 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 ถูกเก็บไว้ใน RAM และหลักฐานได้สูญหายไปพร้อมกับการรีบูต ดังนั้นควรสร้างไดเรกทอรีดังกล่าวไว้ก่อนที่จะเกิดเหตุการณ์ครั้งถัดไป
ฉันควรเพิ่ม swap ให้กับ VPS ขนาดเล็กหรือไม่?
ไฟล์ swap ขนาดเล็กช่วยจัดการกับหน้าหน่วยความจำที่ไม่ได้ใช้งาน (cold pages) ซึ่งถูกจัดสรรไว้ครั้งเดียวแต่ไม่มีการเรียกใช้ซ้ำ แต่มันไม่ได้ช่วยกรณีมีกระบวนการทำงานผิดปกติ เพราะมันจะทำให้การสั่งยุติกระบวนการล่าช้าออกไป และเปลี่ยนจากช่วงเวลาที่บริการหยุดทำงานสั้นๆ เป็นการค้างยาวนานจนคุณไม่สามารถล็อกอินเข้าไปแก้ไขได้ ควรจำกัดขนาด swap ให้พอเหมาะ และตั้งค่า MemorySwapMax=0 ในหน่วยที่คุณยอมรับได้หากต้องถูกยุติ เพื่อให้หน่วยเหล่านั้นถึงขีดจำกัดและรีสตาร์ทได้อย่างรวดเร็ว ในขณะที่บริการที่สำคัญยังคงใช้ swap ได้ตามปกติ
ฉันสามารถจำกัดคำสั่งโดยไม่ต้องเขียนไฟล์ unit ได้หรือไม่?
ได้ sudo systemd-run --scope -p MemoryMax=1G -p CPUQuota=50% ./script.sh จะรันคำสั่งในเทอร์มินัลของคุณภายใน transient scope พร้อมกับขีดจำกัดเหล่านั้น และขีดจำกัดจะหายไปเมื่อคำสั่งสิ้นสุดลง คุณสมบัติทุกอย่างจาก systemd.resource-control สามารถใช้งานได้หลังจาก -p ดังนั้น MemorySwapMax=, TasksMax= และ CPUWeight= จึงใช้งานได้เช่นกัน ให้ละเว้น --scope และเพิ่ม --unit=name เพื่อรันงานในเบื้องหลังโดยให้ผลลัพธ์ไปอยู่ใน journal