SpringCloud升级之路2020.0.x版-23.订制Spring Cloud LoadBalancer

本系列代码地址:https://github.com/HashZhang/spring-cloud-scaffold/tree/master/spring-cloud-iiford
我们使用 Spring Cloud 官方推荐的 Spring Cloud LoadBalancer 作为我们的客户端负载均衡器。上一节我们了解了 Spring Cloud LoadBalancer 的结构,接下来我们来说一下我们在使用 Spring Cloud LoadBalancer 要实现的功能:
- 我们要实现不同集群之间不互相调用,通过实例的
metamap中的zone配置,来区分不同集群的实例。只有实例的metamap中的zone配置一样的实例才能互相调用。这个通过实现自定义的ServiceInstanceListSupplier即可实现 - 负载均衡的轮询算法,需要请求与请求之间隔离,不能共用同一个 position 导致某个请求失败之后的重试还是原来失败的实例。上一节看到的默认的 
RoundRobinLoadBalancer是所有线程共用同一个原子变量position每次请求原子加 1。在这种情况下会有问题:假设有微服务 A 有两个实例:实例 1 和实例 2。请求 A 到达时,RoundRobinLoadBalancer返回实例 1,这时有请求 B 到达,RoundRobinLoadBalancer返回实例 2。然后如果请求 A 失败重试,RoundRobinLoadBalancer又返回了实例 1。这不是我们期望看到的。 
针对这两个功能,我们分别编写自己的实现。

Spring Cloud LoadBalancer 中的 zone 配置
Spring Cloud LoadBalancer 定义了 LoadBalancerZoneConfig:
public class LoadBalancerZoneConfig {
    //标识当前负载均衡器处于哪一个 zone
	private String zone;
	public LoadBalancerZoneConfig(String zone) {
		this.zone = zone;
	}
	public String getZone() {
		return zone;
	}
	public void setZone(String zone) {
		this.zone = zone;
	}
}
如果没有引入 Eureka 相关依赖,则这个 zone 通过 spring.cloud.loadbalancer.zone 配置:
LoadBalancerAutoConfiguration
@Bean
@ConditionalOnMissingBean
public LoadBalancerZoneConfig zoneConfig(Environment environment) {
	return new LoadBalancerZoneConfig(environment.getProperty("spring.cloud.loadbalancer.zone"));
}
如果引入了 Eureka 相关依赖,则如果在 Eureka 元数据配置了 zone,则这个 zone 会覆盖 Spring Cloud LoadBalancer 中的 LoadBalancerZoneConfig:
EurekaLoadBalancerClientConfiguration
@PostConstruct
public void postprocess() {
	if (!StringUtils.isEmpty(zoneConfig.getZone())) {
		return;
	}
	String zone = getZoneFromEureka();
	if (!StringUtils.isEmpty(zone)) {
		if (LOG.isDebugEnabled()) {
			LOG.debug("Setting the value of '" + LOADBALANCER_ZONE + "' to " + zone);
		}
		//设置 `LoadBalancerZoneConfig`
		zoneConfig.setZone(zone);
	}
}
private String getZoneFromEureka() {
	String zone;
	//是否配置了 spring.cloud.loadbalancer.eureka.approximateZoneFromHostname 为 true
	boolean approximateZoneFromHostname = eurekaLoadBalancerProperties.isApproximateZoneFromHostname();
	//如果配置了,则尝试从 Eureka 配置的 host 名称中提取
    //实际就是以 . 分割 host,然后第二个就是 zone
    //例如 www.zone1.com 就是 zone1
	if (approximateZoneFromHostname && eurekaConfig != null) {
		return ZoneUtils.extractApproximateZone(this.eurekaConfig.getHostName(false));
	}
	else {
	    //否则,从 metadata map 中取 zone 这个 key
		zone = eurekaConfig == null ? null : eurekaConfig.getMetadataMap().get("zone");
		//如果这个 key 不存在,则从配置中以 region 从 zone 列表取第一个 zone 作为当前 zone
		if (StringUtils.isEmpty(zone) && clientConfig != null) {
			String[] zones = clientConfig.getAvailabilityZones(clientConfig.getRegion());
			// Pick the first one from the regions we want to connect to
			zone = zones != null && zones.length > 0 ? zones[0] : null;
		}
		return zone;
	}
}
实现 SameZoneOnlyServiceInstanceListSupplier
为了实现通过 zone 来过滤同一 zone 下的实例,并且绝对不会返回非同一 zone 下的实例,我们来编写代码:
SameZoneOnlyServiceInstanceListSupplier
/**
 * 只返回与当前实例同一个 Zone 的服务实例,不同 zone 之间的服务不互相调用
 */
