微服务间通过FeignClient互相调用默认使用的是ribbon的轮询负载均衡策略,而实际场景中我们可能需要自定义一些规则或者约束来实现特定的负载均衡策略。
背景:微服务开发,多租户,API接口隔离,这些在现在开发过程中会经常遇到的问题。服务多节点部署,同时每个service实例都有特定的标签或属性。比如instanceA是提供给租户tenantA服务的,instanceB是提供给租户tenantB服务的,instance是提供通用的服务;再比如instanceA只提供数据处理能力A,携带标签g_data_A,那么一个B请求就不应该被服务注册中心提供instanceA来处理,也就是做了接口层的隔离。为了实现以上特定需求,我们不能使用默认的ribbon负载均衡策略,需要自定义一个规则。
Spring Cloud的ribbon组件为我们提供了诸多负载均衡规则,这些规则都是通过一个抽象接口IRule来扩展的,IRule接口定义了选择负载均衡策略的基本操作。通过调用choose()方法,就可以选择具体的负载均衡策略。
public interface IRule{
/*
* choose one alive server from lb.allServers or
* lb.upServers according to key
*
* @return choosen Server object. NULL is returned if none
* server is available
*/
public Server choose(Object key);
public void setLoadBalancer(ILoadBalancer lb);
public ILoadBalancer getLoadBalancer();
}
抽象类AbstractLoadBalancerRule实现了IRule接口和IClientConfigAware接口,主要对IRule接口的2个方法进行了简单封装。我们通过集成抽象类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;
}
}
ribbon已实现多种负载均衡规则,如下:
1,轮询策略(RoundRobinRule)
2,随机策略(RandomRule)
3,可用过滤策略(AvailabilityFilteringRule)
策略描述:过滤掉连接失败的服务节点,并且过滤掉高并发的服务节点,然后从健康的服务节点中,使用轮询策略选出一个节点返回。
4,响应时间权重策略(WeightedResponseTimeRule)
策略描述:根据响应时间,分配一个权重weight,响应时间越长,weight越小,被选中的可能性越低。
5,轮询失败重试策略(RetryRule)
6,并发量最小可用策略(BestAvailableRule)
7,ZoneAvoidanceRule
策略描述:复合判断server所在区域的性能和server的可用性,来选择server返回。
根据我们的实际需求,我们需要根据特定的约束(租户和接口隔离)来实现自定义负载均衡规则,而ribbon提供了一个AbstractLoadBalancerRule的抽象子类PredicateBasedRule,他是通过AbstractServerPredicate来断言输入的server(从注册中心获取)是否符合需求,不符合则跳过,继续对下一个server进行断言,这个PredicateBasedRule就很符合我们的实际需求,下面我们就可以用他来自定义我们的规则。
public abstract class PredicateBasedRule extends ClientConfigEnabledRoundRobinRule {
public abstract AbstractServerPredicate getPredicate();
@Override
public Server choose(Object key) {
ILoadBalancer lb = getLoadBalancer();
Optional<Server> server = getPredicate().chooseRoundRobinAfterFiltering(lb.getAllServers(), key);
if (server.isPresent()) {
return server.get();
} else {
return null;
}
}
}
我们定义一个动态负载均衡规则类DynamicBalanceRule实现PredicateBasedRule,同时重写其choose(Object key)方法,并实现其getPredicate()方法来自定义断言规则。
1、服务消费方通过FeignClient调用提供方的接口时,会进入到ribbon默认的负载均衡规则,我们自定义的DynamicBalanceRule如果要生效,需要在配置文件中配置(或者通过编码方式),同时我们希望这个规则只应用于服务提供方Whatever-Service的调用,而不影响其他的微服务调用负载均衡策略,那么在配置中我们就需要指定注册中心服务的名称,所以在消费者端的配置如下(指定DynamicBalanceRule的完整类路径即可):
# 类DynamicBalanceRule的完整类路径
Whatever-Service.ribbon.NFLoadBalancerRuleClassName=com.*.*.*.DynamicBalanceRule
2、服务提供方Whatever-Service在启动的时候是携带了所属租户和提供特定接口能力的标签(tags)的,而我们消费方发出请求的时候肯定是知道自己当前请求的用户所属的租户,也知道当前调用的接口信息 。那么只要和提供方的tag匹配即可认为这个实例是可用的,也即断言成立。
具体实现如下:
1)首先我们在通过FeignClient调用提供者提供的接口前,将当前请求所携带的租户和接口信息封装到一个对象中IsolationAttribute,即创建new IsolationAttribute(tenantDomain, interfaceGroup);
2)然后通过ThreadLocal将IsolationAttribute放入当前http线程上下文中TenantIsolationThreadLocal.set(new IsolationAttribute(tenantDomain, interfaceGroup));
3)发起对提供方接口的调用whateverServiceFeignClient.doSomethingBad();
4)此时业务执行会进入到DynamicBalanceRule的choose(Object key)方法。
@Override
public Server choose(Object key) {
try {
// 从指定的租户域获取server
ILoadBalancer lb = getLoadBalancer();
Optional<Server> server = getPredicate().chooseRoundRobinAfterFiltering(lb.getReachableServers(), key);
if (server.isPresent()) {
return server.get();
}
return null;
} finally {
TenantIsolationThreadLocal.remove();
}
}
3、执行getPredicate()方法,我们需要实现自定义的getPredicate()来获取一个自定义的断言。我们定义一个断言DynamicPredicate如下:
public class DynamicPredicate extends AbstractServerPredicate {
public static String defaultTenantDomainKey = DefaultTenantConfig.defaultTenantDomainKey;
public static String defaultInterfaceGroupKey = DefaultTenantConfig.defaultInterfaceGroupKey;
private String tenant;
private String group;
public DynamicPredicate() {
}
public DynamicPredicate(String tenant, String group) {
this.tenant = tenant;
this.group = group;
}
@Override
public boolean apply(PredicateKey input) {
return !shouldSkipServer(input.getServer());
}
private boolean shouldSkipServer(Server server) {
if (server instanceof ConsulServer) {
ConsulServer consulServer = (ConsulServer) server;
Map<String, String> metadata = consulServer.getMetadata();
if (metadata.containsKey(defaultTenantDomainKey)) {
String serverTenantDomain = metadata.get(defaultTenantDomainKey);
if (!tenant.equalsIgnoreCase(serverTenantDomain)) {
return true;
}
} else {
return true;
}
if (StringUtils.isEmpty(group)) {
return false;
}
String serverGroup = server.getMetaInfo().getServerGroup();
if (StringUtils.isEmpty(serverGroup)) {
serverGroup = metadata.get(defaultInterfaceGroupKey);
}
if (StringUtils.isEmpty(serverGroup) || !serverGroup.contains(group)) {
return true;
}
}
return false;
}
}
以上DynamicPredicate继承抽象类AbstractServerPredicate,并实现顶层接口的boolean apply(@Nullable T input)方法,在apply方法中断言当前输入的server是否符合我们的实际需求。在shouldSkipServer(Server server)方法中我们需要判断租户域是否匹配,接口是否匹配,完全匹配则断言成立。
4、根据自定义的断言,我们就可以使用Optional server = getPredicate().chooseRoundRobinAfterFiltering(lb.getReachableServers(), key)来通过轮询策略来筛选符合输入条件的的server。同时,如果我们通过自定义的断言和规则没有找到合适的server,那么我们可以通过FallbackPredicate来实现当找不到server时的处理策略,比如没有找到合适的server时认定所有输入的server对我们自定义的断言都成立,配置如下:
CompositePredicate compositePredicate = CompositePredicate
.withPredicate(new DynamicPredicate(currentTenantDomain, currentInterfaceGroup))
.addFallbackPredicate(AbstractServerPredicate.alwaysTrue()).build();
由此,我们自定义的负载均衡策略已完成。