Spring Cloud Ribbon 全解 (6) - SpringCloud环境下纯Ribbon(不包含Eureka)使用与启动分析

本文涉及的产品
传统型负载均衡 CLB,每月750个小时 15LCU
网络型负载均衡 NLB,每月750个小时 15LCU
应用型负载均衡 ALB,每月750个小时 15LCU
简介: Spring Cloud Ribbon 全解 (6) - SpringCloud环境下纯Ribbon(不包含Eureka)使用与启动分析

本文基于SpringCloud-Dalston.SR5

前面已经分析了Ribbon各个组件详细的源码,以及整体的流程


SpringCloud环境下纯Ribbon(不包含Eureka)使用与启动分析:


示例项目

以下项目可以参考:https://github.com/HashZhang/ScanfoldAll/tree/master/Scanfold-SpringCloud/Scanfold-SpringCloud-Ribbon/Scanfold-SpringCloud-RibbonOnly

我们首先在127.0.0.1:8222,127.0.0.1:8221启动两个进程,一个是正常工作的127.0.0.1:8221:

@RestController
@SpringBootApplication
public class TestService {
    @RequestMapping("/test")
    public String test() {
        return "test";
    }
    public static void main(String[] args) {
        SpringApplication.run(TestService.class, args);
    }
}

令一个是一定会抛出异常(http响应码为500)的127.0.0.1:8222:

@RestController
@SpringBootApplication
public class TestService {
    @RequestMapping("/test")
    public String test() {
        throw new IllegalArgumentException();
    }
    public static void main(String[] args) {
        SpringApplication.run(TestService.class, args);
    }
}

之后在我们的项目中引入依赖:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-ribbon</artifactId>
</dependency>
<!--以下依赖是启动Spring cloud应用需要-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

定义一个RibbonClient:

@Configuration
@RibbonClient(name = "default-test")
public class DefaultConfiguration {
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

在这个Configuration中,我们定义了一个名为“default-test”的RibbonClient(就是IClient),一个负载均衡的RestTemplate(因为@LoadBalanced注解的缘故,普通的Restemplate变成了可以负载均衡的RestTemplate,之后我们会分析这个注解的作用)

再定义一个Service,让它定时调用default-test这个微服务:

@Service
public class DefaultService {
    @Autowired
    private RestTemplate restTemplate;
    @Scheduled(fixedDelay = 5000)
    public void testDefaultRibbon() {
        String forObject = restTemplate.getForObject("http://default-test/test", String.class);
        System.out.println("**********************");
        System.out.println(forObject);
    }
}

编写application.properties:

default-test.ribbon.listOfServers=127.0.0.1:8222,127.0.0.1:8221
# ribbon连接超时
default-test.ribbon.ConnectTimeout=500
# ribbon读超时
default-test.ribbon.ReadTimeout=8000
######## management ########
management.security.enabled=false
endpoints.health.sensitive=false

启动:

@EnableScheduling
@SpringBootApplication
public class Main {
    public static void main(String[] args) {
        SpringApplication.run(Main.class, args);
    }
}

我们可以观察到127.0.0.1:8222,127.0.0.1:8221被依次调用。


示例项目分析


@LoadBalanced注解的源码:

@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Qualifier
public @interface LoadBalanced {
}

注意有@Qualifier这个注解,这个注解用在另外一个注解上(这里是LoadBalanced)时,代表所有含有@LoadBalanced注解的bean都会被标记起来,如果这时有:

@LoadBalanced
@Autowired(required = false)
private List<RestTemplate> restTemplates = Collections.emptyList();

代表所有被LoadBalanced修饰的restTemplates会被装载到这里

在我们的应用启动时,LoadBalancerAutoConfiguration会给所有被@LoadBalanced注解修饰的RestTemplate增加一个LoadBalanceInterceptor:

@Configuration
@ConditionalOnClass(RestTemplate.class)
@ConditionalOnBean(LoadBalancerClient.class)
@EnableConfigurationProperties(LoadBalancerRetryProperties.class)
public class LoadBalancerAutoConfiguration {
  @LoadBalanced
  @Autowired(required = false)
  //所有被LoadBalanced修饰的restTemplates会被装载到这里
  private List<RestTemplate> restTemplates = Collections.emptyList();
    //SmartInitializingSingleton代表所有非lazy单例Bean实例化完成后的回调方法,即所有被LoadBalanced修饰的restTemplates会被初始化并装载到这里之后,这个afterSingletonsInstantiated会被回调
  @Bean
  public SmartInitializingSingleton loadBalancedRestTemplateInitializer(
      final List<RestTemplateCustomizer> customizers) {
    return new SmartInitializingSingleton() {
      @Override
      public void afterSingletonsInstantiated() {
          //所有restTemplate被customizers处理
        for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {
          for (RestTemplateCustomizer customizer : customizers) {
            customizer.customize(restTemplate);
          }
        }
      }
    };
  }
  @Autowired(required = false)
  private List<LoadBalancerRequestTransformer> transformers = Collections.emptyList();
  @Bean
  @ConditionalOnMissingBean
  public LoadBalancerRequestFactory loadBalancerRequestFactory(
      LoadBalancerClient loadBalancerClient) {
    return new LoadBalancerRequestFactory(loadBalancerClient, transformers);
  }
    //在没有包含spring-retry这个依赖时,以下会被初始化,我们上面的项目就是没有加入spring-retry这个依赖
  @Configuration
  @ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
  static class LoadBalancerInterceptorConfig {
      //初始化LoadBalancerInterceptor
    @Bean
    public LoadBalancerInterceptor ribbonInterceptor(
        LoadBalancerClient loadBalancerClient,
        LoadBalancerRequestFactory requestFactory) {
      return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
    }
        //这个RestTemplateCustomizer就是为restTemplate添加一个LoadBalancerInterceptor
    @Bean
    @ConditionalOnMissingBean
    public RestTemplateCustomizer restTemplateCustomizer(
        final LoadBalancerInterceptor loadBalancerInterceptor) {
      return new RestTemplateCustomizer() {
        @Override
        public void customize(RestTemplate restTemplate) {
          List<ClientHttpRequestInterceptor> list = new ArrayList<>(
              restTemplate.getInterceptors());
          list.add(loadBalancerInterceptor);
          restTemplate.setInterceptors(list);
        }
      };
    }
  }
    //在包含spring-retry这个依赖时,以下会被初始化,之后我们会讲这个重试
  @Configuration
  @ConditionalOnClass(RetryTemplate.class)
  public static class RetryAutoConfiguration {
    @Bean
    public RetryTemplate retryTemplate() {
      RetryTemplate template =  new RetryTemplate();
      template.setThrowLastExceptionOnExhausted(true);
      return template;
    }
    @Bean
    @ConditionalOnMissingBean
    public LoadBalancedRetryPolicyFactory loadBalancedRetryPolicyFactory() {
      return new LoadBalancedRetryPolicyFactory.NeverRetryFactory();
    }
  }
}

在执行请求(restTemplate.getForObject("http://default-test/test", String.class);)时,会先经过所有的Interceptor,其中这个LoadBalanceInterceptor,其实就是利用loadBalancer将原请求转化成一个负载均衡请求并执行:


LoadBalancerInterceptor.java

@Override
public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
    final ClientHttpRequestExecution execution) throws IOException {
  final URI originalUri = request.getURI();
  String serviceName = originalUri.getHost();
  Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
  //利用loadBalancer将原请求转化成一个负载均衡请求并执行
  return this.loadBalancer.execute(serviceName, requestFactory.createRequest(request, body, execution));
}

RibbonLoadBalancerClient是SpringCloud对于Ribbon的封装,在这里会初始化Ribbon的配置,所以其实Ribbon的配置是懒加载的:

public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
  //根据serviceId(这里是default-test)获取loadBalancer,如果不存在就创建
  ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
  //从LoadBalancer中获取一个Server
  Server server = getServer(loadBalancer);
  if (server == null) {
    throw new IllegalStateException("No instances available for " + serviceId);
  }
  //封装成为RibbonServer
  RibbonServer ribbonServer = new RibbonServer(serviceId, server, isSecure(server,
      serviceId), serverIntrospector(serviceId).getMetadata(server));
    //执行
  return execute(serviceId, ribbonServer, request);
}

其实这里我们就能看出,Ribbon在这里只是提供一个Server,之后的执行请求并不是Ribbon的IClient负责的。所以,我们之前讨论的Ribbon8大元素,在SpringCloud的环境下,其实只用到了其中七个。SpringCloud实现了自己的负载均衡器RibbonLoadBalancerClient。

总结下,流程大概就是如下图所示:


微信图片_20220624113452.jpg


这里getLoadBalancer是初始化RibbonClientConfiguration这个懒加载的Configuration:

@SuppressWarnings("deprecation")
@Configuration
@EnableConfigurationProperties
//Order is important here, last should be the default, first should be optional
// see https://github.com/spring-cloud/spring-cloud-netflix/issues/2086#issuecomment-316281653
@Import({OkHttpRibbonConfiguration.class, RestClientRibbonConfiguration.class, HttpClientRibbonConfiguration.class})
public class RibbonClientConfiguration {
    //Ribbonclient名称,这里是default-test
  @Value("${ribbon.client.name}")
  private String name = "client";
  @Autowired
  private PropertiesFactory propertiesFactory;
    //从配置文件(application.properties)中读取default-test这个RibbonClient的配置
  @Bean
  @ConditionalOnMissingBean
  public IClientConfig ribbonClientConfig() {
    DefaultClientConfigImpl config = new DefaultClientConfigImpl();
    config.loadProperties(this.name);
    return config;
  }
    //如果没设置,IRule默认为ZoneAvoidanceRule
  @Bean
  @ConditionalOnMissingBean
  public IRule ribbonRule(IClientConfig config) {
    if (this.propertiesFactory.isSet(IRule.class, name)) {
      return this.propertiesFactory.get(IRule.class, config, name);
    }
    ZoneAvoidanceRule rule = new ZoneAvoidanceRule();
    rule.initWithNiwsConfig(config);
    return rule;
  }
    //如果没设置,IPing默认为DummyPing
  @Bean
  @ConditionalOnMissingBean
  public IPing ribbonPing(IClientConfig config) {
    if (this.propertiesFactory.isSet(IPing.class, name)) {
      return this.propertiesFactory.get(IPing.class, config, name);
    }
    return new DummyPing();
  }
    //如果没设置,ServerList默认为ConfigurationBasedServerList
  @Bean
  @ConditionalOnMissingBean
  @SuppressWarnings("unchecked")
  public ServerList<Server> ribbonServerList(IClientConfig config) {
    if (this.propertiesFactory.isSet(ServerList.class, name)) {
      return this.propertiesFactory.get(ServerList.class, config, name);
    }
    ConfigurationBasedServerList serverList = new ConfigurationBasedServerList();
    serverList.initWithNiwsConfig(config);
    return serverList;
  }
    //ServerListUpdater是PollingServerListUpdater,这个只能通过Bean替换,不能通过配置文件配置
  @Bean
  @ConditionalOnMissingBean
  public ServerListUpdater ribbonServerListUpdater(IClientConfig config) {
    return new PollingServerListUpdater(config);
  }
    //如果没设置,ILoadBalancer默认为ZoneAwareLoadBalancer
  @Bean 
  @ConditionalOnMissingBean
  public ILoadBalancer ribbonLoadBalancer(IClientConfig config,
      ServerList<Server> serverList, ServerListFilter<Server> serverListFilter,
      IRule rule, IPing ping, ServerListUpdater serverListUpdater) {
    if (this.propertiesFactory.isSet(ILoadBalancer.class, name)) {
      return this.propertiesFactory.get(ILoadBalancer.class, config, name);
    }
    return new ZoneAwareLoadBalancer<>(config, rule, ping, serverList,
        serverListFilter, serverListUpdater);
  }
    //如果没设置,ServerListFilter默认为ZonePreferenceServerListFilter
  @Bean
  @ConditionalOnMissingBean
  @SuppressWarnings("unchecked")
  public ServerListFilter<Server> ribbonServerListFilter(IClientConfig config) {
    if (this.propertiesFactory.isSet(ServerListFilter.class, name)) {
      return this.propertiesFactory.get(ServerListFilter.class, config, name);
    }
    ZonePreferenceServerListFilter filter = new ZonePreferenceServerListFilter();
    filter.initWithNiwsConfig(config);
    return filter;
  }
  @Bean
  @ConditionalOnMissingBean
  public RibbonLoadBalancerContext ribbonLoadBalancerContext(
      ILoadBalancer loadBalancer, IClientConfig config, RetryHandler retryHandler) {
    return new RibbonLoadBalancerContext(loadBalancer, config, retryHandler);
  }
  @Bean
  @ConditionalOnMissingBean
  public RetryHandler retryHandler(IClientConfig config) {
    return new DefaultLoadBalancerRetryHandler(config);
  }
  @Bean
  @ConditionalOnMissingBean
  public ServerIntrospector serverIntrospector() {
    return new DefaultServerIntrospector();
  }
  @PostConstruct
  public void preprocess() {
    setRibbonProperty(name, DeploymentContextBasedVipAddresses.key(), name);
  }
  static class OverrideRestClient extends RestClient {
    private IClientConfig config;
    private ServerIntrospector serverIntrospector;
    protected OverrideRestClient(IClientConfig config,
        ServerIntrospector serverIntrospector) {
      super();
      this.config = config;
      this.serverIntrospector = serverIntrospector;
      initWithNiwsConfig(this.config);
    }
    @Override
    public URI reconstructURIWithServer(Server server, URI original) {
      URI uri = updateToHttpsIfNeeded(original, this.config, this.serverIntrospector, server);
      return super.reconstructURIWithServer(server, uri);
    }
    @Override
    protected Client apacheHttpClientSpecificInitialization() {
      ApacheHttpClient4 apache = (ApacheHttpClient4) super
          .apacheHttpClientSpecificInitialization();
      apache.getClientHandler()
          .getHttpClient()
          .getParams()
          .setParameter(ClientPNames.COOKIE_POLICY, CookiePolicy.IGNORE_COOKIES);
      return apache;
    }
  }
}