public class SameZoneOnlyServiceInstanceListSupplier extends DelegatingServiceInstanceListSupplier {
    /**
     * 实例元数据 map 中表示 zone 配置的 key
     */
    private final String ZONE = "zone";
    /**
     * 当前 spring cloud loadbalancer 的 zone 配置
     */
    private final LoadBalancerZoneConfig zoneConfig;
    private String zone;
    public SameZoneOnlyServiceInstanceListSupplier(ServiceInstanceListSupplier delegate, LoadBalancerZoneConfig zoneConfig) {
        super(delegate);
        this.zoneConfig = zoneConfig;
    }
    @Override
    public Flux<List<ServiceInstance>> get() {
        return getDelegate().get().map(this::filteredByZone);
    }
    //通过 zoneConfig 过滤
    private List<ServiceInstance> filteredByZone(List<ServiceInstance> serviceInstances) {
        if (zone == null) {
            zone = zoneConfig.getZone();
        }
        if (zone != null) {
            List<ServiceInstance> filteredInstances = new ArrayList<>();
            for (ServiceInstance serviceInstance : serviceInstances) {
                String instanceZone = getZone(serviceInstance);
                if (zone.equalsIgnoreCase(instanceZone)) {
                    filteredInstances.add(serviceInstance);
                }
            }
            if (filteredInstances.size() > 0) {
                return filteredInstances;
            }
        }
        /**
         * @see ZonePreferenceServiceInstanceListSupplier 在没有相同zone实例的时候返回的是所有实例
         * 我们这里为了实现不同 zone 之间不互相调用需要返回空列表
         */
        return List.of();
    }
    //读取实例的 zone,没有配置则为 null
    private String getZone(ServiceInstance serviceInstance) {
        Map<String, String> metadata = serviceInstance.getMetadata();
        if (metadata != null) {
            return metadata.get(ZONE);
        }
        return null;
    }
}

在之前章节的讲述中,我们提到了我们使用 spring-cloud-sleuth 作为链路追踪库。我们想可以通过其中的 traceId,来区分究竟是否是同一个请求。
RoundRobinWithRequestSeparatedPositionLoadBalancer
//一定必须是实现ReactorServiceInstanceLoadBalancer
//而不是ReactorLoadBalancer<ServiceInstance>
//因为注册的时候是ReactorServiceInstanceLoadBalancer
@Log4j2
public class RoundRobinWithRequestSeparatedPositionLoadBalancer implements ReactorServiceInstanceLoadBalancer {
    private final ServiceInstanceListSupplier serviceInstanceListSupplier;
    //每次请求算上重试不会超过1分钟
    //对于超过1分钟的,这种请求肯定比较重,不应该重试
    private final LoadingCache<Long, AtomicInteger> positionCache = Caffeine.newBuilder().expireAfterWrite(1, TimeUnit.MINUTES)
            //随机初始值,防止每次都是从第一个开始调用
            .build(k -> new AtomicInteger(ThreadLocalRandom.current().nextInt(0, 1000)));
    private final String serviceId;
    private final Tracer tracer;
    public RoundRobinWithRequestSeparatedPositionLoadBalancer(ServiceInstanceListSupplier serviceInstanceListSupplier, String serviceId, Tracer tracer) {
        this.serviceInstanceListSupplier = serviceInstanceListSupplier;
        this.serviceId = serviceId;
        this.tracer = tracer;
    }
    @Override
    public Mono<Response<ServiceInstance>> choose(Request request) {
        return serviceInstanceListSupplier.get().next().map(serviceInstances -> getInstanceResponse(serviceInstances));
    }
    private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> serviceInstances) {
        if (serviceInstances.isEmpty()) {
            log.warn("No servers available for service: " + this.serviceId);
            return new EmptyResponse();
        }
        return getInstanceResponseByRoundRobin(serviceInstances);
    }
    private Response<ServiceInstance> getInstanceResponseByRoundRobin(List<ServiceInstance> serviceInstances) {
        if (serviceInstances.isEmpty()) {
            log.warn("No servers available for service: " + this.serviceId);
            return new EmptyResponse();
        }
        //为了解决原始算法不同调用并发可能导致一个请求重试相同的实例
        Span currentSpan = tracer.currentSpan();
        if (currentSpan == null) {
            currentSpan = tracer.newTrace();
        }
        long l = currentSpan.context().traceId();
        AtomicInteger seed = positionCache.get(l);
        int s = seed.getAndIncrement();
        int pos = s % serviceInstances.size();
        log.info("position {}, seed: {}, instances count: {}", pos, s, serviceInstances.size());
        return new DefaultResponse(serviceInstances.stream()
                //实例返回列表顺序可能不同,为了保持一致,先排序再取
                .sorted(Comparator.comparing(ServiceInstance::getInstanceId))
                .collect(Collectors.toList()).get(pos));
    }
}

