当前位置: 首页 > news >正文

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

目录

一、介绍

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

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

四、验证

五、总结


一、介绍

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

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

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

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

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

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

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

public class MyRule extends AbstractLoadBalancerRule {@Overridepublic void initWithNiwsConfig(IClientConfig clientConfig) {}@Overridepublic 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、配置文件方式

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

# 你的服务名
your-service-name:ribbon:NFLoadBalancerRuleClassName: com.xxx.yyy.MyRule

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

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

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

2、@RibbonClient或@RibbonClients方式

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

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

@Configuration
public class MyRuleConfig {@Beanpublic IRule myRule() {return new MyRule();}}

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

@RibbonClient(name = "your-servcie-name", configuration = {MyRuleConfig.class})

@RibbonClients(value = {@RibbonClient(name = "your-servcie-name", configuration = {MyRuleConfig.class})
})

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

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

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

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

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

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

2、简单看看ServiceInstance

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方法,如下

@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方法,如下:

public Server chooseServer(Object key) {if (counter == null) {counter = createCounter();}counter.increment();if (rule == null) {return null;} else {try {// 此处直接调用了自己的属性对象rulereturn 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这个属性对象

private final static IRule DEFAULT_RULE = new RoundRobinRule();protected IRule rule = DEFAULT_RULE;

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

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

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

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

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这个方法,如下

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

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

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

@Resource
private SpringClientFactory springClientFactory;

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

@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,用于映射配置文件的结构

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这个方法。

@ConfigurationProperties(prefix = "services.ribbon")
public class RuleProperties implements InitializingBean, ApplicationContextAware {private List<ServicesRule> rules;private ApplicationContext applicationContext;@Resourceprivate SpringClientFactory springClientFactory;@Overridepublic 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);}}}@Overridepublic 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支持

@EnableConfigurationProperties(value = {RuleProperties.class})
@Configuration
public class RulePropertiesConfiguration {
}

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

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

5、测试

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

6、缺点

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

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

五、总结

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

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 嵌入式Linux:空洞文件
  • 已注册的商标别忘了续展,新注可能难下证!
  • SQL IN 参数超过2100的解决方案
  • 隐私计算实训营学习三:隐私计算框架的架构和技术要点
  • C语言例4-15:从键盘输入一个整数,求其绝对值并输出。
  • mysql服务
  • 【Linux】详解进程终止进程等待
  • Qt5.14.2 全面解锁Qt5事件处理的精髓
  • Partisia Blockchain:如何做到兼顾隐私、高性能和可拓展?
  • PHP页面如何实现设置独立访问密码
  • tewa-707e光猫超级密码获取方法
  • Lua热更新(xlua)
  • 今日讲讲路由配置
  • Docker搭建LNMP环境实战(04):安装VMwareTools共享文件夹
  • Unity 计算两个日期的时间差(含绝对值计算方法)
  • 时间复杂度分析经典问题——最大子序列和
  • 【跃迁之路】【463天】刻意练习系列222(2018.05.14)
  • 03Go 类型总结
  • CSS3 聊天气泡框以及 inherit、currentColor 关键字
  • CSS实用技巧干货
  • fetch 从初识到应用
  • github指令
  • Git学习与使用心得(1)—— 初始化
  • Java反射-动态类加载和重新加载
  • mac修复ab及siege安装
  • 解析带emoji和链接的聊天系统消息
  • 前端攻城师
  • 入职第二天:使用koa搭建node server是种怎样的体验
  • 设计模式 开闭原则
  • 手机app有了短信验证码还有没必要有图片验证码?
  • #1015 : KMP算法
  • #预处理和函数的对比以及条件编译
  • (160)时序收敛--->(10)时序收敛十
  • (2024)docker-compose实战 (8)部署LAMP项目(最终版)
  • (官网安装) 基于CentOS 7安装MangoDB和MangoDB Shell
  • (九)c52学习之旅-定时器
  • (七)MySQL是如何将LRU链表的使用性能优化到极致的?
  • (实战)静默dbca安装创建数据库 --参数说明+举例
  • (一)Dubbo快速入门、介绍、使用
  • .gitignore文件使用
  • .jks文件(JAVA KeyStore)
  • .NET Remoting Basic(10)-创建不同宿主的客户端与服务器端
  • .NET 中创建支持集合初始化器的类型
  • .net6 core Worker Service项目,使用Exchange Web Services (EWS) 分页获取电子邮件收件箱列表,邮件信息字段
  • /run/containerd/containerd.sock connect: connection refused
  • @RequestBody与@ModelAttribute
  • [ 隧道技术 ] 反弹shell的集中常见方式(四)python反弹shell
  • [100天算法】-x 的平方根(day 61)
  • [15] 使用Opencv_CUDA 模块实现基本计算机视觉程序
  • [18] Opencv_CUDA应用之 基于颜色的对象检测与跟踪
  • [Algorithm][综合训练][kotori和气球][体操队形][二叉树中的最大路径和]详细讲解
  • [C++] 轻熟类和对象
  • [CSS]盒子模型
  • [Gstreamer] 消息处理handler的设置
  • [IE编程] IE中对网页进行截图的编程接口