SpringCloud环境下纯Ribbon配置分析


总结起来,在Spring Cloud环境下Ribbon默认的实现类如下所示:

  1. 服务实例列表维护机制实现的接口ServerList: ConfigurationBasedServerList
  2. 负责选取Server的接口ILoadBalancer:ZoneAwareLoadBalancer
  3. 负载均衡选取规则实现的接口IRule:ZoneAvoidanceRule
  4. 检查实例是否存活实现的接口IPing:DummyPing
  5. 服务实例列表更新机制实现的接口ServerListUpdater:PollingServerListUpdater
  6. 服务实例列表过滤机制ServerListFilter:ZonePreferenceServerListFilter

Ribbon的配置有很多,例如上面我们用到的ribbon配置:

# ribbon连接超时
default-test.ribbon.ConnectTimeout=500
# ribbon读超时
default-test.ribbon.ReadTimeout=8000

这些配置是基于Archaius的,我们可以看DefaultClientConfigImpl这个类:

public void loadProperties(String restClientName){
    enableDynamicProperties = true;
    setClientName(restClientName);
    //载入默认值
    loadDefaultValues();
    //载入配置值,包括“微服务名.ribbon.配置”和“ribbon.配置”这两种的所有配置
    Configuration props = ConfigurationManager.getConfigInstance().subset(restClientName);
    for (Iterator<String> keys = props.getKeys(); keys.hasNext(); ){
        String key = keys.next();
        String prop = key;
        try {
            if (prop.startsWith(getNameSpace())){
                prop = prop.substring(getNameSpace().length() + 1);
            }
            setPropertyInternal(prop, getStringValue(props, key));
        } catch (Exception ex) {
            throw new RuntimeException(String.format("Property %s is invalid", prop));
        }
    }
}

