Banyak tim backend memecah monolith menjadi microservices atas dasar asumsi skalabilitas teoretis. Praktik ini sering berakhir pada peningkatan latensi jaringan, distributed transaction complexity, dan lonjakan biaya infrastruktur tanpa menyelesaikan akar masalah performa. Keputusan transisi arsitektur harus berlandaskan data observasi empiris, bukan tren atau estimasi kasar.
Metrik Riil Penentu Pemisahan Service
Sebuah komponen dalam modular monolith hanya valid dipisahkan menjadi microservice independen jika profil pemanfaatan sumber daya sistemik menunjukkan isolasi kebutuhan perangkat keras yang saling bertentangan secara permanen. Analisis metrik berikut sebelum merancang arsitektur terdistribusi:
- CPU Profiling (Flame Graphs): Identifikasi apakah pemakaian CPU tinggi disebabkan oleh logika komputasi murni (misal: encoding media, enkripsi masif, kalkulasi matriks) atau alokasi memori yang tidak efisien akibat Garbage Collector (GC) thrashing. Komputasi murni dapat diisolasi, sedangkan alokasi GC diselesaikan via refactoring alokasi objek.
- I/O Wait dan Database Bottlenecks: Jika metrik
iowaitOS tinggi atau koneksi database jenuh (koneksi pool habis), pemecahan service ke distributed network tetap mengarahkan beban ke database yang sama. Masalah ini diselesaikan via penambahan index yang tepat, query batching, atau in-memory caching. - Tail Latency (p99 dan p99.9): Degradasi latensi di persentil tinggi sering kali disebabkan oleh lock contention di database atau GC pause, bukan kegagalan throughput monolith. Memindahkan endpoint ke service baru via REST/gRPC menambahkan overhead serialisasi dan transit jaringan 1-5 ms pada setiap layer call stack.
Kapan Optimasi Monolith Cukup?
Mayoritas masalah konkurensi dan skalabilitas dapat diselesaikan di dalam boundary monolith melalui optimasi lokal. Tabel berikut membandingkan tindakan teknis berbasis observasi:
| Observasi Metrik | Gejala Teknis | Solusi Hemat Biaya | Kapan Justifikasi Microservice? |
|---|---|---|---|
| P99 tinggi, CPU rendah | Lock contention pada table baris transaksi, slow query. | Tambahkan covering index, perbaiki transaction boundary, Redis read-through cache. | Hanya jika partisi database per domain mutlak terisolasi secara kepatuhan audit data. |
| CPU saturasi 100% | Loop komputasi berat blocking worker pool HTTP. | Pindahkan pemrosesan ke background queue lokal (asynchronous task runner). | Proses membutuhkan resource spesifik (GPU atau memory-bound tinggi) yang mengganggu thread API. |
| Koneksi DB Habis | Connection pool exhaustion akibat synchronous call eksternal. | Implementasikan PgBouncer/Proxy, timeout ketat, circuit breaker. | Domain memiliki lifecycle deployment yang membutuhkan isolasi fault domain ekstrem antar tim engineer. |
Skrip Observasi Throughput dan Latensi (Go)
Sebelum memutuskan migrasi, lakukan baseline benchmarking menggunakan skrip benchmark konkurensi sederhana untuk merekam degradasi latency p50, p90, dan p99 pada target monolith.
package main
import (
"crypto/tls"
"flag"
"fmt"
"net/http"
"sort"
"sync"
"time"
)
func main() {
targetURL := flag.String("url", "http://localhost:8080/api/v1/orders", "Target URL to test")
totalReqs := flag.Int("n", 1000, "Total requests")
concurrency := flag.Int("c", 50, "Concurrency level")
flag.Parse()
client := &http.Client{
Timeout: 5 * time.Second,
Transport: &http.Transport{
MaxIdleConnsPerHost: *concurrency,
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
chJobs := make(chan struct{}, *totalReqs)
for i := 0; i < *totalReqs; i++ {
chJobs <- struct{}{}
}
close(chJobs)
latencies := make([]time.Duration, 0, *totalReqs)
var mu sync.Mutex
var wg sync.WaitGroup
startTime := time.Now()
for i := 0; i < *concurrency; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for range chJobs {
reqStart := time.Now()
resp, err := client.Get(*targetURL)
reqDuration := time.Since(reqStart)
if err == nil {
resp.Body.Close()
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
mu.Lock()
latencies = append(latencies, reqDuration)
mu.Unlock()
}
}
}
}()
}
wg.Wait()
totalDuration := time.Since(startTime)
if len(latencies) == 0 {
fmt.Println("Semua request gagal.")
return
}
sort.Slice(latencies, func(i, j int) bool {
return latencies[i] < latencies[j]
})
p50 := latencies[int(float64(len(latencies))*0.50)]
p90 := latencies[int(float64(len(latencies))*0.90)]
p99 := latencies[int(float64(len(latencies))*0.99)]
rps := float64(len(latencies)) / totalDuration.Seconds()
fmt.Printf("Total sukses: %d/%d\n", len(latencies), *totalReqs)
fmt.Printf("Durasi: %.2fs (%.2f req/sec)\n", totalDuration.Seconds(), rps)
fmt.Printf("p50: %v | p90: %v | p99: %v\n", p50, p90, p99)
}
Tahapan Validasi Sebelum Refactoring
Jalankan tahapan berikut secara berurutan sebelum mengusulkan ekstraksi service:
- Query Tuning dan Indexing: Jalankan
EXPLAIN ANALYZEpada top 5 query paling sering dieksekusi. Eliminasi sequential scan pada tabel berukuran besar. - Connection Pooling dan Caching: Konfigurasikan cache Redis read-through untuk entitas yang jarang berubah (90% read, 10% write). Pastikan connection pool database disetel proporsional terhadap core CPU database server, bukan dibuat tak terbatas.
- Isolasi In-Process (Modular Monolith): Pisahkan boundary domain bisnis secara eksplisit di level kode menggunakan internal package/module terisolasi tanpa inter-module shared database tables.
- Profiling Metrik Ulang: Validasi kembali dengan skrip load test. Jika p99 latency tetap tinggi hanya pada modul spesifik akibat batasan I/O disk atau CPU starvation yang tidak dapat diselesaikan via optimasi database, pisahkan modul tersebut sebagai service independen.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!