Jawaban singkat untuk pertanyaan yang paling sering diajukan pembaca tentang topik ini.
01Sebaiknya horizontal scaling atau vertical scaling dulu?
Untuk aplikasi satu servis, lakukan vertical scaling dulu karena tidak perlu mengubah kode dan tidak menambah komponen baru. Pindah ke horizontal scaling saat Anda perlu bertahan dari kegagalan mesin, resize menyebabkan downtime yang tidak bisa diterima, atau instance terbesar yang bisa disewa sudah tidak cukup.
02Apa kekurangan utama vertical scaling?
Ada batas atas yang keras dan satu titik kegagalan. Anda hanya bisa membeli instance sebesar yang dijual provider, dan jika mesin itu mati, seluruh kapasitas hilang sekaligus. Resize juga sering berarti reboot atau migrasi.
03Kenapa horizontal scaling mengharuskan app yang stateless?
Begitu ada beberapa instance, request apa pun bisa masuk ke instance mana pun. Jika session, keranjang, atau file upload hanya ada di memori atau disk satu proses, request berikutnya tidak akan menemukannya. Memindahkan state ke Redis, database, atau object storage membuat semua instance bisa melayani semua request.
04Bagaimana menangani session saat scale out?
Simpan session di shared store dengan masa berlaku, seperti Redis, supaya instance mana pun bisa memuatnya lewat id. Alternatifnya pakai token bertanda tangan seperti JWT agar state ikut di dalam request. Hindari sticky session, yang menurut Twelve-Factor App tidak boleh diandalkan.
05Kenapa database bagian tersulit untuk di-scale horizontal?
Menambah app server itu mudah, tetapi semuanya menulis ke primary yang sama. Read replica PostgreSQL hanya menjalankan query read-only, jadi menambah kapasitas read dan tidak pernah kapasitas write. Setiap instance app baru juga membuka connection pool sendiri yang bisa menghabiskan max_connections, yang biasanya 100 secara default.
Horizontal vs Vertical Scaling: Mana yang Dipilih Dulu
Scale up dulu, scale out saat butuh redundansi. Bagaimana statelessness, session, database, dan bentuk biaya menentukan pilihan horizontal atau vertical scaling.
Lakukan vertical scaling lebih dulu: naikkan spesifikasi server sampai satu mesin tidak lagi cukup, karena tidak perlu mengubah kode. Pilih horizontal scaling saat butuh redundansi atau kapasitas melebihi satu mesin, yang menuntut app server stateless, session di shared store seperti Redis, dan rencana terpisah untuk database, sebab write tetap masuk ke satu primary.
Qilap, ERP carwash yang saya bangun, awalnya berjalan di satu VPS kecil: NestJS, Postgres, Redis, dan Docker dalam satu mesin. Saat trafik mulai terlihat bisa melampaui kapasitasnya, muncul pertanyaan yang selalu ditanyakan setiap tim: beli server lebih besar, atau tambah server kedua?
Tulisan ini menjawabnya dengan urutan yang akan saya pakai sendiri. Definisi dan batasnya diambil dari sumber di bagian akhir (Twelve-Factor App, manual PostgreSQL, nginx, dan hukum Amdahl), dan semua angka di bawah adalah hitungan dari asumsi yang disebutkan, bukan benchmark.
Apa beda horizontal scaling dan vertical scaling?
Vertical scaling (scale up) memberi satu mesin lebih banyak CPU, memori, atau disk. Horizontal scaling (scale out) menambah jumlah mesin dan membagi beban ke semuanya. Yang pertama mengubah ukuran node; yang kedua mengubah jumlah node.
Perbedaan itu menentukan semuanya. Node yang lebih besar tidak terlihat oleh kode Anda. Node yang lebih banyak terlihat oleh semua bagian: setiap request bisa jatuh ke proses yang berbeda, dan apa pun yang diingat aplikasi di memori tiba-tiba berada di tempat yang salah.
Mana yang sebaiknya dipilih lebih dulu?
Pilih vertical dulu untuk aplikasi satu servis di satu mesin, lalu pindah ke horizontal saat salah satu dari tiga hal ini benar: Anda perlu bertahan saat satu mesin mati, tidak bisa resize di tempat tanpa downtime yang bisa diterima, atau node terbesar yang bisa disewa sudah tidak cukup. Tabel berikut menunjukkan apa yang dituntut tiap pilihan.
Pertanyaan
Vertical (scale up)
Horizontal (scale out)
Perubahan kode
Tidak ada; aplikasi tidak tahu
App harus stateless, session dan file dipindahkan keluar
Jika mesin mati
Seluruh kapasitas hilang sekaligus
Hanya hilang satu bagian kapasitas, bukan semuanya
Batas atas
Instance terbesar yang dijual provider
Jauh lebih tinggi, dibatasi komponen bersama seperti database
Resize
Biasanya reboot atau migrasi
Tambah atau kurangi node di belakang load balancer, tanpa restart
Bentuk biaya
Berundak: Anda membeli ukuran berikutnya
Mulus per node, ditambah load balancer dan kapasitas cadangan
Database
Primary lebih besar membantu write dan read
Replica hanya membantu read; write tetap di satu primary
Baris terakhir yang sering mengejutkan. Menambah app server adalah separuh yang mudah dari horizontal scaling. Lapisan data tidak bisa di-scale out dengan cara yang sama, itulah sebabnya database punya bagian tersendiri di bawah.
Apa yang harus dimiliki aplikasi sebelum bisa scale out?
Aplikasi harus stateless: request apa pun bisa dilayani instance mana pun, karena tidak ada yang dibutuhkan pengguna yang hanya hidup di satu proses. Twelve-Factor App menyatakan aturannya sebagai proses yang stateless dan share-nothing, dengan semua data yang harus bertahan disimpan di backing service.
Cache dan counter di memori yang harus konsisten antar instance (rate limiter, keranjang, antrean job).
File yang ditulis ke disk lokal, seperti upload, PDF, dan struk, yang tidak terlihat oleh instance lain.
Timer ala cron di dalam aplikasi, yang kini berjalan sekali per instance, bukan sekali saja.
Perbaikannya selalu sama: pindahkan state ke layanan yang bisa dijangkau semua instance. Berikut bentuk masalah paling sederhana, versi yang salah dan versi yang benar berdampingan.
// Wrong: this Map lives in ONE process. With two instances behind a
// load balancer, the cart written by instance A is invisible to instance B.
const carts = new Map<string, CartItem[]>();
app.post("/cart/items", (req, res) => {
const items = carts.get(req.userId) ?? [];
carts.set(req.userId, [...items, req.body]);
res.sendStatus(204);
});
// Right: the state lives in a service every instance can reach.
app.post("/cart/items", async (req, res) => {
const key = "cart:" + req.userId;
await redis.rpush(key, JSON.stringify(req.body));
await redis.expire(key, 60 * 60 * 24); // abandoned carts clean themselves up
res.sendStatus(204);
});
Bagaimana menangani session pengguna di banyak server?
Simpan session di shared store dengan masa berlaku, bukan di memori server. Twelve-Factor App menyebut Memcached atau Redis sebagai kandidat yang baik untuk data session karena mendukung time-expiration. Instance mana pun lalu bisa memuat session dari id-nya, sehingga load balancer bebas mengirim request berikutnya ke mana saja.
import Redis from "ioredis";
const redis = new Redis(process.env.REDIS_URL);
const SESSION_TTL_SECONDS = 1800; // 30 minutes, sliding
export async function saveSession(id: string, data: object) {
// SET key value EX seconds: the expiry is set atomically with the write
await redis.set("sess:" + id, JSON.stringify(data), "EX", SESSION_TTL_SECONDS);
}
export async function loadSession(id: string) {
const raw = await redis.get("sess:" + id);
if (!raw) return null;
await redis.expire("sess:" + id, SESSION_TTL_SECONDS); // slide the window
return JSON.parse(raw);
}
# nginx: round-robin is the default; least_conn favours the idle instance.
upstream api {
least_conn;
server 10.0.0.11:3000 max_fails=3 fail_timeout=10s;
server 10.0.0.12:3000 max_fails=3 fail_timeout=10s;
}
server {
listen 80;
location / {
proxy_pass http://api;
}
}
Token bertanda tangan seperti JWT adalah jalur lain: state ikut di dalam request, jadi tidak ada yang perlu dicari. Kompromisnya, token sulit dicabut sebelum kedaluwarsa. Saya membandingkan keduanya di artikel sebelumnya tentang JWT versus session authentication.
Sticky session mengikat pengguna ke satu instance, dan itu menyembunyikan masalah, bukan menyelesaikannya. Twelve-Factor App tegas: sticky session adalah pelanggaran dan tidak boleh diandalkan. Saat instance itu restart atau dicabut, semua session yang dipegangnya hilang.
Kenapa database adalah bagian tersulit dari scaling?
Karena setiap instance app yang Anda tambah tetap berbicara ke primary yang sama. Hot standby PostgreSQL hanya menerima query read-only saat memutar ulang write-ahead log dari primary, dan manualnya menyatakan transaksinya tidak pernah bisa menulis. Jadi replica menambah kapasitas read, bukan kapasitas write.
Hukum Amdahl memberi angka pada batas itu. Jika sebagian s dari pekerjaan bersifat serial, percepatan maksimum dari n worker adalah 1 dibagi (s ditambah (1 dikurangi s) dibagi n). Misalkan 10 persen dari tiap request adalah write serial ke primary. Delapan app server memberi 1 / (0,1 + 0,9/8) = 1 / 0,2125, sekitar 4,7 kali, dan berapa pun jumlah server tidak bisa melewati 10 kali.
-- Amdahl: speedup = 1 / (s + (1 - s) / n), with s = 0.10 serial write share
-- n = 2 -> 1 / (0.10 + 0.45) = 1.82x
-- n = 8 -> 1 / (0.10 + 0.1125) = 4.71x
-- n = inf -> 1 / 0.10 = 10x (the ceiling)
-- Connection budget across the whole fleet, not per process:
-- instances * pool_size must stay below max_connections
-- 4 * 20 = 80 fits under the typical default of 100
-- 5 * 20 = 100 reaches it
SHOW max_connections;
SELECT count(*) FROM pg_stat_activity; -- how many are in use right now
Jebakan kedua adalah koneksi. Manual PostgreSQL menyebut max_connections biasanya 100 secara default. Jika tiap instance membuka pool 20, empat instance sudah butuh 80 koneksi, dan lima instance langsung menyentuh batas 100. Hitung pool untuk seluruh fleet, bukan per proses, sebelum menambah node.
Sebelum scale out app, tanyakan apakah database memang bottleneck-nya. Kalau penyebabnya query lambat atau index yang kurang, server app kedua justru menggandakan beban pada komponen yang sudah kewalahan.
Bagaimana perbedaan biaya antara scale up dan scale out?
Biaya vertical berundak; biaya horizontal mulus tetapi punya overhead. Berikut contoh hitungan dengan input asumsi, bukan hasil pengukuran. Misalkan satu request memakan 40 ms CPU, sehingga satu core melayani 1000 / 40 = 25 request per detik. Dengan target utilisasi 70 persen, itu 17,5 per core. Puncak 120 request per detik lalu butuh 120 / 17,5 = 6,9, jadi 7 core.
assumed CPU per request 40 ms -> 1000 / 40 = 25 req/s per core
target utilisation 70 % -> 25 * 0.7 = 17.5 req/s per core
peak load 120 req/s -> 120 / 17.5 = 6.86 -> 7 cores
1 x 8 vCPU 8 cores fails -> 0 cores -> 0 req/s
4 x 2 vCPU 8 cores lose 1 -> 6 cores -> 105 req/s (below 120)
5 x 2 vCPU 10 cores lose 1 -> 8 cores -> 140 req/s (survives)
price linear in vCPU (assumption): 10 units vs 8 -> 25 % premium for N+1
Satu server 8 vCPU cukup, tetapi jika mati Anda tidak melayani apa pun. Empat server 2 vCPU juga berjumlah 8 core, namun kehilangan satu menyisakan 6 core, atau 105 request per detik, kurang dari puncak 120. Untuk bertahan dari satu kegagalan saat puncak, Anda butuh lima server 2 vCPU: 10 core, dan 8 setelah kehilangan satu, atau 140 per detik. Jika harga naik linear terhadap vCPU (cek provider Anda; ini asumsi), itu 10 unit lawan 8, seperempat lebih mahal, ditambah load balancer. Selisih itulah harga redundansi.
Checklist untuk memutuskan
Kerjakan berurutan. Berhenti di langkah pertama yang menyelesaikan masalah Anda, karena setiap langkah berikutnya butuh lebih banyak rekayasa daripada sebelumnya.
Ukur dulu: temukan bottleneck sebenarnya (CPU, memori, disk, atau database) sebelum mengubah topologi.
Scale up satu node jika bottleneck-nya CPU atau memori dan Anda bisa menerima jendela resize singkat.
Jadikan app stateless: session ke Redis, file ke object storage, job terjadwal ke satu runner.
Scale out di belakang load balancer, tambah read replica untuk beban yang dominan read, dan hitung N+1 supaya satu kegagalan tidak menembus puncak Anda.
Untuk beban ERP atau POS kecil di satu VPS, langkah satu sampai tiga biasanya cukup untuk waktu yang lama, dan tidak ada yang menutup jalan ke langkah empat.
Scale up dulu karena itu perubahan termurah, dan jadikan app stateless sejak awal karena itulah yang menjaga scale out tetap mungkin nanti. Perlakukan database sebagai masalah terpisah: replica menambah read, tidak pernah write, dan setiap instance baru memakan koneksi.