用 Java 代碼實現負載均衡的五種常見算法

在幾年前,負載均衡還是“高端玩家”的遊戲,我這種小白還難以觸及,現在“負載均衡”已經有點看似爛大街的趨勢了。

提起負載均衡,首先要理解負載均衡到底是想解決什麼樣的問題,維基百科有這麼一段描述:

主要作用是將大量作業合理地分攤到多個操作單元上進行執行,用於解決互聯網架構中的高併發高可用的問題。

在現在的互聯網系統中,爲了避免出現單點問題,最常見的做法就是將系統部署到多臺機器上,也就是集羣。但是這樣會出現兩個問題:

  • 如何讓機器均衡或者相對均衡的接收到到流量;
  • 當集羣的某個節點宕機,讓流量不會打到這個節點上;

負載均衡技術就是來解決這兩個問題。但這時候還要考慮兩個問題:

  • 避免負載均衡系統成爲單點;
  • 負載均衡系統是否需要關注服務器自身的狀態差異;

第一點比如可以基於 standby 模式搭建負載均衡系統集羣,現在也有基於 Gossip 實現去中心化的軟件負載解決方案。

關於第二點,首先當服務器宕機或者與負載均衡系統連接中斷的時候,負載均衡系統是必須要能夠感知到的。但是在負載均衡算法中是否需要加入服務器自身狀態的指標也是值得考慮的,如根據負載均衡系統和服務器之間的連接數、服務器的 CPU 負載、I/O 負載、服務器的響應時間等,但是這樣負載均衡系統需要接收服務器的指標狀態,而指標收集可能還會涉及到收集的時間節點,這會大大增加系統的複雜度。

真正的大型高併發系統負載均衡是非常複雜的,比如還需要考慮系統預熱的情況,要想深入瞭解還是需要多看分佈式相關的書籍和進行實踐。本文主要是通過代碼實現幾種簡單的算法,藉此體會負載均衡的思想。

先定義一個 Invoker,存儲一些基本的信息,一般來說這個類會抽象處理:

package dongguabai.demo;

import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

/**
 * @author Dongguabai
 * @Description
 * @Date 創建於 2020-06-24 09:16
 */
@NoArgsConstructor
@Getter
@Setter
@ToString
@EqualsAndHashCode(of = "address")
public class Invoker {

    private String address;

    /**
     * 權重
     */
    private int weight = 1;
    
    private int currentWeight = 0;

    public Invoker(String address) {
        this.address = address;
    }

    public Invoker(String address, int weight) {
        this.address = address;
        this.weight = weight;
    }
}

定義一個抽象類:

package dongguabai.demo;

        import com.google.common.collect.HashMultiset;
        import com.google.common.collect.Multiset;
        import org.springframework.util.CollectionUtils;

        import java.util.Comparator;
        import java.util.List;
        import java.util.Set;
        import java.util.stream.IntStream;

/**
 * @author Dongguabai
 * @Description 抽象類
 * @Date 創建於 2020-06-23 13:41
 */
public abstract class AbstractLoadBalance {

    protected List<Invoker> invokers;

    public AbstractLoadBalance(List<Invoker> invokers) {
        this.invokers = invokers;
    }

    protected final Multiset<Invoker> results = HashMultiset.create();

    public Invoker selectHost(){
        List<Invoker> list = getInvokers();
        if (CollectionUtils.isEmpty(list)) {
            return null;
        }
        if (list.size() == 1) {
            return list.get(0);
        }
        return doSelect();
    }

    public Invoker selectHost(Object client){
        List<Invoker> list = getInvokers();
        if (CollectionUtils.isEmpty(list)) {
            return null;
        }
        if (list.size() == 1) {
            return list.get(0);
        }
        return doSelect(client);
    }

    protected Invoker doSelect(){
        return null;
    }

    protected Invoker doSelect(Object client){
        return null;
    }

    //todo 優化refesh
    protected boolean addInvoker(Invoker invoker){
        if (getInvokers() != null){
            return invokers.add(invoker);
        }
        return false;
    }

    //todo 優化refesh
    protected boolean removeInvoker(Invoker invoker){
        if (getInvokers() != null){
            return invokers.remove(invoker);
        }
        return false;
    }

    //todo 不要返回集合
    protected List<Invoker> getInvokers(){
        return invokers;
    }

    public void result(int loop) {
        results.clear();
        if (loop < 1) {
            throw new IllegalArgumentException();
        }
        IntStream.range(0, loop).forEach(i -> results.add(selectHost()));
        Set<Multiset.Entry<Invoker>> entrySet = results.entrySet();
        entrySet.stream().sorted(Comparator.comparingInt(Multiset.Entry::getCount)).forEach(System.out::println);
    }

}

完全隨機

隨機算法是用得比較多,也是比較簡單的一種算法,屬於任務數量的絕對平分。在調用次數比較多的時候會相對平均分佈到每臺機器上。

package dongguabai.demo.random;

import dongguabai.demo.AbstractLoadBalance;
import dongguabai.demo.Invoker;

import java.util.List;
import java.util.Random;

