Bali, Indonesia
Insights

Runbook Migrasi PHP-FPM ke FrankenPHP Worker Mode: Menghadapi Memory Leak dan Stateful Worker di Lingkungan Produksi

October 13, 2026
0:00
Model eksekusi PHP tradisional berbasis shared-nothing melalui FastCGI Process Manager (PHP-FPM) telah menjadi standar industri selama belasan tahun. Pendekatan ini aman karena setiap request HTTP membersihkan seluruh alokasi memori, variabel global, dan siklus hidup objek setelah respons dikirimkan. Namun, pada framework modern seperti Laravel atau Symfony, overhead untuk melakukan bootstrap framework—membaca ratusan file PHP, mengurai dependency injection container, serta memuat konfigurasi berulang kali—memakan 70 hingga 85 persen dari total waktu respons server. FrankenPHP, yang dibangun di atas server web Caddy dan memanfaatkan integrasi CGO langsung ke engine libphp, menghadirkan worker mode. Dalam mode ini, framework diinisialisasi hanya satu kali di dalam memori saat worker pertama kali berjalan, kemudian worker tersebut terus melayani ribuan request berikutnya secara berurutan. Transformasi ini mengubah karakteristik performa secara drastis, tetapi memperkenalkan tantangan baru: risiko kebocoran memori (memory leak), kontaminasi variabel antar-request (state leakage), dan degradasi kestabilan worker jika tidak dikelola dengan prosedur operasional yang presisi. Berikut adalah panduan eksekusi dan runbook operasional untuk memigrasikan beban kerja produksi skala besar dari PHP-FPM ke FrankenPHP worker mode secara aman.
Sebelum melakukan cutover, tim operasional sistem harus memahami perbedaan mendasar siklus hidup proses di antara kedua runtime ini:
Parameter EvaluasiNginx + PHP-FPM TradisionalFrankenPHP Worker Mode
Model KomunikasiFastCGI via Unix Socket / TCP LoopbackIn-Process Memory Call (CGO libphp)
Siklus Hidup FrameworkBootstrapped per setiap HTTP requestBootstrapped satu kali saat inisialisasi worker
Overhead Cold-Start15ms - 45ms per request (Framework berat)0ms (Tersedia langsung di RAM)
Konsumsi CPU BaselineTinggi (Kompilasi opcode / autoloader loop)Rendah (Hanya pemrosesan business logic)
Risiko State ContaminationNol (Proses dibersihkan total per request)Tinggi (Objek statis & singleton tetap hidup)
Manajemen MemoriOtomatis dibebaskan saat request terminationManual / Bergantung pada Garbage Collection & cycle restart
Dukungan Protokol EdgeBergantung pada Nginx (HTTP/1.1, HTTP/2)Caddy Native (HTTP/1.1, HTTP/2, HTTP/3 QUIC)
TEXT
+-----------------------------------------------------------------------------------+
|              ALIRAN REQUEST RUNTIME: PHP-FPM VS FRANKENPHP WORKER                 |
+-----------------------------------------------------------------------------------+

[Model PHP-FPM Tradisional]
Client Request ---> Nginx ---> FastCGI Protocol ---> PHP-FPM Pool Worker
                                                      |--> Bootstrap Framework
                                                      |--> Eksekusi Logika
                                                      |--> Teardown & Free Memory (Die)

[Model FrankenPHP Worker Mode]
Client Request ---> Caddy Engine (Go) ---> Direct CGO Call ---> Persistent Worker Thread
                                                                 |--> (Framework Sudah Ada di RAM)
                                                                 |--> Reset Request State
                                                                 |--> Eksekusi Logika
                                                                 |--> Flush Output (Worker Tetap Hidup)
Perangkap terbesar saat beralih ke worker mode adalah stateful pollution. Jika aplikasi Anda menyimpan data user atau state koneksi pada variabel statis atau singleton, data tersebut dapat bocor ke user lain pada request berikutnya.
Periksa kode aplikasi Anda terhadap pola-pola berbahaya berikut sebelum menjadwalkan migrasi:
  • Singleton Service Container: Jangan menyimpan konteks request (seperti instance user terautentikasi atau input data) di dalam properti singleton.
  • Unbuffered Database Cursors: Pastikan koneksi database tidak meninggalkan transaksi menggantung (uncommitted transactions) jika request gagal di tengah jalan.
  • Penyimpanan Log Lokal In-Memory: Array statis yang mengakumulasi string error atau query log di dalam memori tanpa batas maksimum.
Framework membutuhkan wrapper worker untuk mereset state antar request. Berikut adalah implementasi wrapper worker untuk aplikasi Symfony/Laravel menggunakan API frankenphp_handle_request():
PHP
<?php
// public/frankenphp-worker.php

