Bali, Indonesia
Insights

Efisiensi Biaya Klaster Web: Mengganti Overhead PHP-FPM dengan FrankenPHP Worker Mode dan OpenResty Lua

October 13, 2026
0:00
Model komputasi web berbasis PHP tradisional selama lebih dari satu dekade bertumpu pada arsitektur share-nothing yang dijalankan oleh PHP-FPM. Dalam model ini, setiap permintaan HTTP yang masuk memaksa runtime menginisialisasi seluruh pohon dependensi aplikasi, membaca berkas autoload Composer, mengeksekusi service provider, hingga akhirnya merespons klien dan membuang (destroy) alokasi memori tersebut kembali ke titik nol. Pada skala ribuan request per detik (RPS) dengan kerangka kerja modern seperti Laravel atau Symfony, siklus cold boot ini menjadi pemborosan CPU dan memori yang sangat masif. Memasuki standar infrastruktur tahun 2026, efisiensi anggaran komputasi menuntut pemangkasan alokasi vCPU dan RAM tanpa menurunkan kualitas layanan. Mengganti tumpukan tradisional Nginx plus PHP-FPM dengan FrankenPHP worker mode yang berjalan di atas engine Caddy berbasis Go, digabungkan dengan mitigasi abuse traffic di layer terluar via OpenResty Lua dan Redis, membuka peluang konsolidasi node komputasi secara radikal.
Masalah utama pembengkakan biaya pada klaster PHP-FPM berakar pada alokasi memori riil (Resident Set Size atau RSS). Saat satu proses worker PHP-FPM melayani framework enterprise modern, konsumsi memori per proses rata-rata berkisar antara 45 MB hingga 90 MB. Jika sebuah instans server harus menangani 200 request konkuren murni tanpa antrean socket, sistem membutuhkan minimal 12 GB hingga 18 GB RAM hanya untuk pool PHP-FPM, di luar alokasi buffer OS dan cache kernel. Sebaliknya, FrankenPHP mengintegrasikan SAPI PHP langsung ke dalam server web Caddy menggunakan CGo. Melalui worker mode, FrankenPHP melakukan booting aplikasi hanya sekali ke dalam RAM. Framework tetap berada dalam memori persisten, dan setiap request HTTP yang datang langsung dialirkan ke loop penanganan (event-driven worker loop) tanpa proses bootstrap ulang.
TEXT
Model Tradisional (PHP-FPM):
Request 1 --> [Fork/Bootstrap Framework] --> [Eksekusi] --> [Destroy Memory]
Request 2 --> [Fork/Bootstrap Framework] --> [Eksekusi] --> [Destroy Memory]
Request 3 --> [Fork/Bootstrap Framework] --> [Eksekusi] --> [Destroy Memory]

Model FrankenPHP Worker Mode:
Inisialisasi --> [Bootstrap Framework Sekali ke RAM]
                      |
  +-------------------+-------------------+
  |                   |                   |
  v                   v                   v
[Request 1]         [Request 2]         [Request 3]
(Eksekusi Instan)   (Eksekusi Instan)   (Eksekusi Instan)
  |                   |                   |
  +-------------------+-------------------+
                      |
            [Reset State Parsial]
Dengan memangkas fase I/O disk dan kompilasi opcodes berulang, latensi respons p99 dapat ditekan secara drastis, sementara footprint memori server menyusut signifikan karena jumlah thread yang dibutuhkan untuk mempertahankan throughput identik jauh lebih sedikit.
Implementasi worker mode menuntut konfigurasi yang mengisolasi eksekusi serta membatasi potensi akumulasi kebocoran memori (memory leak) dari kode legacy. FrankenPHP menyediakan opsi restart worker otomatis setelah mencapai kuota request tertentu. Berikut adalah contoh konfigurasi Caddyfile untuk aplikasi berbasis framework yang diterapkan pada node produksi:
CADDY
{
    frankenphp {
        # Jumlah worker persisten yang dipertahankan dalam RAM
        worker /app/public/index.php {
            num 16
            env APP_ENV production
            env APP_DEBUG false
            max_requests 5000
        }
    }
    order frankenphp before file_server
}

