Spring Cloud升级之路 - Hoxton - 3. 淘汰Ribbon

简介: Spring Cloud升级之路 - Hoxton - 3. 淘汰Ribbon
本系列示例与胶水代码地址: https://github.com/HashZhang/spring-cloud-

scaffold

负载均衡Ribbon替换成Spring Cloud Load Balancer


Spring Cloud Load Balancer并不是一个独立的项目,而是spring-cloud-commons其中的一个模块。 项目中用了Eureka以及相关的 starter,想完全剔除Ribbon的相关依赖基本是不可能的,Spring 社区的人也是看到了这一点,通过配置去关闭Ribbon启用Spring-Cloud-LoadBalancer


spring.cloud.loadbalancer.ribbon.enabled=false

关闭ribbon之后,Spring Cloud LoadBalancer就会加载成为默认的负载均衡器。

Spring Cloud LoadBalancer 结构如下所示:


微信图片_20220624124144.jpg


其中:

  1. 全局只有一个 BlockingLoadBalancerClient,负责执行所有的负载均衡请求。
  2. BlockingLoadBalancerClientLoadBalancerClientFactory里面加载对应微服务的负载均衡配置。
  3. 每个微服务下有独自的LoadBalancerLoadBalancer里面包含负载均衡的算法,例如RoundRobin.根据算法,从ServiceInstanceListSupplier返回的实例列表中选择一个实例返回。


1. 实现zone隔离

要想实现zone隔离,应该从ServiceInstanceListSupplier里面做手脚。默认的实现里面有关于zone隔离的ServiceInstanceListSupplier -> org.springframework.cloud.loadbalancer.core.ZonePreferenceServiceInstanceListSupplier:

private List<ServiceInstance> filteredByZone(List<ServiceInstance> serviceInstances) {
  if (zone == null) {
    zone = zoneConfig.getZone();
  }
  //如果zone不为null,并且该zone下有存活实例,则返回这个实例列表
  //否则,返回所有的实例
  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;
    }
  }
  // If the zone is not set or there are no zone-specific instances available,
  // we return all instances retrieved for given service id.
  return serviceInstances;
}


这里对于没指定zone或者该zone下没有存活实例的情况下,会返回所有查到的实例,不区分zone。这个不符合我们的要求,所以我们修改并实现下我们自己的com.github.hashjang.hoxton.service.consumer.config.SameZoneOnlyServiceInstanceListSupplier:

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;
        }
    }
    //如果没找到就返回空列表,绝不返回其他集群的实例
    return List.of();
}


然后我们来看一下默认的 Spring Cloud LoadBalancer 提供的 LoadBalancer ,它是带缓存的:

org.springframework.cloud.loadbalancer.annotation.LoadBalancerClientConfiguration

@Bean
@ConditionalOnBean(ReactiveDiscoveryClient.class)
@ConditionalOnMissingBean
public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier(
    ReactiveDiscoveryClient discoveryClient, Environment env,
    ApplicationContext context) {
  DiscoveryClientServiceInstanceListSupplier delegate = new DiscoveryClientServiceInstanceListSupplier(
      discoveryClient, env);
  ObjectProvider<LoadBalancerCacheManager> cacheManagerProvider = context
      .getBeanProvider(LoadBalancerCacheManager.class);
  if (cacheManagerProvider.getIfAvailable() != null) {
    return new CachingServiceInstanceListSupplier(delegate,
        cacheManagerProvider.getIfAvailable());
  }
  return delegate;
}


DiscoveryClientServiceInstanceListSupplier每次从Eureka上面拉取实例列表,CachingServiceInstanceListSupplier提供了缓存,这样不必每次从Eureka上面拉取。可以看出CachingServiceInstanceListSupplier是一种代理模式的实现,和SameZoneOnlyServiceInstanceListSupplier的模式是一样的。


我们来组装我们的ServiceInstanceListSupplier,由于我们是同步的环境,只用实现同步的ServiceInstanceListSupplier就行了。

