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();
}
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已经不再维护,但其设计被广泛认可为非常出色。