像这种配置,是基于Archaius的,可以是微服务名.ribbon.配置这么去配置,或者ribbon.配置这么去配置。

但是像默认实现类的配置,只能通过微服务名.ribbon.配置这么去配(通过PropertiesFactory配置的),或者通过自定义同类型Bean去替换:

PropertiesFactory.java:

public class PropertiesFactory {
  @Autowired
  private Environment environment;
  private Map<Class, String> classToProperty = new HashMap<>();
  public PropertiesFactory() {
    classToProperty.put(ILoadBalancer.class, "NFLoadBalancerClassName");
    classToProperty.put(IPing.class, "NFLoadBalancerPingClassName");
    classToProperty.put(IRule.class, "NFLoadBalancerRuleClassName");
    classToProperty.put(ServerList.class, "NIWSServerListClassName");
    classToProperty.put(ServerListFilter.class, "NIWSServerListFilterClassName");
  }
  public boolean isSet(Class clazz, String name) {
    return StringUtils.hasText(getClassName(clazz, name));
  }
  public String getClassName(Class clazz, String name) {
    if (this.classToProperty.containsKey(clazz)) {
      String classNameProperty = this.classToProperty.get(clazz);
      String className = environment.getProperty(name + "." + NAMESPACE + "." + classNameProperty);
      return className;
    }
    return null;
  }
  @SuppressWarnings("unchecked")
  public <C> C get(Class<C> clazz, IClientConfig config, String name) {
    String className = getClassName(clazz, name);
    if (StringUtils.hasText(className)) {
      try {
        Class<?> toInstantiate = Class.forName(className);
        return (C) instantiateWithConfig(toInstantiate, config);
      } catch (ClassNotFoundException e) {
        throw new IllegalArgumentException("Unknown class to load "+className+" for class " + clazz + " named " + name);
      }
    }
    return null;
  }
}

修改这些默认实现类Ribbon的配置我们可以通过在自己的configuration添加对应的Bean,或者通过配置文件中添加如下配置实现:

