Insiden scratch worker yang penuh pada November terselesaikan dalam satu pagi, tetapi pertanyaan yang menyusul lebih sulit: kapan kejadian serupa terulang? Membeli kapasitas berlebih berarti membayar untuk ruang yang menganggur, sedangkan kapasitas kurang berarti insiden berikutnya tinggal menunggu tanggal. Capacity planning menjembatani keduanya dengan cara yang bisa diperiksa: meramalkan pertumbuhan data dari historis, menambah headroom untuk beban puncak, lalu menghitung kapan sumber daya yang ada sekarang akan terlampaui — semua sebelum kejadian, bukan sesudahnya.

Meramalkan Pertumbuhan Data #

Titik awal perencanaan adalah pertumbuhan data itu sendiri. Volume bulanan platform data TokoKita selama 12 bulan terakhir diambil dari katalog tabel, lalu dicocokkan dengan garis lurus lewat regresi kuadrat terkecil: satu slope (pertumbuhan per bulan) dan satu intercept. Keberhasilan pencocokan diukur dengan R² — nilai mendekati 1 berarti pertumbuhan memang mendekati garis lurus, sehingga proyeksi beberapa bulan ke depan layak dipakai sebagai bahan diskusi. Baris per bulan ikut dimodelkan dengan cara yang sama karena beban query dan durasi job ditentukan jumlah baris, bukan hanya ukuran byte.

Headroom: Merencanakan untuk Puncak #

Rata-rata adalah angka yang menyesatkan untuk perencanaan. Kurva kedatangan per jam dari log ingestion menunjukkan puncak malam hari sekitar 3,6 kali rata-rata — fleet yang disiapkan seukuran rata-rata akan kewalahan tepat pada jam tersibuk. Kebutuhan perencanaan dihitung dengan mengalikan proyeksi bulanan dengan rasio puncak/rata-rata tersebut; inilah yang disebut headroom. Praktik right-sizing dari AWS Well-Architected Cost Optimization Pillar menggunakan logika yang sama: ukur beban nyata, lalu pilih ukuran sumber daya yang menutup puncak tanpa membayar idle berlebih.

Proyeksi 12 Bulan dan Titik Jebol #

Skrip berikut menjalankan ketiga langkah tersebut sekaligus: fit linear pada GB dan baris, proyeksi 12 bulan ke depan, lalu pencarian titik jebol dengan dua batas nyata — scratch pool untuk backfill penuh (kasus terburuk reprocessing dari Chapter Recovery dan Reprocessing) dan kapasitas disk terkelola:

python
"""Perencana kapasitas: regresi linear pada volume bulanan (GB dan baris),
proyeksi 12 bulan, headroom dari rasio puncak/rata-rata beban, dan estimasi
titik jebol scratch/disk dari total data berjalan."""
from datetime import date

# Data historis 12 bulan dari katalog tabel (bulanan, Jan-Des 2026)
HISTORIS_GB = [38, 41, 45, 48, 53, 57, 60, 65, 70, 76, 81, 88]
HISTORIS_BARIS = [58, 62, 69, 73, 82, 88, 92, 101, 108, 118, 126, 137]  # juta

# Kurva kedatangan per jam (relatif) dari log ingestion; puncak = flash sale
KURVA_JAM = [0.4, 0.3, 0.25, 0.25, 0.3, 0.5, 0.8, 1.2, 1.6, 1.8, 1.9, 2.0,
             1.9, 1.8, 1.7, 1.8, 2.0, 2.3, 2.8, 3.4, 3.6, 3.2, 2.2, 1.0]

SCRATCH_POOL_GB = 2048   # scratch total klaster (4 worker x 512 GB)
DISK_TOTAL_GB = 4096     # disk terkelola platform data
FAKTOR_SHUFFLE = 1.6     # scratch backfill ~ data x 1.6 (spill + sort)


def fit_linear(ys):
    """Regresi linear kuadrat terkecil; kembalikan (slope, intercept, r2)."""
    n = len(ys)
    xs = range(1, n + 1)
    mx, my = sum(xs) / n, sum(ys) / n
    sxy = sum((x - mx) * (y - my) for x, y in zip(xs, ys))
    sxx = sum((x - mx) ** 2 for x in xs)
    slope = sxy / sxx
    intercept = my - slope * mx
    sstot = sum((y - my) ** 2 for y in ys)
    ssres = sum((y - (intercept + slope * x)) ** 2 for x, y in zip(xs, ys))
    return slope, intercept, 1 - ssres / sstot


def volume_gb(bulan):
    """Volume GB bulan ke-n: historis untuk n<=12, proyeksi linear setelahnya."""
    return HISTORIS_GB[bulan - 1] if bulan <= 12 else intercept_gb + slope_gb * bulan


