SpringCloud 組件:Ribbon 的負載均衡策略及原理

Load Balance 負載均衡是用於解決一臺機器 (一個進程) 無法解決所有請求而產生的一種算法。像 nginx 可以使用負載均衡分配流量,ribbon 爲客戶端提供負載均衡,dubbo 服務調用裏的負載均衡等等,很多地方都使用到了負載均衡。

使用負載均衡帶來的好處很明顯:

負載均衡有好幾種實現策略,常見的有:

ILoadBalance 負載均衡器

ribbon 是一個爲客戶端提供負載均衡功能的服務,它內部提供了一個叫做 ILoadBalance 的接口代表負載均衡器的操作,比如有添加服務器操作、選擇服務器操作、獲取所有的服務器列表、獲取可用的服務器列表等等。

ILoadBalance 的繼承關係如下:

負載均衡器是從 EurekaClient(EurekaClient 的實現類爲 DiscoveryClient)獲取服務信息,根據 IRule 去路由,並且根據 IPing 判斷服務的可用性。

負載均衡器多久一次去獲取一次從 Eureka Client 獲取註冊信息呢?在 BaseLoadBalancer 類下,BaseLoadBalancer 的構造函數,該構造函數開啓了一個 PingTask 任務 setupPingTask();,代碼如下:

 1    public BaseLoadBalancer(String name, IRule rule, LoadBalancerStats stats,
 2            IPing ping, IPingStrategy pingStrategy) {
 3        if (logger.isDebugEnabled()) {
 4            logger.debug("LoadBalancer:  initialized");
 5        }
 6        this.name = name;
 7        this.ping = ping;
 8        this.pingStrategy = pingStrategy;
 9        setRule(rule);
10        setupPingTask();
11        lbStats = stats;
12        init();
13    }
14
15

setupPingTask() 的具體代碼邏輯,它開啓了 ShutdownEnabledTimer 執行 PingTask 任務,在默認情況下 pingIntervalSeconds 爲 10,即每 10 秒鐘,向 EurekaClient 發送一次”ping”。推薦:Java 面試練題寶典

 1void setupPingTask() {
 2        if (canSkipPing()) {
 3            return;
 4        }
 5        if (lbTimer != null) {
 6            lbTimer.cancel();
 7        }
 8        lbTimer = new ShutdownEnabledTimer("NFLoadBalancer-PingTimer-" + name,
 9                true);
10        lbTimer.schedule(new PingTask(), 0, pingIntervalSeconds * 1000);
11        forceQuickPing();
12    }
13
14

PingTask 源碼,即 new 一個 Pinger 對象,並執行 runPinger() 方法。

查看 Pinger 的 runPinger() 方法,最終根據 pingerStrategy.pingServers(ping, allServers) 來獲取服務的可用性,如果該返回結果,如之前相同,則不去向 EurekaClient 獲取註冊列表,如果不同則通知 ServerStatusChangeListener 或者 changeListeners 發生了改變,進行更新或者重新拉取。

完整過程是:

LoadBalancerClient(RibbonLoadBalancerClient 是實現類)在初始化的時候(execute 方法),會通過 ILoadBalance(BaseLoadBalancer 是實現類)向 Eureka 註冊中心獲取服務註冊列表,並且每 10s 一次向 EurekaClient 發送 “ping”,來判斷服務的可用性,如果服務的可用性發生了改變或者服務數量和之前的不一致,則從註冊中心更新或者重新拉取。LoadBalancerClient 有了這些服務註冊列表,就可以根據具體的 IRule 來進行負載均衡。

IRule 路由

IRule 接口代表負載均衡策略:

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

IRule 接口的實現類有以下幾種:

其中 RandomRule 表示隨機策略、RoundRobinRule 表示輪詢策略、WeightedResponseTimeRule 表示加權策略、BestAvailableRule 表示請求數最少策略等等。推薦:Java 面試練題寶典

隨機策略很簡單,就是從服務器中隨機選擇一個服務器,RandomRule 的實現代碼如下:

 1public Server choose(ILoadBalancer lb, Object key) {
 2    if (lb == null) {
 3        return null;
 4    }
 5    Server server = null;
 6 
 7    while (server == null) {
 8        if (Thread.interrupted()) {
 9            return null;
10        }
11        List<Server> upList = lb.getReachableServers();
12        List<Server> allList = lb.getAllServers();
13        int serverCount = allList.size();
14        if (serverCount == 0) {
15            return null;
16        }
17        int index = rand.nextInt(serverCount); // 使用jdk內部的Random類隨機獲取索引值index
18        server = upList.get(index); // 得到服務器實例
19 
20        if (server == null) {
21            Thread.yield();
22            continue;
23        }
24 
25        if (server.isAlive()) {
26            return (server);
27        }
28 
29        server = null;
30        Thread.yield();
31    }
32    return server;
33}
34
35

