← semua materi
05:00 Arsitektur Β· 31 Oct 2026 Β· 05:03

πŸ”¬ BACOT β€” Sesi Deep Learning

πŸ“š Topik: Algoritma Load Balancing & Health Check yang Benar

Kategori: Infra Β· Level: 2/3 (intermediate–advanced)


🎯 Tujuan Belajar

Setelah sesi ini, kamu bisa menjawab dengan alasan teknis β€” bukan selera:

  1. Kapan pakai round robin vs least-request vs P2C vs consistent hashing vs EWMA.
  2. Bedakan health check aktif vs passive (outlier detection) dan kenapa kamu butuh dua-duanya.
  3. Kenapa "health check = GET / return 200" adalah bug, bukan fitur.
  4. Kenapa satu pool health check bisa menjatuhkan seluruh cluster (cascading failure).
  5. Bilang dengan yakin: "di sistem ini, saya sengaja tidak pakai load balancer health check aktif, dan ini alasannya."

🧠 Penjelasan Inti

Load balancer melakukan dua hal berbeda yang sering dicampur:

Peran Pertanyaan Frekuensi Efek salah
Distribusi "Request ini ke instance mana?" per request hot spot, tail latency
Seleksi kesehatan "Instance ini masih layak dikirimi?" periodik / reaktif traffic ke host mati, atau host sehat dibuang

Keduanya punya blind spot berlawanan:

Ini inti sesi: load balancing adalah control loop, bukan fungsi hash. Input = observability signal (latency, error, queue depth), output = pemilihan host. Kalau signal-nya buruk, algoritma terbaik pun jadi noise generator.


βš™οΈ Cara Kerja Internal

1. Keluarga algoritma dan asumsi tersembunyinya

Round robin / weighted RR

next = (last + 1) mod N

Asumsi implisit: biaya tiap request kira-kira sama, kapasitas host sama, latensi host sama.

Asumsi ini pecah kalau: request punya distribusi durasi heavy-tail (p99 100Γ— p50), atau ada host yang "sama secara spesifikasi" tapi noisy neighbor di node yang sama.

Least connections / least requests

pick argmin(active_requests[i] / weight[i])

Lebih baik untuk request heterogen. Masalah: power of two choices β€” memilih min dari N sekaligus butuh lock/atomic read semua counter; min dari 2 hampir sama bagusnya dengan biaya jauh lebih murah.

P2C (Power of Two Choices) β€” pilihan default de-facto di Envoy, Linkerd, NGINX Plus, gRPC.

a = rand(N); b = rand(N)
pick min(load[a], load[b])

Kenapa penting: dengan random murni, max load β‰ˆ O(log N / log log N). Dengan P2C, max load β‰ˆ O(log log N). Untuk N=1000, random murni bisa menumpuk ~10Γ— rata-rata; P2C ~2–3Γ—. Ini hasil analisis klasik "balls into bins" (Azar et al., 1994) β€” bukan benchmark vendor.

P2C butuh definisi "load". Kalau load = jumlah request aktif, host yang pegang request lambat akan cepat "terlihat sibuk" dan traffic menjauh β€” bagus. Tapi kalau request-nya di-cache dan selesai dalam 200Β΅s, counter hampir selalu 0 dan P2C degenerasi ke random. Load metric harus mencerminkan waktu, bukan count.

EWMA latency Skor = exponentially weighted moving average latency. Envoy punya LEAST_REQUEST + Slow Start + opsi selection_degraded; Linkerd secara default pakai P2C dengan EWMA latency β€” ini pilihan desain yang menarik: latency EWMA lebih tahan terhadap request count yang fluktuatif.

Kritik: EWMA latency bisa "mengunci" host cepat yang sudah kewalahan (self-fulfilling: host cepat tetap cepat sampai antrean meledak). Mitigasinya adalah komponen load, bukan murni latency.