public class CommonLoadBalancerConfig {
    /**
     * 同步环境下的ServiceInstanceListSupplier
     * SameZoneOnlyServiceInstanceListSupplier限制仅返回同一个zone下的实例(注意)
     * CachingServiceInstanceListSupplier启用缓存,不每次访问eureka请求实例列表
     *
     * @param discoveryClient
     * @param env
     * @param zoneConfig
     * @param context
     * @return
     */
    @Bean
    @Order(Integer.MIN_VALUE)
    public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier(
            DiscoveryClient discoveryClient, Environment env,
            LoadBalancerZoneConfig zoneConfig,
            ApplicationContext context) {
        ServiceInstanceListSupplier delegate = new SameZoneOnlyServiceInstanceListSupplier(
                new DiscoveryClientServiceInstanceListSupplier(discoveryClient, env),
                zoneConfig
        );
        ObjectProvider<LoadBalancerCacheManager> cacheManagerProvider = context
                .getBeanProvider(LoadBalancerCacheManager.class);
        if (cacheManagerProvider.getIfAvailable() != null) {
            return new CachingServiceInstanceListSupplier(
                    delegate,
                    cacheManagerProvider.getIfAvailable()
            );
        }
        return delegate;
    }
}


2. 实现下一次重试的时候,如果存在其他实例,则一定会重试与本次不同的其他实例


默认的RoundRobinLoadBalancer,其中的轮询position,是一个Atomic类型的,在某个微服务的调用请求下,所有线程,所有请求共用(调用其他的每个微服务会创建一个RoundRobinLoadBalancer)。在使用的时候,会有这样的一个问题:

  • 假设某个微服务有两个实例,实例 A 和实例 B
  • 某次请求 X 发往实例 A,position = position + 1
  • 在请求没有返回时,请求 Y 到达,发往实例 B,position = position + 1
  • 请求 A 失败,重试,重试的实例还是实例 A

这样在重试的情况下,某个请求的重试可能会发送到上一次的实例进行重试,这不是我们想要的。针对这个,我提了个Issue:Enhance RoundRoubinLoadBalancer position。我修改的思路是,我们需要一个单次请求隔离的position,这个position对于实例个数取余得出请求要发往的实例。那么如何进行请求隔离呢?


首先想到的是线程隔离,但是这个是不行的。Spring Cloud LoadBalancer 底层运用了 reactor 框架,导致实际承载选择实例的线程,不是业务线程,而是 reactor 里面的线程池,如图所示:


image.png


所以,不能用ThreadLocal的方式实现position

由于我们用到了sleuth,一般请求的context会传递其中的traceId,我们根据这个traceId区分不同的请求,实现我们的 LoadBalancer

RoundRobinBaseOnTraceIdLoadBalancer

//这个超时时间,需要设置的比你的请求的 connectTimeout + readTimeout 长
private final LoadingCache<Long, AtomicInteger> positionCache = Caffeine.newBuilder().expireAfterWrite(3, TimeUnit.SECONDS).build(k -> new AtomicInteger(ThreadLocalRandom.current().nextInt(0, 1000)));
private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> serviceInstances) {
    if (serviceInstances.isEmpty()) {
        log.warn("No servers available for service: " + this.serviceId);
        return new EmptyResponse();
    }
    //如果没有 traceId,就生成一个新的,但是最好检查下为啥会没有
    //是不是 MQ 消费这种没有主动生成 traceId 的情况,最好主动生成下。
    Span currentSpan = tracer.currentSpan();
    if (currentSpan == null) {
        currentSpan = tracer.newTrace();
    }
    long l = currentSpan.context().traceId();
    int seed = positionCache.get(l).getAndIncrement();
    return new DefaultResponse(serviceInstances.get(seed % serviceInstances.size()));
}


3. 替换默认的负载均衡相关 Bean 实现


我们要用上面的两个类替换默认的实现,先编写一个配置类:

public class CommonLoadBalancerConfig {
    private volatile boolean isValid = false;
    /**
     * 同步环境下的ServiceInstanceListSupplier
     * SameZoneOnlyServiceInstanceListSupplier限制仅返回同一个zone下的实例(注意)
     * CachingServiceInstanceListSupplier启用缓存,不每次访问eureka请求实例列表
     *
     * @param discoveryClient
     * @param env
     * @param zoneConfig
     * @param context
     * @return
     */
    @Bean
    @Order(Integer.MIN_VALUE)
    public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier(
            DiscoveryClient discoveryClient, Environment env,
            LoadBalancerZoneConfig zoneConfig,
            ApplicationContext context) {
        isValid = true;
        ServiceInstanceListSupplier delegate = new SameZoneOnlyServiceInstanceListSupplier(
                new DiscoveryClientServiceInstanceListSupplier(discoveryClient, env),
                zoneConfig
        );
        ObjectProvider<LoadBalancerCacheManager> cacheManagerProvider = context
                .getBeanProvider(LoadBalancerCacheManager.class);
        if (cacheManagerProvider.getIfAvailable() != null) {
            return new CachingServiceInstanceListSupplier(
                    delegate,
                    cacheManagerProvider.getIfAvailable()
            );
        }
        return delegate;
    }
    @Bean
    public ReactorLoadBalancer<ServiceInstance> reactorServiceInstanceLoadBalancer(
            Environment environment,
            ServiceInstanceListSupplier serviceInstanceListSupplier,
            Tracer tracer) {
        if (!isValid) {
            throw new IllegalStateException("should use the ServiceInstanceListSupplier in this configuration, please check config");
        }
        String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
        return new RoundRobinBaseOnTraceIdLoadBalancer(
                name,
                serviceInstanceListSupplier,
                tracer
        );
    }
}


