Studi Kasus: Ketika OOM Killer Membunuh PHP, Kesalahan Klasik Konfigurasi Memory
11 min read

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:

ParameterNilai
Total RAM server4 GB
PHP memory_limit2 GB
PHP-FPM pm.max_children20
SwapMinim / 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.
  • dmesg menunjukkan 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_limit tidak pernah menjadi batas agregat untuk seluruh proses PHP-FPM.
  • Jika ada 20 worker dan masing-masing diberi memory_limit 2GB, 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_childrenmemory_limitPotensi AlokasiRAM ServerStatus
202 GB40 GB4 GBSangat berisiko
20512 MB10 GB4 GBMasih berisiko
10256 MB2.5 GB4 GBRelatif aman
12256 MB3 GB4 GBAman 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_limit besar 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 MemoryCatatan
Kernel & sistem operasiOverhead dasar yang selalu ada
Page cacheDipakai OS untuk caching file I/O
Service lain (Nginx, systemd, cron, dll)Berjalan paralel dengan PHP-FPM
PHP-FPM workerSisa 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_limit ke 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_limit yang 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_limit kecil yang aman dari risiko OOM.
  • Worker queue bisa diberi memory_limit lebih 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_limit di 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 oom saat 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_limit ke angka realistis (misalnya 256MB) dan sesuaikan pm.max_children agar 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.

Portofolio