Consistent hashing / ring hash / Maglev Untuk cache affinity dan stateful routing (session, shard DB, pub/sub partition). Kunci: minimal disruption saat N berubah.

Random β€” sering diremehkan, tapi untuk pool besar dengan request seragam, random hampir sama baiknya dengan RR dan stateless (tidak ada shared counter β†’ tidak ada contention di data plane). Ini yang membuatnya pilihan yang benar untuk sidecar dengan core banyak.

2. Health check aktif β€” anatomi parameter

interval ──┬──> probe ──> [pass] ──> healthy
           β”‚                β”‚
           └──> timeout ─────┴──> [fail] ──> unhealthy_threshold tercapai? ──> eject

Parameter yang benar-benar mengubah perilaku:

Parameter Fungsi Salah set β†’
interval periode probe terlalu pendek = probe storm; terlalu panjang = deteksi lambat
timeout batas probe timeout β‰₯ interval = probe numpuk, tidak pernah "gagal" cepat
unhealthy_threshold berapa kali gagal sebelum dibuang 1 = flap; 10 = 5 menit traffic ke host mati
healthy_threshold berapa kali sukses sebelum diterima 1 = host setengah-warm langsung dapat beban penuh
initialDelaySeconds / startup toleransi boot terlalu pendek = kill loop

Angka nyata, jangan asal: Envoy default interval 10s, unhealthy_threshold 2, healthy_threshold 2, timeout 1s. Artinya worst-case deteksi β‰ˆ 20s sebelum host mati dianggap mati. Untuk SLA ketat, 20s itu selamanya β€” dan inilah alasan passive health check wajib ada, bukan opsional.

Aturan penting: timeout health check harus < timeout request normal, dan health check harus menempuh jalur yang sama dengan request nyata (lihat bagian miskonsepsi).

3. Passive health check / outlier detection β€” mekanisme nyata

Ambil Envoy sebagai referensi konkret (terverifikasi dari dokumentasi resmi, docs versi 1.41.0-dev):

Panic threshold β€” bagian yang paling sering dilewatkan:

Envoy membuang status kesehatan bila host sehat < healthy_panic_threshold (default 50%). Artinya: kalau 6 dari 10 host gagal health check, cluster masuk panic mode dan traffic dikirim ke semua host termasuk yang gagal. Ini bukan bug β€” ini mitigasi cascade failure: lbih baik mengirim traffic ke host yang mungkin rusak daripada mengembalikan 503 ke 100% user.

Set panic_threshold: 0 kalau kamu benar-benar lebih suka "no healthy upstream" daripada degradasi parsial. Itu pilihan sadar, bukan default.

Detail halus dari docs yang layak dicatat: "If your active health check is not validating data plane traffic then in situations where active health checking passes but the traffic is failing, the endpoint will be unejected prematurely." β†’ kombinasi active+passive bisa saling merusak kalau health check tidak representatif. Ini bukan teori; ini penyebab flapping yang sulit didiagnosis.

4. Slow start / warm-up

Host baru butuh JIT warmup, connection pool fill, cache fill. Kalau langsung dapat beban penuh β†’ latensi spike β†’ passive check eject dia β†’ dia makin dingin β†’ siklus.

Solusi: ramp bobot secara linear dari 0/10% ke 100% selama N detik (Envoy slow_start window; NGINX slow_start; Kubernetes tidak punya native-nya, harus via EndpointSlice weight atau proxy).

Ini wajib untuk runtime dengan JIT (JVM, .NET) atau aplikasi dengan lazy init besar.

5. Kenapa "health check yang benar" = satu topik terpisah

Kesalahan fundamental: health endpoint mengecek sesuatu yang tidak pernah gagal dalam produksi.

❌ GET /healthz β†’ 200 OK        (server process hidup β€” selalu 200 selama binary jalan)
βœ… GET /healthz β†’ cek dependency yang benar-benar dipakai request path

Tapi jangan juga mengecek semuanya:

