This article explores the implementation and benefits of Weighted Round Robin (WRR) load balancing as an improvement over naive round robin. It demonstrates how WRR allows for intelligent distribution of traffic based on server capacity, integrating with health checks to avoid overloading degraded nodes. The discussion highlights WRR's predictability, graceful scaling, and simplicity as key architectural advantages for high-throughput systems.
Read original on Dev.to #systemdesignTraditional round robin load balancing distributes requests equally among available servers. While simple, this approach often leads to performance bottlenecks when servers have varying capacities, current loads, or are in degraded states. Sending traffic to an overloaded or warming-up server can cause cascading failures and poor user experience, as exemplified by a scenario where an API sputtered under a traffic spike due to a naive load balancer.
Weighted Round Robin (WRR) is an enhancement that assigns a 'weight' to each server, representing its processing capacity (e.g., based on CPU, memory, or current load score). The load balancer then distributes requests proportionally to these weights. For instance, a server with a weight of 3 will receive three times as many requests as a server with a weight of 1 within a given cycle.
type weightedLB struct {
servers []string
weights []int // same length as servers
currentWeight int // tracks how many more requests we can give to the current server
currentIndex int // points to the server we are serving from
totalWeight int // sum of all weights (used for reset)
}
func (lb *weightedLB) Next() string {
for {
lb.currentIndex = (lb.currentIndex + 1) % len(lb.servers)
if lb.currentIndex == 0 { // new round
// Logic to re-initialize current weights for a new round (simplified here for brevity)
}
// Simplified: in a real impl, you'd iterate through servers, decrementing weight
// and move to next when weight hits 0, resetting currentWeight from weights slice
// when a server's turn comes again.
// The article's provided Go code offers a more robust in-place implementation.
if lb.weights[lb.currentIndex] > 0 { // Check if server has weight (is healthy)
// In a real WRR, you'd decrement current weight for this server before returning it
return lb.servers[lb.currentIndex]
}
}
}Integrating Health Checks
To handle dynamic server conditions, WRR should be combined with robust health checks. If a server becomes unhealthy or degraded, its weight can be dynamically set to zero, effectively removing it from the rotation until it recovers. This prevents the load balancer from sending requests to failing nodes and improves overall system resilience.
Key considerations for implementing WRR include atomically updating weights when server health changes, guarding against scenarios where all servers have zero weight to avoid infinite loops, and re-initializing the load balancer state when the server pool itself changes (e.g., adding or removing nodes). Properly implemented, WRR significantly improves latency distribution and reduces error rates during peak loads, making it a versatile pattern applicable to various system components like HTTP proxies, database clients, or RPC gateways.