最近很久没有写文章啦,刚好遇到了一个问题,其实问题也挺简单,但是还是得对源码有一定了解才能够发现。
最近在实现一个根据请求流量的标签,将请求转发到对应的节点,其实和俗称的灰度请求有点相似,
实现思路如下:
- 首先为特定节点打上标签
- 通过截取请求中的header的标签key,然后存入上下文中
- 在服务转发时(Feign),在负载均衡步骤前将节点的标签和请求标签相匹配,筛选出标签节点。
- 将标签节点进行策略选择一个合适的节点然后转发。
场景定义
实现伪代码:
- 定义策略
public class MyRule extends AbstractLoadBalancerRule {
@Override
public void initWithNiwsConfig(IClientConfig iClientConfig) {
}
@Override
public Server choose(Object o) {
return this.choose(this.getLoadBalancer(),o);
}
public Server choose(ILoadBalancer lb, Object key) {
// 标签匹配
// 节点选择
// 策略选举
return server;
}
}
}
- 注入容器
@Configuration
public class MyRuleConfig {
@Bean
public IRule ribbonRule(){
return new MyRule();
}
}
好,一切准备就绪。
在应用当中很快就遇到了问题,在选择过程中,节点地址出现错乱。
明明请求的是A服务节点,结果转发的是B节点
通过调试发现ILoadBalancer
对象有问题,比如A服务节点持有的节点列表竟然是B的节点列表。
ILoadBalancer: 每个服务都独立持有一个独属于该服务负载列表。比如A服务持有的就是A服务列表,B就是B的.
,但此时却出现了归属于A的ILoadBalancer中节点列表竟然都是B的。
原因梳理
最终排查发现就是注入的方式问题,为什么这么说呢?
由于服务节点在初始化的过程中,都是以服务名作为一个独立配置存在于容器中的:(如下图)
用户服务单独有一套负载均衡规则(IRule),同理order订单服务也是单独一套负载均衡规则,双方各自持有了各自的服务列表(ILoadBalancer)。
但是由于我们要改写IRule的实现,同时注入到容器中,让服务能够获取到我改写的实现,我们直接@Bean给加入了。
此时在初始化这个IRule的时候就会出现问题,因为IRule内部是持有ILoadBalancer的,但是ILoadBalancer针对每个服务都是不一样的。
我们看一下IRule是怎么被加载到用户服务上下文的:
com.netflix.loadbalancer.BaseLoadBalancer#setRule
public void setRule(IRule rule) {
if (rule != null) { // 此时我们是从容器获取到的,默认是个单例
this.rule = rule;
} else {
/* default rule */
this.rule = new RoundRobinRule();
}
if (this.rule.getLoadBalancer() != this) { // 肯定满足
// 将自身交给rule
this.rule.setLoadBalancer(this);
}
}
别忘了,我们通过@Bean加入到容器中时,是单例的。问题也出在这!
就意味着,每次初始化的时候,在设置setLoadBalancer时,就是在单例的IRule
基础上,从后往前覆盖,最终IRule持有的永远都是最后一个服务的服务列表。
大意图就是这个样子:
问题解决
那么我们如何解决这个问题呢?
我们知道了原因是由于单例导致的,那么我们就可以将自定义的策略改成多实例的注入。
@Configuration
public class MyRuleConfig {
@Bean
@Scope("prototype") // 将单例变为原型
public IRule ribbonRule(){
return new MyRule();
}
}
此时每次获取都是一个新的对象,相互不在影响。同理你如果需要覆盖RibbonClientConfiguration配置类中的对象时,也需要避免使用单例模式去定义它!