Ribbon 客戶端負載均衡

Ribbon 客戶端負載均衡

負載均衡

負載均衡 建立在現有網絡結構之上,它提供了一種廉價有效透明的方法擴展網絡設備服務器的帶寬、增加吞吐量、加強網絡數據處理能力、提高網絡的靈活性和可用性。

分類

分類 實現方式 優點 缺點
軟件 在一臺或多臺服務器相應的操作系統上安裝一個或多個附加軟件 基於特定環境,配置簡單,使用靈活,成本低廉 軟件本身耗費資源、可擴展性不好、受操作系統限制、安全問題
硬件 直接在服務器和外部網絡間安裝負載均衡設備 獨立於操作系統、多樣化的負載均衡策略、智能化的流量管理、性能高 成本昂貴
本地 對本地的服務器羣做負載均衡 能有效地解決數據流量過大、網絡負荷過重的問題、不需要購置昂貴的服務器
全局 對分別放置在不同的地理位置、有不同網絡結構的服務器羣間作負載均衡 使全球用戶只以一個IP地址或域名就能訪問到離自己最近的服務器

客戶端與服務端級別的負載均衡

  • 服務器端負載均衡:例如Nginx,通過Nginx進行負載均衡過程如下:先發送請求給nginx服務器,然後通過負載均衡算法,在多個業務服務器之間選擇一個進行訪問;即在服務器端再進行負載均衡算法分配。
  • 客戶端負載均衡:客戶端會有一個服務器地址列表,在發送請求前通過負載均衡算法選擇一個服務器,然後進行訪問,即在客戶端就進行負載均衡算法分配。

Ribbon

Netflix Ribbon 以及 被SpringCloud 封裝過的Ribbon,本質上都是客戶端負載均衡的方式。

原理概述

但凡客戶端負載均衡,大抵可以分爲兩個過程:

  • 獲取服務註冊列表的信息
  • 根據均衡策略進行負載均衡

在SpringCloud中,服務註冊列表的信息來自 EurekaClient。

源碼分析

在Ribbon的整個調度過程中,LoadBalancerClient,是負責整個過程的執行者。

獲取服務註冊列表

拋開SpringCloud中一大堆的框架代碼,直接找到 DynamicServerListLoadBalancer 類中的 updateListOfServers() 方法:

@VisibleForTesting
    public void updateListOfServers() {
        List<T> servers = new ArrayList();
        if (this.serverListImpl != null) {
            servers = this.serverListImpl.getUpdatedListOfServers();
            LOGGER.debug("List of Servers for {} obtained from Discovery client: {}", this.getIdentifier(), servers);
            if (this.filter != null) {
                servers = this.filter.getFilteredListOfServers((List)servers);
                LOGGER.debug("Filtered List of Servers for {} obtained from Discovery client: {}", this.getIdentifier(), servers);
            }
        }

        this.updateAllServerList((List)servers);
    }

繼續根據 serverListImpl.getUpdatedListOfServers() 順藤摸瓜,找到類 DiscoveryEnabledNIWSServerList ,其中有 getInitialListOfServers()getUpdatedListOfServers() 方法:

@Override
    public List<DiscoveryEnabledServer> getInitialListOfServers(){
        return obtainServersViaDiscovery();
    }
@Override
    public List<DiscoveryEnabledServer> getUpdatedListOfServers(){
        return obtainServersViaDiscovery();
    }

這裏定義了兩個方法,一個是獲取初始服務註冊列表的方法,一個是獲取更新後的服務註冊列表的方法,其實是同一個方法……具體實現:

private List<DiscoveryEnabledServer> obtainServersViaDiscovery() {
        List<DiscoveryEnabledServer> serverList = new ArrayList();
        if (this.eurekaClientProvider != null && this.eurekaClientProvider.get() != null) {
            EurekaClient eurekaClient = (EurekaClient)this.eurekaClientProvider.get();
            if (this.vipAddresses != null) {
                String[] var3 = this.vipAddresses.split(",");
                int var4 = var3.length;

                for(int var5 = 0; var5 < var4; ++var5) {
                    String vipAddress = var3[var5];
                    List<InstanceInfo> listOfInstanceInfo = eurekaClient.getInstancesByVipAddress(vipAddress, this.isSecure, this.targetRegion);
                    //省略部分代碼
                }
            }
        }
    }

上述方法分兩個部分:

  • 通過 eurekaClientProvider.get() 獲取 EurekaClient
  • 通過 EurekaClient 來獲取服務註冊列表信息

