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


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

    Ribbon使用

    还是使用spring-cloud-alibaba源码的示例。创建客户端应用,并定义RestTemplate类型的bean注册到spring中,并enable服务发现,将它注册到nacos

    1. @SpringBootApplication
    2. @EnableDiscoveryClient(autoRegister = true)
    3. public class ConsumerApplication {
    4. @LoadBalanced
    5. @Bean
    6. public RestTemplate restTemplate() {
    7. return new RestTemplate();
    8. }
    9. public static void main(String[] args) {
    10. SpringApplication.run(ConsumerApplication.class, args);
    11. }
    12. }

    然后定义一个controller,对外提供访问接口 

    1. @RestController
    2. public class TestController {
    3. @Autowired
    4. private RestTemplate restTemplate;
    5. @GetMapping("/echo-rest/{str}")
    6. public String rest(@PathVariable String str) {
    7. //service-provider为服务端应用名称
    8. return restTemplate.getForObject("http://service-provider/echo/" + str,
    9. String.class);
    10. }
    11. }

    创建服务端应用,并定义如下类,也将服务注册到nacos中。

    1. @EnableDiscoveryClient
    2. @SpringBootApplication
    3. public class ProviderApplication {
    4. public static void main(String[] args) {
    5. SpringApplication.run(ProviderApplication.class, args);
    6. }
    7. @RestController
    8. class EchoController {
    9. @GetMapping("/echo/{string}")
    10. public String echo(@PathVariable String string) {
    11. System.out.println("hello Nacos Discovery " + string);
    12. return "hello Nacos Discovery " + string;
    13. }
    14. }
    15. }

    如此,ribbon的使用进行测试即可,还是很简单的。

    Ribbon客户端负载均衡

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

    1. //@LoadBalanced
    2. @Bean
    3. public RestTemplate restTemplate() {
    4. return new RestTemplate();
    5. }

    @LoadBalanced注解原理

    点开 LoadBalanced注解类,没有参数,只有一个@Qualifier注解,一直在想这是springcloud的注解,解析这个注解的类肯定在spring-cloud-commons中,但是让我失望的是并没有解析这个注解的类,于是上网搜,琢磨了一会才注意到重点就在@Qualifier这个注解中

    1. @Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD})
    2. @Retention(RetentionPolicy.RUNTIME)
    3. @Documented
    4. @Inherited
    5. @Qualifier
    6. public @interface LoadBalanced {
    7. }

    另外,在spring-cloud-commons中还有一个自动配置类,是跟这个注解有关的。

    1. @Configuration(
    2. proxyBeanMethods = false
    3. )
    4. @ConditionalOnClass({RestTemplate.class})
    5. @ConditionalOnBean({LoadBalancerClient.class})
    6. @EnableConfigurationProperties({LoadBalancerRetryProperties.class})
    7. public class LoadBalancerAutoConfiguration {
    8. @LoadBalanced
    9. @Autowired(
    10. required = false
    11. )
    12. private List restTemplates = Collections.emptyList();
    13. @Configuration(
    14. proxyBeanMethods = false
    15. )
    16. @ConditionalOnMissingClass({"org.springframework.retry.support.RetryTemplate"})
    17. static class LoadBalancerInterceptorConfig {
    18. LoadBalancerInterceptorConfig() {
    19. }
    20. @Bean
    21. public LoadBalancerInterceptor loadBalancerInterceptor(LoadBalancerClient loadBalancerClient, LoadBalancerRequestFactory requestFactory) {
    22. return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
    23. }
    24. @Bean
    25. @ConditionalOnMissingBean
    26. public RestTemplateCustomizer restTemplateCustomizer(final LoadBalancerInterceptor loadBalancerInterceptor) {
    27. return (restTemplate) -> {
    28. List list = new ArrayList(restTemplate.getInterceptors());
    29. list.add(loadBalancerInterceptor);
    30. restTemplate.setInterceptors(list);
    31. };
    32. }
    33. }
    34. }

    首先要知道spring是先解析程序员定义的RestTemplate的bean,在解析springboot自动装配类的,所以所有被@LoadBalanced注解修饰的RestTemplate被解析完成后,会全部被注入到LoadBalancerAutoConfiguration中restTemplates集合中,随后spring在解析LoadBalancerInterceptorConfig配置类时遍历restTemplates,将LoadBalancerInterceptor设置到每一个restTemplate中,如此就完成了对restTemplate的改造。

    1. public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {
    2. private LoadBalancerClient loadBalancer;
    3. private LoadBalancerRequestFactory requestFactory;
    4. ...
    5. public ClientHttpResponse intercept(final HttpRequest request, final byte[] body, final ClientHttpRequestExecution execution) throws IOException {
    6. URI originalUri = request.getURI();
    7. String serviceName = originalUri.getHost();
    8. Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
    9. return (ClientHttpResponse)this.loadBalancer.execute(serviceName, this.requestFactory.createRequest(request, body, execution));
    10. }
    11. }

     LoadBalancerInterceptor是对所有使用restTemplate的请求做拦截改造,例如刚刚测试没有被@LoadBalanced注解的restTemplate无法解析service-provider这个应用名称就跟这个类的intercept()方法有关,所以它不仅支持客户端的负载均衡,还负责解析应用名称将它替换为ip:port。在loadBalancer.execute()方法中会进行负载均衡。

    负载均衡原理详解

    为了更好地理解负载均衡策略的原理,我们自己简单的实现基于Nacos权重的负载均衡策略:

    1. public class NacosRandomWithWeightRule extends AbstractLoadBalancerRule {
    2. Logger log = LoggerFactory.getLogger(NacosRandomWithWeightRule.class);
    3. @Autowired
    4. private NacosDiscoveryProperties nacosDiscoveryProperties;
    5. @Override
    6. public Server choose(Object key) {
    7. DynamicServerListLoadBalancer loadBalancer = (DynamicServerListLoadBalancer) getLoadBalancer();
    8. String serviceName = loadBalancer.getName();
    9. NamingService namingService = nacosDiscoveryProperties.namingServiceInstance();
    10. try {
    11. //nacos基于权重的算法
    12. Instance instance = namingService.selectOneHealthyInstance(serviceName);
    13. return new NacosServer(instance);
    14. } catch (NacosException e) {
    15. log.error("获取服务实例异常:{}", e.getMessage());
    16. e.printStackTrace();
    17. }
    18. return null;
    19. }
    20. @Override
    21. public void initWithNiwsConfig(IClientConfig clientConfig) {
    22. }
    23. }

    只需继承AbstractLoadBalancerRule 或者实现IRule接口并实现choose()方法,choose()基于nacos权重实实现负载均衡算法。随后在主类中添加如下代码,Ribbon的默认负载均衡策略就被替换为我们实现的策略了。

    1. @Bean
    2. public IRule ribbonRule() {
    3. return new NacosRandomWithWeightRule();
    4. }

    LoadBalancerInterceptor源码解析

    客户端在发送请求时,这个请求会被LoadBalancerInterceptor#intercept()方法拦截(其实是发送请求时spring在调用该方法),在执行到loadBalancer.execute()方法时,会调用到如下方法

    1. public T execute(String serviceId, LoadBalancerRequest request, Object hint) throws IOException {
    2. ILoadBalancer loadBalancer = this.getLoadBalancer(serviceId);
    3. Server server = this.getServer(loadBalancer, hint);
    4. if (server == null) {
    5. throw new IllegalStateException("No instances available for " + serviceId);
    6. } else {
    7. RibbonLoadBalancerClient.RibbonServer ribbonServer = new RibbonLoadBalancerClient.RibbonServer(serviceId, server, this.isSecure(server, serviceId), this.serverIntrospector(serviceId).getMetadata(server));
    8. return this.execute(serviceId, (ServiceInstance)ribbonServer, (LoadBalancerRequest)request);
    9. }
    10. }

    该方法的第一个参数serviceId是服务端应用名称,首先执行getLoadBalancer()通过serviceId从spring容器中获得一个负载均衡器,而这个负载均衡器默认就是ZoneAwareLoadBalancer。紧接着调用getServer()方法,它会去调用ZoneAwareLoadBalancer#chooseServer()方法

    1. public Server chooseServer(Object key) {
    2. if (ENABLED.get() && this.getLoadBalancerStats().getAvailableZones().size() > 1) {
    3. ...
    4. if (zone != null) {
    5. BaseLoadBalancer zoneLoadBalancer = this.getLoadBalancer(zone);
    6. server = zoneLoadBalancer.chooseServer(key);
    7. }
    8. }
    9. ...
    10. } else {
    11. logger.debug("Zone aware logic disabled or there is only one zone");
    12. return super.chooseServer(key);
    13. }
    14. }

    上面代码段省略了很多源码,首先判断zone是配置了多个(>1),这里只有一个默认的unknown所以执行else的逻辑super.chooseServer();

    1. public Server chooseServer(Object key) {
    2. ...
    3. if (this.rule == null) {
    4. return null;
    5. } else {
    6. try {
    7. return this.rule.choose(key);
    8. } catch (Exception var3) {
    9. logger.warn("LoadBalancer [{}]: Error choosing server for key {}", new Object[]{this.name, key, var3});
    10. return null;
    11. }
    12. }
    13. }

    然后就执行rule.choose()如果程序员配置了自己实现的负载均衡策略,就会执行,否则执行下面的PredicateBaseRule。通过负载均衡策略选择一个符合要求的服务实例替换服务的应用名称,将请求发送出去。 

    小结

    本文讲述了ribbon的使用以及功能,在服务调用的过程中,客户端通过ribbon实现的负载均衡,对应用名称进行替换;其次讲述了@LoadBalanced注解的原理;通过实现一个负载均衡策略来加深对Ribbon的理解;并从源码分析了一个http请求是如何被LoadBalancerInterceptor处理的。

  • 相关阅读:
    STL中string类的实现
    C# WinForm —— 23 Timers.Timer 组件介绍与使用
    Python实验三:Python程序设计之组合数据类型
    错误:软件包:kubelet-1.14.2-0.x86_64 (kubernetes)
    比特币已胜
    模电学习路径
    【七夕快乐篇】Nacos是如何实现服务注册功能的?
    Java核心编程(20)
    设计模式:享元模式
    JAVA JVM 是怎么判定对象已经“死去”?
  • 原文地址:https://blog.csdn.net/weixin_36279234/article/details/126139141