#配置ILoadBalancer
default-test.ribbon.NFLoadBalancerClassName=
#配置IPing
default-test.ribbon.NFLoadBalancerPingClassName=
#配置IRule
default-test.ribbon.NFLoadBalancerRuleClassName=
#配置ServerList
default-test.ribbon.NIWSServerListClassName=
#配置ServerListFilter
default-test.ribbon.NIWSServerListFilterClassName=
相关实践学习
小试牛刀,一键部署电商商城
SAE 仅需一键,极速部署一个微服务电商商城,体验 Serverless 带给您的全托管体验,一起来部署吧!
负载均衡入门与产品使用指南
负载均衡(Server Load Balancer)是对多台云服务器进行流量分发的负载均衡服务,可以通过流量分发扩展应用系统对外的服务能力,通过消除单点故障提升应用系统的可用性。 本课程主要介绍负载均衡的相关技术以及阿里云负载均衡产品的使用方法。
相关文章
|
23天前
|
前端开发 Java Nacos
🛡️Spring Boot 3 整合 Spring Cloud Gateway 工程实践
本文介绍了如何使用Spring Cloud Alibaba 2023.0.0.0技术栈构建微服务网关,以应对微服务架构中流量治理与安全管控的复杂性。通过一个包含鉴权服务、文件服务和主服务的项目,详细讲解了网关的整合与功能开发。首先,通过统一路由配置,将所有请求集中到网关进行管理;其次,实现了限流防刷功能,防止恶意刷接口;最后,添加了登录鉴权机制,确保用户身份验证。整个过程结合Nacos注册中心,确保服务注册与配置管理的高效性。通过这些实践,帮助开发者更好地理解和应用微服务网关。
78 0
🛡️Spring Boot 3 整合 Spring Cloud Gateway 工程实践
|
2月前
|
人工智能 安全 Java
AI 时代:从 Spring Cloud Alibaba 到 Spring AI Alibaba
本次分享由阿里云智能集团云原生微服务技术负责人李艳林主讲,主题为“AI时代:从Spring Cloud Alibaba到Spring AI Alibaba”。内容涵盖应用架构演进、AI agent框架发展趋势及Spring AI Alibaba的重磅发布。分享介绍了AI原生架构与传统架构的融合,强调了API优先、事件驱动和AI运维的重要性。同时,详细解析了Spring AI Alibaba的三层抽象设计,包括模型支持、工作流智能体编排及生产可用性构建能力,确保安全合规、高效部署与可观测性。最后,结合实际案例展示了如何利用私域数据优化AI应用,提升业务价值。
239 4
|
3月前
|
XML Java 数据格式
Spring Core核心类库的功能与应用实践分析
【12月更文挑战第1天】大家好,今天我们来聊聊Spring Core这个强大的核心类库。Spring Core作为Spring框架的基础,提供了控制反转(IOC)和依赖注入(DI)等核心功能,以及企业级功能,如JNDI和定时任务等。通过本文,我们将从概述、功能点、背景、业务点、底层原理等多个方面深入剖析Spring Core,并通过多个Java示例展示其应用实践,同时指出对应实践的优缺点。
81 14
|
3月前
|
消息中间件 监控 Java
如何将Spring Boot + RabbitMQ应用程序部署到Pivotal Cloud Foundry (PCF)
如何将Spring Boot + RabbitMQ应用程序部署到Pivotal Cloud Foundry (PCF)
58 6
|
3月前
|
负载均衡 Java 开发者
深入探索Spring Cloud与Spring Boot:构建微服务架构的实践经验
深入探索Spring Cloud与Spring Boot:构建微服务架构的实践经验
236 5
|
3月前
|
Java 关系型数据库 MySQL
如何将Spring Boot + MySQL应用程序部署到Pivotal Cloud Foundry (PCF)
如何将Spring Boot + MySQL应用程序部署到Pivotal Cloud Foundry (PCF)
92 5
|
3月前
|
缓存 监控 Java
如何将Spring Boot应用程序部署到Pivotal Cloud Foundry (PCF)
如何将Spring Boot应用程序部署到Pivotal Cloud Foundry (PCF)
73 5
|
4月前
|
负载均衡 算法 Java
除了 Ribbon,Spring Cloud 中还有哪些负载均衡组件?
这些负载均衡组件各有特点,在不同的场景和需求下,可以根据项目的具体情况选择合适的负载均衡组件来实现高效、稳定的服务调用。
328 5
|
5月前
|
Java BI API
spring boot 整合 itextpdf 导出 PDF,写入大文本,写入HTML代码,分析当下导出PDF的几个工具
这篇文章介绍了如何在Spring Boot项目中整合iTextPDF库来导出PDF文件,包括写入大文本和HTML代码,并分析了几种常用的Java PDF导出工具。
1026 0
spring boot 整合 itextpdf 导出 PDF,写入大文本,写入HTML代码,分析当下导出PDF的几个工具
|
3月前
|
负载均衡 Java Nacos
常见的Ribbon/Spring LoadBalancer的负载均衡策略
自SpringCloud 2020版起,Ribbon被弃用,转而使用Spring Cloud LoadBalancer。Ribbon支持轮询、随机、加权响应时间和重试等负载均衡策略;而Spring Cloud LoadBalancer则提供轮询、随机及Nacos负载均衡策略,基于Reactor实现,更高效灵活。
226 0