Masalah: Satu Server, Sejuta User
Bayangin kamu punya web app yang makin populer. Awalnya 100 user sehari, terus naik jadi 10.000, 100.000, sampai sejuta.
Satu server — secanggih apapun — punya limit. CPU mentok, RAM penuh, response time naik, dan akhirnya user mulai dapet error 503.
Solusi naif: beli server yang lebih gede (vertical scaling). Bisa, tapi ada ceilingnya dan mahal.
Solusi yang lebih sustainable: tambah server dan bagi traffic-nya (horizontal scaling). Tapi pertanyaannya: siapa yang bagi traffic-nya?
Itu tugasnya Load Balancer.
Apa itu Load Balancer?
Load balancer itu komponen yang duduk di depan sekelompok server dan mendistribusikan traffic ke server-server di belakangnya.
graph LR
U1[User 1] --> LB[Load Balancer]
U2[User 2] --> LB
U3[User 3] --> LB
LB --> S1[Server 1]
LB --> S2[Server 2]
LB --> S3[Server 3]
Dari sisi user, mereka cuma tahu satu alamat (misalnya api.myapp.com). Mereka ga tahu dan ga perlu tahu bahwa di belakang ada 3, 10, atau 100 server.
Load balancer yang putuskan: request ini dikirim ke server mana.
Kenapa Load Balancing Penting?
1. Distribusi beban
Tanpa load balancer, semua traffic masuk ke satu server. Dengan load balancer, beban dibagi rata. Server ga ada yang overload sementara yang lain nganggur.
2. High availability
Salah satu server mati? Load balancer otomatis berhenti kirim traffic ke sana. User ga ngerasain apa-apa.
3. Horizontal scaling
Mau nambah kapasitas? Tinggal tambah server baru di belakang load balancer. Ga perlu downtime, ga perlu ganti server yang ada.
4. Maintenance tanpa downtime
Mau update server satu? Tarik keluar dari pool, update, masukin lagi. Traffic tetap jalan lewat server lain.
Algoritma Load Balancing
Ini bagian paling penting. Gimana load balancer milih server untuk setiap request?
1. Round Robin
Paling simpel. Request dibagi bergiliran.
Request 1 → Server A
Request 2 → Server B
Request 3 → Server C
Request 4 → Server A (balik lagi)
Request 5 → Server B
...
Pro: simpel, ga perlu tracking state
Kontra: ga peduli beban server. Kalau satu request butuh 100ms dan yang lain 10 detik, server yang dapet request berat bakal ketinggalan
Cocok untuk: server dengan kapasitas identik dan request yang relatif seragam
2. Weighted Round Robin
Kayak Round Robin, tapi server dengan kapasitas lebih besar dapet lebih banyak request.
Server A (weight: 5) → dapet 5 dari setiap 8 request
Server B (weight: 2) → dapet 2 dari setiap 8 request
Server C (weight: 1) → dapet 1 dari setiap 8 request
Cocok untuk: cluster dengan server yang kapasitasnya beda-beda (misal server lama dan baru dicampur)
3. Least Connections
Request dikirim ke server yang punya koneksi aktif paling sedikit.
graph LR
LB[Load Balancer] -->|"3 active conn"| S1[Server A]
LB -->|"1 active conn ✓"| S2[Server B]
LB -->|"5 active conn"| S3[Server C]
style S2 fill:#27ae60,color:#fff
Request baru → ke Server B karena dia yang paling sedikit koneksinya.
Pro: lebih fair karena memperhitungkan beban actual
Kontra: butuh tracking koneksi aktif di setiap server
Cocok untuk: request dengan durasi yang bervariasi (long-polling, WebSocket, file upload besar)
4. Weighted Least Connections
Kombinasi Least Connections + Weight. Server dengan kapasitas lebih besar bisa handle lebih banyak koneksi.
Formula: connections / weight. Server dengan ratio terkecil yang dipilih.
Server A: 10 connections, weight 5 → ratio: 2.0
Server B: 4 connections, weight 2 → ratio: 2.0
Server C: 1 connection, weight 1 → ratio: 1.0 ✓ (dipilih)
5. IP Hash
Request dari IP yang sama selalu dikirim ke server yang sama.
Hash(192.168.1.100) mod 3 = 1 → Server B
Hash(10.0.0.50) mod 3 = 0 → Server A
Hash(172.16.0.1) mod 3 = 2 → Server C
Pro: user selalu ke server yang sama (natural sticky session)
Kontra: distribusi bisa ga rata kalau ada IP yang generate traffic jauh lebih banyak (misal corporate NAT)
6. Least Response Time
Request dikirim ke server yang punya response time tercepat dan koneksi aktif paling sedikit.
Pro: paling "smart" — mempertimbangkan actual performance
Kontra: butuh monitoring response time real-time, overhead lebih besar
7. Random
Pilih server secara acak. Serius.
Kedengerannya konyol, tapi secara statistik, dengan jumlah request yang besar, distribusinya mendekati Round Robin. Dan implementasinya paling simpel — ga butuh state apapun.
Beberapa CDN pakai pendekatan ini (dengan modifikasi).
L4 vs L7 Load Balancing
Load balancer bisa beroperasi di layer yang berbeda dari OSI model.
Layer 4 (Transport) Load Balancing
Bekerja di level TCP/UDP. Load balancer cuma lihat:
- IP address source & destination
- Port number
- Protocol (TCP/UDP)
Ga bisa lihat isi request (URL, header, cookie, body).
graph LR
Client -->|TCP SYN| L4[L4 Load Balancer]
L4 -->|forward TCP| S1[Server 1]
L4 -->|forward TCP| S2[Server 2]
Note1["❌ Ga bisa lihat:<br/>URL, Header, Cookie"]
Pro: sangat cepat, overhead rendah, support semua protokol (HTTP, gRPC, database, dll)
Kontra: ga bisa routing berdasarkan konten request
Contoh: AWS NLB, HAProxy (TCP mode), Linux IPVS
Layer 7 (Application) Load Balancing
Bekerja di level HTTP/HTTPS. Load balancer bisa lihat dan inspeksi:
- URL path
- HTTP headers
- Cookies
- Request body
- Host header
graph LR
Client -->|"GET /api/users"| L7[L7 Load Balancer]
L7 -->|"/api/*"| API[API Servers]
L7 -->|"/static/*"| Static[Static Servers]
L7 -->|"/ws/*"| WS[WebSocket Servers]
Bisa routing berdasarkan konten request!
Pro: content-based routing, bisa terminate SSL, bisa rewrite headers, bisa caching
Kontra: lebih lambat (harus parse HTTP), hanya untuk HTTP/HTTPS
Contoh: Nginx, AWS ALB, HAProxy (HTTP mode), Traefik, Envoy
Kapan pakai yang mana?
| Kebutuhan | L4 | L7 |
|---|---|---|
| Speed/latency critical | ✅ | |
| Non-HTTP protocol (database, gRPC) | ✅ | |
| Content-based routing | ✅ | |
| SSL termination | ✅ | |
| URL rewriting, header modification | ✅ | |
| WebSocket upgrade handling | ✅ |
Banyak setup production pakai keduanya: L4 di depan untuk distribusi awal, L7 di belakang untuk intelligent routing.
Health Check
Load balancer harus tahu: server mana yang sehat, server mana yang mati atau sakit.
Kalau load balancer tetap kirim traffic ke server yang down, user dapet error. Itu bukan load balancing, itu load failing.
Tipe Health Check
1. Active Health Check
Load balancer secara berkala mengirim probe ke setiap server.
sequenceDiagram
participant LB as Load Balancer
participant S1 as Server 1
participant S2 as Server 2
loop Setiap 5 detik
LB->>S1: GET /health
S1-->>LB: 200 OK ✅
LB->>S2: GET /health
S2--xLB: Timeout ❌
end
Note over LB: Server 2 unhealthy, stop traffic
Konfigurasi biasanya:
- Interval: seberapa sering cek (misal tiap 5 detik)
- Timeout: berapa lama nunggu response (misal 3 detik)
- Unhealthy threshold: berapa kali gagal berturut sebelum dianggap mati (misal 3x)
- Healthy threshold: berapa kali sukses sebelum dianggap hidup lagi (misal 2x)
2. Passive Health Check
Load balancer ga kirim probe, tapi monitor traffic yang sudah lewat. Kalau response dari server mulai banyak error (5xx) atau timeout, tandai unhealthy.
Pro: ga ada extra traffic dari health check
Kontra: deteksi lebih lambat, baru tahu setelah user kena error
3. Gabungan (paling umum)
Active health check jalan di background. Passive health check sebagai fast-detect kalau tiba-tiba server error di antara interval active check.
Health Check Endpoint
Jangan cuma return 200 OK. Cek dependency juga:
// GET /health
{
"status": "healthy",
"checks": {
"database": "connected",
"redis": "connected",
"disk_space": "72% used"
},
"uptime": "3d 14h 22m"
}
Server yang databasenya mati tapi HTTP-nya nyala itu technically up, tapi functionally down. Health check yang baik bakal detect ini.
Sticky Sessions (Session Affinity)
Masalahnya
User login di Server A → session disimpan di memory Server A. Request berikutnya masuk ke Server B → Server B ga punya session-nya → user disuruh login lagi.
sequenceDiagram
participant U as User
participant LB as Load Balancer
participant S1 as Server A
participant S2 as Server B
U->>LB: Login
LB->>S1: Forward
S1-->>U: Session created ✅
U->>LB: Next request
LB->>S2: Forward (different server!)
S2-->>U: Who are you? ❌
Solusi 1: Sticky Sessions
Load balancer memastikan request dari user yang sama selalu ke server yang sama.
Caranya:
- Cookie-based: load balancer set cookie
SERVERID=server-a. Request berikutnya, load balancer baca cookie dan route ke server-a - IP-based: pakai IP hash (seperti algoritma di atas)
Masalah sticky session:
- Distribusi ga rata — bisa ada server yang kena lebih banyak "heavy users"
- Server mati → semua user yang sticky ke sana kehilangan session
- Scaling jadi susah — tarik server keluar berarti pindahin user-usernya
Solusi 2: Shared Session Store (lebih baik)
Jangan simpan session di memory server. Simpan di shared store yang bisa diakses semua server.
graph TB
LB[Load Balancer]
LB --> S1[Server A]
LB --> S2[Server B]
LB --> S3[Server C]
S1 --> Redis[(Redis - Session Store)]
S2 --> Redis
S3 --> Redis
User masuk ke server mana aja, session-nya sama karena semua server baca dari Redis yang sama.
Ini pendekatan yang direkomendasikan. Sticky session itu workaround, bukan solusi.
Solusi 3: Stateless (paling ideal)
Pakai JWT atau token-based auth. Ga perlu session store sama sekali. Setiap request bawa credential-nya sendiri.
Udah dibahas di catatan JWT Security.
SSL/TLS Termination
HTTPS itu encrypted. Encrypt/decrypt itu butuh CPU. Kalau setiap backend server harus handle SSL, itu buang resource.
SSL Termination di Load Balancer
graph LR
Client -->|"HTTPS (encrypted)"| LB[Load Balancer]
LB -->|"HTTP (plain)"| S1[Server 1]
LB -->|"HTTP (plain)"| S2[Server 2]
Load balancer yang handle SSL. Traffic ke backend pakai HTTP biasa. Backend server ga perlu manage certificate, ga perlu CPU untuk encrypt/decrypt.
Pro: backend lebih ringan, certificate management di satu tempat
Kontra: traffic internal ga encrypted (harus trust internal network)
SSL Passthrough
graph LR
Client -->|"HTTPS"| LB[Load Balancer]
LB -->|"HTTPS (untouched)"| S1[Server 1]
LB -->|"HTTPS"| S2[Server 2]
Load balancer ga decrypt. Langsung forward encrypted traffic ke backend. Backend yang handle SSL.
Pro: end-to-end encryption, load balancer ga lihat data
Kontra: load balancer ga bisa inspect request (jadi harus L4), backend lebih berat
SSL Re-encryption (SSL Bridging)
graph LR
Client -->|"HTTPS"| LB[Load Balancer]
LB -->|"HTTPS (re-encrypted)"| S1[Server 1]
Load balancer decrypt, inspect, lalu encrypt ulang ke backend. Dualisme: bisa inspect tapi tetap encrypted internal.
Cocok untuk: environment yang butuh compliance (PCI-DSS, HIPAA) di mana traffic internal juga harus encrypted.
Real-World Setup
Nginx sebagai Load Balancer
upstream backend {
least_conn; # algoritma
server 10.0.0.1:3000 weight=3;
server 10.0.0.2:3000 weight=2;
server 10.0.0.3:3000 weight=1;
server 10.0.0.4:3000 backup; # hanya dipake kalau yang lain mati
}
server {
listen 443 ssl;
server_name api.myapp.com;
ssl_certificate /etc/ssl/cert.pem;
ssl_certificate_key /etc/ssl/key.pem;
location / {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /health {
proxy_pass http://backend;
proxy_connect_timeout 1s;
proxy_read_timeout 1s;
}
}
Cloud Load Balancer
Di cloud, biasanya ga perlu setup sendiri:
| Cloud | L4 | L7 |
|---|---|---|
| AWS | NLB (Network Load Balancer) | ALB (Application Load Balancer) |
| GCP | Network LB | HTTP(S) LB |
| Azure | Azure Load Balancer | Application Gateway |
Cloud load balancer biasanya udah include: auto-scaling, health check, SSL termination, DDoS protection, dan integration sama service lain.
Common Pitfalls
1. Load balancer sebagai SPOF
Ironis — kamu pasang load balancer buat eliminasi SPOF di backend, tapi load balancer-nya sendiri jadi SPOF.
Solusi: pasang load balancer redundant (active-passive atau active-active).
graph TB
DNS["DNS (Round Robin)"]
DNS --> LB1["LB 1 (Active)"]
DNS --> LB2["LB 2 (Active)"]
LB1 --> S1[Server 1]
LB1 --> S2[Server 2]
LB2 --> S1
LB2 --> S2
2. Ga test dengan beban realistis
Load balancing yang keliatan bagus di development bisa jebol di production karena:
- Connection pool exhaustion
- Thundering herd saat server baru masuk pool
- Slow start problem — server baru belum warm (cache kosong, JIT belum optimal)
3. Ignore X-Forwarded-For
Dengan load balancer, backend server cuma lihat IP load balancer, bukan IP user asli. Kalau butuh IP asli (logging, rate limiting, geo-blocking), harus baca header X-Forwarded-For.
4. Health check yang terlalu simpel
GET / return 200 ≠ server sehat. Database bisa mati, disk bisa penuh, memory bisa leak. Health check endpoint harus cek semua dependency critical.
Ringkasan
| Konsep | Penjelasan |
|---|---|
| Load Balancer | Komponen yang distribusi traffic ke banyak server |
| Round Robin | Giliran, simpel, ga peduli beban |
| Least Connections | Kirim ke server yang paling sedikit koneksi |
| IP Hash | User yang sama selalu ke server yang sama |
| L4 LB | Beroperasi di TCP/UDP, cepat tapi ga bisa inspect |
| L7 LB | Beroperasi di HTTP, bisa content-based routing |
| Health Check | Monitoring otomatis untuk detect server yang down |
| Sticky Session | Force user ke server yang sama (workaround) |
| SSL Termination | Load balancer handle encrypt/decrypt |
| Shared Session | Simpan session di Redis, semua server bisa akses |
Kesimpulan
Load balancing itu salah satu building block paling fundamental di arsitektur modern. Tanpa ini, horizontal scaling ga mungkin.
Yang perlu diingat:
- Pilih algoritma yang sesuai — Round Robin cukup untuk kebanyakan kasus, Least Connections untuk request yang variatif
- L7 untuk web app, L4 untuk yang lain — kalau butuh inspect request, harus L7
- Health check itu wajib — load balancer tanpa health check itu cuma random forwarder
- Hindari sticky session — pakai shared session store atau stateless auth (JWT)
- Load balancer juga butuh redundancy — jangan bikin SPOF baru
- SSL termination di load balancer — kecuali compliance bilang harus end-to-end
Load balancing sering dipandang sebagai hal yang "tinggal pasang Nginx". Tapi di baliknya ada banyak keputusan arsitektur yang bisa bikin atau hancurin sistem kamu di scale yang besar.
