一分鐘瞭解SpringCloud中的ribbon到底是什麼,原理是啥?

1.概念

ribbon是一款客戶端負載均衡器,用於微服務之間的負載均衡。

首先,什麼是客戶端負載均衡?

如圖,ribbon可以透過註冊中心獲取服務列表,然後自己執行自己的負載均衡策略來決定要訪問哪個微服務,這就是客戶端負載均衡,選擇的主導權在客戶端自己手裡。

一分鐘瞭解SpringCloud中的ribbon到底是什麼,原理是啥?

區別於服務端的負載均衡,客戶端的負載均衡可以由客戶端自己選擇。

例如你去食堂吃飯,服務端的負載均衡就是食堂自動給你分配伙食,你沒得選。而客戶端的負載均衡就是進入食堂,你有一個菜譜,你可以按照菜譜自己選擇要吃的伙食。

2.用一段虛擬碼來實現ribbon的客戶端代理

透過重寫RestTemplate 的doExecute方法,來完成客戶端的負載均衡。這裡的策略是隨機一個。

@Slf4jpublic class MyRestTemplate extends RestTemplate { private DiscoveryClient discoveryClient; public MyRestTemplate(DiscoveryClient discoveryClient) { this。discoveryClient = discoveryClient; } protected T doExecute(URI url, @Nullable HttpMethod method, @Nullab le RequestCallback requestCallback, @Nullable ResponseExtractor responseExtractor) throws RestClientExce ption { …… ClientHttpResponse response = null; try { //判斷url的攔截路徑,然後去redis(作為註冊中心)獲取地址隨機選取一個 log。info(“請求的url路徑為:{}”, url); url = replaceUrl(url); log。info(“替換後的路徑:{}”, url); ClientHttpRequest request = createRequest(url, method); if (requestCallback != null) { requestCallback。doWithRequest(request); } response = request。execute(); handleResponse(url, method, response); return (responseExtractor != null ? responseExtractor。extractData(respo nse) : null); …… } /** * 把服務例項名稱替換為ip:埠 * @param url * @return */ private URI replaceUrl(URI url) { //解析我們的微服務的名稱 String sourceUrl = url。toString(); String[] httpUrl = sourceUrl。split(“//”); int index = httpUrl[]。replaceFirst(“/”, “@”)。indexOf(“@”); String serviceName = httpUrl[]。substring(, index); //透過微服務的名稱去nacos服務端獲取 對應的例項列表 List serviceInstanceList = discoveryClient。getInstances(serviceName); if (serviceInstanceList。isEmpty()) { throw new RuntimeException(“沒有可用的微服務例項列表:” + serviceName); } //採取隨機的獲取一個 Random random = new Random(); Integer randomIndex = random。nextInt(serviceInstanceList。size()); log。info(“隨機下標:{}”, randomIndex); String serviceIp = serviceInstanceList。get(randomIndex)。getUri()。toStri ng(); log。info(“隨機選舉的服務IP:{}”, serviceIp); String targetSource = httpUrl[]。replace(serviceName, serviceIp); try { return new URI(targetSource); } catch (URISyntaxException e) { e。printStackTrace(); } return url; }}

3.透過Ribbon元件來實現負載均衡

第一步:依賴

<!—— 加入nocas‐client——> com。alibaba。cloud spring‐cloud‐alibaba‐nacos‐discovery<!—— 加入ribbon ——> org。springframework。cloud spring‐cloud‐starter‐netflix‐ribbon

第二步 配置RestTemplate

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

這樣就可以使用ribbon了。

4.原始碼解析

LoadBalancerAutoConfiguration 這個類配置了ribbon對restTemplate的整合

@Configuration(proxyBeanMethods = false)@ConditionalOnClass(RestTemplate。class)@ConditionalOnBean(LoadBalancerClient。class)@EnableConfigurationProperties(LoadBalancerRetryProperties。class)public class LoadBalancerAutoConfiguration { @LoadBalanced @Autowired(required = false) private List restTemplates = Collections。emptyList(); @Autowired(required = false) private List transformers = Collections。emptyList(); ……}

4.1首先,這個類維護了一個RestTemplate的列表,並且透過RestTemplateCustomizer對這些RestTemplate新增一個攔截器;

// 遍歷新增RestTemplateCustomizer@Beanpublic SmartInitializingSingleton loadBalancedRestTemplateInitializerDeprecated( final ObjectProvider> restTemplateCustomizers) { return () -> restTemplateCustomizers。ifAvailable(customizers -> { for (RestTemplate restTemplate : LoadBalancerAutoConfiguration。this。restTemplates) { for (RestTemplateCustomizer customizer : customizers) { customizer。customize(restTemplate); } } });}

@Configuration(proxyBeanMethods = false)@ConditionalOnMissingClass(“org。springframework。retry。support。RetryTemplate”)static class LoadBalancerInterceptorConfig { // 攔截器註冊bean @Bean public LoadBalancerInterceptor ribbonInterceptor( LoadBalancerClient loadBalancerClient, LoadBalancerRequestFactory requestFactory) { return new LoadBalancerInterceptor(loadBalancerClient, requestFactory); } // 新增聯結器 @Bean @ConditionalOnMissingBean public RestTemplateCustomizer restTemplateCustomizer( final LoadBalancerInterceptor loadBalancerInterceptor) { return restTemplate -> { List list = new ArrayList<>( restTemplate。getInterceptors()); list。add(loadBalancerInterceptor); restTemplate。setInterceptors(list); }; }}

4.2其次,RestTemplateCustomizer 又透過LoadBalancerInterceptor 進行介面攔截,而攔截器的主要就是把serviceId取出來,再使用負載均衡器發起http請求。

public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor { private LoadBalancerClient loadBalancer; private LoadBalancerRequestFactory requestFactory; …… @Override public ClientHttpResponse intercept(final HttpRequest request, final byte[] body, final ClientHttpRequestExecution execution) throws IOException { // url獲取 final URI originalUri = request。getURI(); String serviceName = originalUri。getHost(); // 執行 return this。loadBalancer。execute(serviceName, this。requestFactory。createRequest(request, body, execution)); }}

4.3然後,在RibbonLoadBalancerClient類裡面進行負載均衡的操作

public T execute(String serviceId, LoadBalancerRequest request, Object hint) throws IOException { // 獲取負載均衡物件 ILoadBalancer loadBalancer = getLoadBalancer(serviceId); // 獲取服務 Server server = getServer(loadBalancer, hint); if (server == null) { throw new IllegalStateException(“No instances available for ” + serviceId); } RibbonServer ribbonServer = new RibbonServer(serviceId, server, isSecure(server, serviceId), serverIntrospector(serviceId)。getMetadata(server)); // 執行回請求 return execute(serviceId, ribbonServer, request);}

@Overridepublic T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest request) throws IOException { …… try { // 執行請求 T returnVal = request。apply(serviceInstance); statsRecorder。recordStats(returnVal); return returnVal; …… return null;}

4.4最後在AsyncLoadBalancerInterceptor物件裡面的intercept方法進行http請求

public ListenableFuture intercept(final HttpRequest request, final byte[] body, final AsyncClientHttpRequestExecution execution) throws IOException { URI originalUri = request。getURI(); String serviceName = originalUri。getHost(); return (ListenableFuture)this。loadBalancer。execute(serviceName, new LoadBalancerRequest>() { public ListenableFuture apply(final ServiceInstance instance) throws Exception { HttpRequest serviceRequest = new ServiceRequestWrapper(request, instance, AsyncLoadBalancerInterceptor。this。loadBalancer); return execution。executeAsync(serviceRequest, body); } });}

4.5那麼ribbon怎麼獲取服務呢?回到步驟4.3,這裡透過getServer()方法呼叫ILoadBalancer這一介面的實現類來實現的

protected Server getServer(ILoadBalancer loadBalancer, Object hint) { if (loadBalancer == null) { return null; } //選擇服務,沒有就預設 return loadBalancer。chooseServer(hint != null ? hint : “default”);}

public interface ILoadBalancer { // 初始化服務列表 public void addServers(List newServers); // 從列表中選擇一個服務 public Server chooseServer(Object key); // 標記服務為down public void markServerDown(Server server); // 獲取服務列表 public List getServerList(boolean availableOnly); // 獲取up和reachable的服務 public List getReachableServers(); //獲取所有服務(reachable and unreachable) public List getAllServers();}

一分鐘瞭解SpringCloud中的ribbon到底是什麼,原理是啥?

選擇的最終在BaseLoadBalancer裡面,預設選擇的負載均衡策略為RoundRobinRule

private final static IRule DEFAULT_RULE = new RoundRobinRule();protected IRule rule = DEFAULT_RULE;// 沒有自定義的rule就選預設的public Server chooseServer(Object key) { if (counter == null) { counter = createCounter(); } counter。increment(); if (rule == null) { return null; } else { try { return rule。choose(key); } catch (Exception e) { logger。warn(“LoadBalancer [{}]: Error choosing server for key {}”, name, key, e); return null; } }}

4.6 ribbon中已經有的負載均衡策略

一分鐘瞭解SpringCloud中的ribbon到底是什麼,原理是啥?

①:RandomRule

(隨機選擇一個Server)

②:RetryRule

對選定的負載均衡策略機上重試機制,在一個配置時間段內當選擇Server不成功, 則一直嘗試使用subRule的方式選擇一個可用的server。

③:RoundRobinRule

輪詢選擇, 輪詢index,選擇index對應位置的Server

④:AvailabilityFilteringRule

過濾掉一直連線失敗的被標記為circuit tripped的後端Server,並過濾掉那些高併發的後端 Server或者使用一個AvailabilityPredicate來包含過濾server的邏輯,其實就就是檢查 status裡記錄的各個Server的執行狀態

⑤:BestAvailableRule

選擇一個最小的併發請求的Server,逐個考察Server,如果Server被tripped了,則跳過。

⑥:WeightedResponseTimeRule

根據響應時間加權,響應時間越長,權重越小,被選中的可能性越低;

⑦:ZoneAvoidanceRule

複合判斷Server所在Zone的效能和Server的可用性選擇Server,在沒有Zone的情況下類是 輪詢。

5.總結

ribbon原理和程式碼不難,照著步驟多看看就懂了。