❌ /healthz β†’ SELECT 1 ke DB primary + cek Redis + cek Kafka + cek S3

Ini menciptakan coupled failure domain: DB down β†’ semua 50 instance dianggap unhealthy β†’ LB kosong β†’ 100% outage, padahal sebagian traffic masih bisa dilayani (read dari cache, atau write ke DB replica).

Pola yang benar: pisahkan tiga endpoint.

Endpoint Dipakai oleh Cek apa Jangan cek apa
/livez orchestrator (restart) proses tidak deadlock dependency eksternal
/readyz LB / Service endpoints siap terima traffic (init selesai, warm) dependency yang down sementara
/startupz (K8s) orchestrator (grace period) init selesai sekali apa pun setelahnya

K8s secara eksplisit memisahkan ini: livenessProbe gagal β†’ kubelet restart container; readinessProbe gagal β†’ alamat pod dihapus dari EndpointSlice, container tidak disentuh; startupProbe sukses sekali β†’ berhenti dan menyerahkan ke dua lainnya (sumber: dokumentasi resmi Kubernetes "Configure Liveness, Readiness and Startup Probes").

Konsekuensi yang sering dilupakan: liveness probe yang mengecek DB = restart storm. DB down 30 detik β†’ semua pod di-restart bersamaan β†’ cold start β†’ database dihantam connection storm β†’ down lebih lama.


πŸ—οΈ Diagram / Pseudocode

Control loop lengkap

                 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   request ─────►│  LB                                    β”‚
                 β”‚  1. filter: healthy set                β”‚
                 β”‚  2. panic? β†’ semua host                β”‚
                 β”‚  3. pick: P2C / EWMA / ring / RR       β”‚
                 β”‚  4. slow-start weight ramp             β”‚
                 β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                         β”‚                  β”‚
              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
              β”‚ active probe    β”‚    β”‚ passive observer β”‚
              β”‚ (periodic)      β”‚    β”‚ (per response)   β”‚
              β”‚ interval 10s    β”‚    β”‚ 5xx, timeout,    β”‚
              β”‚ unhealthy=2     β”‚    β”‚ reset            β”‚
              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                         β”‚                  β”‚
                    β”Œβ”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”
                    β”‚ health state per endpoint  β”‚
                    β”‚ + ejection backoff         β”‚
                    β”‚ + max_ejection_percent 10% β”‚
                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

P2C dengan bobot dan slow start (pseudocode)

def pick(endpoints):
    healthy = [e for e in endpoints if e.health == HEALTHY]
    if len(healthy) < panic_threshold * len(endpoints):
        healthy = endpoints                      # panic mode

    a, b = random.sample(healthy, 2)
    return min(a, b, key=lambda e: e.load / e.effective_weight())

def effective_weight(e):
    if e.age < SLOW_START_WINDOW:
        return e.weight * (e.age / SLOW_START_WINDOW)
    return e.weight

e.load harus bukan sekadar jumlah request aktif kalau durasi request bervariasi. Minimal: outstanding_requests. Lebih baik: EWMA(latency) Γ— outstanding (ini dia "load" versi Linkerd/Envoy-ish) β€” karena host dengan 5 request lambat lebih sibuk dari host dengan 50 request cepat.

Health endpoint yang tidak menciptakan coupled failure

# readyz: dependency critical-only, dengan cache agar tidak hammer DB
@app.get("/readyz")
def readyz():
    if not app.initialized:
        return 503, "not initialized"
    if time.monotonic() - db_ok_at > 30:      # cek max 1Γ— / 30s
        if not db.ping(timeout=0.2):
            return 503, "db unavailable"
        db_ok_at = time.monotonic()
    return 200, "ok"

# livez: JANGAN sentuh dependency
@app.get("/livez")
def livez():
    return 200 if event_loop_responsive() else 500

event_loop_responsive() untuk async runtime: jadwalkan callback, ukur delay. Delay > X = event loop blocked (deadlock sinkron di async code) β†’ ini alasan sah untuk restart.


