自定义ILoadBalancer

一、需求背景

业务中遇到这样一个场景,部分应用都接入到了网关,在开发环境下为了做到环境隔离,都会把网关应用拉到对应的开发环境中,如何做到不拉取网关应用,直接调用公共环境的网关应用也能转发到正确的开发环境中呢?不同的环境都对应着不同的域名

  • 生产环境(real)
  • 预发环境(pre)
  • 公共环境(stable)
  • 开发环境(dev)
  • 本地环境(local)

开发环境域名类似这样的格式http://dev-xxx.com,其中xxx是此开发环境的唯一标识,而我就是获取url上的dev-xxx标识,这个标识就是Zone,我就可以在公共环境的网关中根据这个Zone转发到正确的开发环境了,而Ribbon当中也有一个Zone的概念,也就是区域,不同的开发环境就是不同的Zone。

二、前置知识

Ribbon包含以下组件:

  • 1.IRule
  • 2.IPing
  • 3.ServerList
  • 4.ServerListFilter
  • 5.ServerListUpdater
  • 6.IClientConfig
  • 7.ILoadBalancer

因本文整合了Eureka,组件的默认实现类会有些许不同

组件接口组件默认实现类整合Eureka实现类
IClientConfigDefaultClientConfigImplDefaultClientConfigImpl
IRuleZoneAvoidanceRuleZoneAvoidanceRule
IPingDummyPingNIWSDiscoveryPing
ServerListConfigurationBasedServerListDiscoveryEnabledNIWSServerList
ServerListFilterZonePreferenceServerListFilterZonePreferenceServerListFilter
ILoadBalancerZoneAwareLoadBalancerZoneAwareLoadBalancer
ServerListUpdaterPollingServerListUpdaterPollingServerListUpdater

这里我踩坑了许久,没注意项目中引入了 spring-cloud-starter-netflix-eureka-client,我在ServerList获取服务列表的时候一直获取不了,后面才发现用的 ConfigurationBasedServerList这个实现类,而这个实现类是需要将服务写在配置文件里面。所以如果你没有使用到Eureka:

一种方式是吧这个依赖排除

另外一种方式,增加以下配置

ribbon:
  eureka:
    enabled: false

我们其中需要对ILoadBalancer进行改造,那它是干什么的呢?

Ribbon作为一个客户端负载均衡器,它最核心的资源便是一堆Server们,也叫服务列表。对于这些服务列表的获取、更新、维护、探活、选择等等都是由上面的组件构成,而如何把这些组件组合在一起有条不紊的工作,便是ILoadBalancer的作用。

public interface ILoadBalancer {
	// 初始化Server列表。当然后期你可以可以再添加
	// 在某些情况下,你可能想给出更多的“权重”时 该方法有用
	public void addServers(List<Server> newServers);
	// 从load balancer里面找到一个Server
	public Server chooseServer(Object key);
	// 由负载均衡器的客户端调用,以通知服务器停机否则
	// LB会认为它还活着,直到下一个Ping周期
	// 也就说该方法可以手动调用,让Server停机
	public void markServerDown(Server server);
	// 该方法已过期,被下面两个方法代替
	@Deprecated
	public List<Server> getServerList(boolean availableOnly);

	// 只有服务器是可访问的就返回
    public List<Server> getReachableServers();
    // 所有已知的服务器,包括可访问的和不可访问的。
	public List<Server> getAllServers();
}

来看看Ribbon帮我们实现了那些ILoadBalancer

主要讲解一下以下3个Balancer 

(1)BaseLoadBalancer

基本实现了ILoadBalancer所使用的接口。

(2)DynamicServerListLoadBalancer

在BaseLoadBalancer基础上,进一步集成了ServerListFilter、ServerListUpdater、ServerList三大组件,DynamicServerListLoadBalancer具有如下能力:可定制服务列表(例如可指定从注册中心获取,可从配置文件中获取)、可动态获取服务列表(例如配置文件中配置的服务列表变更可自动获取,注册中心中新增一个服务负载均衡可自动获取),服务过滤能力(可对某些条件的服务器过滤)

(3)ZoneAwareLoadBalancer

在DynamicServerListLoadBalancer基础上,增加了额外的特性。据提供服务的实例所处的区域(Zone)过滤掉那些不是同一个区域的实例,也就是说通过chooseServer()服务一定在同一个区域。

综上核心就是在ZoneAwareLoadBalancer上,而它有个核心方法chooseServer,我们只需要改造一下此方法就可以,先来看看它的源码:

  @Override
    public Server chooseServer(Object key) {
    	// 如果禁用了区域意识。或者只有一个zone,那就遵照父类逻辑
        if (!ENABLED.get() || getLoadBalancerStats().getAvailableZones().size() <= 1) {
            return super.chooseServer(key);
        }

        Server server = null;
        try {
            LoadBalancerStats lbStats = getLoadBalancerStats();
			...     		
			// 核心方法:根据triggeringLoad等阈值计算出可用区~~~~
            Set<String> availableZones = ZoneAvoidanceRule.getAvailableZones(zoneSnapshot, triggeringLoad.get(), triggeringBlackoutPercentage.get());
            if (availableZones != null &&  availableZones.size() < zoneSnapshot.keySet().size()) {
			
				// 从可用区里随机选择一个区域(zone里面机器越多,被选中概率越大)
                String zone = ZoneAvoidanceRule.randomChooseZone(zoneSnapshot, availableZones);
                if (zone != null) {
                    BaseLoadBalancer zoneLoadBalancer = getLoadBalancer(zone);
                    // 按照IRule从该zone内选择一台Server出来
                    server = zoneLoadBalancer.chooseServer(key);
                }
            }
        } catch (Exception e) {
            logger.error("Error choosing server using zone aware logic for load balancer={}", name, e);
        }
        if (server != null) {
            return server;
        } else {
        	// 回退到父类逻辑~~~兜底
            return super.chooseServer(key);
        }
    }

三、需求实现

(1)实现一个拦截器将Zone信息存入上下文

@Component
/** 只有当以下三个环境时,才注入此拦截器,也就是才生效 **/
@ConditionalOnExpression("'${spring.profiles.active}'.equals('stable') || '${spring.profiles.active}'.equals('local') || '${spring.profiles.active}'.equals('dev')")
public class ZoneFilter implements GlobalFilter, Ordered {

    private static final Pattern ZONE_PATTERN = Pattern.compile("dev-[0-9]+");

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String uri = exchange.getRequest().getURI().getHost();
        String zoneInUri = matchedZone(uri);

        zoneInUri = zoneInUri == null ? "stable" : zoneInUri;
        if (!StringUtils.isEmpty(zoneInUri)) {
            ZoneUtils.setZoneInfo(zoneInUri);
        }
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return -1;
    }

    private String matchedZone(String zoneUri) {
        //判断是否是开发环境的Url
        Matcher matcher = ZONE_PATTERN.matcher(zoneUri);
        if (matcher.find()) {
            return matcher.group();
        }
        return null;
    }
}


public class ZoneUtils {
    private static ThreadLocal<String> zoneInfo = new ThreadLocal<>();

    public static void setZoneInfo(String zone) {
        zoneInfo.set(zone);
    }


    public static String current() {
        return zoneInfo.get();
    }

    public static void clear() {
        zoneInfo.remove();
    }

}

(2)重写chooseServer方法



@Slf4j
public class DianZoneAwareILoadBalance<T extends Server> extends ZoneAwareLoadBalancer<T> {


    public DianZoneAwareILoadBalance(IClientConfig clientConfig, IRule rule, IPing ping, ServerList<T> serverList, ServerListFilter<T> filter, ServerListUpdater serverListUpdater) {
        super(clientConfig, rule, ping, serverList, filter, serverListUpdater);
    }

    @Override
    public Server chooseServer(Object key) {
  
        String currentZone = ZoneUtils.current();
        ZoneUtils.clear();

        if (StringUtils.isNotBlank(currentZone)) {
            List<Server> upServerList = this.upServerList;
            for (Server zoneServer : upServerList) {
                if (zoneServer.getZone().equals(currentZone)) {
                    return zoneServer;
                }
            }
        }
        return super.chooseServer(key);
    }

(3)定义一个配置类 (注意不要加上@Configuration注解,因为ILoadBalancer加载的方式是饿汉式,当请求第一次来的时候才去初始化它,容器启动的时候并不会去加载,否则你会连容器都起不起来)当然也可以开启懒汉式加载,增加以下配置:

ribbon:
  eager-load:
    enabled: true
    clients:
      - "服务1"
      - "服务2"

这样的话,服务1和服务2对应的Ribbon组件会在容器启动的时候去加载(因为Ribbon对每个客户端服务,都会对其生成一套组件,相互隔离)

public class DynamicUrlZoneConfiguration {

    @Bean
    @ConditionalOnExpression("'${spring.profiles.active}'.equals('stable') || '${spring.profiles.active}'.equals('dev') || '${spring.profiles.active}'.equals('local')")
    public ILoadBalancer stableLoadBalancer(IClientConfig config,
                                            ServerList<Server> serverList, ServerListFilter<Server> serverListFilter,
                                            IRule rule, IPing ping, ServerListUpdater serverListUpdater) {
        //如果是开发环境采用DefaultNIWSServerListFilter不对zone进行过滤
        ServerListFilter<Server> listFilter = new DefaultNIWSServerListFilter<>();
        return new DianZoneAwareILoadBalance<>(config, rule, ping, serverList,
                listFilter, serverListUpdater);
    }

    @Bean
    @ConditionalOnExpression("'${spring.profiles.active}'.equals('real') || '${spring.profiles.active}'.equals('pre')")
    public ILoadBalancer realBalancer(IClientConfig config,
                                            ServerList<Server> serverList, ServerListFilter<Server> serverListFilter,
                                            IRule rule, IPing ping, ServerListUpdater serverListUpdater) {
        //如果是线上环境采用默认值即可
        return new ZoneAwareLoadBalancer<>(config, rule, ping, serverList,
                serverListFilter, serverListUpdater);
    }
}

 这里测试环境为什么要用DefaultNIWSServerListFilter,这是一个空实现,意味着不会对serverList进行过滤操作,因为默认的ZonePreferenceServerListFilter会对serverList按照zone进行过滤,看下源码就知道了:

ZonePreferenceServerListFilter——————————————	

public List<Server> getFilteredListOfServers(List<Server> servers) {
		List<Server> output = super.getFilteredListOfServers(servers);
		if (this.zone != null && output.size() == servers.size()) {
			List<Server> local = new ArrayList<>();
			for (Server server : output) {
				if (this.zone.equalsIgnoreCase(server.getZone())) {
					local.add(server);
				}
			}
			if (!local.isEmpty()) {
				return local;
			}
		}
		return output;
	}

可以看到如果zone不会空的返回的是按照zone进行过滤的serverList,如果测试环境(测试环境这里包含:公共环境+开发环境+)也采用这个过滤器,那么this.upServerList拿到的就是过滤后的zone,就拿不到到开发环境的server,chooseServer的时候就无法返回开发环境的Server。

不理解的可以想一下:测试环境中同一个应用,可能部署在多个开发环境和一个公共环境,如果采用ZonePreferenceServerListFilter这个过滤器,就只能获取公共环境的服务,而我是想要获得当前测试环境中所有的服务。

(4)启动类加上

@RibbonClients(defaultConfiguration = DynamicUrlZoneConfiguration.class)

要注意@RibbonClient和@RibbonClients,前者只针对单个服务应用DynamicUrlZoneConfiguration配置,后者针对所有服务都应用DynamicUrlZoneConfiguration配置,网关场景中,调用下游服务,所以使用@RibbonClients。

其中也是踩了很多坑,也是看了许多源码才一步步解决的

参考文章:

[享学Netflix] 五十六、Ribbon负载均衡器ILoadBalancer(一):BaseLoadBalancer - 云+社区 - 腾讯云

通过【application.yml】配置自定义Ribbon客户端时参数【listOfServers】不起作用 - 走看看

SpringCloud Ribbon(一)之自定义负载均衡器ILoadBalancer_茅坤宝骏氹的博客-CSDN博客_自定义负载均衡器

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值