在上一节,我们提到了可以通过 @LoadBalancerClients 注解配置默认的负载均衡器配置,我们这里就是通过这种方式进行配置。首先在 spring.factories 中添加自动配置类:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.github.hashjang.spring.cloud.iiford.service.common.auto.LoadBalancerAutoConfiguration
然后编写这个自动配置类,其实很简单,就是添加一个 @LoadBalancerClients 注解,设置默认配置类:
@Configuration(proxyBeanMethods = false)
@LoadBalancerClients(defaultConfiguration = DefaultLoadBalancerConfiguration.class)
public class LoadBalancerAutoConfiguration {
}
编写这个默认配置类,将上面我们实现的两个类,组装进去:
DefaultLoadBalancerConfiguration
@Configuration(proxyBeanMethods = false)
public class DefaultLoadBalancerConfiguration {
    @Bean
    public ServiceInstanceListSupplier serviceInstanceListSupplier(
            DiscoveryClient discoveryClient,
            Environment env,
            ConfigurableApplicationContext context,
            LoadBalancerZoneConfig zoneConfig
    ) {
        ObjectProvider<LoadBalancerCacheManager> cacheManagerProvider = context
                .getBeanProvider(LoadBalancerCacheManager.class);
        return  //开启服务实例缓存
                new CachingServiceInstanceListSupplier(
                        //只能返回同一个 zone 的服务实例
                        new SameZoneOnlyServiceInstanceListSupplier(
                                //启用通过 discoveryClient 的服务发现
                                new DiscoveryClientServiceInstanceListSupplier(
                                        discoveryClient, env
                                ),
                                zoneConfig
                        )
                        , cacheManagerProvider.getIfAvailable()
                );
    }
    @Bean
    public ReactorLoadBalancer<ServiceInstance> reactorServiceInstanceLoadBalancer(
            Environment environment,
            ServiceInstanceListSupplier serviceInstanceListSupplier,
            Tracer tracer
    ) {
        String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
        return new RoundRobinWithRequestSeparatedPositionLoadBalancer(
                serviceInstanceListSupplier,
                name,
                tracer
        );
    }
}
这样,我们就实现了自定义的负载均衡器。也理解了 Spring Cloud LoadBalancer 的使用。

我们这一节详细分析在我们项目中使用 Spring Cloud LoadBalancer 要实现的功能,实现了自定义的负载均衡器,也理解了 Spring Cloud LoadBalancer 的使用。下一节我们使用单元测试验证我们要实现的这些功能是否有效。
微信搜索“我的编程喵”关注公众号,每日一刷,轻松提升技术,斩获各种offer:

SpringCloud升级之路2020.0.x版-23.订制Spring Cloud LoadBalancer的更多相关文章
- SpringCloud升级之路2020.0.x版-6.微服务特性相关的依赖说明
		
本系列代码地址:https://github.com/HashZhang/spring-cloud-scaffold/tree/master/spring-cloud-iiford spring-cl ...
 - SpringCloud升级之路2020.0.x版-20. 启动一个 Eureka Server 集群
		
本系列代码地址:https://github.com/HashZhang/spring-cloud-scaffold/tree/master/spring-cloud-iiford 我们的业务集群结构 ...
 - SpringCloud升级之路2020.0.x版-21.Spring Cloud LoadBalancer简介
		
本系列代码地址:https://github.com/HashZhang/spring-cloud-scaffold/tree/master/spring-cloud-iiford 我们使用 Spri ...
 - SpringCloud升级之路2020.0.x版-33. 实现重试、断路器以及线程隔离源码
		
本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent 在前面两节,我们梳理了实现 Feign 断路器以及线程隔离的思路,并说明了如何优化目前的负 ...
 - SpringCloud升级之路2020.0.x版-40. spock 单元测试封装的 WebClient(上)
		
本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent 我们来测试下前面封装好的 WebClient,这里开始,我们使用 spock 编写 gro ...
 - SpringCloud升级之路2020.0.x版-40. spock 单元测试封装的 WebClient(下)
		
本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent 我们继续上一节,继续使用 spock 测试我们自己封装的 WebClient 测试针对 r ...
 - SpringCloud升级之路2020.0.x版-1.背景
		
本系列为之前系列的整理重启版,随着项目的发展以及项目中的使用,之前系列里面很多东西发生了变化,并且还有一些东西之前系列并没有提到,所以重启这个系列重新整理下,欢迎各位留言交流,谢谢!~ Spring ...
 - SpringCloud升级之路2020.0.x版-41. SpringCloudGateway 基本流程讲解(1)
		
本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent 接下来,将进入我们升级之路的又一大模块,即网关模块.网关模块我们废弃了已经进入维护状态的 ...
 - SpringCloud升级之路2020.0.x版-10.使用Log4j2以及一些核心配置
		
本系列代码地址:https://github.com/HashZhang/spring-cloud-scaffold/tree/master/spring-cloud-iiford 我们使用 Log4 ...
 
随机推荐
- Python+Request库+第三方平台实现验证码识别示例
			
1.登录时经常的出现验证码,此次结合Python+Request+第三方验证码识别平台(超级鹰识别平台) 2.首先到超级鹰平台下载对应语言的识别码封装,超级鹰平台:http://www.chaojiy ...
 - vue3 自学(一)基础知识学习和搭建一个脚手架
			
两年前曾自学过几天vue,那时候版本还是vue2,但后来项目中一直没用到,当时也觉得学习成本太高,便没有继续学习下去.初学者可以看下链接文章以前的吐槽~~ 学习 Vue ,从入门到放弃 最近部门决定升 ...
 - xhell、xftp、putty使用教程
			
作为远程登陆工具,上传代码登陆服务器工具 1.XSHELL Xshell是远程连接Linux服务器的工具,基于SSH协议,使用它可以更加方便的操作Linux操作系统,在刚使用时可能需要提前简单的设置下 ...
 - Python基础之tabview
			
以前写过界面,但是没有记录下来,以至于现在得从头学习一次,论做好笔记的重要性. 现在学习的是怎么写一个tabview出来,也就是用tkinter做一个界面切换的效果.参考链接:https://blog ...
 - SaltStack 命令注入漏洞(CVE-2020-16846)
			
SaltStack 是基于 Python 开发的一套C/S架构配置管理工具.2020年11月SaltStack官方披露了CVE-2020-16846和CVE-2020-25592两个漏洞,其中CVE- ...
 - 《Android原生整合虹软SDK开发uniapp插件》
			
1.项目背景 应公司要求,需要开发一套类似人脸打卡功能的app,但是因为我们公司没有很强的原生android开发者,所以根据现状选择了第三方跨平台的uniapp,想必目前大多人都了解这个平台了,我也就 ...
 - CF201C Fragile Bridges TJ
			
本题解依旧发布于洛谷,如果您能点个赞的话--(逃 前言 题目链接 正解:动态规划 思路不是很好想,想出来了应该就没有多大问题了,但是需要处理的细节较多,再加上水水的样例,难度应该是偏难的.个人感觉应该 ...
 - 使用Magicodes.IE快速导出Excel
			
前言 总是有很多朋友咨询Magicodes.IE如何基于ASP.NET Core导出Excel,出于从框架的体验和易用性的角度,决定对Excel的导出进行独立封装,以便于大家更易于使用,开箱即用. 注 ...
 - ElasticSearch入门检索
			
前面简介说到 elsatic是通过RestFul API接口操作数据的,可以通过postman模拟接口请求测试一下 一._cat 1.GET /_cat/nodes:查看所有节点 2.GET /_ca ...
 - CC攻击和C2的区别
			
[一]背景 今天被旁边姐姐问C2.CC是什么,虽然平时老看到这个词,身边也有自己写C2工具的大佬.但好像突然被问到有点懵,不知道怎么回答. [二]内容 CC ( Challenge Collapsar ...