use Illuminate\Contracts\Http\Kernel;
use Illuminate\Http\Request;

ignore_user_abort(true);

require __DIR__ . '/../vendor/autoload.php';
$app = require_once __DIR__ . '/../bootstrap/app.php';
$kernel = $app->make(Kernel::class);

// Handler loop yang dieksekusi terus-menerus oleh FrankenPHP engine
$handler = static function () use ($kernel): void {
    $request = Request::capture();
    
    // Eksekusi request melalui kernel framework
    $response = $kernel->handle($request);
    $response->send();
    
    $kernel->terminate($request, $response);
    
    // Pembersihan state konteks request secara eksplisit
    if (function_exists('gc_collect_cycles')) {
        gc_collect_cycles();
    }
};

// Menjalankan persistent worker loop dengan kuota restart request
$maxRequests = (int)($_ENV['MAX_REQUESTS'] ?? 1000);
for ($nbRequests = 0; $nbRequests < $maxRequests; ++$nbRequests) {
    $keepRunning = \frankenphp_handle_request($handler);
    if (!$keepRunning) {
        break;
    }
}

// Worker keluar secara natural untuk mencegah memory accumulation
exit(0);
Simpan konfigurasi pada /etc/frankenphp/Caddyfile. Konfigurasi ini mengatur isolasi worker pool, batas alokasi request, serta integrasi logging performa tinggi:
CADDYFILE
{
    # Matikan admin endpoint jika tidak digunakan di production
    admin off
    
    # Konfigurasi global FrankenPHP engine
    frankenphp {
        # Tentukan skrip entrypoint worker dan jumlah thread worker
        worker /var/www/html/public/frankenphp-worker.php {
            num 16
            env MAX_REQUESTS 1500
        }
    }
}