def label_bulan(bulan):
    """Label kalender: bulan 1 = Jan 2026."""
    d = date(2026 + (bulan - 1) // 12, (bulan - 1) % 12 + 1, 1)
    return d.strftime("%b %Y")


slope_gb, intercept_gb, r2_gb = fit_linear(HISTORIS_GB)
slope_br, intercept_br, _ = fit_linear(HISTORIS_BARIS)

print(f"Model GB/bulan   : {intercept_gb:.1f} + {slope_gb:.2f} x bulan (R² {r2_gb:.3f})")
print(f"Model baris(juta): {intercept_br:.1f} + {slope_br:.2f} x bulan\n")

print("| Bulan ke | Kalender | GB | Baris (juta) |")
print("|---|---|---|---|")
for bulan in (13, 15, 18, 21, 24):
    print(f"| {bulan} | {label_bulan(bulan)} | {volume_gb(bulan):.0f} |"
          f" {intercept_br + slope_br * bulan:.0f} |")

puncak = max(KURVA_JAM)
rata = sum(KURVA_JAM) / len(KURVA_JAM)
rasio = puncak / rata
print(f"\nHeadroom: beban puncak {puncak:g}x vs rata-rata {rata:.2f}x "
      f"-> rasio {rasio:.2f}")
print(f"Kapasitas rencana bulan ke-24: {volume_gb(24):.0f} GB/bulan x "
      f"{rasio:.2f} = {volume_gb(24) * rasio:.0f} GB ekuivalen hari puncak")

# Titik jebol: total data berjalan (retensi 24 bulan) vs scratch dan disk
total = 0.0
jebol_scratch = jebol_disk = None
for bulan in range(1, 61):
    total += volume_gb(bulan)
    if bulan > 24:                      # geser jendela retensi 24 bulan
        total -= volume_gb(bulan - 24)
    if jebol_scratch is None and total * FAKTOR_SHUFFLE > SCRATCH_POOL_GB:
        jebol_scratch = (bulan, total)
    if jebol_disk is None and total > 0.8 * DISK_TOTAL_GB:
        jebol_disk = (bulan, total)
    if jebol_scratch and jebol_disk:
        break

bulan, total = jebol_scratch
print(f"\nTitik jebol scratch (backfill penuh {FAKTOR_SHUFFLE}x): "
      f"bulan ke-{bulan} ({label_bulan(bulan)})")
print(f"  total data {total:.0f} GB x {FAKTOR_SHUFFLE} = "
      f"{total * FAKTOR_SHUFFLE:.0f} GB > pool {SCRATCH_POOL_GB} GB"
      f" -> backfill harus bertahap per kuartal")
bulan, total = jebol_disk
print(f"Titik jebol disk (80% dari {DISK_TOTAL_GB} GB): bulan ke-{bulan} "
      f"({label_bulan(bulan)}), total {total:.0f} GB (ekstrapolasi, ±)")

Output yang Dihasilkan:

text
Model GB/bulan   : 31.2 + 4.46 x bulan (R² 0.989)
Model baris(juta): 46.8 + 7.08 x bulan

| Bulan ke | Kalender | GB | Baris (juta) |
|---|---|---|---|
| 13 | Jan 2027 | 89 | 139 |
| 15 | Mar 2027 | 98 | 153 |
| 18 | Jun 2027 | 111 | 174 |
| 21 | Sep 2027 | 125 | 195 |
| 24 | Dec 2027 | 138 | 217 |

Headroom: beban puncak 3.6x vs rata-rata 1.62x -> rasio 2.22
Kapasitas rencana bulan ke-24: 138 GB/bulan x 2.22 = 306 GB ekuivalen hari puncak

Titik jebol scratch (backfill penuh 1.6x): bulan ke-18 (Jun 2027)
  total data 1324 GB x 1.6 = 2118 GB > pool 2048 GB -> backfill harus bertahap per kuartal
Titik jebol disk (80% dari 4096 GB): bulan ke-36 (Dec 2028), total 3371 GB (ekstrapolasi, ±)

Mari kita telaah baris-baris kode di atas:

  1. Baris 19-30 (fit_linear): slope dan intercept dihitung manual dengan jumlah kuadrat terkecil; R² membandingkan galat model terhadap galat “tebak rata-rata” sehingga 0,989 berarti model hampir seluruhnya menjelaskan variasi data.
  2. Baris 33-34 (volume_gb): bulan 1-12 memakai angka historis, bulan berikutnya memakai model — satu fungsi melayani kedua rezim sehingga loop titik jebol tidak perlu membedakan keduanya.
  3. Baris 47-51 (headroom): rasio puncak/rata-rata dihitung dari kurva jam nyata, bukan asumsi; hasil 2,22 berarti kapasitas harus menutup hari yang beban dua kali lipat dari rata-rata.
  4. Baris 60-73 (dua titik jebol): total data berjalan dihitung dengan jendela retensi 24 bulan yang bergeser tiap bulan; dua ambang berbeda dicatat terpisah karena keduanya menuntut tindakan berbeda.

Pembacaan hasilnya: pada Juni 2027, backfill penuh 24 bulan tidak lagi muat di scratch pool, sehingga rencana reprocessing harus bertahap per kuartal atau scratch sementara ditambahkan. Batas disk terlampaui pada Desember 2028 — masih dua tahun, tetapi sudah masuk siklus anggaran tahunan.

Ukuran Worker dan Posisi Autoscaling #

Hasil perencanaan diterjemahkan ke ukuran sumber daya. Tabel right-sizing berikut memetakan profil beban ke ukuran worker:

Profil workervCPU/RAMHargaCocok untuk
small4 / 16 GB$0.08/jamtask orkestrasi, ingestion ringan
medium8 / 32 GB$0.15/jamtransform harian, agregat mingguan
large16 / 64 GB$0.29/jamjoin besar, backfill, window panjang

Praktik pemakaiannya berlapis: fleet harian berjalan dengan empat worker medium, backfill kuartalan menambah dua worker large hanya selama jendela reprocessing, dan worker small cukup untuk task orkestrasi yang ringan. Kombinasi tersebut menutup hari puncak bulan ke-24 tanpa membuat profil besar menganggur sepanjang bulan.

Kapasitas yang terencana dengan baik tetap meninggalkan satu pertanyaan berikutnya: dari seluruh yang dibayar, berapa yang benar-benar menghasilkan pekerjaan? Chapter Optimasi Biaya membedah tagihan bulanan dan mencari penghematan terbesar yang bisa dieksekusi.