Saturation #
Sistem yang kewalahan jarang langsung mati. Ia biasanya memberikan peringatan terlebih dahulu — latency yang perlahan naik, queue yang terus bertumbuh, error rate yang mulai merangkak — sebelum akhirnya tidak bisa melayani request apapun. Peringatan-peringatan ini adalah sinyal saturasi: kondisi di mana satu atau beberapa resource dalam sistem sudah mendekati atau melampaui kapasitas maksimalnya.
Saturation adalah salah satu konsep paling penting dalam reliability engineering. Ia berbeda dari utilization (seberapa banyak resource digunakan) dan errors (apakah terjadi kegagalan). Saturation mengukur seberapa banyak pekerjaan yang sedang menunggu untuk dilayani — antrian yang terbentuk karena supply tidak bisa mengimbangi demand. Sebuah sistem yang CPU-nya berjalan di 70% tidak jenuh. Sistem yang CPU-nya 70% tapi ada 50 proses yang mengantri di run queue sudah mulai jenuh.
Memahami saturation memberi engineer kemampuan untuk memprediksi kapan sistem akan bermasalah sebelum masalah terjadi, bukan hanya bereaksi setelah user mengeluh.
USE Method: Kerangka Kerja untuk Memahami Saturasi #
Brendan Gregg memperkenalkan USE Method — Utilization, Saturation, Errors — sebagai kerangka kerja sistematis untuk mendiagnosis performa sistem. Ketiganya harus selalu diukur bersama karena memberikan gambaran yang berbeda:
USE Method:
Utilization:
→ Seberapa sibuk resource (persentase waktu digunakan)
→ CPU 85% = utilisasi tinggi
→ Tapi utilisasi tinggi belum tentu berarti saturasi
Saturation:
→ Seberapa banyak pekerjaan yang MENUNGGU untuk dilayani
→ Load average > jumlah CPU = saturasi
→ Queue panjang = saturasi
→ Ini yang sebenarnya menyebabkan latency naik
Errors:
→ Apakah ada operasi yang gagal
→ Disk write error, network packet loss, allocation failure
→ Seringkali muncul setelah saturasi sudah parah
Hubungan ketiganya:
Utilisasi rendah + tidak ada saturasi = sistem sehat
Utilisasi tinggi + tidak ada saturasi = sibuk tapi masih sehat
Utilisasi tinggi + saturasi tinggi = sistem kewalahan
Utilisasi rendah + saturasi tinggi = ada bottleneck (resource tidak efisien)
graph LR
A[Resource] --> B{Utilization}
B --> |Rendah| C[Sehat]
B --> |Tinggi| D{Saturation?}
D --> |Tidak ada antrian| E[Sibuk tapi sehat]
D --> |Ada antrian| F[Sistem jenuh]
F --> G[Latency naik]
F --> H[Throughput turun]
F --> I[Errors mulai muncul]
style F fill:#ffcccc
style G fill:#ffcccc
style H fill:#ffcccc
style I fill:#ffccccCPU Saturation #
CPU saturation terjadi ketika ada lebih banyak proses/thread yang butuh CPU daripada CPU yang tersedia. Proses yang tidak mendapat CPU masuk ke run queue dan menunggu.
# Mengukur CPU saturation di Linux
# Load average — cara tercepat untuk deteksi CPU saturation
uptime
# Output: load average: 4.20, 3.85, 3.10
# Angka pertama: 1 menit, kedua: 5 menit, ketiga: 15 menit
# Interpretasi load average:
# Load average = jumlah CPU → utilisasi 100%, batas saturasi
# Load average > jumlah CPU → SATURASI — ada proses yang menunggu
# Cek jumlah CPU:
nproc # atau: cat /proc/cpuinfo | grep "^processor" | wc -l
# Jika load average 4.20 dan CPU = 4:
# 4.20 / 4 = 1.05 → sedikit di atas kapasitas → mulai saturasi
# Cara lebih detail dengan vmstat:
vmstat 1 10 # sampling setiap 1 detik, 10 kali
# Kolom yang penting:
# r = run queue (proses yang menunggu CPU) ← ini yang mengukur saturasi
# b = proses yang blocked (menunggu I/O)
# us = user CPU (%)
# sy = system CPU (%)
# id = idle CPU (%)
# wa = wait I/O (%)
# Contoh output yang mengkhawatirkan:
# procs memory swap io system cpu
# r b swpd free buff cache si so bi bo in cs us sy id wa
# 8 2 1024 512 128 4096 0 0 2048 1024 500 800 85 10 0 5
# r=8 dengan 4 CPU → 4 proses menunggu → saturasi!
# top / htop untuk real-time monitoring
top
# Perhatikan:
# - %Cpu(s): us+sy = total usage
# - Load average (kanan atas)
# - VIRT/RES/SHR per proses
# sar untuk historical data
sar -q 1 5 # load average dan run queue history
# atau: sar -u 1 5 # CPU utilization history
// Deteksi CPU saturation secara programatik.
package main
import (
"math"
"os"
"runtime"
"strconv"
"strings"
"time"
)
// checkCPUSaturation melaporkan apakah CPU dalam kondisi saturasi.
// thresholdMultiplier: jika loadAvg > cpuCount * multiplier → saturasi
func checkCPUSaturation(thresholdMultiplier float64) map[string]interface{} {
cpuCount := runtime.NumCPU()
loadAvg1Min := loadAverage1Min() // membaca /proc/loadavg di Linux
utilization := cpuUtilization() // persentase selama satu detik terakhir
// Saturation ratio: > 1.0 berarti ada proses yang menunggu
saturationRatio := loadAvg1Min / float64(cpuCount)
isSaturated := saturationRatio > thresholdMultiplier
severity := "ok"
switch {
case saturationRatio > 2.0:
severity = "critical"
case saturationRatio > 1.5:
severity = "warning"
}
return map[string]interface{}{
"cpu_count": cpuCount,
"load_avg_1min": loadAvg1Min,
"utilization_pct": utilization,
"saturation_ratio": math.Round(saturationRatio*100) / 100,
"is_saturated": isSaturated,
"severity": severity,
}
}
// loadAverage1Min mengurai field pertama dari /proc/loadavg.
func loadAverage1Min() float64 {
data, err := os.ReadFile("/proc/loadavg")
if err != nil {
return 0
}
fields := strings.Fields(string(data))
if len(fields) == 0 {
return 0
}
value, _ := strconv.ParseFloat(fields[0], 64)
return value
}
// cpuUtilization mengambil sampel /proc/stat dua kali, jeda satu detik.
func cpuUtilization() float64 {
sample := func() (idle, total uint64) {
data, err := os.ReadFile("/proc/stat")
if err != nil {
return 0, 0
}
fields := strings.Fields(strings.SplitN(string(data), "\n", 2)[0])
for i, raw := range fields[1:] {
value, _ := strconv.ParseUint(raw, 10, 64)
total += value
if i == 3 { // kolom idle
idle = value
}
}
return idle, total
}
idle1, total1 := sample()
time.Sleep(time.Second)
idle2, total2 := sample()
if total2 == total1 {
return 0
}
return 100 * (1 - float64(idle2-idle1)/float64(total2-total1))
}
Penyebab umum CPU saturation dan cara mitigasinya:
Penyebab:
→ Traffic yang terlalu tinggi untuk kapasitas server
→ Query database yang mahal (full table scan, complex join)
→ Proses yang tidak efisien (algoritma O(n²), regex yang lambat)
→ GC (Garbage Collection) yang sering dan lama di JVM/Go
→ Thread yang terlalu banyak dan sering context switching
Mitigasi jangka pendek:
→ Scale horizontal (tambah server, auto-scaling)
→ Kill atau nice proses yang tidak penting
→ Rate limiting untuk mengurangi beban
Mitigasi jangka panjang:
→ Profiling dan optimasi kode yang CPU-intensive
→ Caching untuk mengurangi komputasi berulang
→ Pisahkan workload berat ke background worker
→ Review dan optimasi query database
Memory Saturation #
Memory saturation terjadi ketika sistem kehabisan RAM fisik dan terpaksa menggunakan swap — area disk yang jauh lebih lambat. Ini adalah bottleneck yang sangat berdampak karena akses disk bisa ribuan kali lebih lambat dari akses RAM.
# Mengukur memory saturation
# free: gambaran umum penggunaan memory
free -h
# Output:
# total used free shared buff/cache available
# Mem: 16Gi 12Gi 500Mi 512Mi 3Gi 3.5Gi
# Swap: 8Gi 4Gi 4Gi
# Yang perlu diperhatikan:
# available: memori yang bisa langsung dipakai aplikasi (lebih akurat dari 'free')
# Swap used: jika swap digunakan → tanda saturasi memory
# vmstat untuk melihat swap activity
vmstat 1 10
# Kolom si (swap in) dan so (swap out):
# si > 0 atau so > 0 → sistem sedang swapping → saturasi!
# Kolom b (blocked processes):
# Proses yang blocked karena menunggu page dari swap
# Pemantauan yang lebih detail
cat /proc/meminfo | grep -E "MemTotal|MemFree|MemAvailable|SwapTotal|SwapFree|SwapCached"
# Cek proses dengan penggunaan memory tertinggi
ps aux --sort=-%mem | head -20
# OOM (Out of Memory) killer events:
dmesg | grep -i "oom\|killed process"
# Jika ada ini → sistem pernah kehabisan memory dan OS membunuh proses
// Monitor memory saturation.
package main
import (
"math"
"os"
"strconv"
"strings"
)
// checkMemorySaturation memeriksa kondisi memory dan mendeteksi saturasi.
func checkMemorySaturation() map[string]interface{} {
meminfo := readMeminfo() // MemTotal, MemAvailable, SwapTotal, SwapFree (kB)
totalKB := meminfo["MemTotal"]
availableKB := meminfo["MemAvailable"]
swapTotalKB := meminfo["SwapTotal"]
swapFreeKB := meminfo["SwapFree"]
// Persentase memory yang tersedia
availablePct := 100 * availableKB / totalKB
// Swap digunakan = sinyal saturasi
swapUsedPct := 0.0
if swapTotalKB > 0 {
swapUsedPct = 100 * (swapTotalKB - swapFreeKB) / swapTotalKB
}
isSaturated := availablePct < 10 || swapUsedPct > 50
severity := "ok"
switch {
case availablePct < 5 || swapUsedPct > 80:
severity = "critical"
case availablePct < 15 || swapUsedPct > 30:
severity = "warning"
}
return map[string]interface{}{
"total_gb": math.Round(float64(totalKB)/(1024*1024)*10) / 10,
"available_gb": math.Round(float64(availableKB)/(1024*1024)*10) / 10,
"used_pct": math.Round((100-availablePct)*10) / 10,
"available_pct": math.Round(availablePct*10) / 10,
"swap_used_pct": math.Round(swapUsedPct*10) / 10,
"swap_total_gb": math.Round(float64(swapTotalKB)/(1024*1024)*10) / 10,
"is_saturated": isSaturated,
"severity": severity,
}
}
// readMeminfo mengurai /proc/meminfo menjadi map key → kB.
func readMeminfo() map[string]float64 {
result := map[string]float64{}
data, err := os.ReadFile("/proc/meminfo")
if err != nil {
return result
}
for _, line := range strings.Split(string(data), "\n") {
fields := strings.Fields(line)
if len(fields) < 2 {
continue
}
key := strings.TrimSuffix(fields[0], ":")
value, err := strconv.ParseFloat(fields[1], 64)
if err == nil {
result[key] = value
}
}
return result
}
Memory saturation — tanda-tanda dan mitigasi:
Tanda-tanda saturasi memory:
→ Swap digunakan secara aktif (vmstat: si/so > 0)
→ Response time naik tajam (akses swap = ribuan kali lebih lambat dari RAM)
→ OOM killer membunuh proses secara otomatis
→ Aplikasi crash dengan error "cannot allocate memory"
Mengapa swap bukan solusi:
→ Disk SSD: ~100.000 IOPS, latensi ~0.1ms
→ RAM: miliaran operasi/detik, latensi ~100 nanosecond
→ Swap adalah solusi darurat, bukan kapasitas tambahan
Mitigasi:
→ Tambah RAM (vertical scaling)
→ Optimize memory usage di aplikasi
→ Pastikan tidak ada memory leak
→ Konfigurasi JVM heap yang tepat
→ Pisahkan workload ke server berbeda
→ Pertimbangkan in-memory caching dengan eviction policy
Disk I/O Saturation #
I/O saturation terjadi ketika disk tidak bisa mengimbangi kecepatan write dan read yang diminta sistem.
# Mengukur disk I/O saturation
# iostat — tool utama untuk I/O analysis
iostat -x 1 10 # extended stats, setiap detik, 10 kali
# Kolom penting di iostat -x:
# %util = seberapa sibuk disk (persentase waktu disk aktif)
# await = rata-rata waktu tunggu per request (ms) ← termasuk queue time
# svctm = rata-rata waktu service per request (ms) ← hanya service time
# avgqu = rata-rata panjang queue ← ini yang mengukur saturasi!
# r/s = read requests per second
# w/s = write requests per second
# Interpretasi:
# %util mendekati 100% → disk sangat sibuk
# avgqu > 1 → ada antrian → saturasi!
# await >> svctm → request menunggu lama di queue → saturasi
# Contoh output yang menunjukkan saturasi:
# Device r/s w/s rkB/s wkB/s await svctm %util avgqu
# sda 50 200 400 3200 45.2 2.1 98.5 8.3
# avgqu=8.3 → 8 request menunggu di queue → saturasi parah!
# iotop: siapa yang paling banyak menggunakan disk
iotop -o # -o: hanya tampilkan proses yang sedang I/O
# Melihat latency per operasi (lebih detail)
biolatency 1 # dari bcc-tools, membutuhkan kernel eBPF support
// Monitoring I/O saturation.
package main
import (
"math"
"os"
"strconv"
"strings"
"time"
)
// checkDiskSaturation memeriksa apakah disk dalam kondisi saturasi.
func checkDiskSaturation(device string) map[string]interface{} {
// Sampling dua kali untuk menghitung rate
diskIO1 := readDiskstats()
time.Sleep(time.Second)
diskIO2 := readDiskstats()
results := map[string]interface{}{}
for name, io2 := range diskIO2 {
if device != "" && name != device {
continue
}
io1, ok := diskIO1[name]
if !ok {
continue
}
readBytes := io2.readSectors - io1.readSectors
writeBytes := io2.writeSectors - io1.writeSectors
readTime := io2.readMs - io1.readMs
writeTime := io2.writeMs - io1.writeMs
// Busy time dalam persen (approximation)
busyTime := (readTime + writeTime) / 10.0 // ms ke persen dari 1 detik
busyPct := math.Min(100, math.Round(busyTime*10)/10)
results[name] = map[string]interface{}{
"read_mb_s": math.Round(float64(readBytes)*512/1024/1024*10) / 10,
"write_mb_s": math.Round(float64(writeBytes)*512/1024/1024*10) / 10,
"busy_pct": busyPct,
"is_saturated": busyTime > 80,
}
}
return results
}
// diskstats menyimpan counter dari satu baris /proc/diskstats.
type diskstats struct {
readSectors uint64
writeSectors uint64
readMs uint64
writeMs uint64
}
// readDiskstats mengurai /proc/diskstats (Linux).
func readDiskstats() map[string]diskstats {
result := map[string]diskstats{}
data, err := os.ReadFile("/proc/diskstats")
if err != nil {
return result
}
for _, line := range strings.Split(string(data), "\n") {
fields := strings.Fields(line)
if len(fields) < 11 {
continue
}
name := fields[2]
s, _ := strconv.ParseUint(fields[5], 10, 64) // sektor dibaca
sw, _ := strconv.ParseUint(fields[9], 10, 64) // sektor ditulis
rms, _ := strconv.ParseUint(fields[6], 10, 64) // ms membaca
wms, _ := strconv.ParseUint(fields[10], 10, 64) // ms menulis
result[name] = diskstats{readSectors: s, writeSectors: sw, readMs: rms, writeMs: wms}
}
return result
}
Penyebab dan mitigasi I/O saturation:
Penyebab umum:
→ Full table scan tanpa index di database besar
→ Log yang ditulis tanpa buffering
→ Backup yang berjalan selama jam sibuk
→ N+1 query yang menghasilkan ribuan disk access
→ Database yang cache-nya terlalu kecil
Mitigasi:
→ Index yang tepat untuk mengurangi disk read
→ Buffer pool/cache database yang lebih besar
→ SSD atau NVMe untuk menggantikan HDD
→ Pisahkan log ke disk terpisah
→ Jadwalkan backup di luar jam sibuk
→ RAID atau storage terdistribusi
→ Database read replica untuk mengurangi beban read di primary
Network Saturation #
Network saturation terjadi ketika bandwidth yang tersedia tidak cukup untuk semua traffic yang harus melewatinya.
# Mengukur network saturation
# sar -n DEV: historical network stats
sar -n DEV 1 10
# Kolom penting:
# rxkB/s = kilobytes per second received
# txkB/s = kilobytes per second transmitted
# rxdrop/s = dropped receive packets (tanda saturasi!)
# txdrop/s = dropped transmit packets (tanda saturasi!)
# iftop: real-time bandwidth per connection
iftop -n -i eth0
# nload: simple bandwidth monitoring
nload eth0
# ss: socket statistics (koneksi dan buffer)
ss -s # summary
ss -ti # TCP info dengan detail
# Cek receive buffer saturation:
ss -ti | grep "rcv_space\|rcvbuf"
# Buffer penuh = network saturation di level socket
# netstat: packet stats termasuk drops
netstat -s | grep -i "drop\|overflow"
# receive buffer errors > 0 → socket buffer overflow → saturasi!
# ip -s link: interface statistics termasuk drop
ip -s link show eth0
# RX: bytes packets errors dropped
# Jika dropped > 0 → network saturation atau buffer overflow
Network saturation — apa yang terjadi:
Skenario 1: Bandwidth penuh
→ rxkB/s atau txkB/s mendekati batas bandwidth interface (1Gbps, 10Gbps)
→ Packet mulai di-drop di kernel atau di network device
→ Latency naik karena packet retransmission
Skenario 2: Socket buffer overflow
→ Application tidak bisa consume data secepat data datang
→ Receive buffer penuh → packet di-drop
→ Terjadi meski bandwidth belum penuh
Skenario 3: Connection table exhaustion
→ Terlalu banyak concurrent connection
→ SYN flood atau legitimate high traffic
Mitigasi:
→ Upgrade bandwidth (10Gbps → 100Gbps)
→ Kompresi data untuk mengurangi bandwidth usage
→ CDN untuk static assets (kurangi traffic ke origin)
→ Connection pooling untuk mengurangi overhead
→ Perbesar socket buffer:
# sysctl -w net.core.rmem_max=134217728
# sysctl -w net.core.wmem_max=134217728
Application-Level Saturation: Thread Pool dan Connection Pool #
Saturation tidak hanya terjadi di level hardware. Thread pool dan connection pool yang habis adalah bentuk saturasi di level aplikasi.
// Monitoring saturasi thread pool.
package main
import (
"sync"
)
// MonitoredThreadPool melacak pekerjaan yang menunggu dan memberi peringatan saat
// queue tumbuh melebihi kapasitas.
type MonitoredThreadPool struct {
maxWorkers int
mu sync.Mutex
pending int
jobs chan func()
wg sync.WaitGroup
}
// NewMonitoredThreadPool menjalankan maxWorkers goroutine.
func NewMonitoredThreadPool(maxWorkers int) *MonitoredThreadPool {
p := &MonitoredThreadPool{
maxWorkers: maxWorkers,
jobs: make(chan func(), maxWorkers*2),
}
for i := 0; i < maxWorkers; i++ {
p.wg.Add(1)
go func() {
defer p.wg.Done()
for job := range p.jobs {
job()
}
}()
}
return p
}
// Submit memasukkan fn ke antrian untuk dieksekusi, mengukur saturasi lebih dulu.
func (p *MonitoredThreadPool) Submit(fn func()) {
p.mu.Lock()
p.pending++
pending := p.pending
p.mu.Unlock()
if pending > p.maxWorkers*2 {
// Queue sudah > 2x kapasitas → saturasi!
logger.Warn("Thread pool saturated",
"pending_tasks", pending,
"max_workers", p.maxWorkers,
"saturation_ratio", float64(pending)/float64(p.maxWorkers))
}
p.jobs <- fn
p.mu.Lock()
p.pending--
p.mu.Unlock()
}
// SaturationRatio returns pending / maxWorkers.
func (p *MonitoredThreadPool) SaturationRatio() float64 {
p.mu.Lock()
defer p.mu.Unlock()
return float64(p.pending) / float64(p.maxWorkers)
}
// Monitoring saturasi database connection pool.
package main
import (
"database/sql"
"log"
"time"
)
// setupPoolMonitoring mencatat statistik pool pada interval tetap.
// database/sql mengeksposnya via db.Stats().
func setupPoolMonitoring(db *sql.DB) {
go func() {
for range time.Tick(30 * time.Second) {
stats := db.Stats()
log.Printf("Pool: inUse=%d idle=%d maxOpen=%d",
stats.InUse, stats.Idle, stats.MaxOpenConnections)
}
}()
}
// checkPoolSaturation memberi peringatan saat pool mendekati batasnya.
func checkPoolSaturation(db *sql.DB) {
stats := db.Stats()
maxSize := stats.MaxOpenConnections
if maxSize > 0 && float64(stats.InUse) >= float64(maxSize)*0.9 {
log.Printf("Database connection pool near saturation: "+
"checked_out=%d max_connections=%d saturation_pct=%d",
stats.InUse, maxSize, stats.InUse*100/maxSize)
}
}
Metrik Saturation yang Wajib Dimonitor #
// Kumpulkan semua saturation metrics dalam satu tempat.
package main
import (
"fmt"
"runtime"
"time"
)
// SaturationReport menyimpan setiap saturation metric untuk satu sampel.
type SaturationReport struct {
Timestamp time.Time
CPULoadRatio float64 // load_avg / cpu_count
MemoryAvailablePct float64 // tersedia dalam %
SwapUsedPct float64 // swap terpakai dalam %
DiskBusyPct map[string]float64
HasWarnings bool
CriticalResources []string
}
// generateSaturationReport menghasilkan laporan untuk semua resource.
func generateSaturationReport() SaturationReport {
warnings := []string{}
critical := []string{}
// CPU
cpuCount := runtime.NumCPU()
loadAvg := loadAverage1Min()
loadRatio := loadAvg / float64(cpuCount)
switch {
case loadRatio > 2.0:
critical = append(critical, fmt.Sprintf("CPU: load ratio %.1fx (critical)", loadRatio))
case loadRatio > 1.5:
warnings = append(warnings, fmt.Sprintf("CPU: load ratio %.1fx (warning)", loadRatio))
}
// Memory
meminfo := readMeminfo()
availPct := 100 * meminfo["MemAvailable"] / meminfo["MemTotal"]
swapPct := 0.0
if meminfo["SwapTotal"] > 0 {
swapPct = 100 * (meminfo["SwapTotal"] - meminfo["SwapFree"]) / meminfo["SwapTotal"]
}
switch {
case availPct < 5:
critical = append(critical, fmt.Sprintf("Memory: only %.1f%% available (critical)", availPct))
case availPct < 15:
warnings = append(warnings, fmt.Sprintf("Memory: only %.1f%% available (warning)", availPct))
}
if swapPct > 50 {
warnings = append(warnings, fmt.Sprintf("Swap: %.0f%% used", swapPct))
}
// Disk (disederhanakan)
diskStats := map[string]float64{}
for name := range readDiskstats() {
diskStats[name] = 0
}
return SaturationReport{
Timestamp: time.Now(),
CPULoadRatio: math.Round(loadRatio*100) / 100,
MemoryAvailablePct: math.Round(availPct*10) / 10,
SwapUsedPct: math.Round(swapPct*10) / 10,
DiskBusyPct: diskStats,
HasWarnings: len(warnings) > 0 || len(critical) > 0,
CriticalResources: critical,
}
}
Alert Threshold yang Berguna #
# Contoh konfigurasi alert (Prometheus AlertManager format konseptual)
# CPU saturation
alert: CPUSaturation
expr: node_load1 / count(node_cpu_seconds_total{mode="idle"}) without (cpu, mode) > 1.5
for: 5m
labels:
severity: warning
annotations:
summary: "CPU saturation detected"
description: "Load average is {{ $value }}x the number of CPUs"
# Memory saturation
alert: MemorySaturation
expr: node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes < 0.10
for: 5m
labels:
severity: warning
annotations:
summary: "Low memory available"
description: "Only {{ $value | humanizePercentage }} memory available"
# Swap usage
alert: SwapUsageHigh
expr: node_memory_SwapFree_bytes / node_memory_SwapTotal_bytes < 0.5
for: 10m
labels:
severity: warning
annotations:
summary: "High swap usage"
description: "More than 50% of swap is in use"
# Disk saturation
alert: DiskIOSaturation
expr: rate(node_disk_io_time_seconds_total[5m]) > 0.8
for: 5m
labels:
severity: warning
annotations:
summary: "Disk I/O saturation"
description: "Disk {{ $labels.device }} is {{ $value | humanizePercentage }} busy"
# Network drops
alert: NetworkDrops
expr: rate(node_network_receive_drop_total[5m]) > 10
for: 5m
labels:
severity: warning
annotations:
summary: "Network packets being dropped"
Mencegah Saturasi: Capacity Planning #
Deteksi saturasi bersifat reaktif. Capacity planning adalah pendekatan proaktif untuk memastikan sistem tidak mencapai titik saturasi secara mengejutkan.
Framework capacity planning sederhana:
1. Ukur baseline saat ini:
→ CPU utilization rata-rata dan peak
→ Memory usage rata-rata dan peak
→ Disk I/O rata-rata dan peak
→ Network throughput rata-rata dan peak
→ Request rate dan latency
2. Tentukan safety margin:
→ Jangan operasikan sistem di atas 70% utilisasi untuk resource kritikal
→ Sisakan 30% headroom untuk traffic spike dan pertumbuhan
→ Tanpa headroom, saturasi terjadi saat ada spike kecil
3. Proyeksikan pertumbuhan:
→ Berapa persen pertumbuhan traffic per bulan?
→ Kapan utilisasi akan mencapai 70% berdasarkan pertumbuhan itu?
→ Rencanakan scaling sebelum titik itu tercapai
4. Load testing:
→ Uji sistem pada kondisi peak yang lebih tinggi dari ekspektasi
→ Temukan bottleneck sebelum production menemukannya
→ Identifikasi resource mana yang pertama kali jenuh
5. Auto-scaling:
→ Horizontal: tambah instance saat CPU/memory mencapai threshold
→ Vertical: upgrade instance size untuk resource yang tidak bisa di-scale horizontal
→ Pastikan auto-scaling cukup cepat merespons sebelum sistem saturasi
Anti-Pattern yang Harus Dihindari #
✗ Anti-pattern 1: monitoring hanya utilization, bukan saturation
CPU 80% terlihat oke. Tapi load average 16 di server 4-core
menunjukkan 12 proses menunggu → sistem sudah sangat jenuh.
✓ Monitor load average dan queue depth, bukan hanya persentase.
✗ Anti-pattern 2: bereaksi setelah user mengeluh
User melaporkan aplikasi lambat → investigasi → ditemukan saturasi.
Saturasi sudah berlangsung mungkin sejak jam yang lalu.
✓ Pasang alert proaktif untuk saturation metrics.
✗ Anti-pattern 3: scaling saat sudah kritis
Auto-scaling dipicu saat CPU 95% → perlu waktu untuk instance baru ready
→ selama itu sistem dalam kondisi saturasi parah.
✓ Trigger scaling lebih awal (threshold 60-70%), bukan saat sudah kritis.
✗ Anti-pattern 4: tidak ada load test
Kapasitas sistem di production tidak diketahui sampai production collapse.
✓ Load test reguler untuk menemukan batas kapasitas sebelum production.
✗ Anti-pattern 5: mengabaikan swap
"Swap masih ada, berarti masih aman."
Swap yang aktif digunakan adalah tanda system sudah dalam kondisi darurat.
✓ Alert saat swap mulai digunakan, bukan saat swap sudah penuh.
Checklist Saturation Monitoring #
MEASUREMENT:
□ Load average dimonitor relatif terhadap jumlah CPU (bukan angka absolut)
□ Memory: available_pct dimonitor (bukan hanya free)
□ Swap usage dimonitor dan di-alert jika mulai digunakan
□ Disk: avgqu (average queue depth) dimonitor via iostat
□ Network: packet drops dimonitor
□ Application: thread pool queue depth dimonitor
□ Database: connection pool saturation dimonitor
ALERTING:
□ CPU: alert jika load ratio > 1.5x selama lebih dari 5 menit
□ Memory: alert jika available < 15%
□ Swap: alert jika swap digunakan > 30%
□ Disk: alert jika disk busy > 80% selama lebih dari 5 menit
□ Network: alert jika ada packet drops
□ DB pool: alert jika connection pool > 80% terpakai
CAPACITY PLANNING:
□ Baseline metrics terdokumentasi
□ Growth rate dihitung dan diproyeksikan
□ Safety margin 30% diterapkan untuk semua resource kritikal
□ Load testing dilakukan secara berkala
□ Auto-scaling dikonfigurasi dengan threshold yang tepat (tidak terlalu late)
INCIDENT RESPONSE:
□ Runbook tersedia untuk setiap jenis saturasi
□ Eskalasi otomatis jika saturasi berlanjut setelah N menit
□ Post-mortem dilakukan setelah setiap insiden saturasi
Ringkasan #
- Saturation mengukur antrian, bukan utilisasi — utilisasi 80% tidak berarti sistem jenuh. Yang berarti jenuh adalah adanya antrian: load average melebihi jumlah CPU, request menunggu di thread pool, query menunggu di connection pool.
- USE Method memberikan gambaran lengkap — Utilization, Saturation, dan Errors harus selalu diukur bersama. Saturation adalah yang paling langsung berkorelasi dengan degradasi performa yang dirasakan user.
- Load average di atas jumlah CPU adalah sinyal CPU saturasi —
load_avg / cpu_count > 1berarti ada proses yang menunggu. Di atas 1.5 perlu perhatian, di atas 2.0 perlu tindakan segera.- Swap yang aktif digunakan adalah kondisi darurat — sistem sudah kehabisan RAM dan menggunakan disk yang ribuan kali lebih lambat. Alert harus berbunyi saat swap mulai digunakan, bukan saat sudah penuh.
- Disk saturation diukur dari avgqu, bukan %util — disk 100% util tapi tidak ada antrian masih bisa melayani dengan baik. Queue depth yang tinggi menunjukkan request menunggu terlalu lama.
- Application-level saturation sama pentingnya — thread pool yang exhausted dan connection pool yang habis adalah bentuk saturasi yang tidak terlihat di metrik OS, tapi sangat terasa di user.
- Scale sebelum saturasi, bukan saat saturasi — auto-scaling yang dipicu saat sistem sudah saturasi terlalu lambat. Trigger di 60-70% utilisasi memberikan waktu untuk instance baru siap sebelum sistem kewalahan.
- Load testing menemukan batas sebelum production — jangan tunggu production untuk menemukan kapasitas maksimum sistem. Load test reguler memberikan data yang diperlukan untuk capacity planning.
- Safety margin 30% adalah minimum — operasi sistem di 70% utilisasi secara konsisten memberikan ruang untuk spike traffic tanpa langsung mencapai saturasi.
- Packet drops adalah sinyal network saturation yang paling jelas — berbeda dari CPU dan memory yang bisa dimonitor via utilization, network saturation paling akurat terdeteksi dari drops.