继上篇博客学习了nacos的使用,今天继续学习Ribbon和Feign的使用,看Ribbon是如何实现客户端负载均衡,如何实现自己的负载均衡策略?
还是使用spring-cloud-alibaba源码的示例。创建客户端应用,并定义RestTemplate类型的bean注册到spring中,并enable服务发现,将它注册到nacos
- @SpringBootApplication
- @EnableDiscoveryClient(autoRegister = true)
- public class ConsumerApplication {
-
- @LoadBalanced
- @Bean
- public RestTemplate restTemplate() {
- return new RestTemplate();
- }
-
- public static void main(String[] args) {
- SpringApplication.run(ConsumerApplication.class, args);
- }
-
- }
然后定义一个controller,对外提供访问接口
- @RestController
- public class TestController {
-
- @Autowired
- private RestTemplate restTemplate;
-
- @GetMapping("/echo-rest/{str}")
- public String rest(@PathVariable String str) {
- //service-provider为服务端应用名称
- return restTemplate.getForObject("http://service-provider/echo/" + str,
- String.class);
- }
-
- }
创建服务端应用,并定义如下类,也将服务注册到nacos中。
- @EnableDiscoveryClient
- @SpringBootApplication
- public class ProviderApplication {
-
- public static void main(String[] args) {
- SpringApplication.run(ProviderApplication.class, args);
- }
-
- @RestController
- class EchoController {
-
- @GetMapping("/echo/{string}")
- public String echo(@PathVariable String string) {
- System.out.println("hello Nacos Discovery " + string);
- return "hello Nacos Discovery " + string;
- }
-
- }
-
- }
如此,ribbon的使用进行测试即可,还是很简单的。
假设现在将定义在RestTemplate上的@LoadBalanced注解注释掉,会发现客户端在发送请求之前无法识别service-provider这个应用名称
- //@LoadBalanced
- @Bean
- public RestTemplate restTemplate() {
- return new RestTemplate();
- }
点开 LoadBalanced注解类,没有参数,只有一个@Qualifier注解,一直在想这是springcloud的注解,解析这个注解的类肯定在spring-cloud-commons中,但是让我失望的是并没有解析这个注解的类,于是上网搜,琢磨了一会才注意到重点就在@Qualifier这个注解中
- @Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD})
- @Retention(RetentionPolicy.RUNTIME)
- @Documented
- @Inherited
- @Qualifier
- public @interface LoadBalanced {
- }
另外,在spring-cloud-commons中还有一个自动配置类,是跟这个注解有关的。
- @Configuration(
- proxyBeanMethods = false
- )
- @ConditionalOnClass({RestTemplate.class})
- @ConditionalOnBean({LoadBalancerClient.class})
- @EnableConfigurationProperties({LoadBalancerRetryProperties.class})
- public class LoadBalancerAutoConfiguration {
- @LoadBalanced
- @Autowired(
- required = false
- )
- private List
restTemplates = Collections.emptyList(); -
- @Configuration(
- proxyBeanMethods = false
- )
- @ConditionalOnMissingClass({"org.springframework.retry.support.RetryTemplate"})
- static class LoadBalancerInterceptorConfig {
- LoadBalancerInterceptorConfig() {
- }
-
- @Bean
- public LoadBalancerInterceptor loadBalancerInterceptor(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);
- };
- }
- }
- }
首先要知道spring是先解析程序员定义的RestTemplate的bean,在解析springboot自动装配类的,所以所有被@LoadBalanced注解修饰的RestTemplate被解析完成后,会全部被注入到LoadBalancerAutoConfiguration中restTemplates集合中,随后spring在解析LoadBalancerInterceptorConfig配置类时遍历restTemplates,将LoadBalancerInterceptor设置到每一个restTemplate中,如此就完成了对restTemplate的改造。
- public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {
- private LoadBalancerClient loadBalancer;
- private LoadBalancerRequestFactory requestFactory;
-
- ...
-
- public ClientHttpResponse intercept(final HttpRequest request, final byte[] body, final ClientHttpRequestExecution execution) throws IOException {
- URI originalUri = request.getURI();
- String serviceName = originalUri.getHost();
- Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
- return (ClientHttpResponse)this.loadBalancer.execute(serviceName, this.requestFactory.createRequest(request, body, execution));
- }
- }
LoadBalancerInterceptor是对所有使用restTemplate的请求做拦截改造,例如刚刚测试没有被@LoadBalanced注解的restTemplate无法解析service-provider这个应用名称就跟这个类的intercept()方法有关,所以它不仅支持客户端的负载均衡,还负责解析应用名称将它替换为ip:port。在loadBalancer.execute()方法中会进行负载均衡。
为了更好地理解负载均衡策略的原理,我们自己简单的实现基于Nacos权重的负载均衡策略:
- public class NacosRandomWithWeightRule extends AbstractLoadBalancerRule {
-
- Logger log = LoggerFactory.getLogger(NacosRandomWithWeightRule.class);
-
- @Autowired
- private NacosDiscoveryProperties nacosDiscoveryProperties;
-
- @Override
- public Server choose(Object key) {
- DynamicServerListLoadBalancer loadBalancer = (DynamicServerListLoadBalancer) getLoadBalancer();
- String serviceName = loadBalancer.getName();
- NamingService namingService = nacosDiscoveryProperties.namingServiceInstance();
- try {
- //nacos基于权重的算法
- Instance instance = namingService.selectOneHealthyInstance(serviceName);
- return new NacosServer(instance);
- } catch (NacosException e) {
- log.error("获取服务实例异常:{}", e.getMessage());
- e.printStackTrace();
- }
- return null;
- }
-
- @Override
- public void initWithNiwsConfig(IClientConfig clientConfig) {
-
- }
- }
只需继承AbstractLoadBalancerRule 或者实现IRule接口并实现choose()方法,choose()基于nacos权重实实现负载均衡算法。随后在主类中添加如下代码,Ribbon的默认负载均衡策略就被替换为我们实现的策略了。
- @Bean
- public IRule ribbonRule() {
- return new NacosRandomWithWeightRule();
- }
LoadBalancerInterceptor源码解析
客户端在发送请求时,这个请求会被LoadBalancerInterceptor#intercept()方法拦截(其实是发送请求时spring在调用该方法),在执行到loadBalancer.execute()方法时,会调用到如下方法
- public
T execute(String serviceId, LoadBalancerRequest request, Object hint) throws IOException { - ILoadBalancer loadBalancer = this.getLoadBalancer(serviceId);
- Server server = this.getServer(loadBalancer, hint);
- if (server == null) {
- throw new IllegalStateException("No instances available for " + serviceId);
- } else {
- RibbonLoadBalancerClient.RibbonServer ribbonServer = new RibbonLoadBalancerClient.RibbonServer(serviceId, server, this.isSecure(server, serviceId), this.serverIntrospector(serviceId).getMetadata(server));
- return this.execute(serviceId, (ServiceInstance)ribbonServer, (LoadBalancerRequest)request);
- }
- }
该方法的第一个参数serviceId是服务端应用名称,首先执行getLoadBalancer()通过serviceId从spring容器中获得一个负载均衡器,而这个负载均衡器默认就是ZoneAwareLoadBalancer。紧接着调用getServer()方法,它会去调用ZoneAwareLoadBalancer#chooseServer()方法
- public Server chooseServer(Object key) {
- if (ENABLED.get() && this.getLoadBalancerStats().getAvailableZones().size() > 1) {
- ...
- if (zone != null) {
- BaseLoadBalancer zoneLoadBalancer = this.getLoadBalancer(zone);
- server = zoneLoadBalancer.chooseServer(key);
- }
- }
- ...
- } else {
- logger.debug("Zone aware logic disabled or there is only one zone");
- return super.chooseServer(key);
- }
- }
上面代码段省略了很多源码,首先判断zone是配置了多个(>1),这里只有一个默认的unknown所以执行else的逻辑super.chooseServer();
- public Server chooseServer(Object key) {
- ...
- if (this.rule == null) {
- return null;
- } else {
- try {
- return this.rule.choose(key);
- } catch (Exception var3) {
- logger.warn("LoadBalancer [{}]: Error choosing server for key {}", new Object[]{this.name, key, var3});
- return null;
- }
- }
- }
然后就执行rule.choose()如果程序员配置了自己实现的负载均衡策略,就会执行,否则执行下面的PredicateBaseRule。通过负载均衡策略选择一个符合要求的服务实例替换服务的应用名称,将请求发送出去。
本文讲述了ribbon的使用以及功能,在服务调用的过程中,客户端通过ribbon实现的负载均衡,对应用名称进行替换;其次讲述了@LoadBalanced注解的原理;通过实现一个负载均衡策略来加深对Ribbon的理解;并从源码分析了一个http请求是如何被LoadBalancerInterceptor处理的。