RoundRobinRule 輪詢策略表示每次都取下一個服務器,比如一共有 5 臺服務器,第 1 次取第 1 臺,第 2 次取第 2 臺,第 3 次取第 3 臺,以此類推:

 1    public Server choose(ILoadBalancer lb, Object key) {
 2        if (lb == null) {
 3            log.warn("no load balancer");
 4            return null;
 5        }
 6 
 7        Server server = null;
 8        int count = 0;
 9        while (server == null && count++ < 10) {
10            List<Server> reachableServers = lb.getReachableServers();
11            List<Server> allServers = lb.getAllServers();
12            int upCount = reachableServers.size();
13            int serverCount = allServers.size();
14 
15            if ((upCount == 0) || (serverCount == 0)) {
16                log.warn("No up servers available from load balancer: " + lb);
17                return null;
18            }
19 
20            int nextServerIndex = incrementAndGetModulo(serverCount);
21            server = allServers.get(nextServerIndex);
22 
23            if (server == null) {
24                /* Transient. */
25                Thread.yield();
26                continue;
27            }
28 
29            if (server.isAlive() && (server.isReadyToServe())) {
30                return (server);
31            }
32 
33            // Next.
34            server = null;
35        }
36 
37        if (count >= 10) {
38            log.warn("No available alive servers after 10 tries from load balancer: "
39                    + lb);
40        }
41        return server;
42    }
43 
44    /**
45     * Inspired by the implementation of {@link AtomicInteger#incrementAndGet()}.
46     *
47     * @param modulo The modulo to bound the value of the counter.
48     * @return The next value.
49     */
50    private int incrementAndGetModulo(int modulo) {
51        for (;;) {
52            int current = nextServerCyclicCounter.get();
53            int next = (current + 1) % modulo;
54            if (nextServerCyclicCounter.compareAndSet(current, next))
55                return next;
56        }
57    }
58
59

WeightedResponseTimeRule 繼承了 RoundRobinRule,開始的時候還沒有權重列表,採用父類的輪詢方式,有一個默認每 30 秒更新一次權重列表的定時任務,該定時任務會根據實例的響應時間來更新權重列表,choose 方法做的事情就是,用一個 (0,1) 的隨機 double 數乘以最大的權重得到 randomWeight,然後遍歷權重列表,找出第一個比 randomWeight 大的實例下標,然後返回該實例,代碼略。

BestAvailableRule 策略用來選取最少併發量請求的服務器:

 1public Server choose(Object key) {
 2    if (loadBalancerStats == null) {
 3        return super.choose(key);
 4    }
 5    List<Server> serverList = getLoadBalancer().getAllServers(); // 獲取所有的服務器列表
 6    int minimalConcurrentConnections = Integer.MAX_VALUE;
 7    long currentTime = System.currentTimeMillis();
 8    Server chosen = null;
 9    for (Server server: serverList) { // 遍歷每個服務器
10        ServerStats serverStats = loadBalancerStats.getSingleServerStat(server); // 獲取各個服務器的狀態
11        if (!serverStats.isCircuitBreakerTripped(currentTime)) { // 沒有觸發斷路器的話繼續執行
12            int concurrentConnections = serverStats.getActiveRequestsCount(currentTime); // 獲取當前服務器的請求個數
13            if (concurrentConnections < minimalConcurrentConnections) { // 比較各個服務器之間的請求數,然後選取請求數最少的服務器並放到chosen變量中
14                minimalConcurrentConnections = concurrentConnections;
15                chosen = server;
16            }
17        }
18    }
19    if (chosen == null) { // 如果沒有選上,調用父類ClientConfigEnabledRoundRobinRule的choose方法,也就是使用RoundRobinRule輪詢的方式進行負載均衡        
20        return super.choose(key);
21    } else {
22        return chosen;
23    }
24}
25
26

使用 Ribbon 提供的負載均衡策略很簡單,只需以下幾部:

1、創建具有負載均衡功能的 RestTemplate 實例

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

使用 RestTemplate 進行 rest 操作的時候,會自動使用負載均衡策略,它內部會在 RestTemplate 中加入 LoadBalancerInterceptor 這個攔截器,這個攔截器的作用就是使用負載均衡。

默認情況下會採用輪詢策略,如果希望採用其它策略,則指定 IRule 實現,如:

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

這種方式對 Feign 也有效。

我們也可以參考 ribbon,自己寫一個負載均衡實現類。

可以通過下面方法獲取負載均衡策略最終選擇了哪個服務實例:

 1 @Autowired
 2 LoadBalancerClient loadBalancerClient; 
 3 
 4 //測試負載均衡最終選中哪個實例
 5 public String getChoosedService() {
 6     ServiceInstance serviceInstance = loadBalancerClient.choose("USERINFO-SERVICE");
 7     StringBuilder sb = new StringBuilder();
 8     sb.append("host: ").append(serviceInstance.getHost()).append(", ");
 9     sb.append("port: ").append(serviceInstance.getPort()).append(", ");
10     sb.append("uri: ").append(serviceInstance.getUri());
11     return sb.toString();
12 }
13
14
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/RzrrQx2TIIVySu0Z9iJFfA