/**
 * @author Dongguabai
 * @Description 隨機
 * @Date 創建於 2020-06-23 18:57
 */
public class SimpleRandomLoadBalance extends AbstractLoadBalance {

    public SimpleRandomLoadBalance(List<Invoker> invokers) {
        super(invokers);
    }

    @Override
    protected Invoker doSelect() {
        return getInvokers().get(new Random().nextInt(getInvokers().size()));
    }

    @Override
    public List<Invoker> getInvokers() {
        return invokers;
    }

}

測試:

    @Test
    public void testSimpleRandomLoadBalance(){
        List<Invoker> invokers = Lists.newArrayList(
                new Invoker("176.170.209.1"),
                new Invoker("176.170.209.2"),
                new Invoker("176.170.209.3"),
                new Invoker("176.170.209.4"),
                new Invoker("176.170.209.5"),
                new Invoker("176.170.209.6"),
                new Invoker("176.170.209.7"),
                new Invoker("176.170.209.8"),
                new Invoker("176.170.209.9"),
                new Invoker("176.170.209.10"));
        AbstractLoadBalance randomLoadBalance = new SimpleRandomLoadBalance(invokers);
        randomLoadBalance.result(20000);
    }

運行結果:

Invoker(address=176.170.209.8, weight=1) x 1933
Invoker(address=176.170.209.9, weight=1) x 1953
Invoker(address=176.170.209.10, weight=1) x 1953
Invoker(address=176.170.209.7, weight=1) x 1983
Invoker(address=176.170.209.3, weight=1) x 2001
Invoker(address=176.170.209.6, weight=1) x 2003
Invoker(address=176.170.209.2, weight=1) x 2017
Invoker(address=176.170.209.5, weight=1) x 2027
Invoker(address=176.170.209.1, weight=1) x 2058
Invoker(address=176.170.209.4, weight=1) x 2072

樣本數量越大,越平均。

加權隨機

實際線上可能機器的性能各不相同,爲了讓性能更好的機器分配更多的流量,可以爲每臺機器設置一個權重,按照一定的比例進行任務的均分。

權重一般是一個整型,最簡單的思路就是機器的權重是多少,那麼在機器的地址集合中就複製多少。

package dongguabai.demo.random;

import dongguabai.demo.Invoker;
import dongguabai.demo.random.SimpleRandomLoadBalance;
import org.springframework.util.CollectionUtils;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.IntStream;

/**
 * @author Dongguabai
 * @Description 加權隨機(複製)
 * @Date 創建於 2020-06-24 10:30
 */
public class CopyWeightRandomLoadBalance extends SimpleRandomLoadBalance {

    private List<Invoker> weightInvokers = new ArrayList<>();

    public CopyWeightRandomLoadBalance(List<Invoker> invokers) {
        super(invokers);
        if (!CollectionUtils.isEmpty(invokers)){
            invokers.forEach(invoker -> {
                if (invoker.getWeight() < 2) {
                    weightInvokers.add(invoker);
                    return;
                }
                IntStream.range(0,invoker.getWeight()).forEach(i->weightInvokers.add(invoker));
            });
        }
    }

    @Override
    public final List<Invoker> getInvokers() {
        return weightInvokers;
    }
}

測試:

    @Test
    public void testCopyWeightRandomLoadBalance(){
        List<Invoker> invokers = Lists.newArrayList(
                new Invoker("176.170.209.1",1),
                new Invoker("176.170.209.2",9),
                new Invoker("176.170.209.3",1),
                new Invoker("176.170.209.4",9),
                new Invoker("176.170.209.5",4),
                new Invoker("176.170.209.6",6),
                new Invoker("176.170.209.7",1),
                new Invoker("176.170.209.8",9),
                new Invoker("176.170.209.9",1),
                new Invoker("176.170.209.10",9));
        AbstractLoadBalance simpleWeightRandomLoadBalance1 = new CopyWeightRandomLoadBalance(invokers);
        simpleWeightRandomLoadBalance1.result(20000);
    }

運行結果:

Invoker(address=176.170.209.1, weight=1) x 376
Invoker(address=176.170.209.3, weight=1) x 386
Invoker(address=176.170.209.7, weight=1) x 397
Invoker(address=176.170.209.9, weight=1) x 398
Invoker(address=176.170.209.5, weight=4) x 1559
Invoker(address=176.170.209.6, weight=6) x 2380
Invoker(address=176.170.209.8, weight=9) x 3567
Invoker(address=176.170.209.10, weight=9) x 3622
Invoker(address=176.170.209.4, weight=9) x 3632
Invoker(address=176.170.209.2, weight=9) x 3683

可以發現是按照權重比例進行分配。

但是這種算法需要對調用信息進行復制,當數量較大時對內存的消耗相對是比較大的,所以一般會使用另外一種方式實現。

還是以這個權重爲例:

 List<Invoker> invokers = Lists.newArrayList(
                new Invoker("176.170.209.1",1),
                new Invoker("176.170.209.2",9),
                new Invoker("176.170.209.3",1),
                new Invoker("176.170.209.4",9),
                new Invoker("176.170.209.5",4),
                new Invoker("176.170.209.6",6),
                new Invoker("176.170.209.7",1),
                new Invoker("176.170.209.8",9),
                new Invoker("176.170.209.9",1),
                new Invoker("176.170.209.10",9));

