Sobes.tech
Middle

/ * There is an application with a microservices architecture. A microservice can be abstracted using the Backend interface. To access a single instance of a microservice, you can use the BackendImpl type, which is already implemented. Each microservice has several dozen running instances, each accessible via its own address addr. However, individual instances of the microservice are unreliable: they can crash, be unavailable, or overloaded. Therefore, you need to implement a Balancer type that also implements the Backend interface and performs client-side load balancing among the microservice instances, choosing the **least loaded** instance each time. */ type Request interface{} type Response interface{} type Backend interface { Invoke(ctx context.Context, req Request) (Response, error) } var _ Backend = &BackendImpl{} // addr contains the ip:port of a specific instance func NewBackend(addr string) *BackendImpl type Balancer struct { // TODO } var _ Backend = &Balancer{} // addrs contain the addresses of all balancable instances func NewBalancer(addrs []string) *Balancer { // TODO }

sobes.tech AI

Answer from AI

To implement a Balancer that selects the least loaded microservice instance, you need to store the load state of each instance and, upon calling Invoke, choose the one with the minimal load.

An approximate approach:

  • In Balancer, store a slice of BackendImpl instances and counters for current requests (loads) for each.
  • When calling Invoke, select the instance with the lowest load.
  • Increment the load counter before the call, and decrement it after completion (in defer).

Example code:

import (
    "context"
    "sync"
)

type Balancer struct {
    backends []*BackendImpl
    mu       sync.Mutex
    loads    []int // number of current requests for each backend
}

func NewBalancer(addrs []string) *Balancer {
    b := &Balancer{
        backends: make([]*BackendImpl, len(addrs)),
        loads:    make([]int, len(addrs)),
    }
    for i, addr := range addrs {
        b.backends[i] = NewBackend(addr)
    }
    return b
}

func (b *Balancer) Invoke(ctx context.Context, req Request) (Response, error) {
    b.mu.Lock()
    // Find index with minimal load
    minIdx := 0
    minLoad := b.loads[0]
    for i, load := range b.loads {
        if load < minLoad {
            minLoad = load
            minIdx = i
        }
    }
    b.loads[minIdx]++
    b.mu.Unlock()

    // Call backend
    resp, err := b.backends[minIdx].Invoke(ctx, req)

    b.mu.Lock()
    b.loads[minIdx]--
    b.mu.Unlock()

    return resp, err
}

Thus, each request is directed to the least loaded instance, ensuring client-side load balancing.