OOM Killer: แก้แอปถูกปิดเองเพราะ RAM ไม่พอบน Linux Server

OOM Killer ปิด MySQL หรือ PHP-FPM เองเมื่อ RAM หมด ยืนยันจาก journalctl และ dmesg ตั้ง oom_score_adj จำกัด MemoryMax ด้วย systemd แล้วเลือกเพิ่ม Swap หรือ RAM

โดย ทีมงาน THAI DATA CLOUD อ่าน 15 นาที

แชร์FacebookXLINELinkedIn
OOM Killer — OOM Killer: แก้แอปถูกปิดเองเพราะ RAM ไม่พอบน Linux Server
OOM Killer: แก้แอปถูกปิดเองเพราะ RAM ไม่พอบน Linux Server

คำตอบสั้น ๆ: OOM Killer ปิด MySQL หรือ PHP-FPM เองเมื่อ RAM หมด ยืนยันจาก journalctl และ dmesg ตั้ง oom_score_adj จำกัด MemoryMax ด้วย systemd แล้วเลือกเพิ่ม Swap หรือ RAM

คู่มือนี้อธิบาย OOM Killer ทีละขั้นตอน พร้อมคำสั่งที่ใช้ได้จริงและปัญหาที่พบบ่อย

OOM Killer (Out-Of-Memory Killer) คือกลไกของเคอร์เนล Linux ที่ทำงานเมื่อหน่วยความจำของเครื่องหมดจริง ๆ ทั้ง RAM และ Swap จนเคอร์เนลคืนหน่วยความจำจากที่อื่นไม่ได้อีก เคอร์เนลจะเลือกโปรเซสหนึ่งตัวแล้วส่งสัญญาณ SIGKILL ปิดทันทีเพื่อให้ระบบทั้งเครื่องยังทำงานต่อได้ อาการที่ผู้ดูแลเห็นคือ MySQL, PHP-FPM, Java หรือแอป Node.js หยุดเองกลางวันโดยไม่มีข้อความผิดพลาดในล็อกของแอปเลย

คู่มือนี้เหมาะกับผู้ดูแล Linux Server ที่สงสัยว่าบริการถูกปิดเพราะ RAM ไม่พอ เมื่อทำครบทุกขั้นตอน คุณจะยืนยันได้จากล็อกว่า OOM Killer เป็นต้นเหตุจริงหรือไม่ รู้ว่าโปรเซสไหนกินหน่วยความจำ ตั้งลำดับความสำคัญไม่ให้ฐานข้อมูลถูกเลือกก่อน จำกัดหน่วยความจำของบริการที่ชอบบวมด้วย systemd และตัดสินใจได้ว่าควรเพิ่ม Swap ปรับค่าแอป หรือขยายสเปกเครื่อง

สิ่งที่ต้องเตรียม

  • สิทธิ์ root หรือบัญชีที่ใช้ sudo ได้บนเครื่องที่มีปัญหา คำสั่งในคู่มือนี้ใช้ได้ทั้ง Ubuntu, Debian, AlmaLinux และ Rocky Linux ที่ใช้ systemd
  • เวลาโดยประมาณที่บริการหยุด เช่น เวลาที่ลูกค้าแจ้งว่าเว็บล่ม หรือเวลาที่ระบบมอนิเตอร์ส่งแจ้งเตือน เพื่อใช้ค้นล็อกให้ตรงช่วง
  • กราฟการใช้ RAM ย้อนหลัง ถ้าใช้ Cloud Server บน TDC Enterprise ดูได้ตามคู่มือ Cloud Server Monitoring
  • รายชื่อบริการที่สำคัญที่สุดของเครื่องนี้ เช่น ฐานข้อมูลต้องอยู่รอดก่อนเว็บ เพราะจะใช้ตัดสินใจเรื่องลำดับความสำคัญในขั้นตอนที่ 4
ลำดับขั้นจากแอปขอหน่วยความจำเพิ่ม RAM และ Swap หมด จนถึง OOM Killer ให้คะแนนและส่ง SIGKILL ปิดโปรเซส
OOM Killer เป็นทางสุดท้ายของเคอร์เนล เมื่อทั้ง RAM และ Swap ไม่เหลือที่ให้คืนแล้ว

ขั้นตอนที่ 1: ยืนยันจากล็อกว่า OOM Killer ปิดโปรเซสจริง

ทุกครั้งที่ OOM Killer ทำงาน เคอร์เนลจะเขียนข้อความลง kernel log เสมอ ให้ค้นจาก journal ของเคอร์เนลก่อน เพราะใช้ได้ทุกดิสโทรที่ใช้ systemd

