Bali, Indonesia
Insights

Kenapa OOM Killer Mematikan MySQL, Bukan Proses yang Paling Boros Memori

October 24, 2026
0:00
Pola ini sering muncul di VPS kecil yang menjalankan web dan database sekaligus: RAM penuh, MySQL mati, situs menampilkan error koneksi database. Saat dicek di top, yang kelihatan paling banyak makan memori justru PHP-FPM, karena worker-nya berderet puluhan. Wajar kalau muncul pertanyaan: kenapa kernel malah mematikan MySQL? Jawabannya ada di cara kernel menilai proses, dan cara itu berbeda dengan cara kita membaca top.
Linux tidak langsung membunuh proses begitu RAM terlihat penuh. Sebagian besar RAM yang "terpakai" biasanya adalah page cache, yaitu salinan file dari disk yang disimpan di memori supaya akses berikutnya cepat. Cache ini bisa dibuang kapan saja. Urutannya kira-kira begini. Sebuah proses meminta halaman memori baru. Kalau tidak ada halaman kosong, kernel masuk ke tahap reclaim: membuang page cache, menulis halaman kotor ke disk, dan memindahkan halaman anonim (memori milik proses yang tidak punya file asal, seperti heap) ke swap kalau swap tersedia. Kalau setelah beberapa kali percobaan reclaim tetap tidak ada halaman yang bisa dibebaskan, kernel menyerah dan memanggil fungsi out_of_memory(). Di titik inilah OOM killer bekerja. OOM killer lalu menelusuri semua proses, memberi masing-masing skor "badness", memilih yang skornya paling tinggi, dan mengirim SIGKILL. Sinyal ini tidak bisa ditangkap atau ditunda oleh proses, jadi MySQL tidak sempat shutdown dengan rapi. InnoDB biasanya bisa recovery saat start ulang lewat redo log, tapi koneksi yang sedang berjalan langsung putus. Satu hal yang sering terlewat: proses yang memicu OOM belum tentu proses yang dibunuh. Bisa saja worker PHP yang meminta memori terakhir, tapi korbannya MySQL, karena yang dihitung adalah skor, bukan siapa yang meminta.
Di kernel modern, perhitungan badness cukup sederhana. Untuk tiap proses, kernel menjumlahkan tiga hal:
  1. RSS (resident set size), yaitu halaman memori yang sedang benar-benar ada di RAM, termasuk memori anonim, halaman file yang dipetakan, dan shared memory.
  2. Jumlah halaman milik proses itu yang sedang ada di swap.
  3. Halaman yang dipakai untuk page table proses tersebut.
Totalnya kemudian disesuaikan dengan nilai oom_score_adj. Nilai ini berada di rentang -1000 sampai 1000, dan dikonversi menjadi proporsi dari total memori yang tersedia. Nilai positif membuat proses lebih mungkin dipilih, nilai negatif membuatnya lebih aman. Proses dengan nilai -1000 dilewati sama sekali. Perhatikan apa yang tidak dihitung. Kolom VIRT di top (memori virtual yang dipesan tapi belum tentu dipakai) tidak masuk perhitungan. Lama proses berjalan, prioritas nice, atau seberapa penting proses itu bagi bisnis kamu juga tidak masuk. Kernel lama dulu memberi potongan skor untuk proses milik root, tapi di kernel yang lebih baru potongan itu sudah dihapus. Yang paling penting: skor dihitung per proses, bukan per layanan. Kernel tidak tahu bahwa 80 proses php-fpm itu sebenarnya satu kesatuan.
Ambil contoh angka bulat (ilustrasi, bukan hasil pengukuran). Sebuah VPS 4 GB menjalankan MySQL dengan RSS 1,5 GB dan 40 worker PHP-FPM yang masing-masing memakai sekitar 50 MB. Total PHP-FPM sekitar 2 GB, lebih besar dari MySQL. Di mata kita, PHP-FPM yang paling boros. Di mata kernel, ada satu proses mysqld dengan skor setara 1,5 GB, dan 40 proses php-fpm dengan skor masing-masing setara 50 MB. Yang tertinggi jelas mysqld. MySQL memang dirancang menahan memori dalam jumlah besar dalam satu proses. InnoDB buffer pool dialokasikan di dalam mysqld, ditambah buffer per koneksi dan per query. Semakin lama server berjalan dan semakin banyak data yang tersentuh, semakin penuh buffer pool, dan RSS-nya naik mendekati batas yang dikonfigurasi. Sementara itu, PHP-FPM, Apache prefork, atau Exim menyebar bebannya ke banyak proses kecil. Ada juga efek dari swap. Kalau sebagian buffer pool MySQL sudah terdorong ke swap, RSS-nya di top terlihat lebih kecil. Tapi kernel tetap menghitung halaman di swap itu, jadi skornya tidak turun. Ini salah satu alasan kenapa MySQL tetap dipilih meskipun angka RES-nya tidak terlihat dominan. Dari sisi support, inilah yang paling sering membingungkan pemilik situs. Mereka melihat MySQL mati lalu menyimpulkan MySQL bermasalah, padahal MySQL hanya korban dari tekanan memori total. Kalau kamu ingin mengenali tanda-tanda tekanan memori sebelum sampai ke tahap ini, ada pembahasannya di How to Spot Memory Pressure on a VPS Before the OOM Killer Hits.
Setiap kali OOM killer bekerja, kernel menulis laporan ke ring buffer. Laporan ini bisa dibaca lewat dmesg atau, di sistem dengan systemd, lewat journalctl -k. Kalau server sempat reboot setelah kejadian, dmesg biasanya sudah kosong, jadi journal lebih bisa diandalkan asalkan journald menyimpan log secara permanen.
BASH
# Log kernel boot saat ini
journalctl -k | grep -i -E 'out of memory|oom-kill|killed process'