:8080 {
    root * /app/public
    encode zstd gzip

    # Proteksi file tersembunyi dan direktori sensitif
    @dotfiles path */.*
    respond @dotfiles 404

    # Eksekusi via FrankenPHP worker
    php_server {
        resolve_root_symlink false
    }
}
Pada sisi aplikasi, berkas entry point (/app/public/index.php) harus membungkus siklus request ke dalam loop penanganan worker. Contoh integrasi manual atau custom bootstrap:
PHP
<?php

use App\Kernel;
use Symfony\Component\HttpFoundation\Request;

require_once dirname(__DIR__).'/vendor/autoload_runtime.php';

$kernel = new Kernel($_SERVER['APP_ENV'] ?? 'prod', (bool) ($_SERVER['APP_DEBUG'] ?? false));
$kernel->boot();

// Handler loop untuk FrankenPHP
$handler = static function () use ($kernel) {
    $request = Request::createFromGlobals();
    $response = $kernel->handle($request);
    $response->send();

    if ($kernel instanceof \Symfony\Component\HttpKernel\TerminableInterface) {
        $kernel->terminate($request, $response);
    }

    // Sanitasi garbage collector untuk mencegah memory leak
    gc_collect_cycles();
};

$maxRequests = (int)($_SERVER['MAX_REQUESTS'] ?? 5000);
for ($nbRequests = 0; $nbRequests < $maxRequests; ++$nbRequests) {
    $keepRunning = \frankenphp_handle_request($handler);
    if (!$keepRunning) {
        break;
    }
}
Perhatikan direktif max_requests 5000 pada Caddyfile serta variabel loop. Jika terdapat library pihak ketiga yang meninggalkan referensi sirkular atau variabel statis tak terbatas, proses worker akan dimatikan secara elegan (graceful termination) dan worker baru akan di-spawn tanpa memutus koneksi klien yang aktif.
Keuntungan latensi rendah dari FrankenPHP worker mode dapat sirna apabila server langsung dihantam oleh trafik bot crawling atau serangan layer-7 rate abuse. Membiarkan request sampah masuk hingga ke worker PHP merupakan kesalahan arsitektur yang mahal, karena setiap worker harus dilindungi agar hanya memproses logika bisnis riil. Solusi paling hemat biaya adalah meletakkan lapisan reverse proxy OpenResty di garda depan untuk memfilter traffic menggunakan modul Lua non-blocking (lua-resty-redis), menerapkan algoritma token bucket sebelum paket diteruskan ke upstream FrankenPHP. Berikut konfigurasi OpenResty pada /etc/openresty/nginx.conf:
NGINX
events {
    worker_connections 16384;
    use epoll;
    multi_accept on;
}