βš–οΈ Trade-off & Kapan JANGAN Dipakai

Ini bagian yang menentukan senior vs junior.

Round robin β€” jangan dipakai kalau:

P2C β€” jangan dipakai kalau:

Consistent hashing β€” jangan dipakai kalau:

Active health check β€” jangan dipakai kalau:

Passive/outlier detection β€” hati-hati:

Health check interval β€” jangan terlalu agresif:

Probe 1s Γ— 200 instance = 200 QPS overhead hanya untuk health check, dan itu masuk ke pool connection + worker thread. Untuk layanan yang butuh latensi ketat, probe itu sendiri jadi noise.


🏭 Contoh Produksi & Failure Mode

1. Readiness = liveness = "cek DB". DB failover 45 detik. Semua pod gagal /healthz β†’ kubelet restart semuanya β†’ 3 menit cold start β†’ DB kena connection storm saat kembali β†’ failover kedua. Outage 45 detik jadi 15 menit. Ini failure mode paling umum dan paling mahal.

2. Health check yang selalu 200. Proses hidup, event loop deadlock (blocking call di async handler). Probe 200. LB kirim traffic. Semua request timeout. Tidak ada yang restart karena liveness OK. Silent total outage dengan dashboard hijau. Deteksi: monitor actual request success rate, bukan probe.

3. Outlier detection di cluster 5 host. max_ejection_percent 10% β†’ 0 host. Satu host rusak terus menerima 20% traffic. Error rate 20% stabil selama berjam-jam. Engineer bingung "kenapa outlier detection tidak jalan" β€” karena matematikanya.

4. Panic threshold menyembunyikan masalah. 60% host down β†’ panic mode β†’ traffic ke semua host termasuk yang down. Error rate 60% tapi load balancer melaporkan "semua host menerima traffic, tidak ada yang di-eject". Kalau kamu tidak tahu panic threshold ada, kamu akan mencari bug di tempat yang salah.

5. Slow start tidak ada di JVM. Deploy rolling 20 pod, masing-masing butuh 40 detik JIT warmup. Pod baru langsung dapat beban penuh β†’ p99 3 detik β†’ readiness tetap 200 (server merespons, cuma lambat) β†’ tidak ada yang eject β†’ semua user kena p99 selama 40 detik Γ— 20 pod. Solusi: slow start window + readiness yang mengecek warmup (misal cache fill % atau JIT threshold), bukan cuma 200 OK.

6. Connection pool per-host tidak di-reset saat rejoin. Host di-eject 300s, kembali, tapi upstream connection pool masih penuh socket mati (half-open). Request pertama kena reset β†’ 5xx β†’ eject lagi. Loop 300s. Mitigasi: max_ejection_time lebih panjang dari TCP keepalive timeout, atau connection draining + GOAWAY di sisi server.

7. Sticky session vs rebalancing. Consistent hashing + host mati tanpa fallback policy β†’ 1/N key hilang total (bukan cuma melambat). Dengan hash_policy fallback ke P2C, key tersebut tetap dilayani, hanya kehilangan cache affinity β€” degradasi, bukan outage.


⚠️ Miskonsepsi Umum

