这是SpringCloud实战系列中第三篇文章,了解前面第两篇文章更有助于更好理解本文内容:html
①SpringCloud 实战:引入Eureka组件,完善服务治理java
②SpringCloud 实战:引入Feign组件,发起服务间调用git
Ribbon 是由 Netflix 发布的一个客户端负载均衡器,它提供了对 HTTP 和 TCP 客户端行为的大量控制。Ribbon 能够基于某些负载均衡的算法,自动为客户端选择发起理论最优的网络请求。常见的负载均衡算法有:轮询,随机,哈希,加权轮询,加权随机等。github
客户端负载均衡的意思就是发起网络请求的端根据本身的网络请求状况来作相应的负载均衡策略,与之相对的非客户端负载均衡就有好比硬件F五、软件Nginx,它们更可能是介于消费者和提供者之间的,并不是客户端。算法
在使用以前咱们先把第二节里面的 eureka-provider 项目改造一下,在HelloController 里面新增一个接口,输出本身项目的端口信息,用于区别验证待会儿客户端负载均衡时所调用的服务。spring
新增接口方法,返回本身的端口号信息:api
@Controller public class HelloController{ @Value("${server.port}") private int serverPort; ... @ResponseBody @GetMapping("queryPort") public String queryPort(){ return "hei, jinglingwang, my server port is:"+serverPort; } }
分别以8082,8083,8084端口启动该项目:eureka-provider
下图是 IDEA 快速启动三个不一样端口项目方法截图,固然你也能够用其余办法网络
而后启动,访问三个接口测试一下是否正常返回了对应端口app
至此,服务提供者的接口准备工做就作好了。负载均衡
咱们使用 Spring Initializr 生成SpringCloud项目基础框架,而后修改pom.xml里面的SpringBoot和SpringCloud的版本,对应版本修改请求以下:
... <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.3.RELEASE</version> <!--修改了版本jinglingwang.cn--> <relativePath/> <!-- lookup parent from repository --> </parent> ... 略 <properties> <java.version>1.8</java.version> <spring-cloud.version>Finchley.SR4</spring-cloud.version><!--修改了版本--> </properties> ... 略 <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
为何要单独修改版本呢?由于从 Spring Cloud Hoxton.M2 版本开始,Spring Cloud 已经再也不默认使用Ribbon来作负载均衡了,而是使用 spring-cloud-starter-loadbalancer
替代。因此咱们在使用 Spring Initializr 生成项目框架的时,若是使用最新版本Spring Cloud将再也不提供Ribbon相关的组件。须要咱们本身引入或者使用低一点的版本。
以后就是在ribbon-client项目引入eureka-client依赖和openfeign的依赖,这个过程省略,若是不会的话请看前两篇文章。
咱们在新建的ribbon-client项目里面来使用三种方式调用eureka-provider
的queryPort接口,由于eureka-provider服务启动了三个节点,到时候只要观察三种方式的响应结果,就能够判断负载均衡是否有生效。
直接使用LoadBalancerClient来得到对应的实例,而后发起URL请求,编写对应的RibbonController:
@RestController public class RibbonController{ @Autowired private LoadBalancerClient loadBalancer; @GetMapping("ribbonApi") public String ribbonApi() throws Exception{ ServiceInstance instance = loadBalancer.choose("eureka-provider"); System.out.println(instance.getUri()); URL url = new URL("http://localhost:" + instance.getPort() + "/queryPort"); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); InputStream inputStream = conn.getInputStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); String line = null; StringBuffer buffer = new StringBuffer(); while ((line = reader.readLine()) != null) { buffer.append(line); } reader.close(); return Observable.just(buffer.toString()).toBlocking().first(); } }
启动Ribbon-Client服务,访问http://localhost:7071/ribbonApi 接口,屡次刷新接口发现采用的是轮询方式,运行效果图以下:
使用 RestTemplate 的话,咱们只须要再结合@LoadBalanced注解一块儿使用便可:
@Configuration public class RestTemplateConfig{ @Bean @LoadBalanced public RestTemplate restTemplate(){ return new RestTemplate(); } }
编写RibbonController:
@RestController public class RibbonController{ @Autowired private RestTemplate restTemplate; @GetMapping("queryPortByRest") public String queryPortByRest(){ return restTemplate.getForEntity("http://eureka-provider/queryPort",String.class).getBody(); } }
启动ribbon-client服务,访问http://localhost:7071/queryPortByRest 接口,屡次刷新接口发现采用的也是轮询方式,运行效果图以下:
新建一个Feign:
@FeignClient(value = "eureka-provider") public interface ProviderFeign{ /** * 调用服务提供方,其中会返回服务提供者的端口信息 * @return jinglingwang.cn */ @RequestMapping("/queryPort") String queryPort(); }
编写调用接口:
@RestController public class RibbonController{ ...略 @Autowired private ProviderFeign providerFeign; ... @GetMapping("queryPort") public String queryPort(){ // 经过feign ribbon-client 调用 eureka-provider return providerFeign.queryPort(); } }
启动ribbon-client服务,访问 http://localhost:7071/queryPort 接口,屡次刷新接口发现采用的也是轮询方式,运行效果图以下:
在配置以前先作一点准备工做,咱们把以前的服务eureka-provider再起3个节点,启动以前把端口改成808五、808六、8087,三个节点的服务名改成eureka-provider-temp。这样作的目的是等会儿咱们新建一个Feign,可是名字和以前的区分开,至关于两个不一样的服务,而且都是多节点的。
以上准备工做作完以后你会在IDEA中看到以下图的6个服务:
在注册中心也能够观察到2个不一样的服务,一共6个节点:
eureka-provide 和 eureka-provide-temp 他们惟一的区别就是服务名不同、端口不同。
如今开始为Feign配置ribbon:
新建一个Feign,命名为:ProviderTempFeign
@FeignClient(value = "eureka-provider-temp") public interface ProviderTempFeign{ @RequestMapping("/queryPort") String queryPort(); }
使用JAVA Bean的方式定义配置项
public class ProviderTempConfiguration{ @Bean public IRule ribbonRule(){ System.out.println("new ProviderTempConfiguration RandomRule"); return new RandomRule(); // 定义一个随机的算法 } @Bean public IPing ribbonPing() { // return new PingUrl(); return new NoOpPing(); } }
使用注解@RibbonClient
配置负载均衡客户端:
@RibbonClient(name = "eureka-provider-temp",configuration = ProviderTempConfiguration.class) public class ProviderTempRibbonClient{ }
在Controller新增一个接口,来调用新增Feign(eureka-provider-temp)的方法
@GetMapping("queryTempPort") public String queryTempPort(){ return providerTempFeign.queryPort(); }
再为另外一个Feign(eureka-provider
)也配置一下ribbon,对外接口仍是上面已经写好了
public class ProviderConfiguration{ @Bean public IRule ribbonRule(){ System.out.println("new ProviderConfiguration BestAvailableRule"); return new BestAvailableRule(); // 选择的最佳策略 } @Bean public IPing ribbonPing() { // return new PingUrl(); return new NoOpPing(); } } @RibbonClient(name = "eureka-provider",configuration = ProviderConfiguration.class) public class ProviderRibbonClient{ }
启动服务以后分别访问两个接口(http://localhost:7071/queryPort 和 http://localhost:7071/queryTempPort),观察接口的端口返回状况
若是以上过程顺利的话,你访问queryPort接口的时候返回的端口不是随机的,几乎没怎么变化,访问queryTempPort接口的时候,接口返回的端口是随机的,说明咱们以上配置是可行的。并且第一次访问接口的时候,咱们在控制台打印了出对应的算法规则,你能够观察一下。
以上的配置也能够写到配置文件中,效果是同样的:
# 经过配置文件 分别为每一个客户端配置 eureka-provider.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.BestAvailableRule eureka-provider.ribbon.NFLoadBalancerPingClassName=com.netflix.loadbalancer.NoOpPing eureka-provider-temp.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RandomRule eureka-provider-temp.ribbon.NFLoadBalancerPingClassName=com.netflix.loadbalancer.NoOpPing
配置的规则是:
CommonClientConfigKey.class
类中查看。经常使用的有:
NFLoadBalancerClassName NFLoadBalancerRuleClassName NFLoadBalancerPingClassName NIWSServerListClassName NIWSServerListFilterClassName
这里须要用到的注解是@RibbonClients
@Configuration() public class DefaultRibbonConfiguration{ @Bean public IRule iRule() { // 轮询 return new RoundRobinRule(); } @Bean public IPing ribbonPing() { return new DummyPing(); } } @RibbonClients(defaultConfiguration = DefaultRibbonConfiguration.class) public class DefaultRibbonClient{ ****}
启动咱们的ribbon-client服务,测试访问下咱们的http://localhost:7071/queryPort 接口,发现返回的数据每次都不同,变为轮询的方式返回接口信息了。
测试到这里的时候,配置文件中的相关配置我并无注释掉,Java Bean方式的@RibbonClient被注释掉了,也就是说测试的时候同时配置了配置文件和@RibbonClients,最后测试下来是@RibbonClients配置生效了,配置文件中配置的策略没有生效。
测试下来,@RibbonClients 的优先级最高,以后是配置文件,再是@RibbonClient,最后是Spring Cloud Netflix 默认值。
若是同时使用@RibbonClients和@RibbonClient,全局默认配置和自定义单个ribbon配置,会按照哪一个配置生效呢?
我把配置文件中的相关配置都注释,而后把两个配置 @RibbonClient 的地方都放开,而后重启项目,访问http://localhost:7071/queryPort 和 http://localhost:7071/queryTempPort
测试结果是都报错,报错信息以下:
org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'com.netflix.loadbalancer.IRule' available: expected single matching bean but found 2: providerRule,iRule
报错信息的意思是预期须要一个bean,可是结果找到了两个(providerRule 和 iRule),结果不知道该用哪个了,因此抛出异常。
那这个问题怎么解决呢?
首先直接说结论吧,就是给你想要生效的那个bean加@Primary
注解,代码以下所示,若是eureka-provider
不加仍是会继续报错:
public class ProviderTempConfiguration{ @Primary @Bean("providerTempRule") public IRule ribbonRule(){ System.out.println("new ProviderTempConfiguration RandomRule"); return new RandomRule(); } ... }
再说下排查这个问题的思路:
经过查看异常输出栈的错误日志信息,定位到抛出异常的地方
以后继续往前面找相关的逻辑,加断点,慢慢调试,发现有一个字段(autowiredBeanName
)为空,才会进入到后面抛异常的逻辑
断点也显示matchingBeans里面有两条数据,说明确实是匹配到了2个bean
而后咱们进入到determineAutowireCandidate
方法,发现里面有个看起来很不通常的字段:primaryCandidate
,若是这个字段不为空,会直接返回,那这个字段的值是怎么确认的呢?
继续进入到determinePrimaryCandidate
方法,发现这个方法的主要功能就是从给定的多个bean中肯定一个主要的候选对象bean,说白了就是选一个bean,那这个方法是怎么选的呢?上源代码:
@Nullable protected String determinePrimaryCandidate(Map<String, Object> candidates, Class<?> requiredType) { String primaryBeanName = null; // candidates 是匹配到的多个bean // requiredType 是要匹配的目标依赖类型 for (Map.Entry<String, Object> entry : candidates.entrySet()) { // 遍历map String candidateBeanName = entry.getKey(); Object beanInstance = entry.getValue(); if (isPrimary(candidateBeanName, beanInstance)) { // 最重要的逻辑,看是否是主要的bean,看到这有经验的其实都知道要加@Primary注解了 if (primaryBeanName != null) { boolean candidateLocal = containsBeanDefinition(candidateBeanName); boolean primaryLocal = containsBeanDefinition(primaryBeanName); if (candidateLocal && primaryLocal) { throw new NoUniqueBeanDefinitionException(requiredType, candidates.size(), "more than one 'primary' bean found among candidates: " + candidates.keySet()); } else if (candidateLocal) { primaryBeanName = candidateBeanName; } } else { primaryBeanName = candidateBeanName; } } } return primaryBeanName; }
进入到isPrimary(candidateBeanName, beanInstance)
方法,最后实际就是返回的如下逻辑:
@Override public boolean isPrimary() { return this.primary; }
因此解决上面的问题,只须要在咱们的ProviderTempConfiguration类里面为bean 再添加一个@Primary
注解
全局默认配置
# 全局ribbon超时时间 #读超时 ribbon.ReadTimeout=3000 #链接超时 ribbon.ConnectTimeout=3000 #同一台实例最大重试次数,不包括首次调用 ribbon.MaxAutoRetries=0 #重试负载均衡其余的实例最大重试次数,不包括首次调用 ribbon.MaxAutoRetriesNextServer=1
为每一个client单独配置
# 为每一个服务单独配置超时时间 eureka-provider.ribbon.ReadTimeout=4000 eureka-provider.ribbon.ConnectTimeout=4000 eureka-provider.ribbon.MaxAutoRetries=0 eureka-provider.ribbon.MaxAutoRetriesNextServer=1
Ribbon定义了如下几个属性支持自定义配置:
<clientName>.ribbon.NFLoadBalancerClassName: Should implement ILoadBalancer <clientName>.ribbon.NFLoadBalancerRuleClassName: Should implement IRule <clientName>.ribbon.NFLoadBalancerPingClassName: Should implement IPing <clientName>.ribbon.NIWSServerListClassName: Should implement ServerList <clientName>.ribbon.NIWSServerListFilterClassName: Should implement ServerListFilter
这里以自定义负载均衡策略规则为例,只须要实现IRule
接口或者继承AbstractLoadBalancerRule
:
public class MyRule implements IRule{ private static Logger log = LoggerFactory.getLogger(MyRule.class); private ILoadBalancer lb; @Override public Server choose(Object key){ if (lb == null) { return null; } Server server = null; while (server == null) { if (Thread.interrupted()) { return null; } List<Server> allList = lb.getAllServers(); int serverCount = allList.size(); if (serverCount == 0) { log.warn("No up servers available from load balancer: " + lb); return null; } // 是轮询、随机、加权、hash?本身实现从server list中选择一个server // 这里写简单点,老是请求第一台服务,这样的逻辑是不会用到真实的环境的 server = allList.get(0); } return server; } @Override public void setLoadBalancer(ILoadBalancer lb){ this.lb = lb; } @Override public ILoadBalancer getLoadBalancer(){ return lb; } }
而后就能够用Java Bean的方式或者配置文件的方式进行配置了,其余像自定义ping的策略也差很少。
@RibbonClients
或@RibbonClient
注解来负载均衡相关策略的配置CommonClientConfigKey
类中查看代码示例:Github ribbon client