journalctl -k --since "today" | grep -iE "out of memory|killed process|invoked oom-killer"
dmesg -T | grep -iE "out of memory|killed process"

ถ้าเครื่องรีบูตไปแล้ว dmesg จะว่างเปล่า ให้ดูบูตก่อนหน้าด้วย journalctl -k -b -1 ซึ่งได้ผลเฉพาะเครื่องที่เก็บ journal แบบถาวร (มีโฟลเดอร์ /var/log/journal) หรือค้นไฟล์ล็อกข้อความตรง ๆ บน Ubuntu และ Debian ที่ /var/log/kern.log และบน AlmaLinux หรือ Rocky Linux ที่ /var/log/messages วิธีอ่าน journal แบบละเอียดดูได้ใน คู่มืออ่าน Log ระบบด้วย journalctl

ข้อความที่ยืนยันว่า OOM Killer ทำงานมีสองบรรทัดหลัก บรรทัดแรกบอกว่าโปรเซสไหนขอหน่วยความจำจนเกิดเหตุ (invoked oom-killer) ซึ่งไม่จำเป็นต้องเป็นตัวที่ถูกปิด บรรทัดที่สองคือ Out of memory: Killed process ตามด้วย PID ชื่อโปรเซส และค่า anon-rss ที่บอกขนาดหน่วยความจำของโปรเซสนั้นตอนถูกปิด

ผลคำสั่ง journalctl แสดงบรรทัด invoked oom-killer และ Out of memory: Killed process ของ mysqld พร้อมผล oom-kill ของบริการ
บรรทัด Killed process บอกชื่อโปรเซสและขนาด anon-rss ตอนถูกปิด ใช้ยืนยันว่า OOM Killer เป็นต้นเหตุ

ถ้าบรรทัดขึ้นต้นด้วย Memory cgroup out of memory แปลว่าเครื่องยังมี RAM เหลือ แต่บริการนั้นใช้เกินเพดานที่ตั้งไว้ใน systemd หรือ Docker ซึ่งต้องแก้ที่เพดาน ไม่ใช่ที่ RAM ของเครื่อง ส่วน journal ของ systemd จะมีบรรทัด Failed with result 'oom-kill' กำกับชื่อบริการด้วย ตรวจได้ด้วย journalctl -u mysql --since today สำหรับคอนเทนเนอร์ Docker ใช้ docker inspect -f '{{.State.OOMKilled}}' ชื่อคอนเทนเนอร์ ถ้าได้ true และ exit code เป็น 137 แปลว่าถูกปิดเพราะหน่วยความจำเกินเช่นกัน

บางดิสโทรมี systemd-oomd ทำงานคู่กันและปิดโปรเซสก่อนที่ OOM Killer ของเคอร์เนลจะทำงาน ข้อความจะอยู่ใน journalctl -u systemd-oomd แทน ตรวจว่ามีบริการนี้หรือไม่ด้วย systemctl status systemd-oomd

ขั้นตอนที่ 2: หาว่าโปรเซสไหนกินหน่วยความจำ

เมื่อยืนยันแล้วว่า OOM Killer เป็นต้นเหตุ ให้ดูภาพรวมของเครื่องตอนนี้ คอลัมน์ที่ต้องดูใน free -h คือ available ไม่ใช่ free เพราะ Linux ใช้ RAM ที่ว่างเป็นแคชไฟล์และคืนให้แอปได้เมื่อต้องการ

free -h
ps -eo pid,user,comm,rss,%mem --sort=-rss | head -15
ps --no-headers -o rss -C php-fpm8.3 | awk '{s+=$1; n++} END {printf "%d workers, %.0f MB total, %.0f MB avg\n", n, s/1024, s/n/1024}'

คำสั่งที่สองเรียงโปรเซสตามหน่วยความจำจริงที่ใช้อยู่ (RSS หน่วย KB) คำสั่งที่สามรวมหน่วยความจำของ worker ทุกตัวในบริการเดียว เปลี่ยน php-fpm8.3 เป็นชื่อโปรเซสบนเครื่องของคุณ เช่น php-fpm บน AlmaLinux บริการที่มี worker หลายตัวอย่าง PHP-FPM หรือ Apache มักไม่ติดอันดับต้นเมื่อดูทีละตัว แต่รวมกันแล้วใช้ RAM มากที่สุดในเครื่อง ซึ่ง OOM Killer ไม่ได้มองแบบรวมเป็นบริการ