首先計算出總的權重,我這裏是 50,生成 [0,50) 的隨機數 n;根據這個權重爲每一個 Invoker 分配一個區間:

01101120243031404150

這個區間包左不包右,比如生成的隨機數 0<= n <1,那麼就是“176.170.209.1”這個機器,以此類推。

具體實現如下:

package dongguabai.demo.random;

import dongguabai.demo.Invoker;

import java.util.List;
import java.util.Random;

/**
 * @author Dongguabai
 * @Description
 * @Date 創建於 2020-06-24 17:10
 */
public class SectionWeightRandomLoadBalance extends SimpleRandomLoadBalance {

    private boolean averageWeight = true;

    private int totalWeight;

    public SectionWeightRandomLoadBalance(List<Invoker> invokers) {
        super(invokers);
        for (int i = 0; i < invokers.size(); i++) {
            Invoker invoker = invokers.get(i);
            if (averageWeight && i > 0 && invoker.getWeight() != invokers.get(i - 1).getWeight()) {
                averageWeight = false;
            }
            totalWeight += invoker.getWeight();
        
    }

    @Override
    protected Invoker doSelect() {
        if (averageWeight || totalWeight < 1) {
            return super.doSelect();
        }
        int index = new Random().nextInt(totalWeight);
        for (Invoker invoker : invokers) {
            if (index < invoker.getWeight()) {
                return invoker;
            }
            index -= invoker.getWeight();
        }
        return super.doSelect();
    }
}

測試:

    @Test
    public void testWeightRandomLoadBalance(){
        List<Invoker> invokers = Lists.newArrayList(
                new Invoker("176.170.209.1",1),
                new Invoker("176.170.209.2",9),
                new Invoker("176.170.209.3",1),
                new Invoker("176.170.209.4",9),
                new Invoker("176.170.209.5",4),
                new Invoker("176.170.209.6",6),
                new Invoker("176.170.209.7",1),
                new Invoker("176.170.209.8",9),
                new Invoker("176.170.209.9",1),
                new Invoker("176.170.209.10",9));
        AbstractLoadBalance simpleWeightRandomLoadBalance1 = new SectionWeightRandomLoadBalance(invokers);
        simpleWeightRandomLoadBalance1.result(20000);
    }

運行結果:

Invoker(address=176.170.209.3, weight=1) x 386
Invoker(address=176.170.209.9, weight=1) x 405
Invoker(address=176.170.209.7, weight=1) x 411
Invoker(address=176.170.209.1, weight=1) x 430
Invoker(address=176.170.209.5, weight=4) x 1598
Invoker(address=176.170.209.6, weight=6) x 2450
Invoker(address=176.170.209.4, weight=9) x 3543
Invoker(address=176.170.209.10, weight=9) x 3557
Invoker(address=176.170.209.2, weight=9) x 3573
Invoker(address=176.170.209.8, weight=9) x 3647

輪詢

輪詢算法也很好理解,就是輪流執行每臺服務器,所以有一個請求序號的概念。

package dongguabai.demo.roundrobin;

import dongguabai.demo.AbstractLoadBalance;
import dongguabai.demo.Invoker;

import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author Dongguabai
 * @Description
 * @Date 創建於 2020-06-25 13:48
 */
public class SimpleRoundRobinLoadBalance extends AbstractLoadBalance {

    protected AtomicInteger offset = new AtomicInteger(-1);

    public SimpleRoundRobinLoadBalance(List<Invoker> invokers) {
        super(invokers);
    }

    @Override
    protected Invoker doSelect() {
        return getInvokers().get(offset.addAndGet(1) % getInvokers().size());
    }

}

測試:

    @Test
    public void testSimpleRoundRobinLoadBalance(){
        List<Invoker> invokers = Lists.newArrayList(
                new Invoker("176.170.209.1"),
                new Invoker("176.170.209.2"),
                new Invoker("176.170.209.3"),
                new Invoker("176.170.209.4"),
                new Invoker("176.170.209.5"),
                new Invoker("176.170.209.6"),
                new Invoker("176.170.209.7"),
                new Invoker("176.170.209.8"),
                new Invoker("176.170.209.9"),
                new Invoker("176.170.209.10"));
        AbstractLoadBalance roundRobinLoadBalance = new SimpleRoundRobinLoadBalance(invokers);
        IntStream.range(0,22).parallel().forEach(i-> System.out.println(roundRobinLoadBalance.selectHost()));
    }

執行結果:

Invoker(address=176.170.209.7, weight=1)
Invoker(address=176.170.209.5, weight=1)
Invoker(address=176.170.209.9, weight=1)
Invoker(address=176.170.209.2, weight=1)
Invoker(address=176.170.209.6, weight=1)
Invoker(address=176.170.209.8, weight=1)
Invoker(address=176.170.209.3, weight=1)
Invoker(address=176.170.209.4, weight=1)
Invoker(address=176.170.209.1, weight=1)
Invoker(address=176.170.209.4, weight=1)
Invoker(address=176.170.209.8, weight=1)
Invoker(address=176.170.209.9, weight=1)
Invoker(address=176.170.209.10, weight=1)
Invoker(address=176.170.209.7, weight=1)
Invoker(address=176.170.209.2, weight=1)
Invoker(address=176.170.209.6, weight=1)
Invoker(address=176.170.209.5, weight=1)
Invoker(address=176.170.209.3, weight=1)
Invoker(address=176.170.209.2, weight=1)
Invoker(address=176.170.209.1, weight=1)
Invoker(address=176.170.209.10, weight=1)
Invoker(address=176.170.209.1, weight=1)

加權輪詢

有一個方式是根據權重複制,同上文介紹的加權隨機的第一種方式。

package dongguabai.demo.roundrobin;

import dongguabai.demo.Invoker;
import org.springframework.util.CollectionUtils;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.IntStream;

/**
 * @author Dongguabai
 * @Description
 * @Date 創建於 2020-06-25 17:33
 */
public class CopyWeightRoundRobinLoadBalance extends SimpleRoundRobinLoadBalance{