:8080 {
    root * /var/www/html/public
    encode zstd gzip
    
    # Header keamanan dasar
    header {
        X-Content-Type-Options nosniff
        X-Frame-Options DENY
        Referrer-Policy strict-origin-when-cross-origin
    }
    
    # Lewatkan request ke PHP runtime
    php_server {
        resolve_root_symlink
    }
    
    # Nonaktifkan akses langsung ke hidden files
    @hidden {
        path /.* */.* /vendor/*
    }
    error @hidden 404
    
    log {
        output file /var/log/frankenphp/access.log {
            roll_size 50mb
            roll_keep 10
        }
        format json
    }
}
Jalankan layanan melalui systemd unit /etc/systemd/system/frankenphp.service:
INI
[Unit]
Description=FrankenPHP High-Performance Server Engine
After=network.target network-online.target
Wants=network-online.target

[Service]
Type=notify
User=www-data
Group=www-data
WorkingDirectory=/var/www/html
ExecStart=/usr/local/bin/frankenphp run --config /etc/frankenphp/Caddyfile
ExecReload=/usr/local/bin/frankenphp reload --config /etc/frankenphp/Caddyfile
TimeoutStopSec=15s
LimitNOFILE=1048576
LimitNPROC=512
PrivateTmp=true
ProtectSystem=full
Restart=always
RestartSec=2s

# Batasan memori cgroups untuk mencegah OOM Killer mematikan seluruh instance
MemoryHigh=3500M
MemoryMax=4000M

[Install]
WantedBy=multi-user.target
Jangan memindahkan 100% trafik secara mendadak. Gunakan layer load balancer atau reverse proxy lokal (misalnya HAProxy) di depan server aplikasi untuk mengalihkan persentase trafik secara bertahap.
TEXT
                    [Load Balancer / Reverse Proxy]
                                   |
               +-------------------+-------------------+
               | (Bobot 90%)                           | (Bobot 10% -> 50% -> 100%)
               v                                       v
       [Klaster PHP-FPM]                      [Klaster FrankenPHP]
        (Port 9000 Unix)                        (Worker Mode :8080)
Sebelum mengirimkan beban trafik, pastikan binary FrankenPHP telah terpasang dengan ekstensi yang identik dengan PHP-FPM eksisting:
BASH
# Verifikasi versi dan ekstensi FrankenPHP
frankenphp php-cli -v
frankenphp php-cli -m | sort > /tmp/frankenphp_modules.txt
php-fpm8.3 -m | sort > /tmp/phpfpm_modules.txt

# Periksa apakah ada ekstensi yang hilang
diff -u /tmp/phpfpm_modules.txt /tmp/frankenphp_modules.txt

# Validasi konfigurasi Caddyfile
frankenphp validate --config /etc/frankenphp/Caddyfile
Jalankan smoke test internal menggunakan curl dengan mengirimkan request secara berurutan pada worker yang sama, memastikan data session tidak tertukar:
BASH
# Kirimkan 50 request beruntun untuk mengecek kebocoran konteks session
for i in {1..50}; do
    TOKEN=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8080/api/v1/auth-check \
         -H 'Authorization: Bearer test-token-user-alpha')
    if [ "$TOKEN" != "200" ]; then
        echo "Peringatan: Response code menyimpang pada iterasi $i: $TOKEN"
        exit 1
    fi
done
echo "Smoke test berhasil tanpa anomali response."
Ubah konfigurasi backend pada proxy gateway untuk mengalirkan 5% lalu lintas data ke FrankenPHP:
BASH
# Contoh manipulasi bobot upstream via HAProxy Runtime API
echo 'set weight backend_app/frankenphp_node1 5' | socat stdio /run/haproxy/admin.sock
echo 'set weight backend_app/phpfpm_node1 95' | socat stdio /run/haproxy/admin.sock
Amati metrik error rate (status 5xx) dan latensi p99 selama 30 menit. Jika metrik stabil, tingkatkan bobot secara berkala:
  1. 5% selama 30 menit.
  2. 25% selama 1 jam.
  3. 50% selama 2 jam.
  4. 100% (Cutover tuntas, PHP-FPM diatur ke mode standby).
Dalam jangka panjang, beberapa pustaka pihak ketiga (third-party packages) mungkin mengalokasikan memori pada static cache internal yang tidak terjaring oleh garbage collector PHP. Untuk menjaga konsumsi memori tetap konstan pada batas yang aman, terapkan strategi Controlled Worker Cycling.
Dengan menetapkan MAX_REQUESTS pada skrip wrapper worker (misalnya 1.000 atau 2.000 request), proses worker akan melakukan exit(0) secara anggun (graceful exit). Engine FrankenPHP di Golang secara otomatis akan melahirkan (spawn) worker thread baru untuk menggantikannya tanpa menyebabkan antrean request terputus.
Gunakan skrip audit berikut untuk memonitor memori RSS (Resident Set Size) dari proses FrankenPHP dan memastikan tidak terjadi lonjakan linier:
BASH
#!/usr/bin/env bash
# /usr/local/bin/check-worker-memory.sh

set -euo pipefail

PID=$(pgrep -f 'frankenphp run' || true)

if [ -z "$PID" ]; then
    echo "CRITICAL: FrankenPHP service tidak berjalan!"
    exit 2
fi

# Ambil RSS memory dalam Megabytes
RSS_MEM_KB=$(ps -p "$PID" -o rss=)
RSS_MEM_MB=$((RSS_MEM_KB / 1024))

THRESHOLD_WARN=2500
THRESHOLD_CRIT=3500

echo "FrankenPHP PID $PID menggunakan ${RSS_MEM_MB} MB RAM."

if [ "$RSS_MEM_MB" -ge "$THRESHOLD_CRIT" ]; then
    echo "CRITICAL: Penggunaan memori melampaui batas kritis (${THRESHOLD_CRIT} MB)!"
    # Kirim sinyal graceful reload ke FrankenPHP
    systemctl reload frankenphp
    exit 2
elif [ "$RSS_MEM_MB" -ge "$THRESHOLD_WARN" ]; then
    echo "WARNING: Penggunaan memori mendekati batas ambang (${THRESHOLD_WARN} MB)."
    exit 1
else
    echo "OK: Alokasi memori berada dalam rentang normal."
    exit 0
fi
Tambahkan skrip di atas ke cron job sistem atau scheduler pemantau setiap 5 menit:
CRON
*/5 * * * * root /usr/local/bin/check-worker-memory.sh > /dev/null 2>&1
Jika aplikasi mengalami anomali session silang, crash segmentasi, atau kebocoran memori ekstrem di tengah migrasi, ikuti langkah darurat ini:
  1. Kembalikan Proxy Traffic ke PHP-FPM:
    BASH
    echo 'set weight backend_app/frankenphp_node1 0' | socat stdio /run/haproxy/admin.sock
    echo 'set weight backend_app/phpfpm_node1 100' | socat stdio /run/haproxy/admin.sock
  2. Hentikan Layanan FrankenPHP untuk Membebaskan Sumber Daya:
    BASH
    systemctl stop frankenphp
  3. Evaluasi Core Dump dan Log Error: Periksa /var/log/frankenphp/access.log dan log sistem journalctl -u frankenphp -n 200 --no-pager untuk mengidentifikasi skrip PHP yang memicu kegagalan siklus worker.
Dengan memadukan pembatasan siklus hidup request, audit isolasi state yang ketat, dan cutover bertahap berbasis bobot proxy, migrasi ke FrankenPHP worker mode menghadirkan efisiensi server yang signifikan tanpa mengorbankan stabilitas operasional pada sistem produksi.