探索springcloud使用com.netflix.ribbon自定义负载均衡loadbalancer

352 阅读5分钟

1、问题描述

在springcloud微服务中使用feign进行远程调用,进行自定义服务的负载均衡策略时,从网上找了相关代码复制粘贴,运行时却发现了一个bug。

@Component
public class MyRule extends RoundRobinRule {
    
    // 自定义负载均衡
    // 从LoadBalancer中的服务选择一个节点
    @Override
    public Server choose(ILoadBalancer lb, Object key) {
        System.out.println(lb.getAllServers());
        // 自定义负载均衡
        return lb.getAllServers().get(0);
    }
}
// 商品服务
@FeignClient(name = "product-service")
public interface ProductFeign {

    @GetMapping("/productInfo")
    String productInfo();
}
// 库存服务
@FeignClient(name = "stock-service")
public interface StockFeign {

    @GetMapping("/stockInfo")
    String stockInfo();
}
// 订单服务 order-service
@RestController
public class OrderController {

    @Autowired
    private ProductFeign productFeign;

    @Autowired
    private StockFeign stockFeign;

    @GetMapping("/createOrder")
    public String createOrder() {
        return productFeign.productInfo() + " : " + stockFeign.stockInfo();
    }
}

由order-service服务对product-service和stock-service服务进行访问,调用order-service服务/createOrder,返回正常结果,但第二次访问时却返回了404,在MyRule的choose方法代码中添加断点,发现第二次访问时首先调用product-service的productInfo方法,但是MyRule的choose方法中的ILoadBalancer对象却是StockFeign的,相应返回的节点ip地址也是stock-service的ip,但是stock-service中并没有/productInfo接口,所以导致了404.

2、问题探索

1、流程探索

从参数ILoadBalancer入手,查看调用来源,发现参数传入的ILoadBalancer对象是父类AbstractLoadBalancerRule的一个属性

public abstract class AbstractLoadBalancerRule implements IRule, IClientConfigAware {

    private ILoadBalancer lb;
        
    @Override
    public void setLoadBalancer(ILoadBalancer lb){
        this.lb = lb;
    }
    
    @Override
    public ILoadBalancer getLoadBalancer(){
        return lb;
    }      
}

对ILoadBalancer属性的赋值也只是通过set方法。到这里基本确认是属性成员ILoadBalancer的问题。

再次通过代码断点进行分析,发现第一次请求order-service服务的createOrder接口首先调用ProductFeign的productInfo时,进行了setLoadBalancer,此时的ILoadBalancer对象是ProductFeign的,然后再调用StockFeign的stockInfo方法时又进行了一次setLoadBalancer,此时的ILoadBalancer对象是StockFeign的;第二次请求product-service服务的productInfo接口,没有再进行setLoadBalance,进入choose方法的ILoadBalancer对象都是StockFeign的。

看到这里,想到如果不使用自定义的负载均衡策略,默认的负载均衡策略也是通过choose方法进行服务选择的,为什么就不会有问题?

继续寻找默认的负责均衡策略的实现类。最终在RibbonClientConfiguration类中找到了默认实现

@Bean
@ConditionalOnMissingBean
public IRule ribbonRule(IClientConfig config) {
    if (this.propertiesFactory.isSet(IRule.class, this.name)) {
        return (IRule)this.propertiesFactory.get(IRule.class, config, this.name);
    } else {
        ZoneAvoidanceRule rule = new ZoneAvoidanceRule();
        rule.initWithNiwsConfig(config);
        return rule;
    }
}

注释掉MyRule,对默认ZoneAvoidanceRule的choose方法打断点分析,进而发现调用ProductFeign和MStockFeign的ZoneAvoidanceRule是两个不同的对象,到这里基本确认问题的原因是MyRule的注入方式不对

进一步探索ZoneAvoidanceRule的注入方式。基于springboot的自动装配,直接去META-INF目录下的spring.factiries文件中找是否有RibbonClientConfiguration类的自动装配。

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.netflix.ribbon.RibbonAutoConfiguration

发现并没有,而是RibbonAutoConfiguration这个类,但其中有一个关键的@Bean,间接使用到了RibbonClientConfiguration。

@Bean
@ConditionalOnMissingBean
public SpringClientFactory springClientFactory() {
    SpringClientFactory factory = new SpringClientFactory();
    factory.setConfigurations(this.configurations);
    return factory;
}
public SpringClientFactory() {
    super(RibbonClientConfiguration.class, "ribbon", "ribbon.client.name");
}

public NamedContextFactory(Class<?> defaultConfigType, String propertySourceName, String propertyName) {
    this.defaultConfigType = defaultConfigType;
    this.propertySourceName = propertySourceName;
    this.propertyName = propertyName;
}

SpringClientFactory继承NamedContextFactory,在进行new实例化的时候,把RibbonClientConfiguration的类对象作为属性赋值给了defaultConfigType。