其中,eurekaClientProvider 的實現類是 LegacyEurekaClientProvider,它是一個獲取 eurekaClient 類,通過靜態的方法去獲取 eurekaClient ,其代碼如下:

class LegacyEurekaClientProvider implements Provider<EurekaClient> {
    private volatile EurekaClient eurekaClient;
    @Override
    public synchronized EurekaClient get() {
        if (eurekaClient == null) {
            eurekaClient = DiscoveryManager.getInstance().getDiscoveryClient();
        }
        return eurekaClient;
    }
}

獲取服務註冊列表的時間間隔

在BaseLoadBalancer類下,BaseLoadBalancer的構造函數,該構造函數開啓了一個PingTask任務:

public BaseLoadBalancer(String name, IRule rule, LoadBalancerStats stats,
            IPing ping, IPingStrategy pingStrategy) {
        //省略部分代碼
        setupPingTask();
         //省略部分代碼
    }
void setupPingTask() {
        //省略部分代碼
        lbTimer.schedule(new PingTask(), 0, pingIntervalSeconds * 1000);
       //省略部分代碼
    }

判斷服務可用性

在BaseLoadBalancer的構造函數中有一個參數IPing ping,它定義了服務的可用性:

public interface IPing {
    boolean isAlive(Server var1);
}

這裏僅僅設置定義了一個布爾值,真正使用的地方,在Pinger的runPinger()方法中:

public void runPinger() throws Exception {
            //省略部分代碼
                    boolean[] resultsx = this.pingerStrategy.pingServers(BaseLoadBalancer.this.ping, allServers);
            //省略部分代碼    
        }

簡單來說,pingerStrategy.pingServers()方法會返回一個布爾值,代表服務註冊列表是否有變化

public interface IPingStrategy {
    boolean[] pingServers(IPing var1, Server[] var2);
}

實現其實很簡單,獲取新的服務註冊列表的 Ping 列表,與已經獲取的進行比較:

