Ribbon的使用、拓展机制以及源码分析

继上篇博客学习了nacos的使用,今天继续学习Ribbon和Feign的使用,看Ribbon是如何实现客户端负载均衡,如何实现自己的负载均衡策略?

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的使用进行测试即可,还是很简单的。

Ribbon客户端负载均衡

假设现在将定义在RestTemplate上的@LoadBalanced注解注释掉,会发现客户端在发送请求之前无法识别service-provider这个应用名称

    //@LoadBalanced
	@Bean
	public RestTemplate restTemplate() {
		return new RestTemplate();
	}

@LoadBalanced注解原理

点开 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<RestTemplate> 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<ClientHttpRequestInterceptor> 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> T execute(String serviceId, LoadBalancerRequest<T> 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处理的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值