Pergeseran Paradigma: Stateless Fork vs In-Memory Worker
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]Konfigurasi Produksi FrankenPHP Worker Mode
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
}
}/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;
}
}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.
Proteksi Lapisan Edge: Rate Limiting Non-Blocking OpenResty
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;
}
}
}Perbandingan Konsumsi Sumber Daya: Data Uji Beban
| Metrik Evaluasi | Nginx + PHP-FPM (pm=static, 128 workers) | FrankenPHP Worker Mode (16 persistent workers) | Efisiensi |
|---|---|---|---|
| Rata-rata Throughput | 1.840 RPS | 4.250 RPS | +130.9% |
| Latensi Median (p50) | 48 ms | 11 ms | -77.0% |
| Latensi Tail (p99) | 340 ms | 42 ms | -87.6% |
| Konsumsi RAM (Peak) | 8.950 MB (8.74 GB) | 2.120 MB (2.07 GB) | -76.3% |
| Rata-rata Beban CPU | 88% (8 vCPU) | 42% (8 vCPU) | -52.2% |
Mengatasi Masalah State Pollution pada Persistent Runtime
-
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). -
Superglobals yang Tertinggal:
Penggunaan array global seperti
$_GET,$_POST, atau$_SESSIONsecara langsung berisiko membocorkan data antar-request. Framework modern yang mengabstraksi HTTP Request ke dalam objek terisolasi wajib direset pada tiap siklus handler. - 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.
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();
}