public boolean[] pingServers(IPing ping, Server[] servers) {
            int numCandidates = servers.length;
            boolean[] results = new boolean[numCandidates];
           //省略部分代碼
            for(int i = 0; i < numCandidates; ++i) {
                results[i] = false;
                try {
                    if (ping != null) {
                        results[i] = ping.isAlive(servers[i]);
                    }
                } //省略部分代碼
            return results;
        }

由此可見,LoadBalancerClient 會向Eureka 獲取服務註冊列表,並且通過10s一次向EurekaClient發送“ping”,來判斷服務的可用性,如果服務的可用性發生了改變或者服務數量和之前的不一致,則更新或者重新拉取。LoadBalancerClient有了這些服務註冊列表,就可以進行負載均衡。

負載均衡

簡而言之,真正的負載均衡是委託給 IRule 實現的:

public Server chooseServer(Object key) {
        //省略部分代碼
        this.counter.increment();
        //省略部分代碼
        return this.rule.choose(key);
        //省略部分代碼
    }

在這裏插入圖片描述

IRule 通過 AbstractLoadBalancerRule 加載過濾規則:

public abstract class AbstractLoadBalancerRule implements IRule, IClientConfigAware {
    private ILoadBalancer lb;

    public AbstractLoadBalancerRule() {
    }

    public void setLoadBalancer(ILoadBalancer lb) {
        this.lb = lb;
    }

    public ILoadBalancer getLoadBalancer() {
        return this.lb;
    }
}
RandomRule

本質上通過隨機數隨機獲取一個服務實例,在index上隨機,選擇index對應位置的server,但如果根據隨機數獲取不到實例,則會出現BUG

RoundRobinRule

按照線性輪詢的方式依次選擇每個服務的實例,如果超過10次獲取不到服務,則嘗試結束,並拋出警告。

RetryRule

RoundRobinRule 基礎上增加了重試機制,在一個配置時間段內當選擇server不成功,則一直嘗試使用subRule的方式選擇一個可用的server

WeightedResponseTimeRule

該方式是 RoundRobinRule 的擴展,增加了根據實例的運行情況來計算權值,並根據權值來選擇實例,其實現主要分3個步驟:

  • 定時任務

    • WeightedResponseTimeRule 策略在初始化時會啓動一個定時任務,每隔30s會執行權值更新的操作
  • 權值計算

    • 權值的值是總響應時間與實例自身平均響應時間的差所累加而得

    • 舉例:假設有四個實例A、B、C、D,他們的平均響應時間爲10、40、80、100,所以總響應時間是230,每個實例的權值如下:

      • A:230-10=220
      • B:220+(230-40)=410
      • C:410+(230-80)=560
      • D:560+(230-100)=690

      這些權值代表的一個區間,總區間是 [0 ,690)

      • A:[0,220]
      • B:(220,410]
      • C:(410,560]
      • B:(560,690)
  • 實例選擇

    • 實例的選擇是隨機一個數字,然後將這個數字所在的權值區間對應的服務實例作爲被選擇的實例
    • 根據上述權值計算髮現,平均響應時間越短,權重區間寬度越大,因此被選中的概率越大
BestAvailableRule

逐個考察實例,如果實例被過濾掉了,則忽略,再選擇其中併發請求數最小的實例。

AvailabilityFilteringRule

該策略有兩個過程,先以線性方式選擇一個實例,再判斷是否滿足,如果滿足就選擇,不滿足就繼續下一個

  • 過濾:
    • 過濾掉那些因爲一直連接失敗而被標記爲circuit tripped的服務實例
    • 過濾掉那些高併發的的服務實例(active connections 超過配置的閾值),閥值默認爲2^32-1,可通過參數<clientName>.<nameSpace>.ActiveConnectionsLimit 來修改
ZoneAvoidanceRule

和 AvailabilityFilteringRule 不一樣的是,ZoneAvoidanceRule是先進行過濾,再輪詢選擇,過濾的條件和 AvailabilityFilteringRule 一樣,不過是先通過過濾把所有的服務找出來,然後再去輪詢選擇。

  • 使用主過濾條件過濾所有實例並返回過濾的清單
  • 依次使用次過濾條件對返回的過濾清單進行過濾
  • 每次過濾完成都需要判斷下面兩個條件,只要有一個符合就不再過濾,並將當前結果返回供線性輪詢選擇:
    • 過濾後是實例總數 >= 最小過濾實例數(默認爲1)
    • 過濾後的實例比例 > 最小過濾百分百(默認爲0)

SpringCloud 對 Ribbon 的封裝

與RestTemplate的結合

使用,增加一個註解 @LoadBalance

@LoadBalanced
    RestTemplate restTemplate() {
        return new RestTemplate();
    }

源碼分析,SpringBoot的一個核心功能就是自動配置,其中以 XXXAutoConfiguration 類爲最關鍵:

找到 LoadBalanceAutoConfiguration 類定義如下:

@Configuration
@ConditionalOnClass(RestTemplate.class)
@ConditionalOnBean(LoadBalancerClient.class)
@EnableConfigurationProperties(LoadBalancerRetryProperties.class)
public class LoadBalancerAutoConfiguration {

	@LoadBalanced
	@Autowired(required = false)
	private List<RestTemplate> restTemplates = Collections.emptyList();
    
    @Bean
	public SmartInitializingSingleton loadBalancedRestTemplateInitializer(
			final List<RestTemplateCustomizer> customizers) {
		return new SmartInitializingSingleton() {
			@Override
			public void afterSingletonsInstantiated() {
				for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {
					for (RestTemplateCustomizer customizer : customizers) {
						customizer.customize(restTemplate);
					}
				}
			}
		};
	}
}

在該類中,首先維護了一個被@LoadBalanced修飾的RestTemplate對象的List,在初始化的過程中,通過調用customizer.customize(restTemplate)方法來給RestTemplate增加攔截器LoadBalancerInterceptor。

而LoadBalancerInterceptor,用於實時攔截,在LoadBalancerInterceptor實現來負載均衡。LoadBalancerInterceptor的攔截方法如下:

@Override
    public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
            final ClientHttpRequestExecution execution) throws IOException {
        final URI originalUri = request.getURI();
        String serviceName = originalUri.getHost();
        Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
        return this.loadBalancer.execute(serviceName, requestFactory.createRequest(request, body, execution));
    }

總結

綜上所述,Ribbon的負載均衡,主要通過LoadBalancerClient來實現的,而LoadBalancerClient具體交給了ILoadBalancer來處理,ILoadBalancer通過配置IRule、IPing等信息,並向EurekaClient獲取註冊列表的信息,並默認10秒一次向EurekaClient發送“ping”,進而檢查是否更新服務列表,最後,得到註冊列表後,ILoadBalancer根據IRule的策略進行負載均衡。

而RestTemplate 被@LoadBalance註解後,能過用負載均衡,主要是維護了一個被@LoadBalance註解的RestTemplate列表,並給列表中的RestTemplate添加攔截器,進而交給負載均衡器去處理。

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