dmesg menampilkan pesan terminasi proses:
TEXT
[14285.892101] Out of memory: Killed process 1824 (mysqld) total-vm:18452144kB, anon-rss:8192340kB, file-rss:0kB, shmem-rss:0kB
[14285.898421] oom_reaper: reaped process 1824 (mysqld), now anon-rss:0kB, memory-backend:0kBpm.max_children PHP-FPM dan ulimit UNIX. Ketika batasan ini gagal atau diatur terlalu longgar untuk mengakomodasi lonjakan traffic sah, kernel Linux kehilangan kendali granular atas batas konsumsi memori per tenant. Untuk memutus siklus degradasi ini, kita harus beralih ke kontrol mekanis kernel: hierarki cgroups v2 dan pemanfaatan direktif memory.high sebagai rem proporsional sebelum hard limit mengorbankan stabilitas host.
Anatomi Kegagalan: Mengapa OOM Killer Memilih Korban yang Salah
mm/oom_kill.c. Fungsi penentu korban adalah oom_badness():
C
unsigned long oom_badness(struct task_struct *p, unsigned long totalpages)
{
long points;
long adj;
if (oom_task_origin(p))
return ULONG_MAX;
/* Proses yang tidak bisa di-kill (misal: init / PID 1) */
if (p->flags & PF_KTHREAD || is_global_init(p) || (p->flags & PF_NO_SETAFFINITY))
return 0;
adj = (long)p->signal->oom_score_adj;
if (adj == OOM_SCORE_ADJ_MIN)
return 0;
/* Titik mula penalti dihitung dari Resident Set Size (RSS) */
points = get_mm_rss(p->mm) + get_mm_counter(p->mm, MM_SWAPENTS) +
atomic_long_read(&p->mm->nr_ptes);
adj *= (long)totalpages / 1000;
points += adj;
return points > 0 ? points : 1;
}badness murni berdasarkan proporsi memori fisik (RSS), swap entries, dan page tables yang saat itu sedang dipegang oleh satu proses tunggal.
Di lingkungan shared hosting, pola serangan exploit PHP-FPM hampir selalu terdistribusi ke puluhan atau ratusan subproses (fork worker). Setiap worker PHP-FPM mungkin hanya mengonsumsi 60 MB hingga 120 MB RAM. Namun, jika ada 80 worker aktif yang mengalami memory bloat secara simultan, total memori yang dihisap tenant tersebut mencapai hampir 10 GB.
Ketika memori fisik host habis dan sistem memasuki status darurat, OOM Killer memindai seluruh process table. OOM Killer melihat mysqld berdiri tegak memegang buffer pool sebesar 8 GB sebagai satu PID tunggal. Sementara itu, 80 worker PHP-FPM tadi masing-masing hanya memegang ratusan megabyte. Hasilnya fatal: mysqld memiliki skor oom_badness paling tinggi di sistem, menjadikannya target paling empuk untuk dieliminasi, memicu cascading failure ke seluruh aplikasi di dalam server.
Membedah Mekanisme Kernel: memory.max vs memory.high
memory.limit_in_bytes. Di cgroups v2, model kontrol memori didesain ulang total untuk membedakan antara hard ceiling yang destruktif dan throttling brake yang adaptif.
| Kontrol Cgroup v2 | Reclaim Strategy | Dampak ke Proses | Respon OOM Killer |
|---|---|---|---|
memory.min | Protected memory | Kernel menolak merebut halaman anonim/file | Kebal OOM jika usage di bawah limit |
memory.low | Best-effort protection | Soft reclaim jika tekanan memori global tinggi | Diproteksi selama cgroup lain masih bisa di-reclaim |
memory.high | Proportional throttle | Direct reclaim sinkron + sleep penalty di kernel | Tidak memicu OOM Killer |
memory.max | Hard enforcement | Direct reclaim agresif sinkron | OOM Killer dipanggil jika reclaim nihil |
Cara Kerja Throttle memory.high di mm/memcontrol.c
memory.high, proses tidak langsung dihentikan atau di-kill. Sebaliknya, kernel Linux mengeksekusi fungsi mem_cgroup_handle_over_high() saat proses hendak kembali dari mode kernel ke userspace (misalnya setelah syscall mmap, brk, atau page fault handling).
Kernel menghitung seberapa jauh alokasi aktual melampaui memory.high dan mengonversinya menjadi durasi penundaan (delay penalty):
C
/* Logika konseptual perhitungan penalty pada kernel Linux */
overage = usage - high;
penalty = (overage * HZ * penalty_rate) / high;
schedule_timeout_killable(penalty);- Melambatkan Eksekusi Worker: Script exploit yang boros memori mendadak mengalami degradasi eksekusi masif. Loop alokasi memori yang tadinya memakan CPU time secara instan dipaksa tidur (sleep) hingga beberapa puluh milidetik per page fault.
- Memberikan Jendela Waktu untuk Direct Reclaim: Sembari proses ditahan di dalam kernel, thread pembersih
kswapddan direct reclaim internal cgroup memiliki waktu yang cukup untuk membersihkan page cache yang kotor dan men-swap halaman memori anonim yang dingin ke zRAM atau swap space tanpa membuat server freeze. - Backpressure ke Web Server: Karena worker PHP-FPM ter-throttle, socket backlog antara reverse proxy (Nginx) dan PHP-FPM upstream mulai terisi penuh. Nginx akan merespons request berlebih dengan HTTP 502/504 Bad Gateway alih-alih membiarkan request baru terus membanjiri pool tenant yang sakit.
memory.max, begitu worker mengeksekusi pemanggilan alokasi memori yang menabrak batas, direct reclaim akan langsung dijalankan secara panik. Jika direct reclaim gagal membebaskan halaman dalam sekian nanosekon, cgroup OOM Killer langsung membunuh worker PHP secara acak. Worker yang mati tiba-tiba ini sering kali meninggalkan file lock, transaksi database yang corrupt, atau state session setengah matang.
Perangkap Konfigurasi PHP-FPM Klasik
/etc/php-fpm.d/tenant.conf):
INI
[tenant_a]
user = tenant_a
group = tenant_a
pm = dynamic
pm.max_children = 50
pm.start_servers = 5
pm.min_spare_servers = 5
pm.max_spare_servers = 10
php_admin_value[memory_limit] = 256Mphp_admin_value[memory_limit]Bukan Jaminan Fisik: Parameter ini hanya menghitung memori yang dialokasikan oleh engine Zend (emalloc()). Alokasi memori yang dilakukan oleh library compiled C sepertiimagick, ekstensi parsing XML, database driver, atau library enkripsi libcmalloc()sering kali lolos dari perhitunganmemory_limitPHP.- Eksplosi RSS Akibat CoW (Copy-on-Write) Decay: Saat worker PHP-FPM pertama kali melakukan fork dari master process, mereka berbagi memori fisik melalui Copy-on-Write. Namun, seiring berjalannya waktu dan eksekusi request exploit yang acak, shared page tersebut pecah (dirty pages) menjadi private dirty RSS di masing-masing subproses. 50 worker yang tadinya berbagi 200 MB dapat membengkak menjadi 50 x 200 MB = 10 GB dalam beberapa detik.
- Exploit Spawn Process: Jika celah keamanan memungkinkan penyerang menjalankan fungsi seperti
shell_exec()atauproc_open(), subproses eksternal yang di-spawn sama sekali tidak tunduk padamemory_limitPHP runtime.
Implementasi Hierarki Cgroups v2 via Systemd Slices
systemd-slice untuk mengelompokkan setiap pool tenant shared hosting ke dalam batas isolasi yang kedap.
Struktur Hierarki Slice
TEXT
-.slice (Root)
+-- system.slice (sshd, mysqld, nginx)
+-- hosting.slice (Total alokasi shared hosting)
+-- hosting-tenant1001.slice
| +-- php-fpm-pool-tenant1001.service
+-- hosting-tenant1002.slice
+-- php-fpm-pool-tenant1002.serviceKonfigurasi Induk: /etc/systemd/system/hosting.slice
SYSTEMD
[Unit]
Description=Shared Hosting Master Slice
Before=slices.target
[Slice]
# Mengamankan minimal 4 GB untuk sistem dari total 32 GB RAM host
MemoryAccounting=yes
MemoryHigh=24G
MemoryMax=26G
MemorySwapMax=4G
CPUAccounting=yes
IOAccounting=yesTemplate Tenant Slice Otomatis: /etc/systemd/system/hosting-.slice.d/override.conf
memory.high di 1.5 GB dan memory.max di 2.0 GB:
SYSTEMD
[Slice]
MemoryAccounting=yes
# Threshold perlambatan agresif dan direct reclaim bertahap
MemoryHigh=1536M
# Batas absolut hard ceiling (OOM trigger spesifik ke slice ini saja)
MemoryMax=2048M
# Batasi konsumsi swap agar tidak melumpuhkan I/O disk NVMe
MemorySwapMax=512M
# Bobot alokasi CPU saat terjadi contention (default 100)
CPUWeight=80
# Proteksi I/O read/write writeback contention
IOWeight=80Mengintegrasikan PHP-FPM Pool ke dalam Systemd Slice
php-fpm.service yang raksasa, pisahkan setiap pool tenant menjadi systemd service instance tersendiri menggunakan template unit.
Buat file template unit /etc/systemd/system/[email protected]:
SYSTEMD
[Unit]
Description=PHP-FPM Pool untuk Tenant %I
After=syslog.target network.target
PartOf=hosting.slice
[Service]
Type=notify
Slice=hosting-%I.slice
PIDFile=/run/php-fpm/php-fpm-%I.pid
ExecStart=/usr/sbin/php-fpm --nodaemonize --fpm-config /etc/php-fpm.d/%I.conf
ExecReload=/bin/kill -USR2 $MAINPID
PrivateTmp=true
ProtectSystem=full
ProtectHome=read-only
ReadOnlyPaths=/usr /bin /sbin
ReadWritePaths=/run /var/log/php-fpm /home/%I
Restart=on-failure
RestartSec=3s
[Install]
WantedBy=multi-user.target/etc/php-fpm.d/tenant1001.conf hanya perlu mengarahkan socket dan user UNIX yang bersangkutan:
INI
[tenant1001]
user = tenant1001
group = tenant1001
listen = /run/php-fpm/tenant1001.sock
listen.owner = nginx
listen.group = nginx
listen.mode = 0660
pm = dynamic
pm.max_children = 40
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 8
pm.max_requests = 1000
; Logging telemetry
access.log = /var/log/php-fpm/tenant1001.access.log
slowlog = /var/log/php-fpm/tenant1001.slow.log
request_slowlog_timeout = 5sBASH
systemctl daemon-reload
systemctl enable --now [email protected][email protected] di dalam hosting-tenant1001.slice. Jika alokasi tetap melonjak hingga 2.0 GB dan reclaim gagal, OOM killer hanya akan membunuh worker di dalam slice tersebut tanpa menyentuh tenant lain, apalagi mysqld host!
Observabilitas & Telemetry: Menangkap Gejala Throttle Sebelum Bencana
Membaca memory.events dan memory.stat dari Cgroup
BASH
# Periksa counter event pada slice tenant 1001
cat /sys/fs/cgroup/hosting.slice/hosting-tenant1001.slice/memory.eventsTEXT
low 0
high 14829
max 0
oom 0
oom_kill 0
oom_group_kill 0high 14829: Tenant ini telah menyentuh batasmemory.highsebanyak 14.829 kali. Kernel telah berkali-kali menjatuhkan delay penalty dan sukses membersihkan halaman memori via direct reclaim.max 0danoom 0: Hard limit tidak pernah tertembus, dan OOM Killer sama sekali tidak pernah diaktifkan!
Mengukur Pressure Stall Information (PSI)
memory.pressure menunjukkan persentase waktu terbuang di mana proses terhambat (stalled) menunggu alokasi memori:
BASH
cat /sys/fs/cgroup/hosting.slice/hosting-tenant1001.slice/memory.pressureTEXT
some avg10=24.50 avg60=8.12 avg300=1.05 total=8492011
full avg10=12.10 avg60=3.20 avg300=0.40 total=2109400full avg10 melompat di atas 10%, artinya seluruh thread di dalam slice tersebut tidak dapat melakukan komputasi CPU karena terkunci menunggu kswapd/direct reclaim membebaskan halaman memori. Ini adalah sinyal presisi bagi tim support hosting untuk segera mengisolasi atau mengabari tenant bahwa website mereka mengalami anomali resource.
Melacak Direct Reclaim Latency Menggunakan Bpftrace
try_to_free_mem_cgroup_pages. Simpan skrip berikut sebagai reclaim_latency.bt:
C
#!/usr/bin/env bpftrace
#include <linux/memcontrol.h>
kprobe:try_to_free_mem_cgroup_pages
{
@start[tid] = nsecs;
}
kretprobe:try_to_free_mem_cgroup_pages
/@start[tid]/
{
$duration_us = (nsecs - @start[tid]) / 1000;
@reclaim_latency_us = hist($duration_us);
delete(@start[tid]);
}
interval:s:10
{
print(@reclaim_latency_us);
clear(@reclaim_latency_us);
}BASH
bpftrace reclaim_latency.btTEXT
@reclaim_latency_us:
[16, 32) 4 |@ |
[32, 64) 18 |@@@@@ |
[64, 128) 142 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
[128, 256) 89 |@@@@@@@@@@@@@@@@@@@@@@@ |
[256, 512) 31 |@@@@@@@@ |
[512, 1024) 6 |@ |
[1024, 2048) 2 | |Penyetelan Sysctl untuk Ketahanan Host
vm) global agar tidak panik saat salah satu slice tenant mengalami turbulensi reclaim.
Tambahkan konfigurasi berikut ke /etc/sysctl.d/99-shared-hosting-vm.conf:
INI
# Nonaktifkan overcommit memori yang ugal-ugalan
# Nilai 0 = Heuristic overcommit (default kernel)
# Untuk hosting padat, tetap gunakan 0 tetapi pantau watermarks
vm.overcommit_memory = 0
# Tingkatkan watermark scale factor untuk memulai kswapd lebih awal
# Default kernel 10 (0.1% dari RAM). Naikkan ke 50 (0.5% dari RAM)
# Ini memberi kswapd buffer lebih tebal sebelum terpaksa direct reclaim
vm.watermark_scale_factor = 50
# Kontrol agresivitas swap anonim vs page cache reclaim (default 60)
# Di shared hosting dengan SSD/NVMe cepat, pertahankan di rentang 30-40
# agar kernel lebih memprioritaskan membersihkan file cache yang aman di-evict
vm.swappiness = 30
# Cegah kswapd mengorbankan dentries dan inodes VFS terlalu agresif
# Nilai 50 menjaga metadata filesystem tetap hangat di RAM
vm.vfs_cache_pressure = 50
# Paksa OOM Killer mematikan seluruh proses dalam cgroup jika menabrak memory.max
# 1 = Panic/Kill seluruh task cgroup, mencegah zombie loop
vm.oom_dump_tasks = 1BASH
sysctl --systemBASH
# Untuk mysqld/mariadb
systemctl edit mysqldSYSTEMD
[Service]
# Lindungi mysqld dari eliminasi OOM Killer global
OOMScoreAdjust=-900sshd:
SYSTEMD
[Service]
OOMScoreAdjust=-1000oom_score_adj ke -900 atau -1000, kernel secara eksplisit dilarang mengeksekusi kedua daemon vital ini selama krisis memori global belum mencapai titik absolut di mana tidak ada proses lain yang tersisa di muka bumi.
Skrip Automasi Pemantauan Tekanan Slice (Auto-Remediation)
memory.high selama berjam-jam tetap membuang resource CPU cycle. Kita membutuhkan daemon ringan yang mengevaluasi metrics PSI dan merespons anomali secara elegan.
Berikut skrip bash daemon produksi /usr/local/bin/slice-psi-monitor.sh yang memanfaatkan antarmuka epoll/inotify atau pooling interval untuk mendeteksi cgroup yang terjebak dalam loop reclaim intensif:
BASH
#!/usr/bin/env bash
set -euo pipefail
BASE_CGROUP="/sys/fs/cgroup/hosting.slice"
THRESHOLD_FULL_SOME=15.00 # 15% stall pressure
LOG_FILE="/var/log/hosting-remediation.log"
echo "[(date -Iseconds)] Memulai Slice PSI Monitoring Daemon..." >> "LOG_FILE"
while true; do
for slice_path in "$BASE_CGROUP"/hosting-*.slice; do
[ -d "$slice_path" ] || continue
slice_name=(basename "slice_path")
pressure_file="$slice_path/memory.pressure"
events_file="$slice_path/memory.events"
if [ -f "$pressure_file" ]; then
# Ambil nilai some avg10
some_avg10=(grep 'some' "pressure_file" | awk '{print $2}' | cut -d'=' -f2)
# Evaluasi apakah pressure melebihi threshold aman
if (( (echo "some_avg10 > $THRESHOLD_FULL_SOME" | bc -l) )); then
oom_count=(grep 'high' "events_file" | awk '{print $2}')
echo "[(date -Iseconds)] WARNING:slice_name stall avg10=some_avg10% (high_events=oom_count)" >> "$LOG_FILE"
# Ambil instance systemd yang berasosiasi
tenant_id=${slice_name#hosting-}
tenant_id=${tenant_id%.slice}
service_name="php-fpm-tenant@${tenant_id}.service"
# Reload graceful PHP-FPM pool untuk me-reap worker bengkak tanpa drop connection kasar
echo "[(date -Iseconds)] ACTION: Graceful reloadservice_name" >> "$LOG_FILE"
systemctl reload "$service_name" || true
# Cooldown untuk mencegah thundering reload loop
sleep 5
fi
fi
done
sleep 10
done/etc/systemd/system/slice-psi-monitor.service:
SYSTEMD
[Unit]
Description=Automated Shared Hosting Slice PSI Remediation Daemon
After=network.target
[Service]
Type=simple
ExecStart=/usr/local/bin/slice-psi-monitor.sh
Restart=always
RestartSec=5s
User=root
[Install]
WantedBy=multi-user.targetBASH
chmod +x /usr/local/bin/slice-psi-monitor.sh
systemctl daemon-reload
systemctl enable --now slice-psi-monitor.serviceEvaluasi Kinerja & Trade-Off di Lingkungan Produksi
memory.high cgroups v2 membawa serangkaian trade-off operasional yang harus dipahami oleh Principal Architect:
1. Latensi Tail HTTP (p99 dan p99.9) Meningkat Signifikan pada Tenant Bermasalah
memory.high, kernel sengaja menunda eksekusi syscall proses tersebut. Pengunjung situs tenant yang bersangkutan akan melihat response time melonjak dari 150 ms menjadi 2.000 ms - 5.000 ms. Ini adalah intended behavior (desain yang disengaja): lebih baik situs tenant yang terinfeksi malware merespons lambat atau timeout, daripada membuat node cluster crash dan merusak service ratusan tenant lain yang tidak bersalah.
2. Overhead CPU Akibat Direct Reclaim Loop
memory.high dan memory.max dipatok terlalu berdekatan (misalnya selisih kurang dari 64 MB), dan workload tenant terus mencoba mengalokasikan dirty memory, kernel akan menghabiskan CPU cycle dalam jumlah masif untuk mencoba membersihkan halaman memori yang sebenarnya tidak bisa dibersihkan (unreclaimable anon memory). Pastikan selalu memberi jarak aman (headroom) minimal 20% hingga 30% antara memory.high dan memory.max (contoh: high di 1536M dan max di 2048M) agar kernel memiliki ruang gerak sebelum terpaksa mengambil tindakan terminasi.
3. Kompatibilitas dengan Storage Engine Non-Buffered I/O
memory.high dapat memicu stall pada thread write engine InnoDB (khususnya page flushing). Oleh karena itu, arsitektur shared hosting modern harus selalu memisahkan database daemon ke slice sistem terlindung (system.slice) dengan reservasi memori yang dijamin oleh MemoryLow atau MemoryMin.
Melalui orkestrasi cgroups v2, integrasi slice systemd, dan kontrol memory.high, kita mengubah watak brutal kernel Linux dari algojo serampangan menjadi pengatur lalu lintas memori yang tenang dan terkontrol. Serangan exploit atau memory leak di satu situs WordPress tidak akan lagi mampu merobohkan benteng shared hosting Anda.