pm = dynamic) hanya membatasi alokasi heap level user-space melalui direktif memory_limit di dalam file php.ini. Direktif ini dieksekusi oleh Zend Engine Memory Manager (Zend MM). Zend MM tidak memiliki visibilitas terhadap page cache kernel, alokasi memori ekstensi biner C (seperti ImageMagick, cURL, atau driver database), fragmentasi memori libc allocator (ptmalloc), atau lonjakan konsumsi memori akibat anonymous pages gabungan dari puluhan child worker yang beroperasi secara konkuren di dalam pool yang sama.
Ketika beberapa tenant di-hosting dalam satu instans sistem operasi yang sama, satu tenant dengan beban alokasi memori abnormal dapat mengonsumsi seluruh memori fisik yang tersedia. Ketika alokasi global melampaui kapasitas RAM dan swap, Linux Out-Of-Memory (OOM) Killer akan aktif melalui fungsi out_of_memory() di kernel. OOM Killer menghitung nilai badness pada setiap proses dan mengeksekusi SIGKILL secara indiscriminatif terhadap proses dengan footprint memori terbesar, yang sering kali merupakan database daemon (mysqld), web server (nginx), atau worker PHP-FPM milik tenant lain yang beroperasi normal.
Untuk mengeliminasi dampak noisy neighbor dan mengisolasi alokasi memori secara deterministik, arsitektur isolasi harus dipindahkan dari runtime PHP ke level kernel Linux menggunakan unified cgroup hierarchy (cgroups v2) dengan perantara systemd slice.
Untuk analisis mendalam terkait dampak cascading OOM pada level cluster hosting, lihat juga arsitektur yang dibahas pada Mencegah OOM Cascading di Shared Hosting: Konfigurasi Cgroups v2 memory.high untuk Menahan Ledakan Worker PHP-FPM.
Limitasi Zend MM dan Kegagalan Boundary memory_limit
mmap() atau brk(), kemudian membaginya ke dalam chunks internal berukuran 2 MB (yang dibagi lagi menjadi pages 4 KB). Parameter memory_limit hanya memeriksa alokasi internal Zend MM:
C
/* Zend/zend_alloc.c (Zend Memory Manager excerpt) */
static zend_always_inline void *zend_mm_alloc_pages(zend_mm_heap *heap, size_t pages_count, size_t alignment)
{
size_t size = pages_count * ZEND_MM_PAGE_SIZE;
if (UNEXPECTED(heap->real_size + size > heap->limit)) {
if (zend_mm_gc(heap) && heap->real_size + size <= heap->limit) {
/* Alokasi berhasil setelah garbage collection */
} else {
zend_error_noreturn(E_ERROR, "Allowed memory size of %zu bytes exhausted", heap->limit);
}
}
/* ... pemanggilan OS allocator ... */
}- Ekstensi C Melompati Zend MM: Pustaka seperti
libjpeg,libxml2, atauvipssering mengalokasikan memori langsung melaluimalloc()bawaan libc sistem operasi atau memori mmap langsung. Alokasi ini sepenuhnya invisible bagiheap->real_sizemilik Zend MM. - Multiplikasi Worker Pool: Jika suatu pool dikonfigurasi dengan
pm.max_children = 50danmemory_limit = 256M, footprint memori teoritis yang dapat dialokasikan oleh satu pool adalah 12,8 GB. Lonjakan lalu lintas mendadak pada satu tenant dapat melipatgandakan worker aktif hingga mencapai ambang batas fisik mesin tanpa memicu satupun errorAllowed memory size exhausteddi level skrip individual. - Fragmentasi Memori Native: Alokasi dan dealokasi berulang terhadap objek berukuran heterogen menyebabkan heap fragmentation pada glibc. Memori yang sudah di-
free()oleh PHP tidak selalu langsung dikembalikan ke kernel melaluimadvise(MADV_DONTNEED)atausbrk(), mempertahankan ukuran Resident Set Size (RSS) proses tetap tinggi.
Arsitektur cgroups v2 Memory Controller: memory.high vs memory.max
memory.limit_in_bytes. Ketika sebuah cgroup menyentuh batas ini, kernel langsung memicu OOM Killer lokal di dalam cgroup tersebut jika memori tidak dapat di-reclaim. Ini menimbulkan terminasi mendadak pada proses PHP-FPM tanpa adanya mekanisme graceful backpressure.
Pada cgroups v2, model kontrol memori dirombak menjadi empat boundary hirarkis: memory.min, memory.low, memory.high, dan memory.max.
| Boundary Parameter | Tipe Pembatas | Reaksi Kernel Saat Dilampaui | Dampak pada Worker PHP-FPM |
|---|---|---|---|
memory.min | Hard Protection | Tidak ada reclaim; memori dilindungi penuh | Mencegah worker terkena reclaim saat load tinggi |
memory.low | Soft Protection | Best-effort reclaim; dihindari selama ada cgroup lain | Proteksi elastis selama sistem global stabil |
memory.high | Soft Limit / Throttling | Kernel memicu direct reclaim sinkron dan penundaan alokasi (throttling) | Eksekusi PHP melambat; proses tidak di-kill; socket queue Nginx tertahan |
memory.max | Hard Limit | OOM Killer aktif lokal pada cgroup jika reclaim gagal | Worker PHP-FPM dihentikan paksa via SIGKILL |
memory.high dan memory.max terletak pada alur penanganan saat threshold tercapai. memory.high adalah mekanisme rate-limiting preventif. Ketika alokasi agregat dari seluruh worker di dalam satu cgroup melewati memory.high, kernel tidak menghentikan proses. Sebaliknya, kernel memaksa proses yang sedang meminta alokasi tersebut untuk mengeksekusi in-thread direct reclaim dan menambahkan sleep latency proporsional sebelum alokasi disetujui.
Mekanisme Kernel: Allocator Path, Direct Reclaim, dan Asynchronous Throttling
alloc_pages()), kernel mengeksekusi pemanggilan fungsi pengecekan cgroup pada subsystem memcg:
TEXT
alloc_pages()
-> __alloc_pages_slowpath()
-> try_charge()
-> mem_cgroup_handle_over_high()
-> memory_high_reclaim()
-> schedule_timeout_killable()try_charge() pada mm/memcontrol.c, kernel menghitung apakah penambahan page ini menyebabkan cgroup melampaui nilai yang ditentukan pada memory.high. Jika terlampaui, bit penanda disetel pada struct task_struct milik thread yang bersangkutan, dan eksekusi diarahkan ke mem_cgroup_handle_over_high() sebelum thread kembali ke user-space.
C
/* mm/memcontrol.c excerpt (Linux Kernel) */
void mem_cgroup_handle_over_high(void)
{
unsigned long penalty_jiffies;
unsigned long overage;
struct mem_cgroup *memcg;
memcg = get_mem_cgroup_from_current();
if (!memcg)
return;
/* Memicu direct reclaim lokal di level cgroup */
mem_cgroup_reclaim_high(memcg, GFP_KERNEL, MEMCG_RECLAIM_SHRINKER);
overage = mem_cgroup_over_high(memcg);
if (!overage)
goto out;
/* Hitung penalty latency proporsional terhadap overage */
penalty_jiffies = calculate_over_high_delay(memcg, overage);
schedule_timeout_killable(penalty_jiffies);
out:
css_put(&memcg->css);
}- Direct Reclaim Lokal: Alih-alih mendelegasikan pembersihan page cache ke daemon latar belakang
kswapd, thread worker PHP-FPM sendiri yang dipaksa membuang inactive file pages (misalnya cache file skrip PHP yang di-include) atau menulis dirty pages ke disk. Langkah ini langsung memperlambat runtime eksekusi worker tersebut. - Dynamic Sleep Penalty (Throttling): Fungsi
calculate_over_high_delay()menghitung selisih memori aktual terhadap batasmemory.high. Makin jauh batas tersebut dilanggar, makin lama durasischedule_timeout_killable()yang dikenakan pada thread (dapat mencapai hingga 2 detik per cycle). Worker akan berada pada statusTASK_KILLABLE(tidak menggunakan CPU, tetapi thread diblokir).
listen() backlog) akan terisi penuh. Nginx kemudian akan mengembalikan response HTTP 502/504 kepada client tenant yang bermasalah tersebut, sementara memori fisik server secara global tetap terlindungi dari OOM cascade.
Integrasi systemd Slices untuk Worker Pool Multi-Tenant
/sys/fs/cgroup/ tidak disarankan karena systemd mengelola cgroup tree secara eksklusif. Setiap modifikasi manual dapat ditimpa oleh cgroup synchonization loop milik systemd.
Struktur isolasi yang tepat mengandalkan unit berjenis .slice. Slice membentuk node hirarkis di cgroups v2. Setiap tenant dialokasikan ke dalam satu slice independen yang mewarisi atau mendefinisikan pembatasan resource.
Hirarki cgroups v2 yang terbentuk di bawah systemd direpresentasikan sebagai berikut:
TEXT
root
|
+-- system.slice
+-- php-fpm.service (master process runner)
|
+-- tenant-alpha.slice
| +-- [email protected]
| +-- php-fpm (worker 1)
| +-- php-fpm (worker 2)
| +-- php-fpm (worker N)
|
+-- tenant-beta.slice
+-- [email protected]
+-- php-fpm (worker 1)
+-- php-fpm (worker N)[email protected]) di dalam slice terpisah, kita memisahkan proses master PHP-FPM per tenant. Langkah ini lebih aman dibandingkan menjalankan satu proses master PHP-FPM global yang memuat banyak pool, karena master process tunggal yang berjalan di bawah satu service unit tidak dapat memisahkan worker-nya ke dalam cgroup yang berbeda secara otomatis tanpa intervensi eksternal.
Konfigurasi Produksi: PHP-FPM Pool, systemd Slice, dan System Resource Protection
memory.high dan fallback memory.max.
1. Definisi Tenant Slice (/etc/systemd/system/tenant-alpha.slice)
memory.high sebesar 1.5 GB dan memory.max sebesar 2.0 GB. Jika tenant menyentuh 1.5 GB, direct reclaim dan penundaan alokasi terjadi. OOM Killer hanya akan dipicu jika konsumsi menembus 2.0 GB.
INI
[Unit]
Description=Resource Slice untuk Tenant Alpha
DefaultDependencies=no
Before=slices.target
[Slice]
# Aktifkan CPU, Memory, dan IO Accounting
MemoryAccounting=yes
CPUAccounting=yes
IOAccounting=yes
# cgroups v2 memory boundaries
MemoryHigh=1536M
MemoryMax=2048M
# Batasi alokasi swap agar tidak membanjiri swap device (max 512M swap per tenant)
MemorySwapMax=512M
# Cegah fork-bomb atau eksploitasi thread pool tak terbatas
TasksMax=1282. Service Template PHP-FPM Per-Tenant (/etc/systemd/system/[email protected])
@), memetakan tenant ke slice yang sesuai secara dinamis menggunakan variabel %I.
INI
[Unit]
Description=PHP-FPM Worker Pool untuk %I
After=network.target
PartOf=%I.slice
[Service]
Type=notify
Slice=%I.slice
PIDFile=/run/php/php8.2-fpm-%I.pid
ExecStart=/usr/sbin/php-fpm8.2 --nodaemonize --fpm-config /etc/php/8.2/fpm/pool.d/%I.conf
ExecReload=/bin/kill -USR2 $MAINPID
# Konfigurasi isolasi keamanan OS
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
ProtectKernelTunables=yes
ProtectControlGroups=yes
# OOM Policy: Pastikan hanya worker/master di dalam slice ini yang dihentikan jika menyentuh MemoryMax
OOMPolicy=stop
OOMScoreAdjust=500
# Restart policy jika proses crash atau terkena hard OOM
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.target3. Konfigurasi Pool PHP-FPM (/etc/php/8.2/fpm/pool.d/tenant-alpha.conf)
INI
[tenant-alpha]
user = tenant-alpha
group = tenant-alpha
listen = /run/php/php-fpm-tenant-alpha.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
listen.backlog = 1024
pm = dynamic
pm.max_children = 40
pm.start_servers = 5
pm.min_spare_servers = 5
pm.max_spare_servers = 10
pm.max_requests = 1000
; Batasi Zend MM heap per worker sebagai defense-in-depth
php_admin_value[memory_limit] = 256M
; Nonaktifkan fungsi sistem yang berpotensi memicu fork di luar kontrol cgroup
php_admin_value[disable_functions] = exec,passthru,shell_exec,system,proc_open,popenBASH
systemctl daemon-reload
systemctl enable --now tenant-alpha.slice
systemctl enable --now [email protected]BASH
# Periksa apakah parameter memory.high dan memory.max terpasang dengan tepat
cat /sys/fs/cgroup/tenant-alpha.slice/memory.high
# Output: 1610612736 (1536M)
cat /sys/fs/cgroup/tenant-alpha.slice/memory.max
# Output: 2147483648 (2048M)
# Periksa proses PHP-FPM yang berada di dalam cgroup ini
cat /sys/fs/cgroup/tenant-alpha.slice/[email protected]/cgroup.procsObservabilitas: bpftrace, PSI, dan Analisis Telemetri Kernel
1. Membaca Pressure Stall Information (PSI)
memory.high).
BASH
cat /sys/fs/cgroup/tenant-alpha.slice/memory.pressureTEXT
some avg10=4.12 avg60=1.85 avg300=0.45 total=12548902
full avg10=1.05 avg60=0.32 avg300=0.08 total=3210452some: Persentase waktu ketika setidaknya satu thread pada cgroup mengalami stall memori (reclaim delay).full: Persentase waktu ketika seluruh thread di dalam cgroup terblokir oleh stall memori (seluruh worker pool berhenti melayani trafik).
some avg10 melampaui rentang 15–20% secara konsisten, tenant tersebut sedang mengalami penundaan eksekusi yang signifikan akibat menabrak memory.high.
2. Tracing Kernel Direct Reclaim Menggunakan bpftrace
bpftrace berikut memantau eksekusi mem_cgroup_handle_over_high secara real-time, menampilkan PID worker, path cgroup, dan waktu yang dihabiskan dalam status kernel throttle:
BASH
cat << 'EOF' > trace_memcg_throttle.bt
#include <linux/memcontrol.h>
kprobe:mem_cgroup_handle_over_high
{
$memcg = (struct mem_cgroup *)arg0;
@start[tid] = nsecs;
}
kretprobe:mem_cgroup_handle_over_high
/@start[tid]/
{
$duration_us = (nsecs - @start[tid]) / 1000;
printf("COMM: %-16s PID: %-6d THROTTLE DURATION: %llu us
",
comm, pid, $duration_us);
delete(@start[tid]);
}
END
{
clear(@start);
}
EOF
bpftrace trace_memcg_throttle.btTEXT
Attaching 2 probes...
COMM: php-fpm8.2 PID: 142105 THROTTLE DURATION: 28412 us
COMM: php-fpm8.2 PID: 142106 THROTTLE DURATION: 45120 us
COMM: php-fpm8.2 PID: 142108 THROTTLE DURATION: 102450 us3. Membedakan Throttling vs OOM Invocations pada Log Kernel
memory.high aktif, kernel menginkrementasi counter high pada file memory.events cgroup dan TIDAK membuang stack trace ke syslog/dmesg:
BASH
cat /sys/fs/cgroup/tenant-alpha.slice/memory.eventsTEXT
low 0
high 14820
max 0
oom 0
oom_kill 0memory.max terlampaui dan kernel gagal melakukan reclaim terhadap anonymous pages melalui swap, OOM Killer lokal terpicu. Output dmesg akan menunjukkan jejak OOM yang terkungkung dalam slice target, bukan pada level sistem:
TEXT
[ 4128.910244] php-fpm8.2 invoked oom-killer: gfp_mask=0x1100cca(GFP_HIGHUSER_MOVABLE), order=0, oom_score_adj=500
[ 4128.910251] CPU: 3 PID: 142109 Comm: php-fpm8.2 Not tainted 6.1.0-21-amd64 #1 Debian 6.1.90-1
[ 4128.910255] Hardware name: Supermicro SYS-E300-9D/X11SDV-4C-TLN2F, BIOS 1.4 03/12/2020
[ 4128.910258] Call Trace:
[ 4128.910260] <TASK>
[ 4128.910264] dump_header+0x4a/0x200
[ 4128.910271] oom_kill_process.cold+0xb/0x10
[ 4128.910275] out_of_memory+0x228/0x560
[ 4128.910281] mem_cgroup_out_of_memory+0x13c/0x160
[ 4128.910286] try_charge_memcg+0x628/0x7b0
[ 4128.910291] mem_cgroup_charge+0xc2/0x2c0
[ 4128.910296] __handle_mm_fault+0x951/0xe90
[ 4128.910302] handle_mm_fault+0xda/0x2c0
[ 4128.910307] do_user_addr_fault+0x1b5/0x650
[ 4128.910313] exc_page_fault+0x76/0x170
[ 4128.910318] asm_exc_page_fault+0x22/0x30
[ 4128.910324] </TASK>
[ 4128.910326] memory: usage 2097152kB, limit 2097152kB, failcnt 1204
[ 4128.910328] swap: usage 524288kB, limit 524288kB, failcnt 32
[ 4128.910330] Memory cgroup out of memory: Killed process 142109 (php-fpm8.2) total-vm:458210kB, anon-rss:210450kB, file-rss:1240kB, shmem-rss:0kB
[ 4128.921402] oom_reaper: reaped process 142109 (php-fpm8.2), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kBMemory cgroup out of memory: Killed process 142109. Ini menegaskan bahwa OOM killer beroperasi strictly di level cgroup-scoped, bukan sistem global. Nginx master, MySQL, dan pool tenant lain di luar tenant-alpha.slice tidak terganggu sama sekali.
Benchmark dan Pengukuran Overhead Latensi Kernel Reclaim
memory.high tercapai, skenario pengujian beban disimulasikan menggunakan generator beban HTTP (wrk) dengan skrip PHP yang mengeksekusi pembacaan file dataset besar ke dalam array memori.
Kondisi Pengujian:
- Tenant Target (Tenant Alpha): 40 worker concurrency, alokasi memori sengaja dinaikkan hingga menyentuh 1.8 GB (melewati
memory.high1.5 GB, di bawahmemory.max2.0 GB). - Tenant Pembanding (Tenant Beta): Pool normal yang melayani endpoint API statis dengan alokasi memori rendah (< 200 MB), berjalan di slice terpisah pada server fisik yang sama.
- Baseline: Tanpa cgroups limits (hanya mengandalkan
memory_limit = 512Mper worker). - Hard-Limit Saja: Menggunakan cgroups v2 dengan konfigurasi kaku (
memory.max = 1.5G, tanpamemory.high). - Dual-Boundary: Menggunakan konfigurasi bertingkat (
memory.high = 1.5G,memory.max = 2.0G).
| Metrik Performa | Baseline (No Cgroup Limit) | Hard-Limit Only (MemoryMax=1.5G) | Dual-Boundary (High=1.5G, Max=2.0G) |
|---|---|---|---|
| Global System OOM Invocations | 3 kali (Nginx & DB killed) | 0 kali | 0 kali |
| Tenant Alpha HTTP Success Rate | 34.2% (OOM crash) | 68.5% (Worker SIGKILL) | 98.9% (Throttled/Latent) |
| Tenant Alpha Latency p99 | 142 ms (sebelum crash) | 2,450 ms (error burst) | 840 ms (controlled reclaim) |
| Tenant Alpha Process Resets | Seluruh pool down | 148 worker terminates | 0 worker terminates |
| Tenant Beta Latency p99 | 1,890 ms (terimbas OOM) | 12.4 ms (terisolasi) | 12.8 ms (terisolasi) |
| System PSI Memory Stall (some) | 48.2% (global) | 3.1% (lokal slice) | 4.2% (lokal slice) |
Analisis Karakteristik Trade-Off:
- Eliminasi OOM Cascading: Kedua konfigurasi cgroups (Hard-Limit dan Dual-Boundary) berhasil melindungi Tenant Beta secara mutlak. Latensi Tenant Beta tetap stabil di kisaran ~12 ms tanpa fluktuasi, terlepas dari fakta bahwa mesin sedang mengalami saturasi memori pada cgroup Tenant Alpha.
- Kestabilan Koneksi vs Latensi pada Tenant Bermasalah: Pada skenario Hard-Limit Only, setiap kali batas 1.5 GB tersentuh, kernel langsung membunuh worker aktif. Klien HTTP menerima response
502 Bad Gatewaykarena koneksi TCP terputus sebelum worker selesai memproses request. Pada model Dual-Boundary (memory.high), response time p99 Tenant Alpha melonjak menjadi 840 ms akibat alur eksekusimem_cgroup_handle_over_high(), namun koneksi HTTP diselesaikan tanpa error (200 OK) dan worker pool tidak perlu melakukan siklus spawning ulang yang mengonsumsi CPU time secara berlebihan. - Overhead CPU Reclaim: Penerapan
memory.highmemindahkan beban pembersihan memori ke thread worker itu sendiri. Pada saat throttling aktif, penggunaan CPU time pada thread worker dalam konteks kernel (sysCPU utilization) akan meningkat antara 5% hingga 12% karena worker berulang kali memindai Least Recently Used (LRU) list di kernel untuk me-reclaim inactive file pages.
memory.high memberikan kontrol deterministik terhadap isolasi memori multi-tenant. Penggunaan mekanisme throttling kernel ini memitigasi kegagalan runtime yang diakibatkan oleh keterbatasan Zend MM, serta mengeliminasi risiko degradasi performa sistem secara menyeluruh akibat OOM Killer.