    private List<Invoker> weightInvokers = new ArrayList<>();

    public CopyWeightRoundRobinLoadBalance(List<Invoker> invokers) {
        super(invokers);
        if (!CollectionUtils.isEmpty(invokers)){
            invokers.forEach(invoker -> {
                if (invoker.getWeight() < 2) {
                    weightInvokers.add(invoker);
                    return;
                }
                IntStream.range(0,invoker.getWeight()).forEach(i->weightInvokers.add(invoker));
            });
        }
    }

    @Override
    public final List<Invoker> getInvokers() {
        return weightInvokers;
    }
}

測試:

    @Test
    public void testCopyWeightRoundRobinLoadBalance(){
        List<Invoker> invokers = Lists.newArrayList(
                new Invoker("176.170.209.1",1),
                new Invoker("176.170.209.2",9),
                new Invoker("176.170.209.3",1),
                new Invoker("176.170.209.4",9),
                new Invoker("176.170.209.5",4),
                new Invoker("176.170.209.6",6),
                new Invoker("176.170.209.7",1),
                new Invoker("176.170.209.8",9),
                new Invoker("176.170.209.9",1),
                new Invoker("176.170.209.10",9));
        AbstractLoadBalance copyWeightRoundRobinLoadBalance = new CopyWeightRoundRobinLoadBalance(invokers);
        IntStream.range(0,50).forEach(i-> System.out.println(copyWeightRoundRobinLoadBalance.selectHost()));
       // copyWeightRoundRobinLoadBalance.result(20000);
    }

輸出結果:

Invoker(address=176.170.209.1, weight=1)
Invoker(address=176.170.209.2, weight=9)
Invoker(address=176.170.209.2, weight=9)
Invoker(address=176.170.209.2, weight=9)
Invoker(address=176.170.209.2, weight=9)
Invoker(address=176.170.209.2, weight=9)
Invoker(address=176.170.209.2, weight=9)
Invoker(address=176.170.209.2, weight=9)
Invoker(address=176.170.209.2, weight=9)
Invoker(address=176.170.209.2, weight=9)
Invoker(address=176.170.209.3, weight=1)
Invoker(address=176.170.209.4, weight=9)
Invoker(address=176.170.209.4, weight=9)
Invoker(address=176.170.209.4, weight=9)
Invoker(address=176.170.209.4, weight=9)
Invoker(address=176.170.209.4, weight=9)
Invoker(address=176.170.209.4, weight=9)
Invoker(address=176.170.209.4, weight=9)
Invoker(address=176.170.209.4, weight=9)
Invoker(address=176.170.209.4, weight=9)
Invoker(address=176.170.209.5, weight=4)
Invoker(address=176.170.209.5, weight=4)
Invoker(address=176.170.209.5, weight=4)
Invoker(address=176.170.209.5, weight=4)
Invoker(address=176.170.209.6, weight=6)
Invoker(address=176.170.209.6, weight=6)
Invoker(address=176.170.209.6, weight=6)
Invoker(address=176.170.209.6, weight=6)
Invoker(address=176.170.209.6, weight=6)
Invoker(address=176.170.209.6, weight=6)
Invoker(address=176.170.209.7, weight=1)
Invoker(address=176.170.209.8, weight=9)
Invoker(address=176.170.209.8, weight=9)
Invoker(address=176.170.209.8, weight=9)
Invoker(address=176.170.209.8, weight=9)
Invoker(address=176.170.209.8, weight=9)
Invoker(address=176.170.209.8, weight=9)
Invoker(address=176.170.209.8, weight=9)
Invoker(address=176.170.209.8, weight=9)
Invoker(address=176.170.209.8, weight=9)
Invoker(address=176.170.209.9, weight=1)
Invoker(address=176.170.209.10, weight=9)
Invoker(address=176.170.209.10, weight=9)
Invoker(address=176.170.209.10, weight=9)
Invoker(address=176.170.209.10, weight=9)
Invoker(address=176.170.209.10, weight=9)
Invoker(address=176.170.209.10, weight=9)
Invoker(address=176.170.209.10, weight=9)
Invoker(address=176.170.209.10, weight=9)
Invoker(address=176.170.209.10, weight=9)

上面是通過複製進行加權,同隨機加權一樣,還有一種通過權重區間進行輪詢。

package dongguabai.demo.roundrobin;

import dongguabai.demo.Invoker;

import java.util.List;

/**
 * @author Dongguabai
 * @Description
 * @Date 創建於 2020-06-27 14:22
 */
public class SectionWeightRoundRobinLoadBalance extends SimpleRoundRobinLoadBalance {

