Frontend Labs
⋯

Day 45 — Load Balancing Algorithms

Learning Objectives

  • Implement round-robin, weighted round-robin, least-connections, and power-of-two
  • Measure load distribution under unequal request costs
  • Know which algorithm to recommend for different traffic patterns

Key Concepts

Algorithm         | State     | Best for
─────────────────────────────────────────────────────
Round-robin       | none      | Homogeneous backends, equal request cost
Weighted RR       | weights   | Backends with different capacity
Least-connections | per-conn  | Variable request duration (API + slow DB queries)
Random            | none      | Simple, scales well, similar to RR
Power-of-two      | sampled   | Best for cache backends (reduces stale connections)

Real usage:
  AWS NLB:         Flow hash (5-tuple) — same client always hits same backend
  nginx upstream:  Round-robin by default, least_conn available
  Cloudflare:      Least-connections with health checking
  HAProxy:         Least-connections by default

Exercise: Four Algorithms, One Interface

package main

import (
    "fmt"
    "math/rand"
    "sync"
    "sync/atomic"
)

type Backend struct {
    Addr        string
    Weight      int
    ActiveConns atomic.Int64
}

type Balancer interface {
    Pick(backends []*Backend) *Backend
}

// RoundRobin — stateless modulo
type RoundRobin struct{ counter atomic.Uint64 }

func (rb *RoundRobin) Pick(backends []*Backend) *Backend {
    if len(backends) == 0 { return nil }
    idx := rb.counter.Add(1) - 1
    return backends[idx%uint64(len(backends))]
}

// WeightedRoundRobin — higher weight = more requests
type WeightedRoundRobin struct {
    mu      sync.Mutex
    current int
    gcd     int
    maxW    int
    cw      int // current weight
}

func gcd(a, b int) int {
    for b != 0 { a, b = b, a%b }
    return a
}

func (wrr *WeightedRoundRobin) Pick(backends []*Backend) *Backend {
    wrr.mu.Lock()
    defer wrr.mu.Unlock()
    n := len(backends)
    if n == 0 { return nil }

    // Compute GCD and max weight
    g := backends[0].Weight
    maxW := backends[0].Weight
    for _, b := range backends[1:] {
        g = gcd(g, b.Weight)
        if b.Weight > maxW { maxW = b.Weight }
    }

    for {
        wrr.current = (wrr.current + 1) % n
        if wrr.current == 0 {
            wrr.cw -= g
            if wrr.cw <= 0 {
                wrr.cw = maxW
            }
        }
        if backends[wrr.current].Weight >= wrr.cw {
            return backends[wrr.current]
        }
    }
}

// LeastConnections — pick the backend with fewest active connections
type LeastConnections struct{}

func (lc *LeastConnections) Pick(backends []*Backend) *Backend {
    if len(backends) == 0 { return nil }
    best := backends[0]
    for _, b := range backends[1:] {
        if b.ActiveConns.Load() < best.ActiveConns.Load() {
            best = b
        }
    }
    return best
}

// PowerOfTwo — sample 2 random backends, pick the less-loaded
type PowerOfTwo struct{}

func (p *PowerOfTwo) Pick(backends []*Backend) *Backend {
    if len(backends) == 0 { return nil }
    if len(backends) == 1 { return backends[0] }
    i := rand.Intn(len(backends))
    j := rand.Intn(len(backends) - 1)
    if j >= i { j++ }
    a, b := backends[i], backends[j]
    if a.ActiveConns.Load() <= b.ActiveConns.Load() {
        return a
    }
    return b
}

func main() {
    backends := []*Backend{
        {Addr: "10.0.0.1:8080", Weight: 1},
        {Addr: "10.0.0.2:8080", Weight: 2},
        {Addr: "10.0.0.3:8080", Weight: 1},
    }

    // Simulate load with variable connection counts
    backends[1].ActiveConns.Store(50)
    backends[2].ActiveConns.Store(10)

    algorithms := map[string]Balancer{
        "round-robin":        &RoundRobin{},
        "weighted-rr":        &WeightedRoundRobin{},
        "least-connections":  &LeastConnections{},
        "power-of-two":       &PowerOfTwo{},
    }

    for name, algo := range algorithms {
        counts := make(map[string]int)
        for i := 0; i < 1000; i++ {
            b := algo.Pick(backends)
            counts[b.Addr]++
        }
        fmt.Printf("\n%s (1000 reqs):\n", name)
        for addr, count := range counts {
            fmt.Printf("  %s: %d (%.1f%%)\n", addr, count, float64(count)/10)
        }
    }
}

Real-World Context

AWS NLB costs $0.006/LCU-hour. Understanding these algorithms helps you answer: "Why does the NLB keep sending traffic to the slow backend?" (Round-robin ignores backend latency — use Least-connections for APIs with variable response time.)


Generated from week7.md · 8 Oct 2026 16:36