2、spring父子容器

NamedContextFactory是spring中父子容器对象的工厂

// 子容器对象集合
private Map<String, AnnotationConfigApplicationContext> contexts = new ConcurrentHashMap<>();

// 对应配置类的名称
private Map<String, C> configurations = new ConcurrentHashMap<>();


// 例进行ProductFeign的productInfo方法调用,此时传入的name为服务名product-service
protected AnnotationConfigApplicationContext getContext(String name) {
    // 根据name判断是否存在该子容器
    if (!this.contexts.containsKey(name)) {
        synchronized(this.contexts) {
            // 如果不存在,进行创建
            if (!this.contexts.containsKey(name)) {
                this.contexts.put(name, this.createContext(name));
            }
        }
    }
    // 返回对应的子容器
    return (AnnotationConfigApplicationContext)this.contexts.get(name);
}

// 创建spring子容器
// 例name为product-service
protected AnnotationConfigApplicationContext createContext(String name) {
    AnnotationConfigApplicationContext context;
    if (this.parent != null) {
       DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
       if (parent instanceof ConfigurableApplicationContext) {
          beanFactory.setBeanClassLoader(
                ((ConfigurableApplicationContext) parent).getBeanFactory().getBeanClassLoader());
       }
       else {
          beanFactory.setBeanClassLoader(parent.getClassLoader());
       }
       // 初始化子容器
       context = new AnnotationConfigApplicationContext(beanFactory);
       context.setClassLoader(this.parent.getClassLoader());
    }
    else {
       context = new AnnotationConfigApplicationContext();
    }
    // 从配置类中查找,是否有名称为product-service的配置类
    if (this.configurations.containsKey(name)) {
       for (Class<?> configuration : this.configurations.get(name).getConfiguration()) {
          // 子容器使用该配置类
          context.register(configuration);
       }
    }
    for (Map.Entry<String, C> entry : this.configurations.entrySet()) {
       // 遍历所有配置类,如果以default.开头,加入子容器的配置
       if (entry.getKey().startsWith("default.")) {
          for (Class<?> configuration : entry.getValue().getConfiguration()) {
             context.register(configuration);
          }
       }
    }
    context.register(PropertyPlaceholderAutoConfiguration.class, this.defaultConfigType);
    context.getEnvironment().getPropertySources().addFirst(new MapPropertySource(this.propertySourceName,
          Collections.<String, Object>singletonMap(this.propertyName, name)));
    if (this.parent != null) {
       // Uses Environment from parent as well as beans
       context.setParent(this.parent);
    }
    context.setDisplayName(generateDisplayName(name));
    // 刷新子容器
    context.refresh();
    // 返回子容器
    return context;
}

在SpringCloud中,微服务之间可能因系统的不同而需要不同的远程调用配置。NamedContextFactory通过创建一系列的子容器,并允许一系列的configuration在每个子容器中定义自己的Bean,从而实现了配置的分组和隔离。而configuration的类型是 NamedContextFactory.Specification,就是简单的name和配置类class对象集合。默认使用的ZoneAvoidanceRule只会在子容器中创建

public interface Specification {

    String getName();

    Class<?>[] getConfiguration();

}

image.png

3、解决

了解了基本原理之后,进行解决的话就简单多了,对MyRule换种方式进行注入。

public class MyRule extends RoundRobinRule {
    
    // 自定义负载均衡
    // 从LoadBalancer中的服务选择一个节点
    @Override
    public Server choose(ILoadBalancer lb, Object key) {
        System.out.println(lb.getAllServers());
        // 自定义负载均衡
        return lb.getAllServers().get(0);
    }
}

新增两个配置类

// 加@Configuration,注入到父容器中
@Configuration
public class MyRobinRuleConfig {
    @Bean
    public RibbonClientSpecification myRibbonClientSpecification() {
        // 以default.开头所有FeignClient子容器都会加载该配置类
        return new RibbonClientSpecification("default.myRibbonClientSpecification", new Class[]{myRoundRobinRuleConfig.class});
        // 也可以对特定的FeignClient子容器进行配置
        // return new RibbonClientSpecification("product-service", new Class[]{myRoundRobinRuleConfig.class});
    }
}

// 不加@Configuration,避免注入到父容器中
public class myRoundRobinRuleConfig {

    @Bean
    public MyRule myRule() {
        return new MyRule();
    }
}

netflix的FeignClient使用父子容器,在微服务架构中,不同的服务可能由不同的团队开发、部署和管理。使用父子容器可以确保服务之间的调用保持一定的隔离性。子容器可以独立地管理其内部的Bean,包括FeignClient的实例,这样,即使某个服务出现问题,也不会影响到其他服务或整个系统的稳定性。netflix的Ribbon已经不再维护,但其设计被广泛认可为非常出色。