    private boolean averageWeight = true;

    private int totalWeight;

    public SectionWeightRoundRobinLoadBalance(List<Invoker> invokers) {
        super(invokers);
        for (int i = 0; i < invokers.size(); i++) {
            Invoker invoker = invokers.get(i);
            if (averageWeight && i > 0 && invoker.getWeight() != invokers.get(i - 1).getWeight()) {
                averageWeight = false;
            }
            totalWeight += invoker.getWeight();
        }
    }

    @Override
    protected Invoker doSelect() {
        if (averageWeight || totalWeight < 1) {
            return super.doSelect();
        }
        int index = offset.addAndGet(1) % totalWeight;
        for (Invoker invoker : invokers) {
            if (index < invoker.getWeight()) {
                return invoker;
            }
            index -= invoker.getWeight();
        }
        return super.doSelect();
    }
}

測試:

    @Test
    public void testSectionWeightRoundRobinLoadBalance(){
        List<Invoker> invokers = Lists.newArrayList(
                new Invoker("176.170.209.1",1),
                new Invoker("176.170.209.2",9),
                new Invoker("176.170.209.3",1),
                new Invoker("176.170.209.4",9),
                new Invoker("176.170.209.5",4),
                new Invoker("176.170.209.6",6),
                new Invoker("176.170.209.7",1),
                new Invoker("176.170.209.8",9),
                new Invoker("176.170.209.9",1),
                new Invoker("176.170.209.10",9));
        AbstractLoadBalance sectionWeightRoundRobinLoadBalance = new SectionWeightRoundRobinLoadBalance(invokers);
        IntStream.range(0,50).forEach(i-> System.out.println(sectionWeightRoundRobinLoadBalance.selectHost()));
    }

運行結果:

Invoker(address=176.170.209.1, weight=1)
Invoker(address=176.170.209.2, weight=9)
Invoker(address=176.170.209.2, weight=9)
Invoker(address=176.170.209.2, weight=9)
Invoker(address=176.170.209.2, weight=9)
Invoker(address=176.170.209.2, weight=9)
Invoker(address=176.170.209.2, weight=9)
Invoker(address=176.170.209.2, weight=9)
Invoker(address=176.170.209.2, weight=9)
Invoker(address=176.170.209.2, weight=9)
Invoker(address=176.170.209.3, weight=1)
Invoker(address=176.170.209.4, weight=9)
Invoker(address=176.170.209.4, weight=9)
Invoker(address=176.170.209.4, weight=9)
Invoker(address=176.170.209.4, weight=9)
Invoker(address=176.170.209.4, weight=9)
Invoker(address=176.170.209.4, weight=9)
Invoker(address=176.170.209.4, weight=9)
Invoker(address=176.170.209.4, weight=9)
Invoker(address=176.170.209.4, weight=9)
Invoker(address=176.170.209.5, weight=4)
Invoker(address=176.170.209.5, weight=4)
Invoker(address=176.170.209.5, weight=4)
Invoker(address=176.170.209.5, weight=4)
Invoker(address=176.170.209.6, weight=6)
Invoker(address=176.170.209.6, weight=6)
Invoker(address=176.170.209.6, weight=6)
Invoker(address=176.170.209.6, weight=6)
Invoker(address=176.170.209.6, weight=6)
Invoker(address=176.170.209.6, weight=6)
Invoker(address=176.170.209.7, weight=1)
Invoker(address=176.170.209.8, weight=9)
Invoker(address=176.170.209.8, weight=9)
Invoker(address=176.170.209.8, weight=9)
Invoker(address=176.170.209.8, weight=9)
Invoker(address=176.170.209.8, weight=9)
Invoker(address=176.170.209.8, weight=9)
Invoker(address=176.170.209.8, weight=9)
Invoker(address=176.170.209.8, weight=9)
Invoker(address=176.170.209.8, weight=9)
Invoker(address=176.170.209.9, weight=1)
Invoker(address=176.170.209.10, weight=9)
Invoker(address=176.170.209.10, weight=9)
Invoker(address=176.170.209.10, weight=9)
Invoker(address=176.170.209.10, weight=9)
Invoker(address=176.170.209.10, weight=9)
Invoker(address=176.170.209.10, weight=9)
Invoker(address=176.170.209.10, weight=9)
Invoker(address=176.170.209.10, weight=9)
Invoker(address=176.170.209.10, weight=9)

平滑加權輪詢

上面兩種輪詢的實現有一個很大的問題就是會造成某臺權重較高的機器在短時間內負載較高,其實輪詢我們希望的是,比如 A(1),B(2),C(3) 三臺機器我們希望的是每 6 次調用,有 1 次是 A,2 次是 B,3 次是 C 即可。當然也可以進行改動,比如複製法可以將複製後的機器列表打亂。

在 Nginx 中有一種平滑加權的輪詢算法。在上面的其他權重算法中,每臺機器會有一個權重 weight 屬性,這個屬性是配置好之後就不變的,在平滑加權輪詢中每臺機器還有一個當前權重的屬性 currentWeight,這個屬性是會變化的。比如 A(1),B(2),C(3) 三臺機器,它們的初始化 currentWeight 都是 0,發生調用的時候,就會每臺機器的 currentWeight += weight,然後選擇所有機器中最大的 currentWeight 所在的機器(如果出現相同,則按照順序選擇第一個),隨後這臺機器的 currentWeight 減去總 weight 權重之和 totalWeight。

調用序號 選擇前當前權重 Max(currentWeight) 選擇機器 選擇後當前權重
1 1,2,3 3 C 1,2,-3
2 2,4,0 4 B 2,-2,0
3 3,0,3 3 A -3,0,3
4 -2,2,6 6 C -2,2,0
5 -1,4,3 4 B -1,-2,3
6 0,0,6 6 C 0,0,0

可以看到經過 6 次調用,每臺機器被調用的次數與權重的佔比是一致的。

這個算法中 currentWeight 總和是不變的,因爲被選擇的機器的 currentWeight 減去 totalWeight 後每臺機器的 currentWeight 又會分別加上自身的 weight,即 weight 越大的機器被選擇的次數就會越多。這個算法的數學證明可以參見:https://www.fanhaobai.com/2018/12/load-balance-smooth-weighted-round-robin.html

代碼實現:

package dongguabai.demo.roundrobin;

import dongguabai.demo.Invoker;

import java.util.List;

/**
 * @author Dongguabai
 * @Description 平滑加權輪詢
 * @Date 創建於 2020-06-27 16:35
 */
public class SmoothWeightRoundRobinLoadBalance extends SimpleRoundRobinLoadBalance {