ดูประวัติการใช้งานประกอบด้วย ถ้า RAM ค่อย ๆ ขึ้นทีละน้อยตลอดหลายวันแล้วตกลงทันทีตอนที่ OOM Killer ทำงาน มักเป็นหน่วยความจำรั่ว (memory leak) ในแอป ถ้ากระโดดขึ้นช่วงสั้น ๆ มักมาจากทราฟฟิกพุ่ง งาน cron หนัก หรือการสำรองข้อมูลที่ตรงกับช่วงใช้งานสูง วิธีอ่านค่า CPU, RAM และ Load Average เพิ่มเติมดูได้ที่ คู่มือตรวจสอบ CPU, RAM และ Load Average

ขั้นตอนที่ 3: ปรับค่าของแอปให้ใช้ RAM ตามจริง

สาเหตุส่วนใหญ่ที่ทำให้ OOM Killer ทำงานบนเซิร์ฟเวอร์เว็บไม่ใช่ RAM น้อย แต่เป็นค่าตั้งของแอปที่รวมกันแล้วเกิน RAM ของเครื่อง จุดที่ควรคำนวณใหม่มีดังนี้

  • PHP-FPM: pm.max_children คูณขนาดเฉลี่ยของ worker จากขั้นตอนที่ 2 ต้องไม่เกิน RAM ที่เหลือหลังหักฐานข้อมูลและระบบปฏิบัติการ เช่น เหลือ 2.5 GB และ worker เฉลี่ย 60 MB ค่าที่ปลอดภัยคือประมาณ 40 ตั้ง pm.max_requests = 500 ด้วยเพื่อให้ worker ที่รั่วถูกสร้างใหม่เป็นระยะ
  • MySQL และ MariaDB: innodb_buffer_pool_size คือก้อนใหญ่ที่สุด บนเครื่องที่รันเว็บและฐานข้อมูลรวมกันไม่ควรตั้งเท่ากับเครื่องฐานข้อมูลเฉพาะ และต้องเผื่อหน่วยความจำต่อการเชื่อมต่อตามค่า max_connections
  • Java: กำหนด -Xmx ให้ชัด ถ้าไม่กำหนด JVM จะเลือกเพดาน heap เองจากขนาด RAM ของเครื่อง ซึ่งอาจชนกับบริการอื่น
  • Node.js: ใช้ --max-old-space-size และตรวจจำนวน instance ใน PM2 ว่าไม่เกินที่เครื่องรับได้

หลังแก้ค่าให้รีสตาร์ตทีละบริการแล้วดู free -h ซ้ำในช่วงที่ทราฟฟิกสูงที่สุดของวัน ไม่ใช่ช่วงกลางคืนที่เครื่องว่าง ถ้าค่า available ยังเหลือพอในช่วงนั้น โอกาสที่ OOM Killer จะทำงานซ้ำก็ลดลงมาก

ขั้นตอนที่ 4: ตั้งลำดับให้ OOM Killer เลือกโปรเซสที่เสียหายน้อยที่สุด

OOM Killer ให้คะแนนทุกโปรเซส (/proc/PID/oom_score) จากหน่วยความจำที่ใช้เป็นหลัก แล้วบวกค่า oom_score_adj ที่ผู้ดูแลกำหนดได้ตั้งแต่ -1000 ถึง 1000 โปรเซสที่คะแนนสูงสุดถูกเลือกก่อน ฐานข้อมูลจึงมักโดนก่อนเพราะใช้ RAM มากที่สุด ทั้งที่การปิดฐานข้อมูลกะทันหันเสียหายมากกว่าการปิด worker ของเว็บ

cat /proc/$(pidof -s mysqld)/oom_score
cat /proc/$(pidof -s mysqld)/oom_score_adj
choom -p $(pidof -s mysqld)

การเขียนค่าลง /proc โดยตรงหายไปเมื่อรีสตาร์ตบริการ ให้ตั้งถาวรผ่าน systemd แทน คำสั่ง systemctl edit จะสร้างไฟล์ override โดยไม่แตะไฟล์ของแพ็กเกจ

sudo systemctl edit mysql
# พิมพ์ในส่วนที่ editor เปิดให้ แล้วบันทึก
[Service]
OOMScoreAdjust=-500

sudo systemctl restart mysql
cat /proc/$(pidof -s mysqld)/oom_score_adj

บน MariaDB รุ่นใหม่ ชื่อบริการคือ mariadb และโปรเซสชื่อ mariadbd ใช้ค่าติดลบปานกลางอย่าง -500 ก็พอให้ฐานข้อมูลถูกเลือกทีหลัง อย่าตั้ง -1000 ซึ่งเท่ากับห้าม OOM Killer ปิดโปรเซสนั้นเลย ถ้าโปรเซสนั้นเองเป็นตัวรั่ว เคอร์เนลจะไล่ปิดโปรเซสอื่นทั้งหมดจนรวมถึง sshd และคุณจะเข้าเครื่องไม่ได้ ในทางกลับกัน ตั้งค่าบวกให้งานที่ปิดแล้วเริ่มใหม่ได้ เช่น worker ประมวลผลคิว

