Kapasitas server utama TokoKita hampir habis: disk terisi 85 persen, agregasi harian makin lambat, dan tim harus memilih arah investasi. Ada dua jalan klasik: vertical scaling (scale up) mengganti mesin dengan yang lebih besar, sedangkan horizontal scaling (scale out) menambah mesin baru yang bekerja bersama. Keduanya menambah kapasitas, tetapi dengan karakter sangat berbeda dari sisi biaya, dampak kegagalan, dan perubahan yang dibutuhkan aplikasi. Chapter ini membedah kedua strategi lewat tabel perbandingan dan benchmark kecil yang bisa kita jalankan langsung.
Dua Arah Menambah Kapasitas #
Vertical scaling menaikkan spesifikasi satu mesin: CPU lebih banyak, RAM lebih besar, disk lebih cepat. Aplikasi tidak perlu berubah karena semua data tetap berada di satu tempat — database PostgreSQL yang pindah dari mesin 8 core ke 32 core tetap diakses seperti biasa. Batasannya bersifat fisik dan finansial: harga mesin kelas atas naik jauh lebih cepat daripada performanya, dan pada titik tertentu tidak ada mesin yang lebih besar untuk dibeli.
Horizontal scaling menambah mesin baru (node) lalu membagi data dan beban ke seluruh node. Cluster Kafka yang ditambah broker baru atau cluster Spark yang ditambah worker adalah contohnya. Konsekuensinya, aplikasi harus tahu cara membagi data (partitioning), menjaga salinan (replication), dan mengoordinasikan pekerjaan — tiga topik inti section ini yang sebelumnya tidak dibutuhkan pada satu mesin.
Perbandingan Kedua Strategi #
| Aspek | Vertical scaling (scale up) | Horizontal scaling (scale out) |
|---|---|---|
| Cara menambah kapasitas | Ganti mesin dengan yang lebih besar | Tambah mesin baru ke cluster |
| Perubahan aplikasi | Nyaris tidak ada | Perlu partitioning, replication, koordinasi |
| Batas kapasitas | Plafon spesifikasi satu mesin | Naik setiap kali node ditambahkan |
| Dampak satu node berhenti | Seluruh sistem berhenti | Sebagian kapasitas hilang, sistem tetap melayani |
| Kurva biaya | Melonjak di kelas atas (hardware khusus) | Relatif linear per node biasa |
| Waktu penyediaan | Hari hingga minggu | Menit pada lingkungan cloud |
Dalam praktik, banyak tim memakai jalur hibrida: tumbuh secara vertical selama biayanya masuk akal, lalu berpindah ke horizontal ketika plafon mesin atau anggaran tercapai. Keputusan pindah bukan sekadar soal kapasitas — tim juga harus siap menanggung kompleksitas distributed system yang dibahas pada Chapter Dasar Distributed Systems.
Benchmark Kecil: Satu Pekerja versus Beberapa Pekerja #
Kita bisa meniru inti kedua strategi di dalam satu mesin dengan Python. Satu pekerja yang mengerjakan tugas satu per satu mewakili satu mesin; beberapa pekerja yang berjalan bersamaan mewakili beberapa node. Dua jenis pekerja disediakan pustaka standar: thread (pekerja ringan dalam satu proses) dan process (pekerja dengan interpreter dan core sendiri, paling dekat dengan analogi node terpisah). Benchmark berikut menguji keduanya pada dua beban berbeda: beban CPU murni dan beban IO yang hanya menunggu.
"""Perbandingan eksekusi sekuensial, thread, dan process pada beban CPU vs IO."""
import time
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
N_ITERASI = 1_200_000 # iterasi per tugas CPU
TUGAS_IO = 6 # jumlah tugas IO
JEDA_IO = 0.05 # detik menunggu per tugas IO (disimulasikan)
def beban_cpu(n):
"""Kerja murni CPU: jumlah kuadrat, pengganti agregasi kolom angka."""
return sum(i * i for i in range(n))
def beban_io(detik):
"""Kerja IO: menunggu respons layanan eksternal; CPU menganggur selama menunggu."""
time.sleep(detik)
return detik
def ukur(label, fungsi, daftar_tugas, ulang=5):
"""Ukur sekuensial: waktu tercepat dari beberapa pengulangan."""
terbaik = None
for _ in range(ulang):
mulai = time.perf_counter()
for tugas in daftar_tugas:
fungsi(tugas)
durasi = time.perf_counter() - mulai
terbaik = durasi if terbaik is None else min(terbaik, durasi)
print(f" {label:<26} {terbaik:.3f} detik")
return terbaik
def ukur_paralel(label, pembuat_pool, fungsi, daftar_tugas, ulang=5):
"""Ukur paralel: pool dibuat sekali, waktu tempuh hanya untuk pemetaan tugas."""
terbaik = None
with pembuat_pool(max_workers=len(daftar_tugas)) as pool:
for _ in range(ulang):
mulai = time.perf_counter()
list(pool.map(fungsi, daftar_tugas))
durasi = time.perf_counter() - mulai
terbaik = durasi if terbaik is None else min(terbaik, durasi)
print(f" {label:<26} {terbaik:.3f} detik")
return terbaik
tugas_cpu = [N_ITERASI] * 4
tugas_io = [JEDA_IO] * TUGAS_IO
print(f"Beban CPU: 4 tugas x {N_ITERASI:,} iterasi (mesin uji: 2 CPU)")
dasar = ukur("sekuensial (1 pekerja)", beban_cpu, tugas_cpu)
ukur_paralel("4 thread", ThreadPoolExecutor, beban_cpu, tugas_cpu)
proses = ukur_paralel("4 process", ProcessPoolExecutor, beban_cpu, tugas_cpu)
print(f" -> process {dasar / proses:.2f}x lebih cepat dari sekuensial")
print(f"Beban IO: {TUGAS_IO} tugas x {JEDA_IO * 1000:.0f} ms menunggu")
dasar = ukur("sekuensial (1 pekerja)", beban_io, tugas_io)
benang = ukur_paralel("6 thread", ThreadPoolExecutor, beban_io, tugas_io)
print(f" -> thread {dasar / benang:.2f}x lebih cepat dari sekuensial")Output yang Dihasilkan:
Beban CPU: 4 tugas x 1,200,000 iterasi (mesin uji: 2 CPU)
sekuensial (1 pekerja) 0.451 detik
4 thread 0.474 detik
4 process 0.283 detik
-> process 1.59x lebih cepat dari sekuensial
Beban IO: 6 tugas x 50 ms menunggu
sekuensial (1 pekerja) 0.300 detik
6 thread 0.050 detik
-> thread 5.98x lebih cepat dari sekuensialMari kita telaah baris-baris kode di atas:
- Baris 10-12 (
beban_cpu): tugas murni CPU tanpa jeda — pengganti agregasi kolom angka pada pipeline nyata. - Baris 15-18 (
beban_io): tugas yang hanya menunggu lewattime.sleep, meniru panggilan API eksternal; CPU menganggur selama menunggu. - Baris 21-31 (
ukur): setiap konfigurasi diukur lima kali dan diambil waktu tercepatnya, praktik umum benchmark agar satu jendela waktu yang tertabrak beban lain tidak menyesatkan. - Baris 34-44 (
ukur_paralel): pool pekerja dibuat sekali di luar pengukuran, sehingga waktu tempuh yang tercatat hanya eksekusipool.map, bukan biaya menyiapkan pool. - Baris 50-54 versus 56-59: blok pertama membandingkan tiga cara mengerjakan empat tugas CPU; blok kedua mengulang perbandingan pada enam tugas IO.
Membaca Hasil: Paralelisme Punya Syarat #
Pada beban CPU, empat thread nyaris tidak mengubah apa pun: 0.474 berbanding 0.451 detik pada eksekusi ini — sedikit lebih lambat, dan pada eksekusi lain kadang sedikit lebih cepat karena beban latar mesin. Penyebabnya pada CPython adalah Global Interpreter Lock (GIL): hanya satu thread yang mengeksekusi bytecode pada satu waktu, sehingga menambah thread pada kerja CPU tidak menambah throughput secara berarti. Empat process memang membantu (1.59x), tetapi jauh dari 4x karena mesin uji hanya punya 2 CPU: percepatan berhenti di jumlah core yang tersedia, dan biaya koordinasi antar proses ikut memotong hasil.
Pada beban IO hasilnya terbalik: enam thread menyelesaikan pekerjaan 5.98x lebih cepat karena saat satu thread menunggu, core bebas melayani thread lain — menunggu bisa ditumpuk tanpa menambah CPU. Pelajaran bagi scaling: menambah pekerja hanya membantu bila pekerjaannya memang bisa berjalan bersamaan. Angka pasti akan berbeda di mesin Anda dan bergeser antar eksekusi karena beban latar; bagian yang stabil dari banyak pengulangan adalah polanya: thread tidak memberi percepatan berarti pada beban CPU, process berhenti di jumlah core, dan thread mempercepat beban IO hampir sebanding jumlahnya.
Skalabilitas horizontal menjanjikan kapasitas yang tumbuh mengikuti jumlah node, tetapi janji tersebut hanya terpenuhi bila data benar-benar bisa dibagi rata ke seluruh node. Cara membagi data itulah pembahasan berikutnya pada Chapter Partitioning dan Sharding.