Least Connections

Least Connections #

Least connections (atau least_conn) adalah algoritma load balancing yang mengirim setiap request baru ke upstream yang saat ini memiliki jumlah koneksi aktif paling sedikit. Berbeda dari round robin yang mendistribusikan berdasarkan urutan, least connections mendistribusikan berdasarkan beban aktual yang sedang ditanggung setiap backend.

Algoritma ini sangat efektif untuk workload di mana durasi pemrosesan request bervariasi secara signifikan — seperti API yang memiliki endpoint ringan (milliseconds) dan endpoint berat (beberapa detik), atau aplikasi yang sesekali melakukan operasi database yang lama.

Cara Kerja Least Connections #

State awal: semua backend kosong (0 koneksi aktif)

Req-1 masuk → [backend-1: 0, backend-2: 0, backend-3: 0]
              Semua sama → pilih backend-1 (atau acak di antara yang sama)
              backend-1 sedang proses Req-1 (memakan 500ms)

Req-2 masuk → [backend-1: 1, backend-2: 0, backend-3: 0]
              backend-2 paling sedikit → pilih backend-2
              backend-2 sedang proses Req-2 (memakan 50ms)

Req-3 masuk → [backend-1: 1, backend-2: 1, backend-3: 0]
              backend-3 paling sedikit → pilih backend-3

Req-2 selesai (hanya 50ms) → backend-2 kembali ke 0 koneksi

Req-4 masuk → [backend-1: 1, backend-2: 0, backend-3: 1]
              backend-2 paling sedikit → pilih backend-2 lagi

Req-5 masuk → [backend-1: 1, backend-2: 1, backend-3: 1]
              Semua sama → pilih secara merata (round-robin-like)

Pola: backend yang cepat menyelesaikan request mendapat lebih banyak request

Konfigurasi Least Connections #

example.com {
    reverse_proxy backend-1:3000 backend-2:3000 backend-3:3000 {
        lb_policy least_conn
    }
}

# Dengan health check — selalu disarankan untuk production
example.com {
    reverse_proxy backend-1:3000 backend-2:3000 backend-3:3000 {
        lb_policy       least_conn
        health_uri      /health
        health_interval 10s
        health_timeout  5s
        health_status   200
    }
}

Least Conn vs Round Robin: Simulasi Nyata #

Mari kita simulasikan skenario yang realistis untuk memahami kapan least_conn unggul:

Skenario: API dengan dua jenis endpoint
  - GET /api/users → cepat, 20ms
  - POST /api/export → berat, 3000ms (export data besar)

10 request masuk hampir bersamaan:
  Req-1:  GET /api/users  (20ms)
  Req-2:  POST /api/export (3000ms)
  Req-3:  GET /api/users  (20ms)
  Req-4:  GET /api/users  (20ms)
  Req-5:  POST /api/export (3000ms)
  Req-6:  GET /api/users  (20ms)
  Req-7:  GET /api/users  (20ms)
  Req-8:  GET /api/users  (20ms)
  Req-9:  GET /api/users  (20ms)
  Req-10: GET /api/users  (20ms)

── Round Robin (3 backend) ────────────────────────────────────────
  backend-1: Req-1(20ms), Req-4(20ms), Req-7(20ms), Req-10(20ms)
  backend-2: Req-2(3000ms), Req-5(3000ms), Req-8 ANTRI!, Req-... ANTRI!
  backend-3: Req-3(20ms), Req-6(20ms), Req-9(20ms)
  
  Problem: backend-2 menjadi bottleneck karena dapat 2 export requests
  backend-2 sibuk hampir 6 detik sementara backend-1 dan 3 idle

── Least Connections (3 backend) ──────────────────────────────────
  T=0:    [b1:0 b2:0 b3:0]
  Req-1 → b1 (20ms)   [b1:1 b2:0 b3:0]
  Req-2 → b2 (3000ms) [b1:1 b2:1 b3:0]
  Req-3 → b3 (20ms)   [b1:1 b2:1 b3:1]
  T=20ms: Req-1,3 selesai [b1:0 b2:1 b3:0]
  Req-4 → b1 (20ms)   [b1:1 b2:1 b3:0]
  Req-5 → b3 (3000ms) [b1:1 b2:1 b3:1]
  T=40ms: Req-4 selesai [b1:0 b2:1 b3:1]
  Req-6 → b1 (20ms)   ... dan seterusnya
  
  Distribusi jauh lebih merata — tidak ada backend yang jadi bottleneck!

Use Case Ideal untuk Least Connections #

✓ API dengan endpoint bervariatif (ringan dan berat bercampur)
✓ Aplikasi yang sesekali melakukan operasi database lambat
✓ Service yang memproses file upload/download berukuran besar
✓ Microservice yang sesekali melakukan external API call (lebih lambat)
✓ Backend yang memiliki response time tidak konsisten
✓ Job queue processor yang waktu pemrosesannya tidak terduga

✗ Kurang optimal untuk:
✗ Request yang semuanya sangat cepat (< 10ms) — overhead least_conn tracking
   tidak sepadan, round robin lebih efisien
✗ Aplikasi stateful yang butuh sticky session (gunakan ip_hash)
✗ Backend dengan kapasitas berbeda (gunakan weighted)

Least Connections pada Lingkungan Heterogen #

Ketika backend memiliki kapasitas berbeda, least_conn murni mungkin masih tidak optimal. Pertimbangkan kombinasi dengan weighted:

# Skenario: backend-1 server besar (8 core), backend-2 server kecil (2 core)