ขั้นตอนที่ 5: จำกัดหน่วยความจำของบริการด้วย MemoryMax

สำหรับบริการที่รู้ว่ารั่วหรือบวมได้ ให้ตั้งเพดานใน cgroup ของ systemd แทนการรอให้ทั้งเครื่องหมด เมื่อบริการใช้เกินเพดาน OOM Killer จะปิดเฉพาะโปรเซสในบริการนั้น บริการอื่นในเครื่องไม่กระทบ ค่านี้ใช้ได้เต็มรูปแบบบนระบบที่ใช้ cgroup v2 ซึ่งเป็นค่าเริ่มต้นของ Ubuntu 22.04, Debian 12 และ AlmaLinux 9 ขึ้นไป

sudo systemctl edit queue-worker
[Service]
MemoryHigh=1200M
MemoryMax=1500M
Restart=on-failure
RestartSec=5

sudo systemctl daemon-reload
sudo systemctl restart queue-worker
systemctl show queue-worker -p MemoryMax -p MemoryHigh -p MemoryCurrent

MemoryHigh เป็นเพดานแบบนุ่ม เมื่อเกินระบบจะเร่งคืนหน่วยความจำและชะลอบริการนั้น ส่วน MemoryMax เป็นเพดานแข็ง เกินแล้ว OOM Killer ทำงานภายในบริการทันที ตั้ง Restart=on-failure คู่กันเพื่อให้ systemd เปิดบริการกลับเองหลังถูกปิด และดูจำนวนครั้งที่เกิดได้จาก journalctl -u queue-worker ชื่อ queue-worker เป็นตัวอย่าง ให้เปลี่ยนเป็นชื่อบริการจริงของคุณ คอนเทนเนอร์ Docker ใช้หลักเดียวกันผ่านตัวเลือก --memory หรือ mem_limit ใน Compose

ตารางเปรียบเทียบ OOMScoreAdjust, MemoryHigh, MemoryMax, Swap และการขยาย RAM ว่าแต่ละวิธีทำอะไรและเหมาะกับกรณีใด
ปรับลำดับและเพดานก่อน เพิ่ม Swap รับยอดพุ่งสั้น ๆ และขยาย RAM เมื่อโหลดปกติก็เกินแล้ว

ขั้นตอนที่ 6: ตัดสินใจว่าจะเพิ่ม Swap หรือขยาย RAM

Swap ช่วยรับช่วงที่หน่วยความจำพุ่งขึ้นชั่วคราว ทำให้เคอร์เนลย้ายหน้าที่ไม่ค่อยได้ใช้ออกไปดิสก์แทนการเรียก OOM Killer ทันที เครื่องคลาวด์หลายรุ่นไม่มี Swap มาให้ ตรวจด้วย swapon --show ถ้าไม่มีผลลัพธ์ ให้เพิ่มตาม คู่มือเพิ่ม Swap File บน Linux Server ขนาดที่นิยมเริ่มต้นบนเซิร์ฟเวอร์ขนาดเล็กคือ 1–2 GB

แต่ Swap ไม่ใช่ RAM ถ้า vmstat 1 แสดงคอลัมน์ si และ so ไม่เป็นศูนย์ตลอดเวลาในช่วงใช้งานปกติ แปลว่าเครื่องอ่านเขียน Swap อยู่ตลอด เว็บจะช้าลงชัดเจน กรณีนี้ถึงเวลาขยาย RAM ใช้หลักนี้ตัดสินใจ

  • OOM Killer ทำงานครั้งเดียวในช่วงที่มีงานพิเศษ เช่น สำรองข้อมูลหรือนำเข้าข้อมูลก้อนใหญ่: ย้ายเวลางานหรือเพิ่ม Swap ก็พอ
  • OOM Killer ยังทำงานซ้ำในช่วงทราฟฟิกปกติหลังปรับค่าแอปตามขั้นตอนที่ 3 แล้ว: ขยาย RAM ดูวิธีเลือกขนาดได้ใน คู่มือเลือก vCPU และ RAM ให้พอดีกับงาน
  • RAM ขึ้นเรื่อย ๆ ไม่เคยลดจนกว่าจะรีสตาร์ต: เป็นหน่วยความจำรั่ว ขยาย RAM แค่ยืดเวลา ต้องแก้ที่แอปหรือตั้ง MemoryMax คุมไว้ระหว่างรอแก้
