Setiap deployment, scaling, dan restart pada akhirnya melakukan hal yang sama: mematikan proses PHP lama. Bila proses dibunuh seketika dengan SIGKILL, request yang sedang setengah diproses terputus menjadi error 500, pekerjaan antrean berhenti di tengah transaksi, dan koneksi database putus tanpa pamit. Graceful shutdown adalah disiplin memberi proses kesempatan menyelesaikan pekerjaan yang sedang berjalan sebelum benar-benar berhenti. Pada chapter ini kita pelajari urutan lengkapnya: membalik readiness, drain request dari load balancer, reload PHP-FPM tanpa memutus koneksi, dan menangani sinyal di worker berumur panjang.
Mengapa Shutdown Perlu Anggun #
Bayangkan rolling deployment dengan lima server: load balancer mengganti satu per satu instance lama dengan instance baru. Setiap penggantian menonaktifkan satu proses yang saat itu menampung puluhan request aktif — halaman checkout yang sedang menyimpan pesanan, upload yang hampir selesai, job antrean yang setengah memproses gambar. Kill paksa memutus semuanya dalam satu milidetik.
Konsekuensinya nyata bagi pengguna dan data. Pengguna melihat error atau harus mengulang pekerjaan dari awal. Transaksi database setengah jalan di-rollback oleh server, atau lebih buruk, job antrean berhenti setelah menandai pesanan “diproses” tetapi sebelum email konfirmasi terkirim. Graceful shutdown mengubah momen ini menjadi sunyi bagi pengguna: trafik baru berhenti dialirkan, pekerjaan yang sedang berjalan diberi waktu untuk selesai, dan proses baru menyala menggantikan tanpa satu pun request gagal.
Reload Anggun PHP-FPM #
PHP-FPM berjalan sebagai satu master process yang mengelola sekelompok child worker. Request tidak pernah ditangani master, melainkan child-child inilah yang mengeksekusi aplikasi. Karena itu mematikan master dengan paksa otomatis mematikan semua child beserta request yang sedang mereka pegang. Sinyal yang tepat membuat master melakukan pergantian dengan anggun:
| Sinyal ke master PHP-FPM | Efek |
|---|---|
| SIGTERM | Master dan seluruh child berhenti |
| SIGUSR1 | Membuka kembali berkas log (untuk log rotation) |
| SIGUSR2 | Reload anggun: child baru dilahirkan, child lama menyelesaikan request yang sedang dipegang lalu keluar |
Mekanisme SIGUSR2 bekerja seperti pergantian shift: master memuat ulang konfigurasi dan pool, melahirkan child baru yang langsung siap menerima request, sementara child lama tidak lagi menerima request baru tetapi dibiarkan hidup sampai request yang sedang mereka proses selesai. Pola ini juga dipakai saat file descriptor dan shared memory perlu dibuat ulang setelah deploy konfigurasi baru.
kill -USR2 "$(cat /run/php/php-fpm.pid)"Perintah di atas mengirim SIGUSR2 ke master process yang PID-nya tersimpan di berkas pid — pola standar yang dipakai skrip init dan systemd, biasanya dibungkus systemctl reload php8.4-fpm pada distribusi modern. Perhatikan bahwa reload anggun tetap butuh kerja sama load balancer bila Anda mengganti binary atau kode: FPM hanya mengurus prosesnya sendiri, bukan trafik yang masih dikirim web server.
Draining dari Load Balancer #
Urutan mematikan instance yang benar selalu dimulai dari load balancer, bukan dari prosesnya. Langkah pertama adalah membalik status readiness menjadi 503 sehingga load balancer berhenti mengirim trafik baru — teknik persis seperti endpoint /healthz pada chapter Health Checks
. Setelah itu kita menunggu request yang sedang berjalan (in-flight) selesai, baru proses benar-benar dihentikan. Simulasi berikut memodelkan fase menunggu dengan deretan cuplikan jumlah request aktif per detik:
<?php
function tungguInflightSelesai(array $inflightPerDetik, int $batasDetik): string
{
$detik = 0;
foreach ($inflightPerDetik as $inflight) {
if ($inflight === 0) {
return "aman: drain selesai pada detik {$detik}";
}
$detik++;
if ($detik >= $batasDetik) {
return "paksa berhenti pada detik {$detik}: {$inflight} request masih aktif";
}
}
return "aman: drain selesai pada detik {$detik}";
}
$lancar = [4, 2, 1, 0];
$macet = [3, 3, 3, 3, 3, 3];
echo tungguInflightSelesai($lancar, 10) . "\n";
echo tungguInflightSelesai($macet, 5) . "\n";Output yang Dihasilkan:
aman: drain selesai pada detik 3
paksa berhenti pada detik 5: 3 request masih aktifSkenario $lancar memperlihatkan drain yang sehat: empat request aktif menyusut jadi dua, satu, lalu nol pada detik ketiga, dan proses boleh berhenti dengan aman. Skenario $macet adalah realisasi batas waktu: request aktif tidak kunjung turun, dan pada detik kelima fungsi memutuskan berhenti paksa meski tiga request masih menggantung. Batas waktu ini bukan kegagalan desain melainkan pengaman — request yang macet selamanya (klien yang putus tanpa menutup koneksi, panggilan eksternal tanpa timeout) tidak boleh menahan deploy selamanya. Pada implementasi nyata, penghitungan in-flight memakai metrik dari FPM atau web server, dan angka deretannya berasal dari cuplikan berkala.
Menangani SIGTERM di Worker #
Worker CLI yang berumur panjang — consumer antrean, cron runner, bot — tidak dilindungi reload FPM. Orchestrator seperti systemd, supervisord, dan Kubernetes menyampaikan niat berhenti lewat sinyal SIGTERM, dan default handler PHP justru menghentikan proses seketika. Ekstensi pcntl memungkinkan kita memasang handler sendiri. Prinsip pentingnya: handler hanya menyalakan bendera, dan loop utama memeriksa bendera itu di antara pekerjaan:
<?php
$berhenti = false;
pcntl_signal(SIGTERM, function () use (&$berhenti): void {
$berhenti = true;
});
$pekerjaanSelesai = 0;
for ($i = 1; $i <= 5; $i++) {
if ($berhenti) {
break;
}
$pekerjaanSelesai++;
if ($i === 3) {
// simulasi supervisor mengirim sinyal berhenti
posix_kill(getmypid(), SIGTERM);
pcntl_signal_dispatch();
}
}
echo "Pekerjaan selesai: {$pekerjaanSelesai} dari 5\n";
echo "Status worker: " . ($berhenti ? "berhenti setelah sinyal" : "tidak menerima sinyal") . "\n";Output yang Dihasilkan:
Pekerjaan selesai: 3 dari 5
Status worker: berhenti setelah sinyalMari kita bedah alurnya. pcntl_signal() mendaftarkan closure yang hanya mengisi variabel $berhenti — sengaja ringan, karena handler sinyal bisa dipanggil kapan saja di tengah eksekusi dan pekerjaan berat di dalamnya berisiko merusak state. Di tengah simulasi, posix_kill() mengirim SIGTERM ke proses sendiri (menggantikan supervisor sungguhan), lalu pcntl_signal_dispatch() memaksa handler terjadwal dieksekusi; pada worker nyata pemanggilan dispatch ini berada di kepala loop, atau Anda mengaktifkan pcntl_async_signals(true) agar handler berjalan otomatis. Loop kemudian memeriksa $berhenti sebelum mengambil pekerjaan berikutnya dan keluar dengan rapi setelah tiga dari lima pekerjaan — dua pekerjaan tersisa dibiarkan untuk worker pengganti.
Selesaikan Pekerjaan Berjalan #
Untuk worker antrean ada satu aturan tambahan: pekerjaan yang sedang dieksekusi harus dituntaskan, sementara pekerjaan berikutnya di antrean tidak diambil. Mesin status kecil berikut memodelkan siklus hidup worker tersebut:
<?php
enum WorkerState
{
case Idle;
case Processing;
case Draining;
case Stopped;
}
class JobWorker
{
public WorkerState $state = WorkerState::Idle;
public int $pekerjaanSelesai = 0;
public function mintaBerhenti(): void
{
if ($this->state === WorkerState::Processing) {
// sedang memegang pekerjaan: tandai draining, selesaikan dulu
$this->state = WorkerState::Draining;
} elseif ($this->state !== WorkerState::Stopped) {
$this->state = WorkerState::Stopped;
}
}
public function proses(array $antrean, ?string $sinyalPada = null): void
{
foreach ($antrean as $pekerjaan) {
if ($this->state === WorkerState::Stopped) {
break;
}
$this->state = WorkerState::Processing;
if ($pekerjaan === $sinyalPada) {
// sinyal SIGTERM tiba saat pekerjaan ini berjalan
$this->mintaBerhenti();
}
$this->pekerjaanSelesai++;
if ($this->state === WorkerState::Draining) {
$this->state = WorkerState::Stopped;
break;
}
}
}
}
$worker1 = new JobWorker();
$worker1->mintaBerhenti();
echo "Skenario 1: selesai={$worker1->pekerjaanSelesai}, state={$worker1->state->name}\n";
$worker2 = new JobWorker();
$worker2->proses(["job-a", "job-b", "job-c"], "job-b");
echo "Skenario 2: selesai={$worker2->pekerjaanSelesai}, state={$worker2->state->name}\n";Output yang Dihasilkan:
Skenario 1: selesai=0, state=Stopped
Skenario 2: selesai=2, state=StoppedPerhatikan perilaku dua skenarionya. Skenario pertama: worker sedang idle ketika permintaan berhenti tiba, maka tidak ada pekerjaan yang dipegang dan state langsung melompat ke Stopped tanpa menyentuh antrean. Skenario kedua: sinyal tiba saat job-b sedang dieksekusi, state berpindah ke Draining, job-b tetap dihitung selesai (baris penambah $pekerjaanSelesai berada setelah pemanggilan mintaBerhenti()), lalu worker berhenti tanpa menyentuh job-c — pekerjaan itu akan diambil worker lain atau worker pengganti setelah restart. Enum WorkerState membuat aturan transisi eksplisit dan mudah diuji, jauh lebih aman daripada menabur flag boolean yang saling bertentangan.
Batas Waktu dan SIGKILL #
Graceful selalu berpasangan dengan batas waktu. Supervisor pada umumnya memberi jendela 30-60 detik setelah SIGTERM; bila proses belum juga keluar, SIGKILL dikirim dan proses mati tanpa bisa berunding. Karena itu desain worker harus menganggap “terputus di tengah pekerjaan” sebagai kemungkinan yang selalu ada, bukan kejadian mustahil.
Konsekuensi praktisnya ada dua. Pertama, pecah pekerjaan panjang menjadi potongan yang bisa dilanjutkan: proses 10.000 gambar sebagai 10.000 job kecil, bukan satu job raksasa, sehingga sinyal berhenti hanya membuang satu potongan kecil. Kedua, buat pekerjaan idempoten — menjalankan ulang pekerjaan yang sama harus aman. Job “kirim email konfirmasi pesanan X” yang idempoten akan memeriksa dulu apakah email sudah terkirim sebelum mengirim lagi; job yang tidak idempoten mengirim email ganda setiap kali terputus dan diulang.
Membalik Status Readiness #
Semua teknik di atas menyusun urutan shutdown lengkap yang dimulai jauh sebelum proses disentuh. Urutannya penting karena setiap langkah menyiapkan langkah berikutnya:
- Balik readiness — endpoint
/healthzmulai mengembalikan503, atau instance ditandai tidak siap di service discovery. - Tunggu load balancer mencabut — beri waktu satu-dua interval polling plus ambang kegagalan sampai trafik baru benar-benar berhenti mengalir.
- Drain request in-flight — pantau jumlah request aktif turun menuju nol dengan batas waktu.
- Kirim sinyal berhenti —
SIGTERMpada worker CLI, atauSIGUSR2untuk reload PHP-FPM. - Tunggu batas waktu, lalu paksa — bila jendela graceful habis, supervisor mengirim
SIGKILL.
Langkah satu sampai tiga terjadi sebelum proses menerima sinyal apa pun — inilah pembeda antara shutdown yang anggun dan shutdown yang hanya berpura-pura anggun. Dengan urutan ini, pengguna tidak melihat apa pun selain mungkin sedikit kenaikan latensi, dan data tetap konsisten karena tidak ada pekerjaan yang putus di tengah.
Ringkasan #
Graceful shutdown menuntut dua kerja sama: load balancer berhenti mengirim trafik baru, dan proses menyelesaikan pekerjaan yang sedang dipegang sebelum keluar. PHP-FPM menyediakannya lewat SIGUSR2, worker CLI memasang handler SIGTERM yang hanya menyalakan bendera dan loop yang memeriksanya, serta mesin status yang memastikan pekerjaan berjalan selesai sementara antrean tersisa tidak disentuh. Batas waktu dan idempotensi menjaga kedisiplinan ini tetap jujur ketika pekerjaan macet. Gabungkan seluruh teknik ini dengan pola rilis symlink dari chapter Deployment Workflows
, dan Anda memperoleh tujuan akhir section ini: deployment tanpa downtime, yang kita rangkai pada chapter Zero-Downtime Deployment
.