然后,指定默认的负载均衡配置采取这个配置, 通过注解:

@LoadBalancerClients(defaultConfiguration = {CommonLoadBalancerConfig.class})
相关实践学习
部署高可用架构
本场景主要介绍如何使用云服务器ECS、负载均衡SLB、云数据库RDS和数据传输服务产品来部署多可用区高可用架构。
负载均衡入门与产品使用指南
负载均衡(Server Load Balancer)是对多台云服务器进行流量分发的负载均衡服务,可以通过流量分发扩展应用系统对外的服务能力,通过消除单点故障提升应用系统的可用性。 本课程主要介绍负载均衡的相关技术以及阿里云负载均衡产品的使用方法。
相关文章
|
20小时前
|
移动开发 前端开发 NoSQL
ruoyi-nbcio从spring2.7.18升级springboot到3.1.7,java从java8升级到17(二)
ruoyi-nbcio从spring2.7.18升级springboot到3.1.7,java从java8升级到17(二)
44 0
|
20小时前
|
监控 安全 Java
Spring cloud原理详解
Spring cloud原理详解
17 0
|
20小时前
|
消息中间件 负载均衡 Java
【Spring Cloud 初探幽】
【Spring Cloud 初探幽】
16 1
|
20小时前
|
安全 Java Docker
|
20小时前
|
Java 开发者 微服务
Spring Cloud原理详解
【5月更文挑战第4天】Spring Cloud是Spring生态系统中的微服务框架,包含配置管理、服务发现、断路器、API网关等工具,简化分布式系统开发。核心组件如Eureka(服务发现)、Config Server(配置中心)、Ribbon(负载均衡)、Hystrix(断路器)、Zuul(API网关)等。本文讨论了Spring Cloud的基本概念、核心组件、常见问题及解决策略,并提供代码示例,帮助开发者更好地理解和实践微服务架构。此外,还涵盖了服务通信方式、安全性、性能优化、自动化部署、服务网格和无服务器架构的融合等话题,揭示了微服务架构的未来趋势。
35 6
|
20小时前
|
JSON Java Apache
Spring Cloud Feign 使用Apache的HTTP Client替换Feign原生httpclient
Spring Cloud Feign 使用Apache的HTTP Client替换Feign原生httpclient
|
20小时前
|
负载均衡 Java 开发者
Spring Cloud:一文读懂其原理与架构
Spring Cloud 是一套微服务解决方案,它整合了Netflix公司的多个开源框架,简化了分布式系统开发。Spring Cloud 提供了服务注册与发现、配置中心、消息总线、负载均衡、熔断机制等工具,让开发者可以快速地构建一些常见的微服务架构。
|
20小时前
|
消息中间件 Java RocketMQ
Spring Cloud RocketMQ:构建可靠消息驱动的微服务架构
【4月更文挑战第28天】消息队列在微服务架构中扮演着至关重要的角色,能够实现服务之间的解耦、异步通信以及数据分发。Spring Cloud RocketMQ作为Apache RocketMQ的Spring Cloud集成,为微服务架构提供了可靠的消息传输机制。
30 1
|
20小时前
|
Dubbo Java 应用服务中间件
Spring Cloud Dubbo: 微服务通信的高效解决方案
【4月更文挑战第28天】在微服务架构的发展中,服务间的高效通信至关重要。Spring Cloud Dubbo 提供了一种基于 RPC 的通信方式,使得服务间的调用就像本地方法调用一样简单。本篇博客将探讨 Spring Cloud Dubbo 的核心概念,并通过具体实例展示其在项目中的实战应用。
19 2
|
20小时前
|
监控 Java Sentinel
Spring Cloud Sentinel:概念与实战应用
【4月更文挑战第28天】在分布式微服务架构中,确保系统的稳定性和可靠性至关重要。Spring Cloud Sentinel 为微服务提供流量控制、熔断降级和系统负载保护,有效预防服务雪崩。本篇博客深入探讨 Spring Cloud Sentinel 的核心概念,并通过实际案例展示其在项目中的应用。
26 0

热门文章

最新文章