Workflow deployment yang kita bangun sejauh ini sudah atomik dan mudah di-rollback, tetapi ada satu pihak yang belum kita tanyakan pendapatnya: pengguna. Zero-downtime deployment adalah standar di mana tidak ada satu pun request yang gagal, menggantung, atau kehilangan sesi login hanya karena kita sedang merilis versi baru. Mencapai standar tersebut bukan sihir dari satu alat, melainkan kombinasi strategi rilis yang tepat ditambah prasyarat data dan sesi yang disiapkan jauh sebelum hari rilis. Pada chapter ini kita pelajari empat strateginya — pergantian symlink, blue-green, rolling update, dan canary — lalu menutupnya dengan kejujuran tentang kapan downtime tetap tidak bisa dihindari.
Arti Zero-Downtime dan Ukurannya #
Kalimat “pengguna tidak menyadari rilis” perlu diterjemahkan menjadi angka agar bisa diverifikasi, bukan sekadar dirasakan. Ukuran yang umum dipakai adalah jumlah request gagal (kode 5xx atau timeout) selama jendela rilis: angka tersebut harus sama dengan baseline hari biasa, artinya nol tambahan kegagalan yang diakibatkan proses deploy. Ukuran kedua adalah sesi: pengguna yang sedang login tidak boleh diminta login ulang hanya karena rilis berganti.
Perlu digarisbawahi bahwa nol downtime bukan berarti tidak ada proses yang mati. Worker PHP-FPM lama boleh berhenti selama pergantian, asalkan setiap worker menyelesaikan request yang sedang dipegang lebih dulu — perilaku yang sudah kita bangun pada chapter Graceful Shutdown . Yang diukur adalah pengalaman pengguna, bukan usia proses di server.
Peta Empat Strategi Rilis #
Empat strategi berikut menyelesaikan masalah yang sama dengan biaya infrastruktur dan profil risiko yang berbeda. Tabel ini menjadi peta sebelum kita bedah masing-masing:
| Strategi | Mekanisme | Kebutuhan infrastruktur | Kecepatan rollback |
|---|---|---|---|
| Pergantian symlink | Rilis baru diekstrak ke direktori terpisah, symlink current dipindah | Satu server cukup | Detik, pindahkan symlink kembali |
| Blue-green | Dua lingkungan penuh, router membalik arah trafik | Dua kali kapasitas aplikasi | Detik, balik arah router |
| Rolling update | Instance diganti bertahap per batch di balik load balancer | Beberapa instance identik | Menit, rolling balik per batch |
| Canary | Versi baru menerima persentase kecil trafik lebih dulu | Router yang bisa membagi trafik + metrik | Detik, arahkan trafik ke versi lama |
Strategi ini bukan pilihan eksklusif. Praktik umum di tim besar adalah canary di atas blue-green: rilis baru naik ke lingkungan idle, menerima 5% trafik untuk diamati, baru kemudian ditingkatkan menjadi 100%.
Fondasi Pergantian Symlink Atomik #
Strategi paling murah sudah kita bangun pada chapter Deployment Workflows
: setiap rilis menempati direktori sendiri di bawah releases/, dan symlink current dipindah dengan pola buat-symlink-sementara-lalu-rename() sehingga pembaca tidak pernah melihat keadaan setengah jadi. Untuk satu server dengan PHP-FPM, pergantian ini praktis sudah zero-downtime karena request baru selalu membaca path current yang konsisten.
/var/www/toko/
├── releases/
│ ├── 202601150800/ ← rilis lama (v1.4.2), masih utuh
│ └── 202601161200/ ← rilis baru (v1.5.0)
├── current -> releases/202601161200
└── shared/ ← unggahan & log yang hidup lintas rilisBlue-Green Deployment #
Blue-green menyiapkan dua lingkungan aplikasi yang identik: satu aktif melayani trafik (misalnya blue), satu nganggur (green). Versi baru dideploy ke lingkungan nganggur, diverifikasi sampai yakin, lalu router membalik seluruh trafik ke lingkungan tersebut dalam satu keputusan. Lingkungan lama tidak langsung dimatikan — lingkungan tersebut menunggu sebagai tombol rollback instan.
Diagram di atas memperlihatkan keadaan sebelum flip: seluruh trafik mengalir ke blue, sementara green sudah membawa v1.5.0 tetapi belum menerima apa pun. Setelah verifikasi kesehatan lulus, load balancer mengubah tujuan trafik dari blue ke green — dari sudut pandang router, inilah seluruh “deployment”-nya. Mari kita modelkan mesin keadaannya sebagai fungsi murni:
<?php
function lingkunganIdle(string $aktif): string
{
return $aktif === "blue" ? "green" : "blue";
}
function deployBlueGreen(array $router, string $versiBaru, bool $verifikasiLolos): array
{
$idle = lingkunganIdle($router["active"]);
$router["environments"][$idle]["version"] = $versiBaru;
$router["environments"][$idle]["status"] = "ready";
if ($verifikasiLolos) {
$router["previous"] = $router["active"];
$router["active"] = $idle;
$router["environments"][$idle]["status"] = "serving";
$router["environments"][$router["previous"]]["status"] = "idle";
}
return $router;
}
$router = [
"active" => "blue",
"previous" => null,
"environments" => [
"blue" => ["version" => "1.4.2", "status" => "serving"],
"green" => ["version" => "1.4.2", "status" => "idle"],
],
];
$berhasil = deployBlueGreen($router, "1.5.0", true);
printf(
"Berhasil : aktif=%s (v%s, %s) | sebelumnya=%s (v%s, %s)\n",
$berhasil["active"],
$berhasil["environments"][$berhasil["active"]]["version"],
$berhasil["environments"][$berhasil["active"]]["status"],
$berhasil["previous"],
$berhasil["environments"][$berhasil["previous"]]["version"],
$berhasil["environments"][$berhasil["previous"]]["status"],
);
$gagal = deployBlueGreen($router, "1.5.1", false);
printf(
"Gagal : aktif=%s (v%s, %s) | %s=v%s siap tapi tidak diaktifkan\n",
$gagal["active"],
$gagal["environments"][$gagal["active"]]["version"],
$gagal["environments"][$gagal["active"]]["status"],
lingkunganIdle($gagal["active"]),
$gagal["environments"][lingkunganIdle($gagal["active"])]["version"],
);Output yang Dihasilkan:
Berhasil : aktif=green (v1.5.0, serving) | sebelumnya=blue (v1.4.2, idle)
Gagal : aktif=blue (v1.4.2, serving) | green=v1.5.1 siap tapi tidak diaktifkanMari kita bedah logikanya. lingkunganIdle() menghitung lawan dari lingkungan aktif — satu baris, tetapi membuat kode pemanggil tidak perlu hardcode “green” yang akan salah begitu arah flip berbalik. deployBlueGreen() selalu mengisi versi baru ke lingkungan idle dan menandainya ready, tetapi hanya membalik active bila $verifikasiLolos bernilai true; skenario kedua memperlihatkan versi 1.5.1 yang sudah terpasang namun router tetap memilih blue karena verifikasi gagal. Kolom previous sengaja disimpan agar rollback tinggal membalik active kembali — tidak ada langkah rebuild.
Harga yang dibayar blue-green adalah kapasitas: kita membayar dua lingkungan penuh meski satu menganggur. Untuk aplikasi kecil hal tersebut terasa mahal; untuk sistem transaksi bernilai tinggi, harga tersebut murah dibanding insiden.
Canary Release dengan Keputusan Berbasis Metrik #
Canary membalik logika blue-green: alih-alih memverifikasi di lingkungan idle yang tidak menerima trafik, versi baru langsung menerima persentase kecil trafik sungguhan — pengguna pertama, biasanya 5%. Router yang mendukung pembagian trafik membuat pola ini mudah; Nginx cukup memakai weight pada upstream:
upstream toko_app {
server versi-stabil.internal weight=95;
server versi-canary.internal weight=5;
}Kekuatan canary terletak pada keputusannya, bukan pembagian trafiknya. Selama periode pengamatan, metrik canary dibandingkan terhadap ambang batas yang disepakati; hasilnya salah satu dari tiga tindakan — promote (naikkan trafik), rollback (tarik canary), atau hold (datanya belum cukup). Fungsi keputusan berikut merangkum aturan tersebut:
<?php
function putuskanCanary(array $metrik, array $ambang): string
{
if ($metrik["errorRate"] >= $ambang["errorRateMax"]
|| $metrik["latencyP95Ms"] >= $ambang["latencyP95MaxMs"]) {
return "rollback";
}
if ($metrik["sampel"] >= $ambang["sampelMinimal"]) {
return "promote";
}
return "hold";
}
$ambang = ["errorRateMax" => 2.0, "latencyP95MaxMs" => 800.0, "sampelMinimal" => 1000];
$skenario = [
"sehat dan cukup data" => ["errorRate" => 0.4, "latencyP95Ms" => 410.0, "sampel" => 1500],
"error rate meledak" => ["errorRate" => 2.7, "latencyP95Ms" => 410.0, "sampel" => 1500],
"latensi memburuk" => ["errorRate" => 0.4, "latencyP95Ms" => 950.0, "sampel" => 1500],
"data masih sedikit" => ["errorRate" => 0.4, "latencyP95Ms" => 410.0, "sampel" => 300],
];
foreach ($skenario as $nama => $metrik) {
printf("%-22s -> %s\n", $nama, putuskanCanary($metrik, $ambang));
}Output yang Dihasilkan:
sehat dan cukup data -> promote
error rate meledak -> rollback
latensi memburuk -> rollback
data masih sedikit -> holdEmpat skenario di atas menguji seluruh cabang keputusan. Error rate 2,7% melewati ambang 2,0% sehingga canary langsung ditarik meski latensi masih sehat; latensi p95 950 ms yang melewati ambang 800 ms memicu keputusan yang sama lewat operator ||. Skenario terakhir menunjukkan aturan yang sering dilupakan: error rate 0,4% pada 300 sampel belum cukup bukti — 300 request yang kebetulan sehat bisa terjadi pada kode yang salah, sehingga keputusan yang jujur adalah hold sampai ambang 1000 sampel tercapai. Ambang-ambang ini sengaja dibuat parameter, bukan konstanta, agar tiap tim menyetel angkanya sesuai kepekaan aplikasi.
Rolling Update Batch demi Batch #
Rolling update adalah strategi untuk armada instance identik di balik satu load balancer. Instance dikelompokkan menjadi batch (misalnya per 25%): batch pertama dikeluarkan dari pool, diganti kodenya, dinyatakan sehat lewat health check, lalu dikembalikan ke pool sebelum batch berikutnya diproses. Selama proses berlangsung, versi lama dan versi baru melayani trafik secara bersamaan — dan fakta inilah yang menjadikan kompatibilitas skema menjadi syarat mutlak, bukan saran.
Keunggulan rolling adalah biaya: tidak perlu kapasitas dua kali lipat, cukup selisih satu batch. Konsekuensinya, rollback lebih lambat karena harus dirolling balik batch demi batch, dan blast radius kegagalan tidak pernah sekecil canary — begitu satu batch naik, seluruh trafik batch tersebut menanggung versi baru. Kubernetes dan orchestrator lain menjadikan pola ini default dengan parameter maxUnavailable dan maxSurge yang mengatur ukuran batch.
Prasyarat di Balik Semua Strategi #
Keempat strategi di atas hanya mengurus pergantian kode. Zero-downtime yang sesungguhnya ditentukan oleh empat prasyarat berikut:
Skema database yang kompatibel dua arah. Selama jendela pergantian, kode versi lama dan versi baru membaca tabel yang sama. Migrasi harus mengikuti pola expand-contract — tambah kolom/struktur baru dulu, rilis kodenya, baru hapus yang lama pada rilis berikutnya — persis seperti yang dibahas pada chapter Database Deployment .
Session dan cache di luar server aplikasi. PHP menyimpan session sebagai berkas di disk lokal secara default; dua lingkungan blue-green tidak bisa berbagi disk tersebut, sehingga pengguna yang login di blue tiba-tiba menjadi tamu di green. Pindahkan penyimpanan session dan cache ke penyimpanan bersama seperti Redis:
inisession.save_handler = redis session.save_path = "tcp://redis.internal:6379"Detail penggunaan Redis dibahas pada chapter Redis Caching . Alternatif yang lebih murah tetapi lebih kaku adalah sticky session di load balancer — dengan biaya distribusi trafik menjadi tidak merata.
Health check sebagai sinyal kesiapan. Router butuh jawaban standar kapan lingkungan baru layak menerima trafik; kontrak
/healthzdari chapter Health Checks adalah mekanismenya.Pemadaman yang anggun. Worker yang dikeluarkan dari pool harus menuntaskan request yang sedang berjalan, bukan diputus di tengah jalan — kembali ke disiplin Graceful Shutdown .
Kapan Downtime Tetap Tidak Terhindarkan #
Ada perubahan yang tidak bisa dihaluskan oleh strategi mana pun. Migrasi destruktif pada tabel raksasa yang mengunci penulisan berjam-jam, perubahan format session yang memang mendesain ulang siapa yang dianggap login, atau pemindahan data lintas sistem dengan kontrak yang berubah total — semuanya memutus pengalaman pengguna lewat jalur data, bukan jalur kode. Memaksakan zero-downtime pada kasus tersebut biasanya menghasilkan lapisan shim yang rumit dan rapuh, lebih berbahaya daripada downtime yang jujur.
Ketika downtime tak terhindarkan, kelola dengan sengaja: umumkan jadwal ke pengguna, siapkan halaman maintenance yang ramah, pastikan backup terbaru sudah teruji restore-nya, dan tetapkan durasi maksimum sebelum rencana darurat dijalankan. Downtime yang direncanakan 10 menit dengan komunikasi jelas hampir selalu lebih baik daripada insiden tak terduga dua jam akibat trik zero-downtime yang gagal.
Ringkasan #
Zero-downtime adalah properti yang dibangun dari tiga lapisan: strategi pergantian kode (symlink atomik untuk satu server, blue-green untuk flip instan, rolling untuk armada, canary untuk risiko terukur), prasyarat data (skema expand-contract, session eksternal), dan prasyarat proses (health check, graceful shutdown). Strategi terbaik bergantung pada kapasitas dan toleransi risiko tim, dan kejujuran untuk mengakui kapan downtime direncanakan adalah bagian dari kematangan engineering. Seluruh perjalanan section ini kini siap dirangkum menjadi satu gerbang terakhir sebelum aplikasi menghadapi dunia nyata: production readiness checklist.