SpringCloud 組件:Ribbon 的負載均衡策略及原理
Load Balance 負載均衡是用於解決一臺機器 (一個進程) 無法解決所有請求而產生的一種算法。像 nginx 可以使用負載均衡分配流量,ribbon 爲客戶端提供負載均衡,dubbo 服務調用裏的負載均衡等等,很多地方都使用到了負載均衡。
使用負載均衡帶來的好處很明顯:
-
當集羣裏的 1 臺或者多臺服務器 down 的時候,剩餘的沒有 down 的服務器可以保證服務的繼續使用
-
使用了更多的機器保證了機器的良性使用,不會由於某一高峯時刻導致系統 cpu 急劇上升
負載均衡有好幾種實現策略,常見的有:
-
隨機 (Random)
-
輪詢 (RoundRobin)
-
一致性哈希 (ConsistentHash)
-
哈希 (Hash)
-
加權(Weighted)
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