Least Connections #
Mendistribusikan lalu lintas web ke klaster server secara merata tidak selalu cukup jika setiap permintaan (request) memiliki beban komputasi yang berbeda-beda. Di sinilah algoritma Least Connections (least_conn) menjadi pilihan utama dibandingkan Round Robin biasa. Dengan memantau jumlah koneksi aktif secara real-time pada setiap server backend, Caddy dapat mengarahkan lalu lintas baru secara cerdas ke server yang memiliki beban kerja paling ringan saat itu. Kita akan mengupas tuntas cara kerja algoritma ini, penanganan backend heterogen, mitigasi masalah lonjakan awal (slow-start), serta perbandingan performa pada beban kerja nyata.
Cara Kerja Algoritma Least Connections #
Algoritma Least Connections didasarkan pada prinsip keadilan beban kerja aktual, bukan sekadar keadilan jumlah transaksi. Caddy memelihara sebuah alat ukur internal (gauge counter) untuk melacak jumlah permintaan aktif yang sedang diproses oleh masing-masing server backend (upstream) pada satu waktu.
Secara internal pada kode sumber bahasa Go Caddy, setiap upstream direpresentasikan sebagai objek struktur data (Struct) yang mencatat alamat dial, status kesehatan, dan counter koneksi aktif secara atomik:
// Representasi internal Upstream di Go source code Caddy
type Upstream struct {
Dial string
Healthy bool
Conns int32 // Diubah nilainya menggunakan sync/atomic secara thread-safe
}
Ketika ada permintaan baru masuk dari klien:
- Caddy memeriksa status gauge counter (
Conns) seluruh upstream terdaftar yang berstatus sehat (Healthy == true). - Caddy membandingkan jumlah koneksi aktif di antara mereka.
- Permintaan baru disalurkan ke upstream yang memiliki angka koneksi aktif terkecil.
- Caddy menaikkan nilai gauge counter untuk upstream terpilih sebesar +1 secara atomik (
atomic.AddInt32(&upstream.Conns, 1)). - Setelah backend selesai memproses request dan mengirimkan respons kembali, Caddy menurunkan nilai gauge counter backend tersebut sebesar -1 (
atomic.AddInt32(&upstream.Conns, -1)).
Berikut adalah gambaran logika pemilihan upstream di dalam Caddy yang disederhanakan:
// Ilustrasi algoritma pemilihan Least Connections di Caddy
func SelectLeastConn(upstreams []*Upstream) *Upstream {
var best *Upstream
var minConns int32 = -1
for _, upstream := range upstreams {
if !upstream.Healthy {
continue
}
// Baca jumlah koneksi secara atomik untuk keamanan konkurensi
currentConns := atomic.LoadInt32(&upstream.Conns)
if minConns == -1 || currentConns < minConns {
minConns = currentConns
best = upstream
}
}
return best
}
Berikut adalah diagram perbandingan antara Round Robin dan Least Connections saat menghadapi request dengan durasi pemrosesan yang bervariasi:
flowchart TD
subgraph Round_Robin_Flow["Rotasi Buta (Round Robin)"]
direction TB
R1["Req 1 (Cepat: 50ms)"] -->|"Rotasi"| RR1["Server 1 (Beban: 1 conn)"]
R2["Req 2 (Lambat: 5s)"] -->|"Rotasi"| RR2["Server 2 (Beban: 3 conn - Kewalahan!)"]
R3["Req 3 (Lambat: 5s)"] -->|"Rotasi"| RR2
R4["Req 4 (Lambat: 5s)"] -->|"Rotasi"| RR2
end
subgraph Least_Conn_Flow["Penyeimbang Cerdas (Least Connections)"]
direction TB
L1["Req 1 (Cepat: 50ms)"] -->|"Evaluasi Beban"| LC1["Server 1 (Beban: 1 conn)"]
L2["Req 2 (Lambat: 5s)"] -->|"Evaluasi Beban"| LC2["Server 2 (Beban: 1 conn)"]
L3["Req 3 (Lambat: 5s)"] -->|"Kirim ke Server 1 karena beban 0"| LC1
L4["Req 4 (Lambat: 5s)"] -->|"Kirim ke Server 1 karena beban 1 vs 2"| LC1
end
style RR2 stroke:#e53935,stroke-width:2px
style LC1 stroke:#43a047,stroke-width:2px
style LC2 stroke:#43a047,stroke-width:2pxDalam skenario Round Robin (kiri), Server 2 menjadi kewalahan karena terus menerima request baru tanpa mempedulikan bahwa Server 2 masih sibuk memproses request lambat sebelumnya. Sebaliknya, Least Connections (kanan) mendeteksi bahwa Server 2 sedang sibuk dan mengalihkan request baru ke Server 1 yang memiliki kapasitas siaga lebih banyak.
Konfigurasi Least Connections #
Untuk mengaktifkan kebijakan Least Connections di Caddy, kita wajib mendefinisikan subdirektif lb_policy least_conn di dalam blok reverse_proxy Caddyfile kita:
# Mengonfigurasi penyeimbang beban Least Connections
example.com {
reverse_proxy backend-1:3000 backend-2:3000 backend-3:3000 {
# Set kebijakan load balancing ke Least Connections
lb_policy least_conn
# Konfigurasi toleransi kegagalan koneksi
lb_try_duration 5s
lb_try_interval 200ms
# Pengaturan transport TCP ke backend
transport http {
dial_timeout 3s
keep_alive 90s
}
}
}
lb_policy least_conn: Menginstruksikan modul proxy Caddy untuk mengevaluasi jumlah koneksi aktif sebelum merutekan request.dial_timeout 3s: Memastikan Caddy memutuskan koneksi dengan cepat jika backend terpilih gagal merespons soket TCP dalam waktu 3 detik, agar request dapat dialihkan ke backend dengan koneksi terendah berikutnya.
Mekanisme Slow-Start (Mitigasi Thundering Herd) #
Meskipun Least Connections sangat efektif untuk menyeimbangkan beban, algoritma ini memiliki satu kelemahan kritis ketika kita menambahkan server backend baru ke dalam klaster (baik karena proses auto-scaling maupun setelah server pulih dari kondisi crash). Masalah ini dikenal sebagai Thundering Herd atau Cold Start Problem.
Masalah Thundering Herd pada Least Connections #
Bayangkan kita memiliki 2 server aktif (Server A (100 conn) dan Server B (100 conn)) yang sedang sibuk memproses koneksi aktif. Ketika Server C baru menyala dengan status 0 koneksi aktif, seluruh request baru yang masuk dalam waktu bersamaan akan langsung diarahkan seluruhnya ke Server C karena nilainya masih jauh di bawah Server A dan B. Server C yang baru menyala dan memorinya belum terisi penuh (cold memory) akan langsung ambruk akibat lonjakan beban yang ekstrim secara instan.
Solusi Desain: Antrean Proxy via max_conns_per_host dan lb_try_duration
#
Untuk memitigasi thundering herd pada Least Connections di Caddy, kita dapat menerapkan pola antrean aman (connection queuing). Kita membatasi kapasitas maksimal koneksi aktif per host menggunakan max_conns_per_host di tingkat transport, dikombinasikan dengan lb_try_duration di tingkat proxy:
# Antrean aman untuk meredam lonjakan backend baru
example.com {
reverse_proxy backend-1:3000 backend-2:3000 backend-3:3000 {
lb_policy least_conn
# Berikan waktu request mengantre di Caddy daripada langsung error 503
lb_try_duration 10s
lb_try_interval 100ms
transport http {
# Batasi koneksi maksimum aktif ke tiap backend
max_conns_per_host 120
dial_timeout 3s
}
}
}
Dengan konfigurasi di atas:
- Ketika Server C yang baru pulih menerima luapan request instan dari 0 hingga 120 koneksi, Caddy mendeteksi bahwa Server C telah menyentuh batas maksimum
max_conns_per_host 120. - Request ke-121 tidak akan dikirimkan ke Server C. Request tersebut akan ditahan di dalam antrean Caddy (proxy buffer) selama durasi
lb_try_duration 10s. - Selama masa tunggu, jika ada satu koneksi aktif di Server C yang selesai diproses, request antrean ke-121 langsung masuk menggantikannya secara tertib.
- Ini memberi ruang bagi Server C untuk memproses request secara terkontrol tanpa kolaps.
Masalah Thundering Herd pada Re-koneksi WebSockets massal #
Skenario thundering herd di atas menjadi sangat berbahaya jika klaster kita melayani koneksi persisten berumur panjang seperti WebSockets atau gRPC streams.
1. Server C (:3003) di-restart untuk pemeliharaan rutin.
2. 2.000 klien WebSocket yang sedang terhubung ke Server C putus seketika.
3. Klien-klien tersebut langsung mengirimkan request re-koneksi otomatis secara bersamaan.
4. Karena Server C baru saja menyala kembali dengan beban 0 koneksi, Least Connections secara konsisten akan melempar SEMUA 2.000 request re-koneksi tersebut ke Server C kembali.
5. Server C langsung kolaps di tingkat CPU sebelum sempat melakukan handshake SSL/TLS.
Solusi Klien: Penambahan Jitter dan Exponential Backoff #
Solusi terbaik untuk masalah ini adalah dengan menyisipkan variasi waktu acak (Jitter) pada logika re-koneksi otomatis di sisi kode aplikasi klien kita (JavaScript browser):
// ANTI-PATTERN: Re-koneksi instan yang memicu thundering herd
socket.onclose = () => {
setTimeout(connectWebSocket, 1000); // Semua klien re-connect tepat dalam 1 detik
};
// BENAR: Menggunakan Exponential Backoff dengan Jitter acak
socket.onclose = () => {
// Tambahkan faktor acak antara 1 hingga 5 detik
const jitter = Math.random() * 4000 + 1000;
setTimeout(connectWebSocket, jitter);
};
Perbandingan Komparatif: Kapan Memilih Algoritma yang Tepat #
Berikut adalah matriks perbandingan performa antara Round Robin, Least Connections, dan IP Hashing di berbagai jenis beban lalu lintas:
| Skenario Beban Kerja | Round Robin | Least Connections | IP Hashing | Pemenang & Penjelasan |
|---|---|---|---|---|
| Static Assets (HTML/CSS/JS) | Performa Maksimal | Overhead Tambahan | Rawan Hotspot | Round Robin. Karena pemrosesan file statis sangat cepat (mikrodetik), overhead pelacakan koneksi di Least Connections tidak diperlukan. |
| REST API CRUD Ringan | Distribusi Seragam | Distribusi Seragam | Rawan Hotspot | Round Robin. Struktur request homogen membuat hasil pembagian beban kedua algoritma hampir identik. |
| Kueri SQL Berat (Reports) | Risiko Botleneck | Distribusi Cerdas | Tidak Optimal | Least Connections. Mencegah antrean penumpukan request berat di satu server backend tertentu. |
| WebSocket / Chat App | Resiko Tidak Seimbang | Distribusi Sesuai Kapasitas | Terkunci di IP | Least Connections. Mengarahkan pengguna baru ke server yang tidak dibebani oleh koneksi WebSocket persisten yang lama. |
| File Upload (Heavy Payload) | Soket Tertahan Secara Acak | Soket Dibagi Rata | Tidak Optimal | Least Connections. Memastikan server yang sedang memproses upload file besar tidak dipaksa menerima request upload baru. |
| Layanan Heterogen (Beda Spek CPU) | Tidak Efisien | Sangat Efektif | Tidak Optimal | Least Connections. Backend dengan spesifikasi CPU lebih tinggi akan menyelesaikan request lebih cepat, sehingga koneksinya menurun dan otomatis ditarik lebih banyak oleh Caddy. |
Simulasi & Analisis Performa #
Untuk memahami mengapa Least Connections unggul dalam menangani lalu lintas heterogen, mari kita perhatikan tabel simulasi jalannya request di bawah ini.
Skenario Simulasi #
Kita memiliki 2 server backend:
- Server A (VM Cepat): Memproses request database CRUD biasa dalam waktu 10ms.
- Server B (VM Lambat): Memproses request pembuatan file PDF berat dalam waktu 100ms.
Kita mengirimkan 5 request masuk secara bersamaan pada waktu T = 0ms:
- Request 1: Kategori Ringan
- Request 2: Kategori Berat
- Request 3: Kategori Ringan
- Request 4: Kategori Ringan
- Request 5: Kategori Ringan
Hasil Jalannya Request pada Round Robin (Default) #
| Waktu (T) | Permintaan Masuk | Upstream Terpilih | Status Koneksi Aktif | Hasil Pemrosesan |
|---|---|---|---|---|
0ms | Request 1 (Ringan) | Server A | Server A: 1 conn, Server B: 0 conn | Selesai pada T=10ms |
0ms | Request 2 (Berat) | Server B | Server A: 1 conn, Server B: 1 conn | Selesai pada T=100ms |
0ms | Request 3 (Ringan) | Server A | Server A: 2 conn, Server B: 1 conn | Selesai pada T=20ms |
0ms | Request 4 (Ringan) | Server B | Server A: 2 conn, Server B: 2 conn | Antre di Server B! Selesai pada T=200ms |
0ms | Request 5 (Ringan) | Server A | Server A: 3 conn, Server B: 2 conn | Selesai pada T=30ms |
- Masalah: Request 4 (ringan) terpaksa mengantre di belakang Request 2 di Server B yang lambat. Pengguna pada Request 4 harus menunggu selama 200ms meskipun request-nya sangat ringan, sementara Server A sudah kosong (idle) sejak
T=30ms.
Hasil Jalannya Request pada Least Connections #
| Waktu (T) | Permintaan Masuk | Upstream Terpilih | Status Koneksi Aktif | Hasil Pemrosesan |
|---|---|---|---|---|
0ms | Request 1 (Ringan) | Server A | Server A: 1 conn, Server B: 0 conn | Selesai pada T=10ms |
0ms | Request 2 (Berat) | Server B | Server A: 1 conn, Server B: 1 conn | Selesai pada T=100ms |
0ms | Request 3 (Ringan) | Server A | Server A: 2 conn, Server B: 1 conn | Selesai pada T=20ms |
0ms | Request 4 (Ringan) | Server A | Server A: 3 conn, Server B: 1 conn | Diarahkan ke Server A karena Server B sibuk! Selesai pada T=30ms |
0ms | Request 5 (Ringan) | Server A | Server A: 4 conn, Server B: 1 conn | Selesai pada T=40ms |
- Hasil: Request 4 diarahkan secara cerdas oleh Caddy ke Server A yang memiliki kapasitas proses cepat. Pengguna Request 4 menerima respons dalam 30ms (jauh lebih cepat dibandingkan 200ms pada Round Robin). Server B dibiarkan fokus memproses satu tugas beratnya tanpa diganggu antrean request baru.
Implementasi Kode Backend Pendukung #
Untuk melakukan pengujian mandiri kinerja penyeimbang beban Least Connections Caddy di komputer lokal kita, kita dapat membuat aplikasi backend simulasi menggunakan bahasa pemrograman Python. Kode di bawah ini secara sengaja mensimulasikan tugas berat dengan parameter delay tidur dinamis:
# app.py (Aplikasi Backend Simulasi via Python Flask)
import os
import time
import sys
from flask import Flask, jsonify, request
app = Flask(__name__)
# Baca konfigurasi nama instance dari environment variable
INSTANCE_NAME = os.getenv("INSTANCE_NAME", "Backend-Default")
PORT = int(os.getenv("PORT", 5000))
# Counter lokal untuk mencatat jumlah request yang telah diproses
request_counter = 0
@app.route("/api/data")
def process_data():
global request_counter
request_counter += 1
# Ambil parameter delay dari query string (default: 0 detik)
# Contoh: /api/data?sleep=5 (menstimulasi tugas berat 5 detik)
sleep_duration = float(request.args.get("sleep", 0))
print(f"[{INSTANCE_NAME}] Menerima request ke-{request_counter}. Durasi proses: {sleep_duration}s", file=sys.stderr)
if sleep_duration > 0:
time.sleep(sleep_duration)
return jsonify({
"status": "success",
"instance": INSTANCE_NAME,
"processed_requests": request_counter,
"slept_for": sleep_duration
})
@app.route("/healthz")
def health_check():
return jsonify({"status": "UP"}), 200
if __name__ == "__main__":
app.run(host="0.0.0.0", port=PORT)
Konfigurasi Docker Compose untuk Simulasi #
Untuk menjalankan pengujian ini secara utuh, buat berkas docker-compose.yml berikut:
version: "3"
services:
caddy:
image: caddy:2.8-alpine
ports:
- "80:80"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
networks:
- net
backend-a:
build: .
environment:
- INSTANCE_NAME=Server-A
- PORT=5000
networks:
- net
backend-b:
build: .
environment:
- INSTANCE_NAME=Server-B
- PORT=5000
networks:
- net
networks:
net:
Gunakan perintah Apache Bench (ab) di bawah ini untuk mengirimkan request secara konkuren dan amati log kontainer Docker untuk melihat bagaimana Caddy memilah lalu lintas secara cerdas:
# Kirim 100 request dengan 10 konkurensi ke Caddy
ab -n 100 -c 10 http://localhost/api/data?sleep=1
Ringkasan #
- Definisi Utama: Algoritma Least Connections memilih server backend yang memiliki jumlah koneksi aktif paling sedikit pada saat permintaan baru masuk.
- Connection Gauge: Caddy memelihara counter numerik di memori secara real-time untuk mendeteksi kapan backend mulai sibuk memproses tugas komputasi berat.
- Backend Heterogen: Merupakan pilihan terbaik untuk klaster server dengan kapasitas CPU/RAM bervariasi, serta request dengan waktu respons tidak homogen.
- Bahaya Thundering Herd: Server baru yang baru dinyalakan berisiko ambruk karena menerima akumulasi request instan akibat jumlah koneksinya masih nol.
- Solusi Thundering Herd: Atur batasan
max_conns_per_hostpada bagian transport untuk membatasi lonjakan beban awal pada server yang baru diaktifkan.- Kasus Ideal: Sangat direkomendasikan untuk aplikasi pengunggahan berkas besar, API kueri berat, serta layanan koneksi persisten seperti WebSocket dan gRPC.