年後到如今一直很忙,都沒什麼時間記錄東西了,其實以前工做中積累了不少知識點,一直都堆在備忘錄裏,只是由於近幾個月經歷了一些事情,沒有太多的經從來寫了,可是一些重要的東西,我仍是但願能堅持記錄下來。正好最近公司用到了一些本篇文章的知識點,因此就抽空記錄一下。html
本文代碼github地址:https://github.com/shaweiwei/RibbonTest/tree/masternginx
ribbon 是一個客戶端負載均衡器,它和nginx的負載均衡相比,區別是一個是客戶端負載均衡,一個是服務端負載均衡。ribbon能夠單獨使用,也能夠配合eureka使用。git
1.首先咱們先在原來的基礎上新建一個Ribbon模塊,以下圖:github
如今咱們單獨使用ribbon,在Ribbon模塊下添加依賴,以下圖所示:算法
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-ribbon</artifactId> <version>1.4.0.RELEASE</version> </dependency>
修改application.yml文件,以下所示:spring
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:8081app
下面這個例子是在以前這篇文章的例子上改的,Spring Cloud(二):Spring Cloud Eureka Server高可用註冊服務中心的配置負載均衡
先看下寫好的結構
先介紹下大體功能,EurekaServer提供服務註冊功能,RibbonServer裏會調用ServiceHello裏的接口,ServiceHello和ServiceHello2是一樣的服務,只是爲了方便分佈式部署。
<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>
@SpringBootApplication @EnableEurekaServer public class BootApplication { public static void main(String[] args) { SpringApplication.run(BootApplication.class, args); } }
server.port=8760 spring.application.name=eureka-server #eureka.instance.hostname=peer1 eureka.client.serviceUrl.defaultZone=http://localhost:${server.port}/eureka
紅色部分代碼是關鍵
@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); } }
@Configuration public class RibbonConfig { @Bean @LoadBalanced public RestTemplate restTemplate(){ return new RestTemplate(); } @Bean public IRule ribbonRule() { return new RoundRobinRule(); } }
@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(); } }
server.port=8618 spring.application.name=ribbon-service eureka.client.serviceUrl.defaultZone=http://localhost:8760/eureka/
@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"; } }
server.port=8788 spring.application.name=service-hi eureka.client.serviceUrl.defaultZone=http://localhost:8760/eureka/ #service-hi.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RoundRobinRule
和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調用。
RestTemplate中有一個屬性是List<ClientHttpRequestInterceptor> interceptors,若是interceptors裏面的攔截器數據不爲空,在RestTemplate進行http請求時,這個請求就會被攔截器攔截進行,攔截器實現接口ClientHttpRequestInterceptor,須要實現方法是
ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException;
也就是說攔截器須要完成http請求,並封裝一個標準的response返回。
在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); }; } }
定義了攔截器,天然須要把攔截器注入到、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註解發揮做用的時候了。
嚴格上來講,這個註解是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來實現不一樣的負載方式。