負載均衡的基本概念
負載均衡是系統高可用、緩解網絡流量和處理能力擴容的重要手段,廣義的負載均衡指的是服務端負載均衡,如硬件負載均衡(F5)和軟件負載均衡(Nginx)。負載均衡設備會維護一份可用的服務器的信息,當客戶端請求到達負載均衡設備之后,設備會根據一定的負載均衡算法從可用的服務器列表中取出一台可用的服務器,然后將請求轉發到該服務器。對應的負載均衡架構如下圖所示:
負載均衡架構示意圖
- 獨立進程單元,通過負載均衡策略,將請求轉發到不同的執行單元,如Nginx;
- 將負載均衡邏輯以代碼形式封裝在服務消費者的客戶端上,客戶端維護一份服務提供者的信息列表,通過負載均衡策略將請求分攤給多個服務提供者,從而達到負載均衡的目的,如Ribbon。
Ribbon是Netflix發布的雲中間層服務開源項目,其主要功能是提供客戶端實現負載均衡算法。Ribbon客戶端組件提供一系列完善的配置項如連接超時,重試等。簡單的說,Ribbon是一個客戶端負載均衡器,我們可以在配置文件中Load Balancer后面的所有機器,Ribbon會自動的幫助你基於某種規則(如簡單輪詢,隨機連接等)去連接這些機器,我們也很容易使用Ribbon實現自定義的負載均衡算法。
Ribbon的策略
- Ribbon + RestTemplate
- Ribbon + Feign
RestTemplate + Ribbon消費服務


<T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException; <T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException; ServiceInstance choose(String serviceId);


負載均衡策略


- RoundRobinRule 【輪詢】默認嘗試10次
- RandomRule 【隨機】
- AvailabilityFilteringRule 【可用過濾】會先過濾掉由於多次訪問故障而處於斷路器跳閘狀態的服務,還有並發的連接數超過閾值的服務,然后對剩余的服務列表進行輪詢
- WeightedResponseTimeRule 【響應時間權重】根據平均響應時間計算所有服務的權重,響應時間越快服務權重越大被選中的概率越高。剛啟動時,如果統計信息不足,則使用輪詢策略,等信息足夠,切換到 WeightedResponseTimeRule
- RetryRule 【在選定負載均衡策略上使用輪詢的方式重試】先按照輪詢策略獲取服務,如果獲取失敗則在指定時間內重試,獲取可用服務
- BestAvailableRule 【選擇最小請求數的服務器】選過濾掉多次訪問故障而處於斷路器跳閘狀態的服務,然后選擇一個並發量最小的服務,如果沒找到,使用隨機輪詢策略選取;
- ZoneAvoidanceRule (默認) 【根據服務器所屬服務區的整體運行狀況來輪詢選擇】符合判斷server所在區域的性能和server的可用性選擇服務,根據服務器所屬服務區的運行狀況和可用性來進行負載均衡。
IPing


- DummyPing 直接返回true
- NIWSDiscoveryPing 根據DiscoveryEnabledServer的InstanceInfo的status進行判斷,如果為UP,表明可用;
- NoOpPing 不真實ping,直接返回true;
- PingConstant 固定返回某服務是否可用,是一個常量值;
- PingUrl 使用HttpClient進行ping操作,根據返回結果判定是否可用。
private List<DiscoveryEnabledServer> obtainServersViaDiscovery() { List<DiscoveryEnabledServer> serverList = new ArrayList<DiscoveryEnabledServer>(); if (eurekaClientProvider == null || eurekaClientProvider.get() == null) { logger.warn("EurekaClient has not been initialized yet, returning an empty list"); return new ArrayList<DiscoveryEnabledServer>(); } EurekaClient eurekaClient = eurekaClientProvider.get(); if (vipAddresses!=null){ for (String vipAddress : vipAddresses.split(",")) { // if targetRegion is null, it will be interpreted as the same region of client List<InstanceInfo> listOfInstanceInfo = eurekaClient.getInstancesByVipAddress(vipAddress, isSecure, targetRegion); for (InstanceInfo ii : listOfInstanceInfo) { if (ii.getStatus().equals(InstanceStatus.UP)) { if(shouldUseOverridePort){ if(logger.isDebugEnabled()){ logger.debug("Overriding port on client name: " + clientName + " to " + overridePort); } // copy is necessary since the InstanceInfo builder just uses the original reference, // and we don't want to corrupt the global eureka copy of the object which may be // used by other clients in our system InstanceInfo copy = new InstanceInfo(ii); if(isSecure){ ii = new InstanceInfo.Builder(copy).setSecurePort(overridePort).build(); }else{ ii = new InstanceInfo.Builder(copy).setPort(overridePort).build(); } } DiscoveryEnabledServer des = createServer(ii, isSecure, shouldUseIpAddr); serverList.add(des); } } if (serverList.size()>0 && prioritizeVipAddressBasedServers){ break; // if the current vipAddress has servers, we dont use subsequent vipAddress based servers } } } return serverList; }
對於ribbon從Eureka Client獲取注冊信息,由於服務可能存在更新,所以需要定時從Eureka Client獲取最新的注冊信息。
class PingTask extends TimerTask { public void run() { try { new Pinger(pingStrategy).runPinger(); } catch (Exception e) { logger.error("LoadBalancer [{}]: Error pinging", name, e); } } }
在LoadBalancerAutoConfiguration類中,首先維護了一個被LoadBalanced修飾的RestTemplate對象的list。初始化過程中,通過調用customer.customize(restTemplate)方法給RestTemplate增加攔截器LoadBalancerInterceptor,LoadBalancerInterceptor用於實時攔截,在LoadBalancerInterceptor中實現了負載均衡的方法。
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, this.requestFactory.createRequest(request, body, execution)); }
結論