# ANTI-PATTERN: Least conn murni tanpa mempertimbangkan kapasitas
example.com {
    reverse_proxy backend-1:3000 backend-2:3000 {
        lb_policy least_conn
        # backend-2 akan dapat beban yang sama dengan backend-1
        # padahal kapasitasnya 4x lebih kecil
    }
}

# BENAR: Gunakan weighted jika kapasitas berbeda
example.com {
    reverse_proxy {
        to backend-1:3000 {
            weight 4   # 4x lebih banyak dapat request
        }
        to backend-2:3000 {
            weight 1
        }
        lb_policy round_robin  # atau least_conn untuk distribusi lebih dinamis
    }
}

Implementasi Detail: Apa yang Dihitung sebagai “Connection” #

Penting dipahami bahwa yang dihitung adalah koneksi aktif yang sedang dalam pemrosesan, bukan:

Yang DIHITUNG sebagai koneksi aktif:
  ✓ Request yang sedang diproses backend (dari terima request s.d. kirim response)
  ✓ Streaming response yang sedang aktif
  ✓ WebSocket connection yang sedang terbuka
  ✓ Long-polling connection

Yang TIDAK dihitung:
  ✗ Koneksi idle di connection pool
  ✗ Request yang sudah selesai
  ✗ Health check requests

Untuk WebSocket dan long-lived connections, least_conn bisa menyebabkan distribusi yang tidak merata karena koneksi WebSocket yang lama akan terus dihitung selama koneksi terbuka. Dalam kasus ini, pertimbangkan ip_hash untuk sticky sessions.


Monitoring Least Connections di Production #

# Lihat jumlah koneksi aktif per upstream
curl -s http://localhost:2019/reverse_proxy/upstreams/ | jq '
  .[] | {
    address: .address,
    healthy: .healthy,
    active_requests: .num_requests
  }
'

# Access log analysis: distribusi ke upstream
cat /var/log/caddy/access.log | \
    jq -r '.upstream_latency + " " + .upstream_addr' | \
    sort -n | awk '{
        sum[$2] += $1;
        count[$2]++
    } END {
        for (addr in count) {
            printf "%s: %d requests, avg latency: %.1fms\n",
                addr, count[addr], sum[addr]/count[addr]
        }
    }'

# Real-time monitoring dengan watch
watch -n 2 'curl -s http://localhost:2019/reverse_proxy/upstreams/ | \
    jq ".[] | {address, healthy, requests: .num_requests}"'

Konfigurasi Lengkap dengan Tuning #

{
    email [email protected]
}

api.example.com {
    log {
        output file /var/log/caddy/api.log
        format json
    }
    
    encode gzip zstd
    
    reverse_proxy backend-1:8080 backend-2:8080 backend-3:8080 {
        lb_policy least_conn
        
        # Active health check
        health_uri      /health
        health_interval 10s
        health_timeout  5s
        health_status   200
        
        # Passive health check
        health_fails    3
        health_passes   2
        
        # Header forwarding
        header_up X-Real-IP         {remote_host}
        header_up X-Forwarded-Proto {scheme}
        header_up X-Request-ID      {http.request.uuid}
        
        # Transport tuning
        transport http {
            dial_timeout            5s
            response_header_timeout 60s   # Lebih lama untuk endpoint berat
            keep_alive              90s
            max_idle_conns_per_host 100
        }
        
        # Retry untuk transient failures
        lb_try_duration  5s
        lb_try_interval  250ms
    }
    
    handle_errors {
        @down expression {err.status_code} in [502, 503, 504]
        handle @down {
            respond "Service unavailable, please try again shortly." 503
        }
    }
}

random_choose — Hybrid Random + Least Conn #

Caddy juga menyediakan random_choose N sebagai kompromi antara kecepatan random dan keadilan least_conn:

example.com {
    reverse_proxy backend-1:3000 backend-2:3000 backend-3:3000 backend-4:3000 {
        # Pilih 2 upstream secara acak, lalu kirim ke yang paling sedikit koneksinya
        # Lebih cepat dari least_conn murni (tidak cek semua upstream)
        # Lebih adil dari random murni
        lb_policy random_choose 2
    }
}

Pendekatan ini sangat efektif untuk pool upstream yang besar (10+), di mana overhead memeriksa semua koneksi aktif untuk least_conn murni bisa menjadi signifikan. Dengan memilih 2 kandidat acak lalu pilih yang least_conn di antara keduanya, kamu mendapatkan distribusi yang hampir seoptimal least_conn murni dengan overhead yang jauh lebih kecil.


Ringkasan #

  • least_conn mengirim request ke backend dengan koneksi aktif paling sedikit — bukan berdasarkan urutan seperti round robin.
  • Sangat efektif untuk workload dengan durasi request yang bervariasi — backend yang cepat menyelesaikan request akan otomatis mendapat lebih banyak request.
  • Untuk request yang semua sangat cepat (< 10ms), round_robin mungkin lebih efisien karena overhead tracking koneksi least_conn tidak sepadan.
  • WebSocket dan long-polling connections dihitung sebagai koneksi aktif selama koneksi terbuka — bisa menyebabkan distribusi skewed untuk aplikasi WebSocket-heavy.
  • Pada backend dengan kapasitas berbeda, kombinasikan dengan weighted untuk mendistribusikan beban secara proporsional.
  • Selalu monitor distribusi via Admin API dan access log untuk memastikan least_conn memberikan distribusi yang diharapkan di production.

← Sebelumnya: Round Robin   Berikutnya: IP Hash →

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