Miskonsepsi Koreksi
"Health check = cek server hidup" Server hidup β‰  bisa melayani. /livez β‰  /readyz.
"Readiness dan liveness bisa pakai endpoint sama" Kalau sama, dependency down β†’ restart storm. Ini bug desain, bukan konfigurasi.
"Least connections selalu lebih baik dari RR" Hanya kalau durasi request heterogen dan load metric akurat. Untuk request seragam sub-ms, RR/random lebih murah tanpa kerugian.
"Consistent hashing menyelesaikan hot key" Tidak. Hot key = masalah partisi key. Consistent hashing hanya mengatur penempatan, bukan distribusi key.
"Outlier detection otomatis menyelamatkan cluster kecil" max_ejection_percent default 10% β†’ cluster < 10 host praktis tidak eject apa pun.
"Probe lebih sering = deteksi lebih cepat = lebih baik" Probe storm + false positive dari latency GC sesaat. Deteksi cepat datang dari passive check, bukan interval probe lebih pendek.
"Panic threshold itu bug / harus diset 0" Itu mitigasi cascade failure yang disengaja. Set 0 hanya kalau kamu lebih suka 100% error daripada degradasi parsial.
"Load balancer menyembunyikan host lambat" Hanya kalau ada mekanisme latency-aware. RR tidak akan pernah tahu host lambat sampai host itu error.
"Weighted RR = menjamin proporsi" Weighted RR menjamin proporsi request, bukan proporsi waktu. Host berat request-nya akan lebih sibuk dari yang diharapkan.

βœ… Cek Pemahaman

  1. Cluster 8 host, satu host punya bug yang menyebabkan 30% request-nya timeout (bukan error β€” timeout). Envoy dengan konfigurasi default (outlier detection aktif, max_ejection_percent 10%, consecutive_5xx 5). Apakah host itu akan di-eject? Jelaskan mekanismenya, dan sebutkan minimal dua perubahan konfigurasi untuk membuatnya benar-benar ter-eject tanpa membuat cluster rapuh.

  2. Kamu punya layanan read-heavy dengan cache lokal per-instance dan 200 pod. Setiap pod butuh 90 detik untuk fill cache dari DB (kalau kosong, p99 = 4s). Deploy rolling update menghabiskan 20 menit. Jelaskan kenapa readiness probe saja tidak cukup, dan desain mekanisme apa yang mencegah p99 spike selama rollout. Sebutkan di mana mekanisme itu harus hidup (aplikasi, orchestrator, atau proxy) dan trade-off-nya.

  3. Sebuah tim mengeluh: "Outlier detection kita eject host terus, tapi setelah 30 detik host itu balik dan langsung di-eject lagi, terus begitu selamanya. Kami sudah naikkan base_ejection_time jadi 5 menit, tetap saja." Sebutkan tiga hipotesis berbeda yang bisa menjelaskan gejala ini, dan untuk masing-masing, satu observability signal yang bisa membedakannya dari hipotesis lain.


πŸ”‘ Poin Kunci


➑️ Eksplorasi Lanjut


Sumber terverifikasi: - Envoy Proxy docs, "Outlier detection" & "Panic threshold" (docs versi 1.41.0-dev, diakses 2026-10-31) β€” algoritma ejeksi, default max_ejection_percent 10%, base_ejection_time 30s, max_ejection_time 300s, interval 10s, panic threshold default 50%. - Envoy API reference config.cluster.v3.OutlierDetection β€” default consecutive_5xx 5, success_rate_stdev_factor 1900, failure_percentage_threshold 85, successful_active_health_check_uneject_host true, max_ejection_time_jitter 0s. - Kubernetes docs, "Configure Liveness, Readiness and Startup Probes" β€” semantik restart vs endpoint removal vs startup handoff. - Azar, Broder, Karlin, Upfal, "Balanced Allocations" (STOC 1994) β€” dasar analitis P2C. - Eisenbud et al., "Maglev: A Fast and Reliable Software Network Load Balancer" (NSDI 2016) β€” consistent hashing dengan lookup table. - Linkerd default: P2C + EWMA latency (dari desain default proxy; bukan benchmark, klaim arsitektur).

DIKETAHUI (dari docs resmi): semua angka default Envoy/K8s di atas. DIINFERENSI (dari prinsip, bukan diukur): contoh estimasi "outage 45 detik jadi 15 menit" adalah ilustrasi mekanisme, bukan data insiden spesifik. BELUM PASTI: perilaku persis implementasi Maglev di tiap proxy berbeda (ukuran tabel, rebuild strategy) β€” verifikasi di implementasi yang kamu pakai sebelum mengandalkan angkanya.