# Log kernel dari boot sebelumnya (kalau server sempat restart)
journalctl -k -b -1 | grep -i -E 'out of memory|oom-kill|killed process'

# Alternatif tanpa journal, dengan timestamp yang terbaca
dmesg -T | grep -i -E 'out of memory|killed process'

# Skor OOM saat ini per proses, urut dari yang paling mungkin dibunuh
for p in /proc/[0-9]*; do printf '%s %s %s
' "(catp/oom_score 2>/dev/null)" "(catp/oom_score_adj 2>/dev/null)" "(catp/comm 2>/dev/null)"; done | sort -rn | head -n 15
Perintah terakhir hanya membaca file di /proc, jadi aman dijalankan di server produksi. Kolom pertama adalah skor, kolom kedua oom_score_adj, kolom ketiga nama proses. Angka skor ini sudah dinormalisasi, jadi gunakan untuk membandingkan antarproses di server yang sama, bukan antarserver dengan kernel berbeda. Laporan OOM di log biasanya terdiri dari tiga bagian: siapa yang memicu, tabel proses, dan keputusan akhir. Contoh format output (disederhanakan, kolom dan susunannya berbeda antarversi kernel):
TEXT
php-fpm invoked oom-killer: gfp_mask=0x..., order=0, oom_score_adj=0
...
[  pid  ]   uid  tgid total_vm      rss pgtables_bytes swapents oom_score_adj name
[   1200]    27  1200   600000   380000        1000000    20000             0 mysqld
[   2101]    33  2101    90000    12000         200000        0             0 php-fpm
...
Out of memory: Killed process 1200 (mysqld) total-vm:2400000kB, anon-rss:1500000kB, file-rss:0kB, shmem-rss:0kB, UID:27 pgtables:1000kB oom_score_adj:0
Baris pertama menunjukkan pemicu, di contoh ini php-fpm. Tabel proses menunjukkan kondisi semua proses saat itu. Perlu dicatat, kolom rss dan swapents di tabel ini dalam satuan halaman (umumnya 4 KB), bukan kilobyte. Baris terakhir menyebutkan korbannya. Ada dua variasi yang perlu dikenali. Kalau log menyebut Memory cgroup out of memory, artinya yang habis adalah batas memori sebuah cgroup (misalnya batas per akun atau per container), bukan RAM server secara keseluruhan. Pemilihan korbannya hanya di dalam cgroup tersebut. Selain itu, beberapa distro memakai systemd-oomd, yaitu daemon di userspace yang bisa mematikan proses lebih awal berdasarkan tekanan memori. Jejaknya ada di log unit systemd-oomd, bukan di log kernel, jadi kalau grep di atas kosong padahal MySQL mati mendadak, cek juga log itu.
Kalau MySQL adalah komponen yang paling mahal untuk mati (karena recovery InnoDB butuh waktu dan semua situs ikut error), wajar ingin melindunginya. Cara yang paling rapi di sistem dengan systemd adalah lewat directive OOMScoreAdjust di unit service, bukan menulis langsung ke /proc/<pid>/oom_score_adj, karena nilai di /proc hilang setiap kali proses restart. Cek dulu nilai yang sedang berlaku. Nama unit bisa mysql, mysqld, atau mariadb, tergantung distro dan paket:
INI
# Cek nilai saat ini:  systemctl show mysql -p OOMScoreAdjust
# Buat override:       systemctl edit mysql
# Isi override:
[Service]
OOMScoreAdjust=-500
# Setelah itu jalankan: systemctl restart mysql
Restart MySQL memutus koneksi yang sedang berjalan, jadi lakukan di jam sepi. Setelah restart, pastikan nilainya terpasang dengan membaca /proc/<pid>/oom_score_adj milik mysqld. Aku pribadi lebih memilih nilai negatif moderat daripada -1000. Dengan -1000, MySQL dikecualikan total, dan kernel akan mencari korban lain. Korban berikutnya bisa saja sshd, agen monitoring, atau proses yang kamu butuhkan untuk masuk dan memperbaiki server. Kalau hampir semua proses besar dikecualikan, server bisa berakhir dalam kondisi yang jauh lebih buruk, bahkan kernel panic di beberapa konfigurasi. Nilai moderat seperti contoh di atas membuat MySQL tetap bisa dipilih kalau memang tidak ada pilihan lain. Yang lebih penting dipahami: oom_score_adj hanya mengubah siapa yang mati, bukan mencegah memori habis. Kalau MySQL dilindungi, korban berikutnya biasanya worker PHP-FPM. Itu lebih ringan dampaknya karena master process akan membuat worker baru, tapi masalah dasarnya tetap ada. Kalau OOM terjadi berulang, yang perlu dibenahi adalah anggaran memorinya: ukuran buffer pool, jumlah maksimal worker PHP-FPM, dan batas memori per kelompok proses. Untuk yang terakhir, pendekatan dengan cgroup dibahas di Mencegah OOM Cascading di Shared Hosting: Konfigurasi Cgroups v2 memory.high untuk Menahan Ledakan Worker PHP-FPM. Sebagai pegangan praktis: saat MySQL mati karena OOM, buka log kernel dulu dan lihat siapa pemicunya, bukan hanya siapa korbannya. Bandingkan total memori per layanan, bukan per proses, lalu hitung ulang apakah jumlah semuanya masuk akal untuk RAM yang ada. Atur OOMScoreAdjust kalau memang ingin MySQL jadi pilihan terakhir, tapi anggap itu sebagai pengaman, bukan perbaikan.