    private boolean averageWeight = true;

    private int totalWeight;

    private final Object lock = new Object();

    public SmoothWeightRoundRobinLoadBalance(List<Invoker> invokers) {
        super(invokers);
        for (int i = 0; i < invokers.size(); i++) {
            Invoker invoker = invokers.get(i);
            if (averageWeight && i > 0 && invoker.getWeight() != invokers.get(i - 1).getWeight()) {
                averageWeight = false;
            }
            totalWeight += invoker.getWeight();
        }
    }

    @Override
    protected Invoker doSelect() {
        if (averageWeight || totalWeight < 1) {
            return super.doSelect();
        }
        synchronized (lock) {
            Invoker doInvoker = selectInvoker(invokers);
            before(doInvoker);
            return doInvoker;
        }
    }

    private Invoker selectInvoker(List<Invoker> invokers) {
        Invoker maxCurrentInvoker = invokers.get(0);
        for (Invoker invoker : invokers) {
            invoker.setCurrentWeight(invoker.getWeight() + invoker.getCurrentWeight());
            if (maxCurrentInvoker.getCurrentWeight() < invoker.getCurrentWeight()) {
                maxCurrentInvoker = invoker;
            }
        }
        return maxCurrentInvoker;
    }

    private void before(Invoker doInvoker) {
        doInvoker.setCurrentWeight(doInvoker.getCurrentWeight() - totalWeight);
    }
}

測試:

    @Test
    public void testSmoothWeightRoundRobinLoadBalance(){
        List<Invoker> invokers = Lists.newArrayList(
                new Invoker("A",1),
                new Invoker("B",2),
                new Invoker("C",3));
        AbstractLoadBalance smoothWeightRoundRobinLoadBalance = new SmoothWeightRoundRobinLoadBalance(invokers);
        IntStream.range(0,6).forEach(i-> System.out.println(smoothWeightRoundRobinLoadBalance.selectHost()));
    }

運行結果:

Invoker(address=C, weight=3, currentWeight=-3)
Invoker(address=B, weight=2, currentWeight=-2)
Invoker(address=A, weight=1, currentWeight=-3)
Invoker(address=C, weight=3, currentWeight=0)
Invoker(address=B, weight=2, currentWeight=-2)
Invoker(address=C, weight=3, currentWeight=0)

哈希

哈希算法在分佈式中的應用場景是很多的,如分佈式的數據存儲、負載均衡等,如 Redis Cluster 中的 Hash Slot 算法。在負載均衡中就是根據一些請求信息進行哈希運算,將相同的哈希值分配到同一臺機器上。如源地址 Hash、SessionId Hash,用戶信息 Hash 等,也能順帶解決了 分佈式 Session 的問題。

一個最簡單的想法是直接用哈希來計算, 對機器數取模。在請求信息足夠分散的情況下,是可以滿足均衡性的,但一旦有機器加入或者宕機時,所有的原有機器都會受到影響。無法保證穩定性。

這時候就有了一致性哈希算法。一致性哈希算法會將哈希值空間組織成一個虛擬的圓環,即哈希環,普通哈希算法僅僅只針對請求信息進行哈希運算,但是一致性哈希會對服務器也進行哈希運算,從而將兩者的哈希值映射在一個圓環上,即哈希環:

在這裏插入圖片描述

當請求哈希後落在 Node1 和 Node2 之間,就會根據順時針選擇到 Node2 機器上。這樣保證了穩定性,機器的增加或者退出不會影響所有的機器,但是還是會存在一個問題,假如 Node3 機器宕機,那麼 Node1 到 Node4 的請求都會落在 Node1 機器上,這樣 Node1 機器的壓力就會變大。

於是又有了虛擬節點,即對每一個服務節點計算多個哈希,每個計算結果位置都放置一個此服務節點,通過這種方式將機器節點分散開來,通過虛擬節點均衡各個機器的請求量。

在這裏插入圖片描述

在網上找到了一個哈希的算法實現:

package dongguabai.demo.hash;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;

/**
 * @author Dongguabai
 * @Description
 * @Date 創建於 2020-06-27 21:45
 */
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class HashUtils {

    /**
     * 引用:https://www.jianshu.com/p/4660a8a1f132
     * 計算Hash值, 使用FNV1_32_HASH算法
     *
     * @param str
     * @return
     */
    public static int getHash(String str) {
        final int p = 16777619;
        int hash = (int) 2166136261L;
        for (int i = 0; i < str.length(); i++) {
            hash = (hash ^ str.charAt(i)) * p;
        }
        hash += hash << 13;
        hash ^= hash >> 7;
        hash += hash << 3;
        hash ^= hash >> 17;
        hash += hash << 5;

        if (hash < 0) {
            hash = Math.abs(hash);
        }
        return hash;
    }
}

一致性哈希算法實現,這裏是不關心權重的:

package dongguabai.demo.hash;

import com.google.common.collect.Multiset;
import dongguabai.demo.AbstractLoadBalance;
import dongguabai.demo.Invoker;

import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.stream.IntStream;

/**
 * @author Dongguabai
 * @Description
 * @Date 創建於 2020-06-27 21:38
 */
public class ConsistentHashLoadBalance extends AbstractLoadBalance {

    /**
     * 虛擬節點數量
     */
    private static final int VIRTUAL_NODE_NUM = 1000;

    /**
     * hash 和 虛擬節點映射關係
     */
    private SortedMap<Integer, Invoker> virtualNodesMap = new TreeMap<>();

    public ConsistentHashLoadBalance(List<Invoker> invokers) {
        super(invokers);
        putVirtualNodes(invokers);
    }


    /**
     * 添加虛擬節點映射(環)
     * @param invokers
     */
    private void putVirtualNodes(List<Invoker> invokers) {
        for (Invoker invoker : invokers) {
            for (int i = 0; i < VIRTUAL_NODE_NUM; i++) {
                int hash = HashUtils.getHash(invoker.getAddress() + "-" + i);
                virtualNodesMap.put(hash,invoker);
            }
        }
    }

    /**
     * 根據客戶端的信息獲取 Invoker
     * @param client
     * @return
     */
    public Invoker getInvoker(Object client){
        if (client == null){
            return null;
        }
        //獲取大於等於 hash 的第一個 Node
        SortedMap<Integer, Invoker> subMap = virtualNodesMap.tailMap(HashUtils.getHash(client.toString()));
        if (subMap.isEmpty()) {
            // hash值在最尾部,映射到第一個Node上
            return virtualNodesMap.get(virtualNodesMap.firstKey());
        }
        return subMap.get(subMap.firstKey());
    }


    @Override
    public void result(int loop) {
        results.clear();
        if (loop < 1) {
            throw new IllegalArgumentException();
        }
        IntStream.range(0, loop).forEach(i -> results.add(selectHost("test-client-"+i)));
        Set<Multiset.Entry<Invoker>> entrySet = results.entrySet();
        entrySet.stream().sorted(Comparator.comparingInt(Multiset.Entry::getCount)).forEach(System.out::println);
    }

    @Override
    protected boolean addInvoker(Invoker invoker) {
        if (super.addInvoker(invoker)){
            List<Invoker> invokers = getInvokers();
            virtualNodesMap.clear();
            putVirtualNodes(invokers);
            return true;
        }
        return false;
    }

    @Override
    protected boolean removeInvoker(Invoker invoker) {
        if (super.removeInvoker(invoker)){
            List<Invoker> invokers = getInvokers();
            virtualNodesMap.clear();
            putVirtualNodes(invokers);
            return true;
        }
        return false;
    }

    @Override
    protected Invoker doSelect(Object client) {
        return getInvoker(client);
    }
}

測試:

    @Test
    public void testConsistentHashLoadBalance(){
        List<Invoker> invokers = Lists.newArrayList(
                new Invoker("176.170.209.1",1),
                new Invoker("176.170.209.2",9),
                new Invoker("176.170.209.3",1),
                new Invoker("176.170.209.4",9),
                new Invoker("176.170.209.5",4),
                new Invoker("176.170.209.6",6),
                new Invoker("176.170.209.7",1),
                new Invoker("176.170.209.8",9),
                new Invoker("176.170.209.9",1),
                new Invoker("176.1709.10",9));
        AbstractLoadBalance consistentHashLoadBalance = new ConsistentHashLoadBalance(invokers);
        consistentHashLoadBalance.result(1000000);
        Invoker changeInvoker = new Invoker("176.1709.11", 1);
        System.out.println("add......");
        consistentHashLoadBalance.addInvoker(changeInvoker);
        consistentHashLoadBalance.result(1000000);
        System.out.println("remove.....");
        consistentHashLoadBalance.removeInvoker(changeInvoker);
        consistentHashLoadBalance.result(1000000);
    }

運行結果:

Invoker(address=176.170.209.8, weight=9, currentWeight=0) x 93728
Invoker(address=176.1709.10, weight=9, currentWeight=0) x 95566
Invoker(address=176.170.209.1, weight=1, currentWeight=0) x 98669
Invoker(address=176.170.209.5, weight=4, currentWeight=0) x 99376
Invoker(address=176.170.209.4, weight=9, currentWeight=0) x 99596
Invoker(address=176.170.209.6, weight=6, currentWeight=0) x 101373
Invoker(address=176.170.209.7, weight=1, currentWeight=0) x 101706
Invoker(address=176.170.209.3, weight=1, currentWeight=0) x 102884
Invoker(address=176.170.209.9, weight=1, currentWeight=0) x 103137
Invoker(address=176.170.209.2, weight=9, currentWeight=0) x 103965
add......
Invoker(address=176.1709.10, weight=9, currentWeight=0) x 85667
Invoker(address=176.170.209.8, weight=9, currentWeight=0) x 86560
Invoker(address=176.170.209.1, weight=1, currentWeight=0) x 90289
Invoker(address=176.170.209.7, weight=1, currentWeight=0) x 90905
Invoker(address=176.170.209.4, weight=9, currentWeight=0) x 90932
Invoker(address=176.1709.11, weight=1, currentWeight=0) x 91236
Invoker(address=176.170.209.5, weight=4, currentWeight=0) x 92081
Invoker(address=176.170.209.9, weight=1, currentWeight=0) x 92808
Invoker(address=176.170.209.2, weight=9, currentWeight=0) x 92847
Invoker(address=176.170.209.6, weight=6, currentWeight=0) x 93085
Invoker(address=176.170.209.3, weight=1, currentWeight=0) x 93590
remove.....
Invoker(address=176.170.209.8, weight=9, currentWeight=0) x 93728
Invoker(address=176.1709.10, weight=9, currentWeight=0) x 95566
Invoker(address=176.170.209.1, weight=1, currentWeight=0) x 98669
Invoker(address=176.170.209.5, weight=4, currentWeight=0) x 99376
Invoker(address=176.170.209.4, weight=9, currentWeight=0) x 99596
Invoker(address=176.170.209.6, weight=6, currentWeight=0) x 101373
Invoker(address=176.170.209.7, weight=1, currentWeight=0) x 101706
Invoker(address=176.170.209.3, weight=1, currentWeight=0) x 102884
Invoker(address=176.170.209.9, weight=1, currentWeight=0) x 103137
Invoker(address=176.170.209.2, weight=9, currentWeight=0) x 103965

可以看到增加或者減少節點也是可以保證負載均衡的。

負載最低優先

前面幾種算法主要是站在負載均衡服務的角度,保證每個機器節點獲得的調用次數均衡或者相對均衡,但是實際生產環境調用次數相同並不一定真的能夠讓每臺機器真的實現均衡,於是就有了負載最低優先的策略,負載最低優先可以使用如最小連接數、CPU/IO 負載,這裏引入《從零開始學架構》中的介紹:

負載最低優先算法基本上能夠比較完美地解決輪詢算法的缺點,因爲採用這種算法後,負載均衡系統需要感知服務器當前的運行狀態。當然,其代價是複雜度大幅上升。通俗來講,輪詢可能是 5 行代碼就能實現的算法,而負載最低優先算法可能要 1000 行才能實現,甚至需要負載均衡系統和服務器都要開發代碼。負載最低優先算法如果本身沒有設計好,或者不適合業務的運行特點,算法本身就可能成爲性能的瓶頸,或者引發很多莫名其妙的問題。所以負載最低優先算法雖然效果看起來很美好,但實際上真正應用的場景反而沒有輪詢(包括加權輪詢)那麼多。

總結

本文主要簡單的用 Java 代碼實現了幾種常見的負載均衡策略及其代碼實現,但是有一個值得思考的問題是,要儘可能的將系統關鍵的節點進行簡化,如機器權重,在容器化技術流行的今天,是否可以保證每臺機器的性能一致,從而避免因爲權重問題增加負載均衡的複雜度;同時負載均衡策略的選擇可以具體根據業務場景進行不同的選擇,在一些複雜的業務處理場景,如訂單業務中根據訂單號進行哈希從而將相同的訂單交由相同的機器進行處理,避免出現多臺機器間數據的不同步,也能夠保證業務的處理順序。

References

歡迎關注公衆號
​​​​​​在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章