年后到現在一直很忙,都沒什么時間記錄東西了,其實之前工作中積累了很多知識點,一直都堆在備忘錄里,只是因為近幾個月經歷了一些事情,沒有太多的經歷來寫了,但是一些重要的東西,我還是希望能堅持記錄下來。正好最近公司用到了一些本篇文章的知識點,所以就抽空記錄一下。
本文代碼github地址:https://github.com/shaweiwei/RibbonTest/tree/master
簡介
ribbon 是一個客戶端負載均衡器,它和nginx的負載均衡相比,區別是一個是客戶端負載均衡,一個是服務端負載均衡。ribbon可以單獨使用,也可以配合eureka使用。
使用
單獨使用
1.首先我們先在原來的基礎上新建一個Ribbon模塊,如下圖:
現在我們單獨使用ribbon,在Ribbon模塊下添加依賴,如下圖所示:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-ribbon</artifactId> <version>1.4.0.RELEASE</version> </dependency>
修改application.yml文件,如下所示:
server: port: 8082 spring: application: name: Ribbon-Consumer #providers這個是自己命名的,ribbon,listOfServer這兩個是規定的 providers: ribbon: listOfServers: localhost:8080,localhost:8081
在Ribbon模塊下新建一個測試類如下代碼 * Created by cong on 2018/5/8. */
@RestController public class ConsumerController {
//注入負載均衡客戶端 @Autowired
private LoadBalancerClient loadBalancerClient; @RequestMapping("/consumer") public String helloConsumer() throws ExecutionException, InterruptedException {
//這里是根據配置文件的那個providers屬性取的 ServiceInstance serviceInstance = loadBalancerClient.choose("providers");
//負載均衡算法默認是輪詢,輪詢取得服務 URI uri = URI.create(String.format("http://%s:%s", serviceInstance.getHost(), serviceInstance.getPort())); return uri.toString();
}
運行結果如下:
會輪詢的獲取到兩個服務的URL 訪問第一次,瀏覽器出現http://localhost:8080 訪問第二次就會出現http://localhost:8081
在eureka環境下使用
下面這個例子是在之前這篇文章的例子上改的,Spring Cloud(二):Spring Cloud Eureka Server高可用注冊服務中心的配置
先看下寫好的結構
先介紹下大致功能,EurekaServer提供服務注冊功能,RibbonServer里會調用ServiceHello里的接口,ServiceHello和ServiceHello2是同樣的服務,只是為了方便分布式部署。
EurekaServer
pom依賴
<dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka-server</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-config</artifactId> </dependency> </dependencies>
BootApplication
@SpringBootApplication @EnableEurekaServer public class BootApplication { public static void main(String[] args) { SpringApplication.run(BootApplication.class, args); } }
application.properties
server.port=8760 spring.application.name=eureka-server #eureka.instance.hostname=peer1 eureka.client.serviceUrl.defaultZone=http://localhost:${server.port}/eureka
RibbonServer
BootApplication
紅色部分代碼是關鍵
@SpringBootApplication @EnableDiscoveryClient @RestController @RibbonClients(value={ @RibbonClient(name="service-hi",configuration=RibbonConfig.class) }) public class BootApplication { public static void main(String[] args) { SpringApplication.run(BootApplication.class, args); } }
RibbonConfig
@Configuration public class RibbonConfig { @Bean @LoadBalanced public RestTemplate restTemplate(){ return new RestTemplate(); } @Bean public IRule ribbonRule() { return new RoundRobinRule(); } }
TestController
@RestController public class TestController { @Autowired @LoadBalanced private RestTemplate restTemplate; @Autowired SpringClientFactory springClientFactory; @RequestMapping("/consumer") public String helloConsumer() throws ExecutionException, InterruptedException { ILoadBalancer loadBalancer = springClientFactory.getLoadBalancer("service-hi"); List<Server> servers = loadBalancer.getReachableServers(); System.out.println(",......"+servers.size()); return restTemplate.getForEntity("http://service-hi/hi",String.class).getBody(); } }
application.properties
server.port=8618 spring.application.name=ribbon-service eureka.client.serviceUrl.defaultZone=http://localhost:8760/eureka/
ServiceHello
BootApplication
@SpringBootApplication @EnableDiscoveryClient @RestController public class BootApplication { public static void main(String[] args) { SpringApplication.run(BootApplication.class, args); } @RequestMapping(value="/hi",method=RequestMethod.GET) public String hi(){ return "hi"; } }
application.properties
server.port=8788 spring.application.name=service-hi eureka.client.serviceUrl.defaultZone=http://localhost:8760/eureka/ #service-hi.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RoundRobinRule
ServiceHello2
和ServiceHello一樣,只是端口不同,另外為了區分,接口hi返回的值也改成不一樣。
查看結果
然后就是分別啟動各個服務。
查看eureka信息,可以看到服務都啟動了。
瀏覽器里輸入http://localhost:8618/consumer,多調用幾次,可以看到分別結果是hi和hi2交替出現。
這說明負載均衡實現了,而且我選擇的負載均衡策略是輪詢,所以hi和hi2肯定是交替出現。
負載均衡策略
Ribbon的核心組件是IRule,是所有負載均衡算法的父接口,其子類有:
每一個類就是一種負載均衡算法
RoundRobinRule 輪詢
RandomRule 隨機
AvailabilityFilteringRule 會先過濾掉由於多次訪問故障而處於斷路器跳閘狀態的服務,還有並發的連接數超過閾值的服務,然后對剩余的服務列表進行輪詢
WeightedResponseTimeRule 權重 根據平均響應時間計算所有服務的權重,響應時間越快服務權重越大被選中的概率越高。剛啟動時,如果統計信息不足,則使用輪詢策略,等信息足夠,切換到 WeightedResponseTimeRule
RetryRule 重試 先按照輪詢策略獲取服務,如果獲取失敗則在指定時間內重試,獲取可用服務
BestAvailableRule 選過濾掉多次訪問故障而處於斷路器跳閘狀態的服務,然后選擇一個並發量最小的服務
ZoneAvoidanceRule 符合判斷server所在區域的性能和server的可用性選擇服務
原理與源碼分析
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來實現不同的負載方式。