spring cloud Ribbon用法及原理解析
這篇文章主要介紹了spring cloud Ribbon用法及原理解析,文中通過示例程式碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下
簡介
這篇文章主要介紹一下ribbon在程式中的基本使用,在這裡是單獨拿出來寫用例測試的,實際生產一般是配置feign一起使用,更加方便開發。同時這裡也通過原始碼來簡單分析一下ribbon的基本實現原理。
基本使用
這裡使用基於zookeeper註冊中心+ribbon的方式實現一個簡單的客戶端負載均衡案例。
服務提供方
首先是一個服務提供方。程式碼如下。
application.properties配置檔案
spring.application.name=discovery-service
service-B.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RandomRule
bootstrap.properties配置檔案
spring.cloud.zookeeper.connect-string=192.168.0.15:2181
載入程式,提供了一個ribbonService的rest介面服務,註冊程式到zookeeper中。
@SpringBootApplication @EnableDiscoveryClient @RestController public class DiscoverClient { public static void main(String[] args) { SpringApplication.run(DiscoverClient.class,args); } @RequestMapping("/ribbonService") public String ribbonService(){ return "hello too ribbon"; } }
服務呼叫方
服務呼叫方就是進行負載均衡的一方,利用ribbo的RestTemplate進行負載呼叫服務。
RibbonConfig,配置ribbon的RestTemplate,通過@LoadBalanced註解實現,具體原理稍後分析。
@Configuration public class RibbonConfig { /** * 例項化ribbon使用的RestTemplate * @return */ @Bean @LoadBalanced public RestTemplate rebbionRestTemplate(){ return new RestTemplate(); } /** * 配置隨機負載策略,需要配置屬性service-B.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RandomRule */ @Bean public IRule ribbonRule() { return new RandomRule(); } }
載入程式
@SpringBootApplication(scanBasePackages = "garine.learn.ribbon.loadblance") @EnableDiscoveryClient @RestController public class TestRibbonApplocation { public static void main(String[] args) { SpringApplication.run(TestRibbonApplocation.class,args); } @Autowired @LoadBalanced RestTemplate restTemplate; @GetMapping("/{applicationName}/ribbonService") public String ribbonService(@PathVariable("applicationName") String applicationName){ return restTemplate.getForObject("http://" + applicationName+"/ribbonService",String.class); } }
配置檔案同上,服務名稱修改即可。
測試
啟動兩個discovery-service,由於埠設定為0,所以是隨機埠。
啟動服務呼叫方
瀏覽器訪問服務呼叫方的提供的介面,路徑引數需要加上呼叫的服務名稱,例如http://localhost:8080/discovery-service/ribbonService,然後服務呼叫方使用ribbon的RestTemplate呼叫服務提供方的介面。
結果返回:hello too ribbon ,同時服務提供方啟動的兩個服務都可能被呼叫,取決於怎麼配置負載策略。
上面就是一個簡單使用ribbon的例子,結合feign使用基本上是做類似上面所寫的工作,那麼ribbon到底是怎麼實現的呢?
原理與原始碼分析
ribbon實現的關鍵點是為ribbon定製的RestTemplate,ribbon利用了RestTemplate的攔截器機制,在攔截器中實現ribbon的負載均衡。負載均衡的基本實現就是利用applicationName從服務註冊中心獲取可用的服務地址列表,然後通過一定演算法負載,決定使用哪一個服務地址來進行http呼叫。
Ribbon的RestTemplate
RestTemplate中有一個屬性是List<ClientHttpRequestInterceptor> interceptors,如果interceptors裡面的攔截器資料不為空,在RestTemplate進行http請求時,這個請求就會被攔截器攔截進行,攔截器實現介面ClientHttpRequestInterceptor,需要實現方法是
ClientHttpResponse intercept(HttpRequest request,byte[] body,ClientHttpRequestExecution execution) throws IOException;
也就是說攔截器需要完成http請求,並封裝一個標準的response返回。
ribbon中的攔截器
在Ribbon 中也定義了這樣的一個攔截器,並且注入到RestTemplate中,是怎麼實現的呢?
在Ribbon實現中,定義了一個LoadBalancerInterceptor,具體的邏輯先不說,ribbon就是通過這個攔截器進行攔截請求,然後實現負載均衡呼叫。
攔截器定義在org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration.LoadBalancerInterceptorConfig#ribbonInterceptor
@Configuration @ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate") static class LoadBalancerInterceptorConfig { @Bean //定義ribbon的攔截器 public LoadBalancerInterceptor ribbonInterceptor( LoadBalancerClient loadBalancerClient,LoadBalancerRequestFactory requestFactory) { return new LoadBalancerInterceptor(loadBalancerClient,requestFactory); } @Bean @ConditionalOnMissingBean //定義注入器,用來將攔截器注入到RestTemplate中,跟上面配套使用 public RestTemplateCustomizer restTemplateCustomizer( final LoadBalancerInterceptor loadBalancerInterceptor) { return restTemplate -> { List<ClientHttpRequestInterceptor> list = new ArrayList<>( restTemplate.getInterceptors()); list.add(loadBalancerInterceptor); restTemplate.setInterceptors(list); }; } }
ribbon中的攔截器注入到RestTemplate
定義了攔截器,自然需要把攔截器注入到、RestTemplate才能生效,那麼ribbon中是如何實現的?上面說了攔截器的定義與攔截器注入器的定義,那麼肯定會有個地方使用注入器來注入攔截器的。
在org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration#loadBalancedRestTemplateInitializerDeprecated方法裡面,進行注入,程式碼如下。
@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 loadBalancedRestTemplateInitializerDeprecated( final ObjectProvider<List<RestTemplateCustomizer>> restTemplateCustomizers) { //遍歷context中的注入器,呼叫注入方法。 return () -> restTemplateCustomizers.ifAvailable(customizers -> { for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) { for (RestTemplateCustomizer customizer : customizers) { customizer.customize(restTemplate); } } }); } //...... }
遍歷context中的注入器,呼叫注入方法,為目標RestTemplate注入攔截器,注入器和攔截器都是我們定義好的。
還有關鍵的一點是:需要注入攔截器的目標restTemplates到底是哪一些?因為RestTemplate例項在context中可能存在多個,不可能所有的都注入攔截器,這裡就是@LoadBalanced註解發揮作用的時候了。
LoadBalanced註解
嚴格上來說,這個註解是spring cloud實現的,不是ribbon中的,它的作用是在依賴注入時,只注入例項化時被@LoadBalanced修飾的例項。
例如我們定義Ribbon的RestTemplate的時候是這樣的
@Bean @LoadBalanced public RestTemplate rebbionRestTemplate(){ return new RestTemplate(); }
因此才能為我們定義的RestTemplate注入攔截器。
那麼@LoadBalanced是如何實現這個功能的呢?其實都是spring的原生操作,@LoadBalance的原始碼如下
/** * Annotation to mark a RestTemplate bean to be configured to use a LoadBalancerClient * @author Spencer Gibb */ @Target({ ElementType.FIELD,ElementType.PARAMETER,ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @Qualifier public @interface LoadBalanced { }
很明顯,‘繼承'了註解@Qualifier,我們都知道以前在xml定義bean的時候,就是用Qualifier來指定想要依賴某些特徵的例項,這裡的註解就是類似的實現,restTemplates通過@Autowired注入,同時被@LoadBalanced修飾,所以只會注入@LoadBalanced修飾的RestTemplate,也就是我們的目標RestTemplate。
攔截器邏輯實現
LoadBalancerInterceptor原始碼如下。
public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor { private LoadBalancerClient loadBalancer; private LoadBalancerRequestFactory requestFactory; public LoadBalancerInterceptor(LoadBalancerClient loadBalancer,LoadBalancerRequestFactory requestFactory) { this.loadBalancer = loadBalancer; this.requestFactory = requestFactory; } public LoadBalancerInterceptor(LoadBalancerClient loadBalancer) { // for backwards compatibility this(loadBalancer,new LoadBalancerRequestFactory(loadBalancer)); } @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)); } }
攔截請求執行
@Override public <T> T execute(String serviceId,LoadBalancerRequest<T> request) throws IOException { ILoadBalancer loadBalancer = getLoadBalancer(serviceId); //在這裡負載均衡選擇服務 Server server = getServer(loadBalancer); 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); }
我們重點看getServer方法,看看是如何選擇服務的
protected Server getServer(ILoadBalancer loadBalancer) { if (loadBalancer == null) { return null; } // return loadBalancer.chooseServer("default"); // TODO: better handling of key }
程式碼配置隨機loadBlancer,進入下面程式碼
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; } } }
這裡配置的是RandomRule,所以進入RandomRule程式碼
public Server choose(ILoadBalancer lb,Object key) { if (lb == null) { return null; } Server server = null; while (server == null) { if (Thread.interrupted()) { return null; } //獲取可用服務列表 List<Server> upList = lb.getReachableServers(); List<Server> allList = lb.getAllServers(); //隨機一個數 int serverCount = allList.size(); if (serverCount == 0) { /* * No servers. End regardless of pass,because subsequent passes * only get more restrictive. */ return null; } int index = rand.nextInt(serverCount); server = upList.get(index); if (server == null) { /* * The only time this should happen is if the server list were * somehow trimmed. This is a transient condition. Retry after * yielding. */ Thread.yield(); continue; } if (server.isAlive()) { return (server); } // Shouldn't actually happen.. but must be transient or a bug. server = null; Thread.yield(); } return server; }
隨機負載規則很簡單,隨機整數選擇服務,最終達到隨機負載均衡。我們可以配置不同的Rule來實現不同的負載方式。
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援我們。