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:#ffcccc

CPU 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 saturasiload_avg / cpu_count > 1 berarti 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.

← Sebelumnya: Firewall   Berikutnya: OOM Killer →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact