网关层针对各微服务动态修改Ribbon路由策略

目录

一、介绍

二、常规的微服务设置路由算法方式

三、通过不懈努力,找到解决思路

四、验证

五、总结


一、介绍

最近,遇到这么一个需求:

1、需要在网关层(目前使用zuul)为某一个服务指定自定义算法IP Hash路由策略

2、除第一次改造重启后,后续为微服务添加路由算法时,zuul网关不能重启,因为会导致用户短时间内不会使用,也就是说,需要动态的为服务修改路由算法

基于上诉两点,本人查找过不少资料,发现没有找到符合的解决方案,也可能是关键词条不准确的问题,导致很长一段时间陷入泥潭,后来通过编程式选择服务进行远程调用上,找到了修改的思路。

二、常规的微服务设置路由算法方式

通过网上查找资料,发现有以下两种方式进行配置:配置文件、@RibbonClient或@RibbonClients

为了方便后续这两种方式的使用案例,以下先提供一个简单的IP Hash路由算法,根据ip去计算索引:

java 复制代码
public class MyRule extends AbstractLoadBalancerRule {

    @Override
    public void initWithNiwsConfig(IClientConfig clientConfig) {
        
    }

    @Override
    public Server choose(Object key) {
        return this.choose(this.getLoadBalancer(), key);
    }

    public Server choose(ILoadBalancer lb, Object key) {
        if (lb == null) {
            return null;
        }

        List<Server> reachableServers = lb.getReachableServers();
        // 排序一次,避免从新拉取的服务顺序不一致
        reachableServers = reachableServers.stream().sorted(Comparator.comparing(Server::getId)).collect(Collectors.toList());

        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        String remoteAddr = request.getRemoteAddr();

        int hashCode = Math.abs(remoteAddr.hashCode());
        
        int  index = hashCode % reachableServers.size();
        
        return reachableServers.get(index);
    }
}

1、配置文件方式

需要在配置文件中加入以下配置

java 复制代码
# 你的服务名
your-service-name:
  ribbon:
    NFLoadBalancerRuleClassName: com.xxx.yyy.MyRule

通过配置文件配置的方式,发现,启动网关项目之后,通过网关访问该服务的接口,自定的路由策略能够生效。

但是,当后续远程配置文件发生更新之后,也就是,当我们在配置文件中给其他服务也添加上自定义路由算法时(上诉配置复制一份,修改服务名即可),然后执行refresh或bus-refresh通知网关服务进行配置刷新,发现,新增上去的服务还是默认使用轮询算法,也就意味着无法动态为微服务修改路由算法,需要想要生效,就得重启网关服务。

目前经过我自己测试,发现是无法动态修改算法的,也可能是我操作有误之类的。

2、@RibbonClient或@RibbonClients方式

这种方式就不可能实现动态算法修改了,但以下还是简单介绍下这种方式的使用。

1)需要准备一个配置类如下,有一点非常重要,这个配置类不能在标注了@SpringBootApplication注解的启动类扫描路径下,否则,该算法将被全局共享,而达不到微服务定制化。

java 复制代码
@Configuration
public class MyRuleConfig {
    
    @Bean
    public IRule myRule() {
        return new MyRule();
    }
    
}

2)可以在启动器类上添加如下注解

java 复制代码
@RibbonClient(name = "your-servcie-name", configuration = {MyRuleConfig.class})

java 复制代码
@RibbonClients(value = {
        @RibbonClient(name = "your-servcie-name", configuration = {MyRuleConfig.class})
})

重启网关服务后,通过网关访问该服务,就可以发现使用了自定义的路由算法。

三、通过不懈努力,找到解决思路

温馨提示:这一节涉及到了源码,如果有不想看的小伙伴,直接跳到第四节即可。

1、上述两种方式都不能实现动态配置微服务的路由算法,我查找了很多资料,也没有找到有相对应的解决方案,长时间陷入泥潭。

于是,我换了个思路,能不能通过编程式的方式去修改微服务的路由算法,就是我不通过配置文件,也不通过注解,就通过某个类或类对象,去修改微服务的路由算法,抱着试一试的心态,就去查找到了手动选择微服务实例的实例代码,如下:

java 复制代码
@Resource
LoadBalancerClient loadBalancerClient;

ServiceInstance serviceInstance = loadBalancerClient.choose("USERINFO-SERVICE");

2、简单看看ServiceInstance

java 复制代码
public interface ServiceInstance {

	/**
	 * @return The unique instance ID as registered.
	 */
	default String getInstanceId() {
		return null;
	}

	/**
	 * @return The service ID as registered.
	 */
	String getServiceId();

	/**
	 * @return The hostname of the registered service instance.
	 */
	String getHost();

	/**
	 * @return The port of the registered service instance.
	 */
	int getPort();

	/**
	 * @return Whether the port of the registered service instance uses HTTPS.
	 */
	boolean isSecure();

	/**
	 * @return The service URI address.
	 */
	URI getUri();

	/**
	 * @return The key / value pair metadata associated with the service instance.
	 */
	Map<String, String> getMetadata();

	/**
	 * @return The scheme of the service instance.
	 */
	default String getScheme() {
		return null;
	}
}

发现ServiceInstance实例对象其实是一个具体待调用服务相关信息,也就意味着,在调用choose这个方法时,已经是通过路由算法选择出了一个服务来的,那么重点就是LoadBalancerClient的choose方法,RibbonLoadBalancerClient实现了LoadBalancerClient,我们只需要看这个类即可。

3、查看RibbonLoadBalancerClient的choose方法,如下

java 复制代码
@Override
public ServiceInstance choose(String serviceId) {
    return choose(serviceId, null);
}

public ServiceInstance choose(String serviceId, Object hint) {
    // 此处已经拿到了具体的服务信息,那么getLoadBalancer就是关键
    Server server = getServer(getLoadBalancer(serviceId), hint);
    if (server == null) {
        return null;
    }
    return new RibbonServer(serviceId, server, isSecure(server, serviceId),
				serverIntrospector(serviceId).getMetadata(server));
}


protected ILoadBalancer getLoadBalancer(String serviceId) {
    return this.clientFactory.getLoadBalancer(serviceId);
}

protected Server getServer(ILoadBalancer loadBalancer, Object hint) {
    if (loadBalancer == null) {
        return null;
    }
    // Use 'default' on a null hint, or just pass it on?
    return loadBalancer.chooseServer(hint != null ? hint : "default");
}

通过choose方法的层级调用可以知道,选择出一个具体服务需要由loadBalancer.chooseServer获取到。

4、点击chooseServer的实现,可以看到有以三个类可以选择

BaseLoadBalancer

ZoneAwareLoadBalancer

NoOpLoadBalancer

那么到底选谁呢?遇事不决,最简单的就是给三个类chooseServer中都打个断点,然后发起一次接口调用,看看会进入哪个类,不出意外的话,就是ZoneAwareLoadBalancer的chooseServer方法,然而,BaseLoadBalancer又是ZoneAwareLoadBalancer的父类,所以,绕了一下,我们看BaseLoadBalancer的chooseServer方法就完事了。

5、查看BaseLoadBalancer的chooseServer方法,如下:

java 复制代码
public Server chooseServer(Object key) {
    if (counter == null) {
        counter = createCounter();
    }
    counter.increment();
    if (rule == null) {
        return null;
    } else {
        try {
            // 此处直接调用了自己的属性对象rule
            return rule.choose(key);
        } catch (Exception e) {
            logger.warn("LoadBalancer [{}]:  Error choosing server for key {}", name, key,     e);
        return null;
        }
    }
}

可以看到关键的调用rule.choose(key)就拿到了待调用的服务信息,那么rule从那里来?不急,我们往上翻,找到这个属性。

6、在BaseLoadBalancer类的开头可以找到rule这个属性对象

java 复制代码
private final static IRule DEFAULT_RULE = new RoundRobinRule();

protected IRule rule = DEFAULT_RULE;

看到这,我们也有意识到了,默认情况下,它就是默认创建轮询算法的

接着,我们简单看看这个类的无参构造函数,如下:

java 复制代码
public BaseLoadBalancer() {
        this.name = DEFAULT_NAME;
        this.ping = null;
        setRule(DEFAULT_RULE);
        setupPingTask();
        lbStats = new LoadBalancerStats(DEFAULT_NAME);
    }

看到了设置setRule,在点进去看看

java 复制代码
public void setRule(IRule rule) {
        if (rule != null) {
            this.rule = rule;
        } else {
            /* default rule */
            this.rule = new RoundRobinRule();
        }
        if (this.rule.getLoadBalancer() != this) {
            this.rule.setLoadBalancer(this);
        }
    }

重要的来了,敲黑板!

在setRule中, 我们可以看到没在给loadBalancer设置完rule之后,还得把它自己设置给rule,这在第二节中,我们自定义路由算法时,将this.loadBalancer传递到了我们自定义的choose方法中用到了。

看到了这一步,我们就要有这么一个大致流程意识了,就是:

1、在第一步通过loadBalancerClient.choose拿到loadBalancer

2、再通过loadBalancer.chooseServer找到实现类BaseLoadBalancer的chooseServer

3、通过BaseLoadBalancer的chooseServer调用到自己的属性对象的rule.choose拿到具体待调用的服务信息

所以,通过上诉流程,我们简单得出一个结论,就是,我们可以通过获得微服务对应loadBalancer,然后修改它的rule,从而实现动态更改路由算法。但是,我们又如何拿到服务对应的loadBalancer呢,我们目前流程都没有提到过怎么去拿,是因为上面所有流程只是说明了最终是调用loadBalancer中rule的choose方法,接下来我们就讲如何拿到微服务对应的loadBalancer。

7、回到第三步,我们看getLoadBalancer这个方法,如下

java 复制代码
protected ILoadBalancer getLoadBalancer(String serviceId) {
	return this.clientFactory.getLoadBalancer(serviceId);
}

我们发现,它是通过this.clientFactory获取到的loadBalancer,而this.clientFactory的类型是SpringClientFactory。

至此,我就在想,我能不能也通过SpringClientFactory去获取服务的loadBalancer,抱着试一试的态度,我将SpringClientFactory注入进自己的类中,最后发现,真能用,如下:

java 复制代码
@Resource
private SpringClientFactory springClientFactory;

我们也能注入使用SpringClientFactory,但它是ribbon包下的一个类,而不是spring默认就有提供的,原因就是在spring-cloud-netflix-ribbon包下有一个类RibbonAutoConfiguration把这个类注册成了Bean,包括像我们第一步所使用LoadBalancerClient,所以我们也能用到这个Bean

java 复制代码
@Bean
public SpringClientFactory springClientFactory() {
    SpringClientFactory factory = new SpringClientFactory();
    factory.setConfigurations(this.configurations);
    return factory;
}

@Bean
@ConditionalOnMissingBean(LoadBalancerClient.class)
public LoadBalancerClient loadBalancerClient() {    
    return new RibbonLoadBalancerClient(springClientFactory());
}

好了,自此我们就能通过SpringClientFactory获取到服务对应的loadBalancer,继而改掉loadBalancer的rule,从而实现动态修改rule,接下来就是实践了。

四、验证

1、ServicesRule,用于映射配置文件的结构

java 复制代码
public class ServicesRule {

    private String strategy;

    private Set<String> services;

    public String getStrategy() {
        return strategy;
    }

    public void setStrategy(String strategy) {
        this.strategy = strategy;
    }

    public Set<String> getServices() {
        return services;
    }

    public void setServices(Set<String> services) {
        this.services = services;
    }
}

2、 RuleProperties,用于与配置文件做关联映射,此处使用@ConfigurationProperties的原因是:当远程配置文件发生变动,执行了refresh或bus-fresh操作时,被该注解修饰的Bean会重新装配,能够重新执行Bean创建的生命周期(不包括重新创建一个新对象),以该类为例,就是每次刷新时,就会执行afterPropertiesSet这个方法。

java 复制代码
@ConfigurationProperties(prefix = "services.ribbon")
public class RuleProperties implements InitializingBean, ApplicationContextAware {

    private List<ServicesRule> rules;

    private ApplicationContext applicationContext;

    @Resource
    private SpringClientFactory springClientFactory;

    @Override
    public void afterPropertiesSet() throws Exception {

        if(rules == null || rules.isEmpty()) {
            return;
        }

        for(ServicesRule rule : rules) {
            if(StringUtils.isEmpty(rule.getStrategy()) || rule.getServices().isEmpty()) {
                continue;
            }

            Class clazz = Class.forName(rule.getStrategy());

            if(clazz == null) {
                continue;
            }

            if(!IRule.class.isAssignableFrom(clazz)) {
                continue;
            }

            for(String service : rule.getServices()) {

                BaseLoadBalancer baseLoadBalancer = (BaseLoadBalancer) springClientFactory.getLoadBalancer(service);

                if(baseLoadBalancer == null) {
                    return;
                }

                IRule preRule = baseLoadBalancer.getRule();

                if(preRule.getClass().equals(clazz)) {
                    continue;
                }

                AutowireCapableBeanFactory beanFactory = applicationContext.getAutowireCapableBeanFactory();

                IRule iRule = (IRule) beanFactory.createBean(clazz);

                iRule.setLoadBalancer(baseLoadBalancer);
                baseLoadBalancer.setRule(iRule);

            }
        }
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    public List<ServicesRule> getRules() {
        return rules;
    }

    public void setRules(List<ServicesRule> rules) {
        this.rules = rules;
    }
}

3、 @ConfigurationProperties需要@EnableConfigurationProperties支持

java 复制代码
@EnableConfigurationProperties(value = {RuleProperties.class})
@Configuration
public class RulePropertiesConfiguration {
}

4、配置文件添加以下配置:

java 复制代码
# 自动义的可动态更新路由算法的配置
services:
  ribbon:
    rules:
      - strategy: com.xxx.yyy.myRule
        services:
          - your-service-name

5、测试

能够正常为服务动态添加自定义的路由算法,无须重启网关服务

6、缺点

1、不支持懒加载,通过我们上述操作,提前将服务对应loadBalancer加载出来了。

2、在替换rule的这个过程中可能存在并发问题,如果不介意在切换rule过程中,可能存在一瞬间用户调用存在问题的情况的话,就可以不做处理。否则,需要自己选择更为稳妥的方式进行动态路由实现。

五、总结

通过这么长度篇幅讲了如何动态修改微服务的rule,主要是做一次记录,给大家提供思路,也给我后期遇到同类问题做参考。

相关推荐
人活一口气10 小时前
从JVM调优到MCP协议:Java全栈技术体系深度总结与企业级架构实践
java·spring boot
NE_STOP12 小时前
Vibe Coding -- 完整项目案例实操
java
荣码12 小时前
GraphRAG:普通RAG只能回答"点"的问题,我踩了4个坑才搞懂
java·python
SimonKing12 小时前
Google第三方授权登录
java·后端·程序员
明月光81812 小时前
从一行 @Builder 说起:重新拾起 Java 的 Lombok、注解与 Builder 模式
java
考虑考虑21 小时前
Mybatis实现批量插入
java·后端·mybatis
咖啡八杯1 天前
GoF设计模式——中介者模式
java·后端·spring·设计模式
fanly111 天前
Surging AI Agent 完整产品介绍
微服务·microservice
青石路1 天前
记一次多JDK版本问题的排查,一坑套一坑,差点没爬上来
java
Java陈序员1 天前
企业级!一个基于 Java 开发的开源 AI 应用开发平台!
spring boot·agent·mcp