Load Balancing is an algorithm used to solve the problem that one machine (one process) can’t handle all requests.

Load balancing is used in many places like nginx to distribute traffic, ribbon to provide load balancing for clients, load balancing in dubbo service calls, etc.

The benefits of using load balancing are obvious.

  1. when one or more servers in the cluster go down, the remaining servers that are not down can keep the service running
  2. more machines are used to ensure the benign use of machines, so that the system cpu does not rise sharply due to a peak moment

There are several implementation strategies for load balancing, the common ones are.

  1. Random
  2. RoundRobin
  3. ConsistentHash
  4. Hash
  5. Weighted

Let’s take the implementation of ribbon as a basis to see how some of these algorithms are implemented.

The ribbon is a service that provides load balancing functionality for clients. It provides an internal interface called ILoadBalance to represent the operations of the load balancer, such as adding a server operation, selecting a server operation, getting a list of all servers, getting a list of available servers, and so on.

An interface called IRule is also provided to represent the load balancing policy.

1
2
3
4
5
public interface IRule{
    public Server choose(Object key);
    public void setLoadBalancer(ILoadBalancer lb);
    public ILoadBalancer getLoadBalancer();    
}

The IRule interface has the following implementation classes.

Among them, RandomRule means random strategy, RoundRobin means polling strategy, WeightedResponseTimeRule means weighted strategy, BestAvailableRule means least number of requests strategy, etc.

The random policy is very simple, it is a random selection of a server from the servers, the code to implement RandomRule is as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public Server choose(ILoadBalancer lb, Object key) {
    if (lb == null) {
        return null;
    }
    Server server = null;

    while (server == null) {
        if (Thread.interrupted()) {
            return null;
        }
        List<Server> upList = lb.getReachableServers();
        List<Server> allList = lb.getAllServers();
        int serverCount = allList.size();
        if (serverCount == 0) {
            return null;
        }
        int index = rand.nextInt(serverCount); // Use jdk's internal Random class to get the index value randomly
        server = upList.get(index); // Get the server instance

        if (server == null) {
            Thread.yield();
            continue;
        }

        if (server.isAlive()) {
            return (server);
        }

        server = null;
        Thread.yield();
    }
    return server;
}

The RoundRobin polling policy means that the next server is taken each time, for example, if there are 5 servers, the first one is taken for the first time, the second one for the second time, the third one for the third time, and so on.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public Server choose(ILoadBalancer lb, Object key) {
    if (lb == null) {
        log.warn("no load balancer");
        return null;
    }

    Server server = null;
    int count = 0;
    while (server == null && count++ < 10) { // Retry 10 times
        List<Server> reachableServers = lb.getReachableServers();
        List<Server> allServers = lb.getAllServers();
        int upCount = reachableServers.size();
        int serverCount = allServers.size();

        if ((upCount == 0) || (serverCount == 0)) {
            log.warn("No up servers available from load balancer: " + lb);
            return null;
        }

        int nextServerIndex = incrementAndGetModulo(serverCount); // The incrementAndGetModulo method internally uses the nextServerCyclicCounter AtomicInteger property to increment the serverCount atomically to get the index value.
        server = allServers.get(nextServerIndex); // Get the server instance

        if (server == null) {
            Thread.yield();
            continue;
        }

        if (server.isAlive() && (server.isReadyToServe())) {
            return (server);
        }

        server = null;
    }

    if (count >= 10) {
        log.warn("No available alive servers after 10 tries from load balancer: "
                + lb);
    }
    return server;
}

The BestAvailableRule policy is used to select the server with the least number of concurrent requests.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public Server choose(Object key) {
    if (loadBalancerStats == null) {
        return super.choose(key);
    }
    List<Server> serverList = getLoadBalancer().getAllServers(); // Get a list of all servers
    int minimalConcurrentConnections = Integer.MAX_VALUE;
    long currentTime = System.currentTimeMillis();
    Server chosen = null;
    for (Server server: serverList) { // Iterate through each server
        ServerStats serverStats = loadBalancerStats.getSingleServerStat(server); // Get the status of each server
        if (!serverStats.isCircuitBreakerTripped(currentTime)) { // Continue if no circuit breaker is triggered
            int concurrentConnections = serverStats.getActiveRequestsCount(currentTime); // Get the number of requests from the current server
            if (concurrentConnections < minimalConcurrentConnections) { // Compare the number of requests between servers, then select the server with the lowest number of requests and put it in the chosen variable
                minimalConcurrentConnections = concurrentConnections;
                chosen = server;
            }
        }
    }
    if (chosen == null) { // If not selected, call the parent class ClientConfigEnabledRoundRobinRule's choose method, that is, use RoundRobinRule polling for load balancing        
        return super.choose(key);
    } else {
        return chosen;
    }
}