http {
    lua_package_path '/usr/local/openresty/lualib/?.lua;;';
    lua_shared_dict ip_blacklist 10m;

    upstream frankenphp_cluster {
        server 10.0.1.10:8080 max_fails=3 fail_timeout=10s;
        server 10.0.1.11:8080 max_fails=3 fail_timeout=10s;
        keepalive 64;
    }

    server {
        listen 80;
        server_name api.infra.internal;

        location / {
            access_by_lua_block {
                local redis = require 'resty.redis'
                local red = redis:new()
                red:set_timeout(100) -- batas waktu 100ms agar non-blocking ketat

                local ok, err = red:connect('10.0.2.5', 6379)
                if not ok then
                    ngx.log(ngx.WARN, 'Redis connection failed: ', err)
                    return -- Fallback buka akses jika cache store down
                end

                local client_ip = ngx.var.remote_addr
                local key = 'ratelimit:' .. client_ip
                local limit = 120 -- kuota request
                local window = 60 -- window waktu dalam detik

                local current, err = red:incr(key)
                if current == 1 then
                    red:expire(key, window)
                end

                -- Kembalikan koneksi ke connection pool OpenResty
                red:set_keepalive(10000, 100)

                if current > limit then
                    ngx.status = ngx.HTTP_TOO_MANY_REQUESTS
                    ngx.header['Content-Type'] = 'application/json'
                    ngx.say('{"error": "Rate limit exceeded. Try again later."}')
                    ngx.exit(ngx.HTTP_TOO_MANY_REQUESTS)
                end
            }

            proxy_pass http://frankenphp_cluster;
            proxy_http_version 1.1;
            proxy_set_header Connection '';
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }
    }
}
Dengan arsitektur ini, trafik ilegal dihentikan langsung di level epoll OpenResty dengan penggunaan CPU kurang dari 1% dan latensi sub-milidetik, mencegah saturasi thread pada FrankenPHP.
Pengujian dilakukan menggunakan k6 selama durasi 10 menit dengan beban konstan 1.500 virtual users (VUs) yang mengeksekusi endpoint autentikasi terenkripsi berbasis JWT dan parsing database:
Metrik EvaluasiNginx + PHP-FPM (pm=static, 128 workers)FrankenPHP Worker Mode (16 persistent workers)Efisiensi
Rata-rata Throughput1.840 RPS4.250 RPS+130.9%
Latensi Median (p50)48 ms11 ms-77.0%
Latensi Tail (p99)340 ms42 ms-87.6%
Konsumsi RAM (Peak)8.950 MB (8.74 GB)2.120 MB (2.07 GB)-76.3%
Rata-rata Beban CPU88% (8 vCPU)42% (8 vCPU)-52.2%
Pengurangan konsumsi memori lebih dari 76 persen membuktikan bahwa sebagian besar RAM pada arsitektur PHP-FPM konvensional terbuang sia-sia hanya untuk mengalokasikan context stack, tabel simbol internal Zend Engine, dan buffer I/O yang terus-menerus dibuat dan dihancurkan.
Kelemahan terbesar saat beralih ke persistent memory model adalah ancaman state pollution (pencemaran state antarklien). Beberapa jebakan umum di lingkungan produksi meliputi:
  1. Koneksi Database yang Terputus: Driver PDO atau database client yang disimpan secara statis dapat mengalami timeout dari server MySQL/PostgreSQL (server has gone away). Gunakan mekanisme koneksi yang memverifikasi keaktifan soket atau manfaatkan connection pool eksternal (seperti PgBouncer untuk PostgreSQL).
  2. Superglobals yang Tertinggal: Penggunaan array global seperti $_GET, $_POST, atau $_SESSION secara langsung berisiko membocorkan data antar-request. Framework modern yang mengabstraksi HTTP Request ke dalam objek terisolasi wajib direset pada tiap siklus handler.
  3. Static Property Bloat: Singletons yang menyimpan data spesifik request (misalnya data user yang sedang login) akan menetap di memori dan berpotensi diakses oleh pengguna berikutnya. Pastikan framework container mendukung mekanisme scoped container atau sandbox resetting di antara pemanggilan request.
Contoh pembersihan state manual pada container dependency injection kustom sebelum worker menyelesaikan siklus:
PHP
<?php

// Eksekusi sanitasi state pada akhir setiap request
function resetApplicationState(Container $container): void
{
    // Hapus instance request-scoped
    $container->forgetInstance('current_user');
    $container->forgetInstance('request_context');

    // Reset state buffer log
    if ($container->has('logger')) {
        $container->get('logger')->reset();
    }

    // Lepaskan referensi error handler lokal
    restore_error_handler();
    restore_exception_handler();
}
Transformasi tumpukan web runtime dari model stateless murni ke FrankenPHP worker mode yang diproteksi oleh OpenResty non-blocking di lapisan edge bukan sekadar tren teknologi, melainkan keputusan finansial infrastruktur yang terukur. Dengan meminimalkan overhead bootstrap yang tidak perlu, klaster komputasi dapat memproses throughput yang jauh lebih masif pada spesifikasi perangkat keras yang jauh lebih ramping. Untuk tim operasional yang saat ini masih mempertahankan instans komputasi berspesifikasi tinggi hanya demi menampung pool PHP-FPM yang membengkak, transisi ke arsitektur worker persisten memberikan ruang penghematan biaya cloud secara langsung tanpa menuntut penulisan ulang (rewrite) basis kode aplikasi ke bahasa lain.