คำตอบสั้น ๆ: 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
ขั้นตอนที่ 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 ที่บอกขนาดหน่วยความจำของโปรเซสนั้นตอนถูกปิด
ถ้าบรรทัดขึ้นต้นด้วย 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 ประมวลผลคิว