Studi Kasus: Ketika OOM Killer Membunuh PHP, Kesalahan Klasik Konfigurasi Memory
Proses PHP mati mendadak tanpa fatal error di log aplikasi adalah salah satu gejala paling membingungkan yang bisa ditemui engineer. Server terlihat baik-baik saja, CPU rendah, tidak ada exception yang tertangkap, tapi request tiba-tiba terputus begitu saja. Penyebabnya hampir selalu sama: OOM Killer di level kernel mengeksekusi proses PHP karena sistem kehabisan memory. Artikel ini membedah studi kasus nyata — server 4GB RAM, memory_limit PHP 2GB, dan 20 PHP-FPM worker — untuk menunjukkan bagaimana kombinasi angka yang terlihat wajar ini sebenarnya adalah bom waktu.
Studi Kasus Nyata
Konfigurasi server yang jadi sumber masalah terlihat seperti ini:
| Parameter | Nilai |
|---|---|
| Total RAM server | 4 GB |
PHP memory_limit | 2 GB |
PHP-FPM pm.max_children | 20 |
| Swap | Minim / tidak ada |
Gejala yang muncul di production:
- Proses PHP mati mendadak, request terputus tanpa respons yang jelas ke client.
- Tidak ada fatal error atau exception di log aplikasi PHP.
dmesgmenunjukkan OOM Killer aktif, dengan proses PHP sebagai korban.- Log monitoring menunjukkan penggunaan memory oleh PHP sempat mencapai sekitar 3GB sebelum proses di-kill.
Hipotesis awal saat investigasi: beberapa proses PHP secara bersamaan memakan memory besar, dan ketika total penggunaan mendekati limit RAM sistem, kernel memilih proses dengan konsumsi terbesar sebagai korban. Hipotesis ini terbukti benar — tapi akar masalahnya bukan bug, melainkan kesalahan memahami satu konsep dasar: bagaimana memory_limit PHP sebenarnya bekerja dalam konteks banyak worker.
Bagaimana Memory PHP-FPM Sebenarnya Bekerja
PHP-FPM (FastCGI Process Manager) tidak menjalankan satu proses besar yang melayani semua request. Ia menjalankan satu proses master yang mengelola sekumpulan proses worker independen. Setiap worker adalah proses sistem operasi yang terpisah, dengan memory space-nya sendiri.
flowchart TD
A[Nginx / Apache] -->|FastCGI request| B[PHP-FPM Master Process]
B --> C[Worker 1]
B --> D[Worker 2]
B --> E[Worker 3]
B --> F[Worker ... N]
C -.->|memory_limit per proses| G[(Memory Space Independen)]
D -.->|memory_limit per proses| H[(Memory Space Independen)]
E -.->|memory_limit per proses| I[(Memory Space Independen)]Setiap worker menangani satu request pada satu waktu, dan setiap worker punya alokasi memory sendiri yang sama sekali tidak dibagi dengan worker lain. Inilah akar dari kesalahan klasik yang sangat umum terjadi, terutama di konfigurasi lama:
SALAH PAHAM UMUM:
"memory_limit = 2GB berarti PHP secara keseluruhan
hanya boleh memakai 2GB memory."
FAKTA SEBENARNYA:
memory_limit adalah limit PER WORKER / PER REQUEST,
bukan limit global untuk seluruh proses PHP-FPM.
memory_limittidak pernah menjadi batas agregat untuk seluruh proses PHP-FPM.- Jika ada 20 worker dan masing-masing diberi
memory_limit2GB, kernel berpotensi menghadapi permintaan hingga 40GB memory secara bersamaan — jauh melebihi RAM fisik 4GB yang tersedia.- Linux secara default mengizinkan overcommit memory, sehingga PHP-FPM bisa start dan berjalan normal untuk waktu lama sebelum akhirnya beberapa request berat terjadi bersamaan dan memicu OOM Killer.
Anatomi Kesalahan: Matematika Memory yang Diabaikan
Kesalahan konfigurasi semacam ini jarang terlihat di awal karena dalam kondisi normal, sebagian besar request PHP hanya memakai beberapa puluh megabyte memory. Masalah muncul ketika beberapa request yang lebih berat — query besar, loop data, parsing file — terjadi bersamaan, dan kombinasi max_children × memory_limit ternyata jauh melebihi RAM fisik yang tersedia.
Rumus kasarnya:
Potensi Alokasi Maksimum = pm.max_children × memory_limit
Kasus studi ini:
20 worker × 2GB = 40GB potensi alokasi memory
RAM fisik server: 4GB
Rasio over-provisioning: 10x lipat dari RAM yang tersedia
Tabel berikut menunjukkan bagaimana kombinasi max_children dan memory_limit memengaruhi tingkat risiko pada server dengan RAM yang sama:
max_children | memory_limit | Potensi Alokasi | RAM Server | Status |
|---|---|---|---|---|
| 20 | 2 GB | 40 GB | 4 GB | Sangat berisiko |
| 20 | 512 MB | 10 GB | 4 GB | Masih berisiko |
| 10 | 256 MB | 2.5 GB | 4 GB | Relatif aman |
| 12 | 256 MB | 3 GB | 4 GB | Aman dengan overhead |
Angka “potensi alokasi” ini bukan jaminan bahwa sistem akan langsung crash — selama tidak semua worker memakai memory maksimum secara bersamaan, sistem bisa berjalan tanpa masalah untuk waktu yang lama. Tapi inilah yang membuat kesalahan ini berbahaya: ia tidak gagal secara konsisten. Ia gagal secara acak, biasanya saat traffic sedang tinggi-tingginya, yang membuatnya sulit direproduksi di environment staging.
- Setting
memory_limitbesar terasa “aman” karena jarang terlihat error langsung — sampai beberapa request berat kebetulan terjadi bersamaan.- Semakin tinggi
pm.max_children, semakin besar jumlah worker yang bisa memakai memory secara bersamaan saat traffic naik, bukan hanya semakin banyak request yang bisa dilayani.
Cara Kerja OOM Killer
Ketika kernel Linux mendeteksi bahwa sistem hampir kehabisan memory dan tidak bisa melakukan reclaim lebih lanjut (membersihkan cache, swap, dsb), mekanisme Out-Of-Memory Killer akan aktif untuk mencegah sistem benar-benar freeze atau crash total.
flowchart TD
A[Sistem hampir kehabisan memory] --> B{Masih bisa reclaim memory?}
B -- Ya: dari cache/buffer --> C[Reclaim cache, lanjut normal]
B -- Tidak bisa reclaim lagi --> D[OOM Killer diaktifkan]
D --> E[Hitung oom_score tiap proses]
E --> F[Pilih proses dengan score tertinggi]
F --> G[Kirim SIGKILL ke proses tersebut]
G --> H[Memory proses dibebaskan]
H --> I[Sistem kembali stabil]Kernel menghitung oom_score untuk setiap proses yang berjalan, dengan mempertimbangkan beberapa faktor:
- Jumlah memory yang sedang dipakai proses tersebut (faktor paling dominan).
- Privilege proses — proses milik root cenderung diberi skor lebih rendah agar tidak gampang jadi korban.
- Lama hidup proses dan beberapa heuristik tambahan dari kernel.
Proses dengan oom_score tertinggi — biasanya yang memakai memory paling besar — dipilih sebagai “kandidat paling murah untuk dikorbankan”, lalu kernel mengirim SIGKILL ke proses tersebut. Sinyal ini tidak bisa ditangkap atau ditunda oleh aplikasi, berbeda dengan SIGTERM yang masih memberi kesempatan cleanup.
Dalam studi kasus ini, proses PHP-FPM worker yang sedang menangani request berat dengan penggunaan memory terbesar adalah kandidat paling jelas, sehingga proses itulah yang dieksekusi kernel.
OOM Killer bukan fitur yang eksklusif untuk PHP. Mekanisme ini bekerja di level kernel dan bisa menarget proses apapun — Node.js, Java, Python, bahkan database — jika proses tersebut yang punya skor memory tertinggi saat sistem kehabisan resource.
Membaca Sinyal di Kernel Log
Karena OOM Killer bekerja di level kernel, bukan di level aplikasi, PHP tidak pernah sempat menulis error ke log-nya sendiri. Request yang sedang diproses terputus begitu saja, tanpa stack trace, tanpa exception. Inilah alasan kenapa kasus ini sering terlihat “misterius” — engineer mencari di log aplikasi padahal jawabannya ada di log kernel.
Cara memverifikasi dugaan OOM Killer:
# Cek riwayat OOM Killer di kernel ring buffer
dmesg | grep -i oom
# Alternatif di sistem dengan systemd journal
journalctl -k | grep -i "out of memory"
# Cek proses mana yang baru saja mati dan PID-nya
dmesg -T | grep -i "killed process"
Output yang biasanya muncul memuat informasi seperti nama proses yang dieksekusi, PID, dan total memory yang sedang dipakai proses tersebut tepat sebelum dibunuh — inilah angka yang dipakai untuk mengonfirmasi proses mana yang jadi korban dan seberapa besar memory yang sempat dipakainya.
Pola memory yang biasanya terlihat sebelum kill bukan lonjakan tiba-tiba (spike), melainkan kenaikan bertahap selama request diproses — disebabkan oleh request yang memang berat secara intrinsik (memproses dataset besar, generate file), bukan oleh memory leak klasik dalam pengertian aplikasi yang gagal membebaskan memory antar request.
Kenapa Mati di Sekitar 3GB, Bukan Tepat di 4GB?
Salah satu hal yang membingungkan dalam kasus ini: kenapa OOM Killer aktif saat penggunaan memory PHP baru mencapai sekitar 3GB, padahal RAM total server adalah 4GB? Jawabannya karena RAM 4GB itu tidak seluruhnya tersedia untuk PHP.
| Konsumen Memory | Catatan |
|---|---|
| Kernel & sistem operasi | Overhead dasar yang selalu ada |
| Page cache | Dipakai OS untuk caching file I/O |
| Service lain (Nginx, systemd, cron, dll) | Berjalan paralel dengan PHP-FPM |
| PHP-FPM worker | Sisa memory yang benar-benar tersedia |
Sebagai aturan praktis, zona bahaya biasanya mulai terlihat ketika penggunaan memory mencapai 70–80% dari total RAM fisik. Pada server 4GB, itu berarti sekitar 2.8GB–3.2GB — selaras dengan angka ~3GB yang tercatat di studi kasus ini sebelum proses PHP di-kill.
Faktor-Faktor yang Memperparah Situasi
Konfigurasi memory_limit dan max_children yang tidak proporsional adalah akar masalah, tapi beberapa faktor lain mempercepat datangnya kondisi OOM:
Concurrency tinggi. Beberapa request berat yang kebetulan berjalan bersamaan jauh lebih berbahaya daripada satu request berat yang berjalan sendirian, karena penggunaan memory bersifat akumulatif di seluruh worker yang aktif.
Worker PHP terlalu banyak relatif terhadap RAM. pm.max_children yang ditentukan tanpa memperhitungkan memory budget biasanya didasarkan pada asumsi “semakin banyak worker, semakin banyak request yang bisa dilayani” — tanpa mempertimbangkan bahwa setiap worker tambahan adalah tambahan beban memory potensial.
Request PHP yang secara intrinsik berat. Beberapa jenis operasi memang butuh memory besar:
Operasi yang rawan memory tinggi:
✗ Export CSV / Excel dengan dataset besar
✗ Generate PDF dari konten kompleks
✗ Image processing (resize, convert format)
✗ Query database tanpa pagination yang memuat seluruh hasil ke memory
Swap minim atau tidak ada. Tanpa swap, kernel kehilangan satu lapisan buffer waktu sebelum harus mengambil keputusan ekstrem. OOM Killer jadi jauh lebih agresif karena tidak ada ruang tambahan untuk menampung lonjakan memory sementara.
Memory fragmentation. PHP yang menjalankan ekstensi native (image processing, PDF generation, dsb) bisa menyebabkan fragmentasi memory yang membuat alokasi gagal lebih cepat dari yang diperkirakan berdasarkan total pemakaian saja.
Perbaikan: Menghitung Memory Budget dengan Benar
Solusi untuk kasus ini bukan sekadar “naikkan RAM server” — meski itu bisa membantu — melainkan menghitung memory budget secara sadar berdasarkan resource yang benar-benar tersedia.
Rumus dasarnya:
(pm.max_children × memory_limit) + overhead sistem < Total RAM
Untuk server 4GB di studi kasus ini, konfigurasi yang jauh lebih aman terlihat seperti berikut:
; ANTI-PATTERN: tidak proporsional dengan RAM 4GB
; memory_limit = 2G
; pm.max_children = 20
; Potensi alokasi: 40GB pada server 4GB RAM
; BENAR: memory budget dihitung sadar terhadap RAM fisik
[www]
pm = dynamic
pm.max_children = 12
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 6
; php.ini atau pool-level php_admin_value
php_admin_value[memory_limit] = 256M
Dengan memory_limit 256MB dan max_children 12, potensi alokasi maksimum adalah sekitar 3GB — masih menyisakan ruang untuk overhead sistem operasi, page cache, dan service lain di server 4GB.
- Menurunkan
memory_limitke angka yang jauh lebih kecil (256MB, kadang bahkan lebih rendah untuk aplikasi CRUD sederhana) bukan langkah mundur — ini adalah best practice lama yang sering dilupakan di konfigurasi modern.memory_limityang kecil justru membantu menemukan bug lebih cepat: request yang memakai memory tidak wajar akan gagal dengan fatal error PHP yang jelas, bukan menumpuk diam-diam sampai OOM Killer turun tangan.
Memisahkan Beban: Queue untuk Request Berat
Salah satu kesalahan yang sering menyertai kasus seperti ini adalah menjalankan operasi berat — export dataset besar, generate PDF, image processing — langsung di PHP-FPM web worker yang sama yang melayani traffic normal. Worker web idealnya dioptimalkan untuk request cepat dengan memory kecil, bukan untuk job yang butuh waktu lama dan memory besar.
flowchart LR
A[Client] -->|Request export| B[Nginx]
B --> C[PHP-FPM Web Worker]
C -->|Push job ke queue| D[(Redis / RabbitMQ / SQS)]
C -->|Response cepat: job diterima| A
D --> E[Worker Khusus Background Job]
E -->|Memory limit lebih besar, concurrency terkontrol| F[(Hasil: file export, PDF, dll)]Dengan memisahkan beban berat ke worker queue khusus:
- Web worker tetap ringan dan cepat merespons, dengan
memory_limitkecil yang aman dari risiko OOM. - Worker queue bisa diberi
memory_limitlebih besar dan concurrency yang jauh lebih terkontrol, biasanya hanya beberapa proses paralel saja. - Kegagalan job berat tidak ikut menjatuhkan kapasitas untuk melayani traffic web normal.
Teknologi queue yang umum dipakai untuk pola ini antara lain Redis (dengan library seperti Laravel Queue atau Symfony Messenger), RabbitMQ, atau Amazon SQS untuk yang sudah berada di ekosistem AWS.
Monitoring Proaktif Sebelum OOM Terjadi Lagi
Mencegah kasus serupa terulang membutuhkan visibilitas terhadap penggunaan memory per worker, bukan hanya memory total server.
# Lihat memory usage tiap proses, urut dari yang terbesar
ps aux --sort -rss | head -n 20
# Filter khusus proses PHP-FPM
ps aux --sort -rss | grep php-fpm
PHP-FPM juga menyediakan status page bawaan yang bisa diaktifkan untuk memantau jumlah active process, idle process, dan request yang sedang diproses:
; di pool configuration PHP-FPM
pm.status_path = /status
Untuk visibilitas jangka panjang, APM (Application Performance Monitoring) seperti New Relic atau Datadog bisa melacak tren memory per request dan memberi alert sebelum kondisi mendekati zona bahaya, jauh lebih awal dibanding menunggu dmesg melaporkan korban OOM Killer berikutnya.
Kapan Swap Membantu, dan Kapan Tidak
Mengaktifkan swap pada server dengan RAM terbatas sering dianggap kontroversial karena swap pada dasarnya jauh lebih lambat dari RAM. Tapi dalam konteks mencegah OOM Killer, fungsi swap di sini bukan untuk performa — melainkan untuk memberi waktu observasi sebelum kernel mengambil keputusan ekstrem.
- Swap yang terlalu besar pada server dengan disk lambat bisa membuat sistem terasa “menggantung” (thrashing) alih-alih crash cepat — kadang ini lebih sulit didiagnosis daripada OOM Killer yang langsung bertindak.
- Swap secukupnya (misalnya 1–2GB pada server 4GB RAM) cukup untuk memberi buffer tanpa membuat sistem terjebak dalam thrashing berkepanjangan.
Swap bukan pengganti dari memory budget yang dihitung dengan benar — ia hanya jaring pengaman tambahan, bukan solusi utama.
Ringkasan
memory_limitdi PHP adalah limit per worker / per request, bukan limit global untuk seluruh proses PHP-FPM.- Potensi alokasi maksimum dihitung dari
pm.max_children × memory_limit— jika angka ini jauh melebihi RAM fisik, sistem hanya “beruntung” belum crash, bukan benar-benar aman.- OOM Killer bekerja di level kernel dengan menghitung oom_score tiap proses dan mengeksekusi SIGKILL ke proses dengan skor tertinggi — biasanya proses dengan konsumsi memory terbesar.
- Karena bekerja di level kernel, OOM Killer tidak meninggalkan jejak di log aplikasi PHP — selalu cek
dmesg | grep -i oomsaat menemui proses yang mati tanpa fatal error.- Zona bahaya biasanya mulai terlihat di 70–80% RAM terpakai, bukan tepat di 100%, karena OS dan service lain juga memakai memory.
- Turunkan
memory_limitke angka realistis (misalnya 256MB) dan sesuaikanpm.max_childrenagar potensi alokasi maksimum tetap di bawah RAM fisik yang tersedia.- Pisahkan request berat (export, PDF, image processing) ke worker queue khusus dengan concurrency terkontrol, jangan jalankan di web worker biasa.
- Swap secukupnya membantu memberi waktu observasi, tapi bukan pengganti dari memory budget yang dihitung dengan sadar.