วิธีจำกัดการใช้ CPU และ RAM ของโพรเซสด้วย systemd
เรียนรู้วิธีตั้งค่า MemoryMax และ CPUQuota บน systemd เพื่อคุมทรัพยากร VPS ป้องกันปัญหาเครื่องค้างจากโพรเซสที่ทำงานผิดปกติ พร้อมวิธีตรวจสอบค่าผ่าน 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 และขึ้นต้นด้วยหัวข้อ [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 เลวร้ายกว่าบนแล็ปท็อป ประการแรก หน่วยเก็บข้อมูลมักเป็นแบบเครือข่ายหรือแบบใช้ร่วมกัน ดังนั้นแต่ละ 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% ของเวลาทั้งหมด งานที่พร้อมประมวลผล ทุกงาน บนเครื่องต้องหยุดชะงักเพื่อรอการจัดการหน่วยความจำ ทำให้ไม่มีงานใดทำงานได้เลย เซิร์ฟเวอร์ที่มีสถานะปกติจะมีค่าบน 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 นั้น และสั่งยุติกระบวนการทำงานหนึ่งอย่างของ unit นั้นเอง
เหตุผลสำคัญที่ควรตั้งค่า MemoryMax= สำหรับสิ่งที่คุณไม่ไว้วางใจอย่างเต็มที่คือส่วนหลังนี้ หากไม่มีการจำกัด เมื่อเกิดภาวะขาดแคลนหน่วยความจำจะกลายเป็นปัญหาของทั้งระบบ และ OOM killer ระดับโกลบอลจะเลือกเหยื่อโดยพิจารณาจาก oom_score ซึ่งส่วนใหญ่มักหมายถึงกระบวนการที่ใช้หน่วยความจำมากที่สุด ซึ่งมักจะเป็นฐานข้อมูลของคุณ ไม่ใช่สคริปต์ที่เกิดหน่วยความจำรั่วไหล แต่หากมีการจำกัดไว้ การสั่งยุติการทำงานจะเกิดขึ้นเฉพาะภายใน unit ที่เป็นต้นเหตุเท่านั้น
ควรตั้งค่าทั้งสองอย่าง โดยให้ MemoryHigh= ต่ำกว่า MemoryMax= ประมาณ 20 ถึง 30 เปอร์เซ็นต์ ช่องว่างนี้เปรียบเสมือนโซนแจ้งเตือน หากมีการรั่วไหลอย่างช้าๆ การใช้งานจะข้าม High และแสดงผลเป็นบริการที่ทำงานช้าลง ในขณะที่หากมีการใช้งานพุ่งสูงขึ้นอย่างกะทันหัน มันจะทะลุผ่าน Max และถูกสั่งยุติการทำงานทันที
ค่าเปอร์เซ็นต์จะถูกคำนวณจากหน่วยความจำกายภาพที่ติดตั้งอยู่ ดังนั้น MemoryMax=25% บนแผน 4 GB จะเท่ากับ 1 GB และจะยังคงเป็นหนึ่งในสี่ของระบบหลังจากที่คุณปรับขนาดแผนแล้ว MemorySwapMax=0 จะช่วยป้องกันไม่ให้ unit นั้นใช้ swap โดยสิ้นเชิง ซึ่งจะเปลี่ยนจากการทำงานที่อืดอาดเป็นเวลานานไปสู่การสั่งยุติการทำงานที่รวดเร็วและชัดเจนแทน
การจำกัดหน่วยความจำจำเป็นต้องมีนโยบายการเริ่มการทำงานใหม่ (restart policy) ควบคู่ไปด้วย มิฉะนั้นการสั่งยุติการทำงานจะทิ้งให้คุณเหลือเพียงบริการที่หยุดทำงานไปแล้ว
[Unit]
StartLimitIntervalSec=300
StartLimitBurst=5
[Service]
Restart=on-failure
RestartSec=5sStartLimit* ควรอยู่ใน [Unit] และ Restart= ควรอยู่ใน [Service] หากใส่ผิดส่วน systemd จะเพิกเฉยต่อการตั้งค่าเหล่านั้น การเริ่มการทำงานใหม่ 5 ครั้งภายใน 5 นาทีถือเป็นสัญญาณของการรั่วไหลมากกว่าจะเป็นเพียงความผิดพลาดชั่วคราว ดังนั้นหลังจากนั้น systemd จะยอมแพ้และปล่อยให้ unit อยู่ในสถานะ failed ซึ่งเป็นสถานะที่คุณต้องการพบในภายหลัง แทนที่จะปล่อยให้เกิด crash loop ที่คอยบดบังปัญหาที่แท้จริงไว้
จำกัดการใช้งาน CPU ด้วย CPUQuota หรือจัดสรรด้วย CPUWeight
CPUQuota= จะกำหนดสัดส่วนเปอร์เซ็นต์ของเวลาที่ใช้งานได้บน CPU หนึ่งคอร์ CPUQuota=50% คือครึ่งหนึ่งของหนึ่งคอร์ CPUQuota=200% เทียบเท่ากับการใช้งานสองคอร์ ซึ่งยูนิตนั้นอาจกระจายภาระงานไปยังเธรดจำนวนเท่าใดก็ได้ตามต้องการ บนแผนบริการแบบ 2 vCPU ค่า CPUQuota=200% จะหมายถึงการใช้งานเครื่องทั้งหมด
CPUWeight= เป็นค่าเริ่มต้นที่เหมาะสมกว่าสำหรับบริการส่วนใหญ่ โดยเป็นค่าสัดส่วนสัมพัทธ์ตั้งแต่ 1 ถึง 10000 ซึ่งค่าเริ่มต้นของ kernel คือ 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 แบบวนซ้ำ เพราะการ 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 ออกและตั้งชื่อให้กับงานนั้น จากนั้นงานจะทำงานในเบื้องหลังในฐานะ 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 เมื่อหน่วยความจำเต็ม ระบบจะถึงขีดจำกัดและมีบางอย่างหยุดทำงานภายในไม่กี่วินาที การหยุดชะงักนี้จะเกิดขึ้นอย่างรวดเร็วและชัดเจน ซึ่งสามารถตรวจสอบได้ง่ายใน journal หลังจากนั้น แต่ถ้ามี swap เคอร์เนลจะเขียน anonymous pages ที่ไม่ได้ใช้งานลงดิสก์เพื่อซื้อเวลา หากกระบวนการนั้นหยุดใช้หน่วยความจำเพิ่ม swap จะช่วยคุณไว้ได้ แต่ถ้าเป็นกระบวนการที่ทำงานผิดปกติ (runaway) swap จะเปลี่ยนการหยุดชะงักเพียง 5 วินาทีให้กลายเป็นอาการค้างนาน 20 นาที ซึ่งอาการค้างนั้นแย่กว่า เพราะกระบวนการที่หยุดทำงานไปแล้วยังคงเหลือ shell ให้คุณใช้งานได้ ในขณะที่เครื่องที่เกิดอาการ thrashing จะไม่ตอบสนองเลย
swapon --show
free -hทางสายกลางที่ใช้งานได้จริงบน VPS ขนาดเล็กคือ การสร้าง swap file ขนาดพอเหมาะสำหรับเก็บข้อมูลที่ถูกจองไว้ครั้งเดียวแต่ไม่ได้ใช้งานอีก และตั้งค่า MemorySwapMax=0 บน unit ที่คุณยอมรับได้หากมันจะหยุดทำงาน บริการที่สำคัญจะยังคงใช้ 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 ทั้ง unit ไม่ใช่แค่ child process ที่หลงเหลืออยู่ โดย unit จะต้องเลือกเข้าร่วมด้วยการตั้งค่า ManagedOOMMemoryPressure=kill หรือ ManagedOOMSwap=kill และเกณฑ์การตั้งค่าจะอยู่ใน /etc/systemd/oomd.conf
systemctl status systemd-oomd
oomctloomctl จะแสดงรายการสิ่งที่กำลังเฝ้าสังเกตอยู่ ซึ่งบน image ของเซิร์ฟเวอร์มักจะไม่แสดงอะไรเลยเนื่องจากต้องตั้งค่า opt-in แยกตาม unit ให้เลือกเพียง daemon เดียวก็เพียงพอ การรันทั้งสองตัวพร้อมกันจะทำให้เกิดการแย่งกันตัดสินใจเลือกเป้าหมาย และจะทำให้หาสาเหตุที่แท้จริงเมื่อมีการสั่งยุติการทำงานได้ยากขึ้น
หน่วยใดเป็นผู้รับผิดชอบ
ให้เริ่มตรวจสอบจาก kernel เนื่องจาก 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= และถูกจำกัดความเร็ว max นับจำนวนครั้งที่หน่วยนั้นชนเพดานสูงสุด (hard cap) และ oom_kill นับจำนวน process ที่ถูกสั่ง kill จริงๆ ค่า high ที่สูงพร้อมกับ oom_kill 0 คือกรณีเงียบที่กล่าวถึงก่อนหน้านี้: บริการยังคงทำงานอยู่ แต่ช้าลงจนแทบไม่ขยับและไม่ได้รายงานความผิดพลาดให้ใครทราบ memory.peak (สำหรับ Linux 5.19 ขึ้นไป) จะเก็บค่าการใช้งานสูงสุดที่ cgroup เคยไปถึง ซึ่งเป็นตัวเลขที่ควรนำไปใช้กำหนดขนาด MemoryMax= ไฟล์ทั้งสองจะถูกรีเซ็ตเมื่อหน่วยนั้นเริ่มทำงานใหม่ เนื่องจาก systemd จะสร้าง cgroup ขึ้นมาใหม่
มีข้อกำหนดเบื้องต้นประการหนึ่งที่อยู่ภายใต้กระบวนการทั้งหมดนี้ หาก /var/log/journal ไม่มีอยู่จริง journal จะถูกเก็บไว้ใน RAM และทุกบรรทัดจะหายไปหลังจากที่คุณ reboot เครื่องเพื่อกู้คืนระบบ
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 ของฉันถึงค้างแทนที่จะสั่ง kill กระบวนการที่ทำงานผิดปกติ?
เพราะ kernel ประเมินความคืบหน้าจากการที่ reclaim คืนหน้าหน่วยความจำได้หรือไม่ ไม่ใช่จากระยะเวลาที่ใช้ ในขณะที่หน่วยความจำเหลือน้อย kernel จะไล่ page cache ออก รวมถึงหน้า executable ของโปรแกรมที่กำลังทำงานอยู่ แล้วค่อยอ่านกลับเข้ามาใหม่เมื่อถึงคำสั่งถัดไป ทุกอย่างจึงต้องรอการอ่านเขียนจาก storage และในทางเทคนิคยังไม่มีการจัดสรรหน่วยความจำใดที่ล้มเหลว OOM killer จึงไม่ถูกเรียกใช้งาน ให้ตรวจสอบ /proc/pressure/memory ในขณะที่เกิดปัญหา หากค่า full avg10 สูงกว่า 40 หมายความว่าแทบไม่มีงานใดได้ประมวลผลเลยในช่วงสิบวินาทีที่ผ่านมา การใช้ userspace daemon เช่น earlyoom จะช่วยสั่ง kill กระบวนการก่อนที่เครื่องจะเข้าสู่สภาวะดังกล่าว
MemoryHigh และ MemoryMax แตกต่างกันอย่างไร?
MemoryHigh= คือเพดานแบบผ่อนปรน (soft cap) ที่ทำหน้าที่หน่วงการทำงาน kernel จะพยายาม reclaim หน่วยความจำจาก unit นั้นอย่างหนักและชะลอการจัดสรรหน่วยความจำ แต่การใช้งานจริงสามารถเกินตัวเลขนี้ได้และไม่มีกระบวนการใดถูก kill ส่วน MemoryMax= คือเพดานแบบเด็ดขาด (hard cap): หากมีการจัดสรรหน่วยความจำที่เกินขีดจำกัดนี้ OOM killer จะถูกเรียกใช้งานภายใน cgroup ของ unit นั้นโดยเฉพาะ ทำให้กระบวนการที่เป็นต้นเหตุถูก kill แทนที่จะเป็นกระบวนการที่ใช้หน่วยความจำมากที่สุดในเครื่อง ให้ตั้งค่า 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 และหลักฐานสูญหายไปพร้อมกับการ reboot ดังนั้นควรสร้าง directory ดังกล่าวไว้ก่อนที่จะเกิดเหตุการณ์ครั้งถัดไป
ฉันควรเพิ่ม swap ให้กับ VPS ขนาดเล็กหรือไม่?
swap file ขนาดเล็กช่วยจัดการกับ cold pages ที่ถูกจัดสรรไว้ครั้งเดียวแต่ไม่มีการเรียกใช้งานอีกเลย แต่มันไม่ได้ช่วยกรณีที่กระบวนการทำงานผิดปกติ เพราะมันจะทำให้การ kill ล่าช้าออกไปและเปลี่ยนจากช่วงที่ระบบหยุดทำงานสั้นๆ เป็นการค้างยาวจนคุณไม่สามารถล็อกอินเข้าไปแก้ไขได้ ควรตั้งค่า swap ไว้พอประมาณ และกำหนด MemorySwapMax=0 ให้กับ unit ที่คุณยอมรับได้หากต้องสูญเสียไป เพื่อให้ unit เหล่านั้นชนเพดานและ restart ได้อย่างรวดเร็ว ในขณะที่บริการสำคัญยังคงใช้งาน swap ได้ตามปกติ
ฉันสามารถจำกัดคำสั่งโดยไม่ต้องเขียน unit file ได้หรือไม่?
ได้ sudo systemd-run --scope -p MemoryMax=1G -p CPUQuota=50% ./script.sh จะรันคำสั่งใน terminal ของคุณภายใน transient scope พร้อมกับข้อจำกัดเหล่านั้น และข้อจำกัดจะหายไปเมื่อคำสั่งสิ้นสุดลง คุณสมบัติทุกอย่างจาก systemd.resource-control สามารถใช้งานได้หลังจาก -p ดังนั้น MemorySwapMax=, TasksMax= และ CPUWeight= จึงใช้งานได้เช่นกัน ให้ละ --scope และเพิ่ม --unit=name เพื่อรันงานในเบื้องหลังโดยให้ผลลัพธ์ถูกบันทึกลงใน journal