Round Robin #
Menyediakan pemerataan beban lalu lintas ke beberapa server aplikasi adalah langkah wajib dalam membangun sistem yang memiliki ketersediaan tinggi (high availability) dan toleran terhadap kegagalan (fault tolerance). Sebagai penyeimbang beban (load balancer), Caddy menyediakan algoritma Round Robin sebagai kebijakan default untuk mendistribusikan lalu lintas secara bergantian dan merata. Kita akan mempelajari bagaimana cara kerja algoritma klasik ini, mekanisme penanganan konkurensi di tingkat sistem operasi, penerapannya dalam klaster containerized, serta implikasi pentingnya terhadap stateful vs stateless architecture.
Cara Kerja Algoritma Round Robin #
Secara konseptual, Round Robin bekerja seperti antrean melingkar (circular ring buffer). Ketika ada serangkaian permintaan masuk dari browser klien, Caddy akan menyalurkannya ke server backend (upstream) yang tersedia secara berurutan:
flowchart TD
K1["Klien Permintaan 1"] --> CP1["Caddy Proxy"] --> SA["Upstream Server A (Port :3001)"]
K2["Klien Permintaan 2"] --> CP2["Caddy Proxy"] --> SB["Upstream Server B (Port :3002)"]
K3["Klien Permintaan 3"] --> CP3["Caddy Proxy"] --> SC["Upstream Server C (Port :3003)"]
K4["Klien Permintaan 4"] --> CP4["Caddy Proxy"] --> SA2["Upstream Server A (Mulai berputar kembali)"]Berikut adalah visualisasi aliran sirkular algoritma Round Robin pada Caddy:
flowchart TD
Req1["Request 1"] --> Caddy{"Caddy Proxy"}
Req2["Request 2"] --> Caddy
Req3["Request 3"] --> Caddy
Req4["Request 4"] --> Caddy
subgraph Circular_Buffer["Rotasi Upstream (Circular Buffer)"]
direction LR
S1["Server 1 (:3001)"]
S2["Server 2 (:3002)"]
S3["Server 3 (:3003)"]
end
Caddy -->|"1. Kirim ke Server 1"| S1
Caddy -->|"2. Kirim ke Server 2"| S2
Caddy -->|"3. Kirim ke Server 3"| S3
Caddy -->|"4. Putar ke Server 1"| S1
style S1 stroke:#0288d1,stroke-width:2px
style S2 stroke:#0288d1,stroke-width:2px
style S3 stroke:#0288d1,stroke-width:2pxKonkurensi Tanpa Mutex (Atomic Operations) #
Pada server dengan lalu lintas tinggi, Caddy memproses ribuan permintaan secara bersamaan (konkuren) melalui goroutine yang berbeda-beda. Jika Caddy menggunakan kunci penguncian tradisional (Mutex Lock) untuk mencatat indeks server mana yang harus menerima request berikutnya, server akan mengalami kelambatan akibat perebutan kunci (lock contention) antar goroutine.
Untuk mencegah masalah performa ini, Caddy secara internal mengimplementasikan pencatatan indeks rotasi secara thread-safe menggunakan operasi atomik (sync/atomic dalam bahasa Go). Operasi atomik bekerja langsung di tingkat instruksi CPU, memungkinkan goroutine menaikkan angka counter penunjuk indeks secara aman tanpa perlu mengunci thread sistem.
flowchart TD
G1["Goroutine Request A"] -->|"Atomic.AddUint32()"| Counter["Counter Global (Atomic)"]
G2["Goroutine Request B"] -->|"Atomic.AddUint32()"| Counter
G3["Goroutine Request C"] -->|"Atomic.AddUint32()"| Counter
Counter -->|"Hitung Indeks: Counter % Jumlah Upstream"| IndexCalculated["Indeks Target Upstream"]
IndexCalculated --> Dispatch["Salurkan ke Server Backend"]
style Counter stroke:#43a047,stroke-width:2pxPembagian beban dengan Round Robin menjamin pemerataan lalu lintas dari segi kuantitas (jumlah request). Namun, algoritma ini tidak mempedulikan seberapa berat komputasi yang dijalankan untuk setiap request. Jika Server A menerima request kueri laporan keuangan yang memakan waktu 5 detik, sementara Server B menerima request file gambar kecil yang selesai dalam 10 milidetik, Server A akan terlihat jauh lebih sibuk meskipun jumlah request yang diterimanya sama dengan Server B.
Konfigurasi Round Robin #
Karena Round Robin merupakan algoritma bawaan (default) di Caddy, kita tidak wajib mendefinisikannya secara eksplisit di dalam Caddyfile. Kita cukup mendaftarkan seluruh upstream di belakang direktif reverse_proxy.
# Konfigurasi implisit (Round Robin berjalan secara otomatis)
example.com {
reverse_proxy app-node-1:3000 app-node-2:3000 app-node-3:3000
}
Namun, untuk memperjelas dokumentasi infrastruktur bagi tim DevOps kita, menuliskan kebijakan ini secara eksplisit merupakan praktik yang baik:
# Konfigurasi eksplisit
example.com {
reverse_proxy app-node-1:3000 app-node-2:3000 app-node-3:3000 {
# Tentukan load balancing policy secara tertulis
lb_policy round_robin
# Konfigurasi pendukung produksi yang direkomendasikan
lb_try_duration 5s
lb_try_interval 250ms
}
}
lb_policy round_robin: Menginstruksikan Caddy secara tegas untuk menggunakan metode distribusi berurutan melingkar.lb_try_duration: Memberikan toleransi waktu 5 detik bagi Caddy untuk mencoba memindahkan request ke server backend berikutnya jika backend pertama yang dituju mengalami kegagalan respons.
Deployment Klaster dengan Docker Compose #
Dalam ekosistem containerized modern, pola deployment yang paling umum adalah menempatkan container Caddy di garda depan untuk membagi beban ke beberapa container aplikasi replika di belakangnya.
Berikut adalah konfigurasi file docker-compose.yml untuk membuat klaster 3 replika aplikasi NodeJS di belakang Caddy:
# docker-compose.yml
version: "3.8"
services:
caddy-proxy:
image: caddy:2.8-alpine
container_name: caddy_gateway
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
networks:
- internal_cluster
depends_on:
- node-app-1
- node-app-2
- node-app-3
node-app-1:
image: node:20-alpine
container_name: backend_node_1
working_dir: /app
volumes:
- ./app:/app
command: node index.js
environment:
- PORT=3000
- INSTANCE_NAME=Server-Node-A
networks:
- internal_cluster
node-app-2:
image: node:20-alpine
container_name: backend_node_2
working_dir: /app
volumes:
- ./app:/app
command: node index.js
environment:
- PORT=3000
- INSTANCE_NAME=Server-Node-B
networks:
- internal_cluster
node-app-3:
image: node:20-alpine
container_name: backend_node_3
working_dir: /app
volumes:
- ./app:/app
command: node index.js
environment:
- PORT=3000
- INSTANCE_NAME=Server-Node-C
networks:
- internal_cluster
networks:
internal_cluster:
driver: bridge
volumes:
caddy_data:
caddy_config:
Serta berkas Caddyfile pendukungnya yang merutekan lalu lintas secara melingkar:
# Caddyfile
{
email [email protected]
}
app.example.com {
encode gzip zstd
# Hubungkan ke ketiga container Docker menggunakan DNS internal bridge network
reverse_proxy node-app-1:3000 node-app-2:3000 node-app-3:3000 {
lb_policy round_robin
# Integrasikan dengan sistem health check aktif agar kontainer mati otomatis di-skip
health_uri /healthz
health_interval 10s
health_timeout 3s
}
}
Penskalaan Dinamis (Dynamic Scaling) #
Salah satu keunggulan terbesar arsitektur Caddy adalah dukungannya terhadap konfigurasi dinamis via REST API tanpa perlu melakukan restart proses sistem. Hal ini sangat berguna jika kita ingin melakukan otomatisasi penskalaan (auto-scaling) server backend.
Di balik layar, Caddy menyimpan seluruh konfigurasinya dalam format dokumen JSON terpusat. Ketika kita menggunakan perintah API, Caddy memodifikasi dokumen JSON di memori RAM secara langsung. Untuk melihat representasi JSON dari Caddyfile yang sedang berjalan, kita dapat mengeksekusi perintah adaptasi:
# Mengonversi Caddyfile menjadi format JSON terstruktur Caddy
caddy adapt --config /etc/caddy/Caddyfile --pretty
Ketika tim DevOps kita menambahkan server backend baru (misalnya node-app-4:3000) pada jam sibuk, kita tidak perlu memutus koneksi pengguna aktif dengan melakukan restart Caddy. Kita cukup mengirimkan HTTP request POST baru ke port admin Caddy (:2019):
# 1. Cek daftar upstream aktif saat ini dari API Admin Caddy
curl -s http://localhost:2019/config/apps/http/servers/srv0/routes | jq
# 2. Tambahkan upstream baru secara instan ke daftar penyeimbang beban
curl -X POST http://localhost:2019/config/apps/http/servers/srv0/routes/0/handle/0/routes/0/handle/0/upstreams \
-H "Content-Type: application/json" \
-d '{"dial": "node-app-4:3000"}'
Caddy akan langsung menyisipkan node-app-4:3000 ke dalam sirkuit rotasi Round Robin secara graceful. Proses ini terjadi sepenuhnya di memori RAM tanpa memutus satu pun koneksi aktif.
Skrip Otomatisasi Integrasi Auto-scaler #
Berikut adalah contoh skrip shell bash sederhana yang dapat dijalankan secara berkala oleh Cron atau sistem orkestrasi internal untuk memantau beban CPU dan mendaftarkan kontainer cadangan secara dinamis ke penyeimbang beban Caddy:
#!/bin/bash
# autoscale_caddy.sh
THRESHOLD_CPU=80
CADDY_API="http://localhost:2019/config/apps/http/servers/srv0/routes/0/handle/0/routes/0/handle/0/upstreams"
# Dapatkan rata-rata penggunaan CPU saat ini
CPU_USAGE=$(vmstat 1 2 | tail -1 | awk '{print 100 - $15}')
echo "Penggunaan CPU saat ini: $CPU_USAGE%"
if [ "$CPU_USAGE" -gt "$THRESHOLD_CPU" ]; then
echo "Beban tinggi terdeteksi! Menjalankan container backend tambahan..."
# Jalankan container baru via docker-compose
docker compose up -d --no-recreate node-app-4
# Tunggu 3 detik agar container baru warm-up
sleep 3
# BENAR: Daftarkan ke load balancer Caddy dinamis
curl -s -X POST "$CADDY_API" \
-H "Content-Type: application/json" \
-d '{"dial": "node-app-4:3000"}' \
-o /dev/null
echo "node-app-4 berhasil didaftarkan ke Caddy!"
fi
Stateless vs Stateful Load Balancing #
Saat kita mendesain arsitektur aplikasi di belakang penyeimbang beban Round Robin, satu aturan mutlak yang harus dipatuhi adalah: aplikasi backend kita harus bersifat stateless.
Masalah Stateful Architecture di Round Robin #
Jika aplikasi backend kita menyimpan data sesi (session state) di dalam memori RAM lokal server masing-masing, skenario bencana berikut akan terjadi:
sequenceDiagram
autonumber
actor Client as Browser Klien
participant Caddy as Caddy Proxy
participant ServerA as Server A (:3001)
participant ServerB as Server B (:3002)
Client->>Caddy: Kirim request login
Caddy->>ServerA: Teruskan request (Round Robin)
Note over ServerA: Proses login & simpan<br>data sesi di RAM lokal
ServerA-->>Client: Sukses Login
Client->>Caddy: Request halaman profil (Klik profil)
Caddy->>ServerB: Teruskan request (Round Robin)
Note over ServerB: Periksa RAM lokal...<br>Data sesi tidak ditemukan!
ServerB-->>Client: Error "401 Unauthorized"Solusi 1: Ubah Arsitektur Menjadi Stateless (Sangat Direkomendasikan) #
Jangan pernah menyimpan data sesi pengguna (seperti file sesi PHP, session Node.js, atau keranjang belanja) di memori lokal server aplikasi. Pindahkan penyimpanan sesi tersebut ke database terpusat yang diakses bersama oleh seluruh backend, seperti Redis atau Memcached:
flowchart LR
Klien["Klien"] --> Caddy["Caddy Proxy"]
subgraph Backends["Server Backend"]
direction TB
ServerA["Server A (:3001)"]
ServerB["Server B (:3002)"]
ServerC["Server C (:3003)"]
end
Caddy --> ServerA
Caddy --> ServerB
Caddy --> ServerC
ServerA --> Redis["Redis Session Store"]
ServerB --> Redis
ServerC --> Redis
style Caddy stroke:#0288d1,stroke-width:2px
style Redis stroke:#43a047,stroke-width:2px
style Backends stroke:#757575,stroke-width:1px,stroke-dasharray:5,5Dengan arsitektur stateless ini, ke mana pun Caddy mengarahkan request klien (baik ke Server A, B, atau C), semua backend tetap bisa membaca data sesi yang sama dari database Redis bersama.
Solusi 2: Gunakan Sticky Sessions (Alternatif) #
Jika kita terpaksa harus menggunakan aplikasi pihak ketiga yang tidak mendukung penyimpanan sesi eksternal, kita harus mengubah algoritma load balancing dari Round Robin biasa menjadi Sticky Sessions (menggunakan kebijakan ip_hash atau cookie di Caddy):
# Contoh fallback menggunakan sticky cookie jika backend stateful
example.com {
reverse_proxy app-1:3000 app-2:3000 {
# Mengikat klien ke backend yang sama berdasarkan cookie
lb_policy cookie {
name session_binding
}
}
}
Analisis Simulasi Kasus Khusus: Long-lived Connections (WebSockets) #
Perilaku load balancing Round Robin sangat bervariasi tergantung tipe koneksi yang dikirim oleh klien:
- HTTP Request Pendek (Rest API): Round Robin bekerja dengan sempurna karena setiap request langsung selesai, sehingga sirkuit rotasi Caddy dapat membagi beban secara matematis 33%-33%-33% dengan sangat seimbang ke ketiga server.
- Koneksi Persisten Panjang (WebSockets / gRPC): Di sini Round Robin bisa menyebabkan ketidakseimbangan beban yang parah jika salah satu backend di-restart untuk pemeliharaan rutin.
Skenario Kegagalan Re-koneksi WebSockets #
Bayangkan kita melayani 3.000 koneksi WebSocket aktif secara konkuren yang dibagi rata oleh Round Robin ke Server A (1.000), Server B (1.000), dan Server C (1.000).
1. Server C (:3003) di-restart untuk update OS.
2. 1.000 koneksi WebSocket pada Server C terputus seketika.
3. Klien-klien tersebut segera melakukan re-koneksi secara otomatis dalam waktu bersamaan.
4. Karena Caddy menggunakan Round Robin biasa:
- Request re-koneksi 1 -> Server A
- Request re-koneksi 2 -> Server B
- Request re-koneksi 3 -> Server A
- Request re-koneksi 4 -> Server B
(Seluruh 1.000 klien yang terputus langsung dibagi rata ke Server A dan B)
5. Ketika Server C menyala kembali, Server C dalam kondisi 0 koneksi, sementara Server A dan B memikul beban ekstra masing-masing 1.500 koneksi.
6. Round Robin TIDAK akan memindahkan koneksi WebSocket yang sudah terikat di Server A/B ke Server C karena koneksi WebSocket bersifat persisten dan tidak pernah ditutup.
Untuk skenario koneksi persisten berumur panjang seperti ini, beralih ke algoritma Least Connections (least_conn) adalah keputusan desain yang jauh lebih tepat dibandingkan mempertahankan Round Robin.
Integrasi Pemantauan Kesehatan Tingkat Lanjut #
Agar penyeimbang beban Round Robin kita tidak mengirimkan lalu lintas ke server yang sedang mengalami kerusakan internal (misalnya koneksi database backend terputus tetapi server backend masih hidup), kita wajib membuat endpoint pemeriksaan kesehatan (health check endpoint) yang representatif.
Berikut adalah contoh implementasi endpoint /healthz yang tangguh menggunakan NodeJS/Express untuk dibaca secara berkala oleh Caddy:
// index.js (Aplikasi NodeJS Backend)
const express = require('express');
const mysql = require('mysql2/promise');
const app = express();
// Konfigurasi koneksi MySQL database
const dbPool = mysql.createPool({
host: process.env.DB_HOST || 'localhost',
user: 'root',
database: 'production_db'
});
// Endpoint pemeriksaan kesehatan yang dipanggil oleh Caddy
app.get('/healthz', async (req, res) => {
const healthInfo = {
status: 'UP',
timestamp: new Date().toISOString(),
details: {
database: 'UP',
memory: 'OK'
}
};
try {
// 1. Uji koneksi database dengan kueri ringan
const connection = await dbPool.getConnection();
await connection.query('SELECT 1');
connection.release();
} catch (err) {
// Jika database mati, tandai status kesehatan sebagai DEGRADED
healthInfo.status = 'DOWN';
healthInfo.details.database = `ERROR: ${err.message}`;
}
// 2. Uji konsumsi RAM server backend
const memoryUsage = process.memoryUsage().heapUsed / 1024 / 1024;
if (memoryUsage > 500) { // Limit 500MB
healthInfo.status = 'DOWN';
healthInfo.details.memory = `CRITICAL: Memory usage is ${memoryUsage.toFixed(2)} MB`;
}
// Tentukan kode status HTTP response
const statusCode = healthInfo.status === 'UP' ? 200 : 503;
// BENAR: Kembalikan respons terstruktur untuk validasi Caddy
res.status(statusCode).json(healthInfo);
});
app.listen(3000, () => console.log('Backend berjalan di port 3000'));
Dengan mengembalikan kode status 503 Service Unavailable ketika database internal backend bermasalah, Caddy akan langsung mengetahui bahwa instance backend tersebut tidak layak menerima lalu lintas normal pengguna dan akan mengeluarkannya dari rotasi Round Robin secara otomatis.
Ringkasan #
- Definisi Dasar: Algoritma Round Robin menyalurkan request masuk ke server backend secara bergantian dalam rotasi sirkular yang teratur.
- Performa Konkuren: Caddy menggunakan operasi atomik tingkat rendah (
sync/atomic) untuk mengelola indeks target rotasi secara aman tanpa penguncian thread (lock-free concurrency).- Keterbatasan Beban: Round Robin membagi rata dari segi jumlah request, bukan berat beban komputasi. Backend heterogen sebaiknya menggunakan
least_conn.- Syarat Arsitektur: Wajib menggunakan arsitektur stateless (sesi disimpan di Redis/Database bersama) agar pengguna tidak terlempar keluar dari sesi login saat dipindahkan antar backend.
- Otomatisasi API: Penambahan backend baru pada klaster autoscaling dapat disuntikkan secara dinamis ke dalam rotasi melalui admin API port
:2019tanpa restart.- Koneksi Persisten: Untuk protokol koneksi persisten yang berumur panjang seperti WebSocket dan gRPC, Round Robin rawan memicu ketidakseimbangan beban ketika backend kembali dari restart.