เช็กลิสต์สิ่งที่ต้องตรวจหลัง OOM Killer ปิดบริการ ตั้งแต่ยืนยันล็อก คำนวณ pm.max_children ถึงการตัดสินใจขยาย RAM
แก้ที่ค่าของแอปและลำดับความสำคัญก่อน แล้วค่อยตัดสินใจเรื่องสเปกเครื่อง

ตรวจสอบหลังแก้ไขว่า OOM Killer ไม่ทำงานซ้ำ

เฝ้าดูอย่างน้อยหนึ่งสัปดาห์ที่มีทราฟฟิกครบทุกรูปแบบ ค้นล็อกซ้ำด้วยคำสั่งในขั้นตอนที่ 1 ทุกวัน ถ้าไม่พบบรรทัด Killed process อีกและค่า available ในช่วงใช้งานสูงสุดยังเหลืออย่างน้อย 15–20% ของ RAM ถือว่าปลอดภัย ควรตั้งแจ้งเตือนเมื่อ RAM ที่ใช้ได้ต่ำกว่าเกณฑ์ เพื่อรู้ก่อนที่ OOM Killer จะทำงานครั้งถัดไป

ปัญหาที่พบบ่อยเกี่ยวกับ OOM Killer

ปิด OOM Killer ไปเลยได้หรือไม่?

ไม่ควร ถ้าไม่มี OOM Killer เครื่องที่หน่วยความจำหมดจะค้างทั้งเครื่อง ไม่ตอบสนองแม้แต่ SSH หรือถ้าตั้งให้เคอร์เนล panic ก็จะรีบูตเอง ซึ่งเสียหายกว่าการปิดโปรเซสเดียว ทางที่ถูกคือลดความต้องการหน่วยความจำ ตั้งลำดับด้วย oom_score_adj และกันบริการที่บวมไว้ด้วย MemoryMax ตามเอกสาร Documentation for /proc/sys/vm ของเคอร์เนล

ทำไม OOM Killer ปิด MySQL ทั้งที่ PHP เป็นตัวที่กิน RAM?

OOM Killer เลือกจากคะแนนรายโปรเซส ไม่ได้รวมตามบริการ worker ของ PHP แต่ละตัวมีขนาดเล็ก ส่วน mysqld เป็นโปรเซสเดียวที่ใหญ่ที่สุดในเครื่อง จึงถูกเลือกก่อนเสมอ แก้ด้วยการลด pm.max_children ให้สัมพันธ์กับ RAM และตั้ง OOMScoreAdjust ติดลบให้ฐานข้อมูลตามขั้นตอนที่ 4

free บอกว่า RAM เหลือ แต่ OOM Killer ยังทำงาน เกิดจากอะไร?

มักเป็นการชนเพดานของ cgroup ไม่ใช่ RAM ของเครื่อง ดูว่าล็อกขึ้นต้นด้วย Memory cgroup out of memory หรือไม่ แล้วตรวจ MemoryMax ของบริการด้วย systemctl show หรือค่า mem_limit ของคอนเทนเนอร์ อีกกรณีคือหน่วยความจำพุ่งขึ้นและลดลงเร็วมากระหว่างช่วงที่ระบบมอนิเตอร์เก็บค่า กราฟรายนาทีจึงมองไม่เห็นยอด

รีบูตแล้วหาล็อกของ OOM Killer ไม่เจอ ทำอย่างไร?

ค้นใน /var/log/kern.log หรือ /var/log/messages ซึ่งเขียนลงดิสก์ผ่าน rsyslog และเปิด journal แบบถาวรเพื่อให้ครั้งต่อไปที่ OOM Killer ทำงานยังมีหลักฐานหลังรีบูต โดยสร้างโฟลเดอร์ /var/log/journal แล้วรีสตาร์ต systemd-journald หลังจากนั้นใช้ journalctl -k -b -1 ดูล็อกของบูตก่อนหน้าได้

ถ้าต้องการให้ทีมงานช่วยวิเคราะห์การใช้หน่วยความจำ ปรับค่าฐานข้อมูลและเว็บเซิร์ฟเวอร์ หรือประเมินว่าควรขยายสเปกเท่าไร ติดต่อทีมงาน THAI DATA CLOUD ได้ทุกวันทำการ และถ้ากำลังมองหาเครื่องที่ปรับ RAM ได้ตามโหลดจริง ดูรายละเอียดได้ที่หน้า Cloud Linux Server

อ่านเพิ่มเติมเกี่ยวกับ OOM Killer

แชร์FacebookXLINELinkedIn

มีคำถามเรื่องบริการ? ติดต่อทีมงาน