Example to verify the LoadBalance function in Ribbon

There are 3 instances provided in ServerList, which are.

1
2
3
compute-service:2222
compute-service:2223
compute-service:2224

Then use the different IRule policies to see the load balancing implementation.

Start by using the LoadBalanced annotation provided by ribbon on top of the RestTemplate, which automatically constructs the implementation class of the LoadBalancerClient interface and registers it in the Spring container.

1
2
3
4
5
@Bean
@LoadBalanced
RestTemplate restTemplate() {
    return new RestTemplate();
}

Next, when using RestTemplate for rest operation, it will automatically use the load balancing policy, and it will internally add LoadBalancerInterceptor as an interceptor in RestTemplate.

In the example, the name of our instance is called compute-service, which provides a method add for adding 2 values of type Integer.

The specific operation of loadbalance.

1
2
3
4
5
6
7
8
public String loadbalance() {
    ServiceInstance serviceInstance = loadBalancerClient.choose("compute-service");
    StringBuilder sb = new StringBuilder();
    sb.append("host: ").append(serviceInstance.getHost()).append(", ");
    sb.append("port: ").append(serviceInstance.getPort()).append(", ");
    sb.append("uri: ").append(serviceInstance.getUri());
    return sb.toString();
}

RandomRule

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@Configuration
public class RibbonConfiguration {

    @Autowired
    private SpringClientFactory springClientFactory;

    @Bean
    public IRule ribbonRule() {
        return new RandomRule();
    }

}

The test results are as follows, which are indeed obtained randomly.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
host: 192.168.31.113, port: 2222, uri: http://192.168.31.113:2222
host: 192.168.31.113, port: 2222, uri: http://192.168.31.113:2222
host: 192.168.31.113, port: 2222, uri: http://192.168.31.113:2222
host: 192.168.31.113, port: 2224, uri: http://192.168.31.113:2224
host: 192.168.31.113, port: 2224, uri: http://192.168.31.113:2224
host: 192.168.31.113, port: 2222, uri: http://192.168.31.113:2222
host: 192.168.31.113, port: 2223, uri: http://192.168.31.113:2223
host: 192.168.31.113, port: 2222, uri: http://192.168.31.113:2222
host: 192.168.31.113, port: 2222, uri: http://192.168.31.113:2222
host: 192.168.31.113, port: 2223, uri: http://192.168.31.113:2223   

RoundRobinRule

1
2
3
4
@Bean
public IRule ribbonRule() {
    return new RoundRobinRule();
}

The test results are as follows, which do poll each server.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
host: 192.168.31.113, port: 2223, uri: http://192.168.31.113:2223
host: 192.168.31.113, port: 2222, uri: http://192.168.31.113:2222
host: 192.168.31.113, port: 2224, uri: http://192.168.31.113:2224
host: 192.168.31.113, port: 2223, uri: http://192.168.31.113:2223
host: 192.168.31.113, port: 2222, uri: http://192.168.31.113:2222
host: 192.168.31.113, port: 2224, uri: http://192.168.31.113:2224
host: 192.168.31.113, port: 2223, uri: http://192.168.31.113:2223
host: 192.168.31.113, port: 2222, uri: http://192.168.31.113:2222
host: 192.168.31.113, port: 2224, uri: http://192.168.31.113:2224
host: 192.168.31.113, port: 2223, uri: http://192.168.31.113:2223
host: 192.168.31.113, port: 2222, uri: http://192.168.31.113:2222
host: 192.168.31.113, port: 2224, uri: http://192.168.31.113:2224

BestAvailableRule

1
2
3
4
@Bean
public IRule ribbonRule() {
    return new BestAvailableRule();
}

If the browser is accessed directly, the test results are as follows (because the number of requests becomes 0 after each visit, and the next traversal will always be an instance of port 2223).

1
2
3
4
5
6
7
host: 192.168.31.113, port: 2223, uri: http://192.168.31.113:2223
host: 192.168.31.113, port: 2223, uri: http://192.168.31.113:2223
host: 192.168.31.113, port: 2223, uri: http://192.168.31.113:2223
host: 192.168.31.113, port: 2223, uri: http://192.168.31.113:2223
host: 192.168.31.113, port: 2223, uri: http://192.168.31.113:2223
host: 192.168.31.113, port: 2223, uri: http://192.168.31.113:2223
..

Simulating concurrent requests using wrk results in multiple instances of.

1
wrk -c 1000 -t 10 -d 10s http://localhost:3333/test

Reference https://fangjian0423.github.io/2017/01/29/loadbalance/