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

2021-04-12 15:05:18 浏览数 (1)

本文基于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:

代码语言:javascript复制
@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:

代码语言:javascript复制
@RestController
@SpringBootApplication
public class TestService {
    @RequestMapping("/test")
    public String test() {
        throw new IllegalArgumentException();
    }
    public static void main(String[] args) {
        SpringApplication.run(TestService.class, args);
    }
}

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

代码语言:javascript复制
<dependency>
    <groupId>org.springframework.cloudgroupId>
    <artifactId>spring-cloud-starter-ribbonartifactId>
dependency>



<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-actuatorartifactId>
dependency>

定义一个RibbonClient:

代码语言:javascript复制
@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这个微服务:

代码语言:javascript复制
@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:

代码语言:javascript复制
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

启动:

代码语言:javascript复制
@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注解的源码:

代码语言:javascript复制
@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Qualifier
public @interface LoadBalanced {
}

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

代码语言:javascript复制
@LoadBalanced
@Autowired(required = false)
private List restTemplates = Collections.emptyList();

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

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

代码语言:javascript复制
@Configuration
@ConditionalOnClass(RestTemplate.class)
@ConditionalOnBean(LoadBalancerClient.class)
@EnableConfigurationProperties(LoadBalancerRetryProperties.class)
public class LoadBalancerAutoConfiguration {

    @LoadBalanced
    @Autowired(required = false)
    //所有被LoadBalanced修饰的restTemplates会被装载到这里
    private List restTemplates = Collections.emptyList();

    //SmartInitializingSingleton代表所有非lazy单例Bean实例化完成后的回调方法,即所有被LoadBalanced修饰的restTemplates会被初始化并装载到这里之后,这个afterSingletonsInstantiated会被回调
    @Bean
    public SmartInitializingSingleton loadBalancedRestTemplateInitializer(
            final List 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 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 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

代码语言:javascript复制
@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的配置是懒加载的:

代码语言:javascript复制
public  T execute(String serviceId, LoadBalancerRequest 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。

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

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

代码语言:javascript复制
@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 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 serverList, ServerListFilter 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 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配置:

代码语言:javascript复制
# ribbon连接超时
default-test.ribbon.ConnectTimeout=500
# ribbon读超时
default-test.ribbon.ReadTimeout=8000

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

代码语言:javascript复制
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:

代码语言:javascript复制
public class PropertiesFactory {
    @Autowired
    private Environment environment;

    private Map 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 get(Class 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,或者通过配置文件中添加如下配置实现:

代码语言:javascript复制
#配置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=

0 人点赞