How the Go-Kratos Weighted Round-Robin Load Balancer Works in Fabrica-Kit

Fabrica-Kit implements a weighted round-robin load balancer for go-kratos that combines the classic NGINX WRR algorithm with route-table-based sticky routing to ensure consistent node selection across distributed calls.

The go-kratos weighted round-robin load balancer in Fabrica-Kit extends the standard Kratos selector framework with advanced routing capabilities. Located in the router/balancer package, this implementation manages traffic distribution across service nodes while maintaining session affinity through an external route table.

Core Architecture

The balancer architecture centers on two primary components: the weightBalancer struct that manages selection state, and the balancerBuilder that integrates with Kratos' selector framework.

The weightBalancer Struct

In router/balancer/balancer.go, the weightBalancer struct maintains the runtime state required for weighted selection:

  • A mutex protecting concurrent access to weight calculations
  • A currentWeight map tracking dynamic weights per node address
  • A ReadOnlyRouteTable reference for sticky routing lookups
  • A balancerType field distinguishing between TypeMaster and replica configurations

The struct implements the selector.Selector interface, allowing seamless integration with Kratos clients.

Builder Pattern Integration

The balancerBuilder in router/balancer/balancerbuilder.go constructs balancers using functional options:

builder := balancer.NewBuilder(
    balancer.WithBalancerType(balancer.TypeMaster),
    balancer.WithRouteTable(rt),
)

This builder wraps the balancer in a Kratos-compatible selector through selector.NewBuilder(builder).Build().

Selection Flow and Sticky Routing

The Pick method orchestrates the complete node selection workflow, prioritizing route-table consistency before falling back to weighted distribution.

Pick Method Implementation

When invoked with a context and node slice, Pick in router/balancer/balancer.go executes the following sequence:

  1. Validation: Returns selector.ErrNoAvailable if the node slice is empty
  2. Metadata extraction: Retrieves OID (object ID) and color (routing group) from the outgoing context using xcontext.OIDFromOutgoingContext and xcontext.ColorFromOutgoingContext
  3. Sticky lookup: Queries the route table via p.routeTable.Get(color, oid); if an address exists, returns that specific node immediately
  4. Weighted selection: Invokes weightSelect to determine the target node when no sticky entry exists

Master-Type Persistence

For TypeMaster balancers, the implementation adds atomic persistence:

if p.balancerType == TypeMaster {
    mrt := p.routeTable.(routetable.MasterRouteTable)
    ok, addr, err := mrt.SetNxOrGet(ctx, color, oid, selected.Address())
    // If another balancer won the race, use the stored address instead
}

The SetNxOrGet operation provides "set-if-not-exists" semantics, ensuring that concurrent initial requests for the same OID resolve to a single canonical node.

Weighted Round-Robin Algorithm

The weightSelect method implements the classic NGINX weighted round-robin algorithm with float64 precision weights.

weightSelect Implementation

Located in router/balancer/balancer.go, the algorithm maintains fairness while respecting node weights:

func (p *weightBalancer) weightSelect(nodes []selector.WeightedNode) selector.WeightedNode {
    var totalWeight float64
    var selected selector.WeightedNode
    var selectWeight float64

    p.mu.Lock()
    defer p.mu.Unlock()

    for _, node := range nodes {
        totalWeight += node.Weight()
        cwt := p.currentWeight[node.Address()] + node.Weight()
        p.currentWeight[node.Address()] = cwt

        if selected == nil || selectWeight < cwt {
            selectWeight = cwt
            selected = node
        }
    }

    p.currentWeight[selected.Address()] = selectWeight - totalWeight
    return selected
}

Key mechanics:

  • Each node's current weight increments by its static weight every round
  • The node with the highest current weight wins selection
  • After selection, the winner's weight decreases by the total weight of all nodes
  • Float64 precision enables fine-grained weighting (e.g., 0.5 or 2.3 ratios)

Route Table Integration

The route table abstraction in router/routetable/routetable.go enables persistent sticky routing across service restarts.

ReadOnlyRouteTable provides Get and BatchGet methods for address lookups. MasterRouteTable extends this with SetNxOrGet for atomic write operations. The balancer accepts either interface depending on whether it needs to persist selections or merely respect existing mappings.

Practical Implementation Example

Create a master balancer with Redis-backed sticky routing:

import (
    "github.com/go-pantheon/fabrica-kit/router/balancer"
    "github.com/go-pantheon/fabrica-kit/router/routetable/redis"
    "github.com/go-kratos/kratos/v2/selector"
)

rt, _ := redis.NewRouteTable(redisClient, "myservice")

builder := balancer.NewBuilder(
    balancer.WithBalancerType(balancer.TypeMaster),
    balancer.WithRouteTable(rt),
)

sel := selector.NewBuilder(builder).Build()

Use the selector in a Kratos HTTP client:

client := http.NewClient(
    http.WithEndpoint("http://example.com"),
    http.WithDiscovery(sel),
)

The client now routes requests via the weighted round-robin algorithm while maintaining OID-based session affinity through the Redis route table.

Summary

  • The go-kratos weighted round-robin load balancer in Fabrica-Kit resides in router/balancer/balancer.go and implements the NGINX WRR algorithm using float64 precision weights and mutex-protected state.
  • Sticky routing is achieved through the ReadOnlyRouteTable interface, with TypeMaster balancers utilizing SetNxOrGet for atomic address persistence.
  • The selection flow extracts OID and color from context, checks the route table first, then falls back to weightSelect if no entry exists.
  • Builder pattern integration in router/balancer/balancerbuilder.go allows configuration of balancer types and route table injection via NewBuilder.

Frequently Asked Questions

How does the balancer handle concurrent requests to the same OID?

The weightBalancer protects its currentWeight map with a mutex during the weightSelect operation. For sticky routing, TypeMaster balancers use the SetNxOrGet atomic operation to ensure that only the first request for a given OID establishes the node mapping, while subsequent concurrent requests receive the stored address.

What is the difference between TypeMaster and other balancer types?

TypeMaster balancers write selected node addresses back to the route table using MasterRouteTable.SetNxOrGet, establishing persistent sticky sessions. Other types (such as TypeReplica) perform read-only route table lookups and weighted selection without persisting the choice, making them suitable for stateless or read-only operations.

Why does the algorithm use float64 for weights instead of integers?

Float64 weights provide fine-grained control over traffic distribution, allowing weight ratios like 0.5:1 or 2.3:1. This precision enables more accurate proportional load balancing compared to integer-only systems, particularly useful when dealing with heterogeneous node capacities or gradual traffic shifting during deployments.

Where is the route table implementation typically stored?

The route table interface abstracts the storage backend, with implementations available in subdirectories like router/routetable/redis or router/routetable/postgresql. The balancer interacts only with the ReadOnlyRouteTable or MasterRouteTable interfaces, making the storage backend pluggable while the core weighted round-robin logic remains storage-agnostic.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →