zoukankan      html  css  js  c++  java
  • SpringCloud之Ribbon的使用及源码解析

    Ribbon简介

    什么是Ribbon?

    Ribbon是Netflix发布的负载均衡器,它可以帮我们控制HTTP和TCP客户端的行为。只需为Ribbon配置服务提供者地址列表,Ribbon就可基于负载均衡算法计算出要请求的目标服务地址。

    Ribbon默认为我们提供了很多的负载均衡算法,例如轮询、随机、响应时间加权等——当然,为Ribbon自定义负载均衡算法也非常容易,只需实现IRule 接口即可。

    SpringCloud提供了Ribbon用来做客户端负载均衡,通过SpringCloud对Rbbon的封装,我们可以很轻松的通过负载均衡去调用我们开发的rest服务,不需要手动去处理因负载均衡而出现的各种棘手情况,
    Ribbon并不需要像eureka和网关那样单独部署,它是和每一个微服务耦合在一起的
    使用resttemplate与ribbon整合去实现负载均衡的调用,启用方式是需要我们在RestTemplate 实例配置上面添加@LoadBalanced注解。
    可以去了解一下RestTemplate的api使用。
     
    怎么用?
     
    加依赖
    基于已经有的SpirngCloud项目,由于spring-cloud-starter-netflix-eureka-client 已经包含 spring-cloud-starter-netfilx-ribbon ,故而无需额外添加依赖。
     
    写代码
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
      return new RestTemplate();
    }

    只需在RestTemplate 上添加LoadBalanced 注解,即可让RestTemplate整合Ribbon!

    调用

    @GetMapping("/users/{id}")
    public User findById(@PathVariable Long id) {
      // 这里用到了RestTemplate的占位符能力
      User user = this.restTemplate.getForObject(
        "http://microservice-provider-user/users/{id}",
        User.class,
        id
      );
      // ...业务...
      return user;
    }

    我们将请求的目标服务改成了http://microservice-provider-user/users/{id} ,也就是http://{目标服务名称}/{目标服务端点} 的形式,Ribbon会自动在实际调用时,将目标服务名替换为该服务的IP和端口

    WARNING

    事实上,这里的目标服务名称,在Ribbon里叫虚拟主机名 ,主机名是不能包含_ 等特殊字符的——这意味着,一般不建议配置spring.application.name = xxx_xxx ,如果你的应用名称一定带有下划线这种字符,

    那么请额外配置eureka.instance.virtual-host-name = 一个合法的主机名 ,否则Ribbon将会提示虚拟主机名不合法的异常(在早期的版本则是报空指针)!

    什么时候用Ribbon,用在哪?

    上面知道Ribbon实现在客户端的负载均衡,所以当我们需要调用多台部署了相同项目的服务器时,就可以使用Ribbon。既然时客户端的负载均衡,那自然是在调用方使用了。

    比如服务调用方A调用服务提供方B/C/D,就可以再A使用Ribbon,负载均衡的调用BCD。

    深入

    那么Ribbon是如何实现负载均衡的呢?
    根据我们前面说Ribbon实现的的客户端负载均衡,所以它自己肯定有一个可用的服务列表,服务列表里面存储的是可用服务的地址,这是第一个条件;
    第二个是我们在RestTemplate上面添加了注解后,它就自动实现了负载均衡,这个我们可以想到它拦截了我们的请求,所以这里肯定会有一个拦截器在帮助我们实现此功能;
    第三个是为什么我们加上了注解后,也可以使用服务名直接去调用了呢,肯定也有组件帮我们实现了替换的
     
    看源码!!
    因为时通过注解@LoadBalanced开启的Ribbon,所以就从这个注解开始看。
     
    @Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Inherited
    @Qualifier
    public @interface LoadBalanced {
    }

    可以看到类里面是空的。那想到在springboot中,有一个申明式的注解,必定在其同名的包下面会有一个这样的(xxxAutoConfiguration)配置类,去配置这个注解,

    我们定位到该包下面,看到同名的包下面确实有这样一个配置类LoadBalancerAutoConfiguration

    点进去是这样的:

    @Configuration(//@Configuration:注解可以用java代码的形式实现spring中xml配置文件配置的效果。
        proxyBeanMethods = false
    )
    @ConditionalOnClass({RestTemplate.class})//@ConditionalOnClass:其用途是判断当前classpath下是否存在指定类,若是则将当前的配置装载入spring容器
    @ConditionalOnBean({LoadBalancerClient.class})//@ConditionalOnBean:当给定的在bean存在时,则实例化当前Bean
    @EnableConfigurationProperties({LoadBalancerRetryProperties.class})
    public class LoadBalancerAutoConfiguration {
        @LoadBalanced
        @Autowired(
            required = false
        )
    //这里面会注入有@loadbalence注解的所有的restTemplate实例
    private List<RestTemplate> restTemplates = Collections.emptyList(); @Autowired( required = false ) //还有很多。。。 }

     @EnableConfigurationProperties:如果该类只使用了@ConfigurationProperties注解,然后该类没有在扫描路径下或者没有使用@Component等注解,导致无法被扫描为bean,那么就必须在配置类上使用@EnableConfigurationProperties注解去指定这个类,这个时候就会让该类上的@ConfigurationProperties生效,然后作为bean添加进spring容器中

    再点击RestTemplate进去,看到此类继承了
    InterceptingHttpAccessor
    public class RestTemplate extends InterceptingHttpAccessor implements RestOperations {
    //。。。。
    }

    看来真的有拦截器在起作用,我们可以看到InterceptingHttpAccessor类中有一个setInterceptors方法

    public void setInterceptors(List<ClientHttpRequestInterceptor> interceptors) {
        Assert.noNullElements(interceptors, "'interceptors' must not contain null elements");
        if (this.interceptors != interceptors) {
            this.interceptors.clear();
            this.interceptors.addAll(interceptors);
            AnnotationAwareOrderComparator.sort(this.interceptors);
        }
    }

    然后我们看看他是在哪里调用这份方法的,我们再回到LoadBalancerAutoConfiguration配置类中,看到这段方法:

            @Bean
            @ConditionalOnMissingBean
            public RestTemplateCustomizer restTemplateCustomizer(final RetryLoadBalancerInterceptor loadBalancerInterceptor) {
                return (restTemplate) -> {
                    List<ClientHttpRequestInterceptor> list = new ArrayList(restTemplate.getInterceptors());
                    list.add(loadBalancerInterceptor);
                    restTemplate.setInterceptors(list);
                };
            }

    看到这里在往resttemplate中添加拦截器loadBalancerInterceptor,我们点击这个拦截器进去,主要看这段代码

    public ClientHttpResponse intercept(final HttpRequest request, final byte[] body, final ClientHttpRequestExecution execution) throws IOException {
        URI originalUri = request.getURI();//获取URI
        String serviceName = originalUri.getHost();//获取serviceName 就是每个微服务的应用名称 
        Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
        LoadBalancedRetryPolicy retryPolicy = this.lbRetryFactory.createRetryPolicy(serviceName, this.loadBalancer);
        RetryTemplate template = this.createRetryTemplate(serviceName, request, retryPolicy);
        return (ClientHttpResponse)template.execute((context) -> {
            ServiceInstance serviceInstance = null;
            if (context instanceof LoadBalancedRetryContext) {
                LoadBalancedRetryContext lbContext = (LoadBalancedRetryContext)context;
                serviceInstance = lbContext.getServiceInstance();
            }
    
            if (serviceInstance == null) {
                serviceInstance = this.loadBalancer.choose(serviceName);//查看这个choose方法,这里通过负载均衡选择一个server
            }
    
            ClientHttpResponse response = (ClientHttpResponse)this.loadBalancer.execute(serviceName, serviceInstance, this.requestFactory.createRequest(request, body, execution));
            int statusCode = response.getRawStatusCode();
            if (retryPolicy != null && retryPolicy.retryableStatusCode(statusCode)) {
                byte[] bodyCopy = StreamUtils.copyToByteArray(response.getBody());
                response.close();
                throw new ClientHttpResponseStatusCodeException(serviceName, response, bodyCopy);
            } else {
                return response;
            }
        }, new LoadBalancedRecoveryCallback<ClientHttpResponse, ClientHttpResponse>() {
            protected ClientHttpResponse createResponse(ClientHttpResponse response, URI uri) {
                return response;
            }
        });
        

    查看choose方法:

    public ServiceInstance choose(String serviceId, Object hint) {
        Server server = this.getServer(this.getLoadBalancer(serviceId), hint);//通过serviceID找到对应的服务实例均衡器,loadBalancer 这里面保存了所有的服务实例,可用的,宕机的都在里面
        return server == null ? null : new RibbonLoadBalancerClient.RibbonServer(serviceId, server, this.isSecure(server, serviceId), this.serverIntrospector(serviceId).getMetadata(server));
    }

    使用均衡算法拿到可用的服务实例(从几个中选择一个,达到均衡的作用),返回server,然后拿到这个server就可以继续执行目标请求。

    均衡器里面会保存allServerList,里面会有upServeLlist里面是我们启动的服务实例,然后使用负载均衡算法选择一个服务。

    ribbon的常见配置

    1.禁用eureka

      当我们在resttemplate上面添加loadbalence注解后,就可以使用服务名去调用,如果我们想关闭这个功能,可以使用ribbon.eureka.enable=false

    2.配置接口地址列表

      如果我们关闭了eureka之后,还想用服务名去调用,就需要手动配置服务配置列表

      服务名.ribbon.listOfServers=IP:PORT1,IP:PORT2

    3.配置负载均衡策略

      服务名.ribbon.NFLoadBalencerRuleClassName=策略class全类名

    4.超时时间

      ribbon中有两种和超时时间相关的配置

        ribbon.ConnectTimeout=2000  请求连接的超时时间

        ribbon.ReadTimeout=5000 请求处理的超时时间

      可以在前面加上具体的服务名,为指定的服务配置

    5.并发参数

      ribbon.MaxTotalConnections=500  最大连接数

      ribbon,MaxConnectionsPerHost=500  每个host最大连接数

  • 相关阅读:
    【poj1195】Mobile phones(二维树状数组)
    【2018年全国多校算法寒假训练营练习比赛(第五场)-E】情人节的电灯泡(二维树状数组单点更新+区间查询)
    【2018年全国多校算法寒假训练营练习比赛(第五场)-G】 送分啦-QAQ(斐波那契博弈)
    【Wannafly挑战赛10
    【2018年全国多校算法寒假训练营练习比赛(第四场)- E】通知小弟(强连通缩点)
    JS中的forEach、$.each、map方法推荐
    关于echarts3地图下钻省市
    Vue2.0总结———vue使用过程常见的一些问题
    我理解的关于Vue.nextTick()的正确使用
    一个用 vue 写的树层级组件 vue-ztree
  • 原文地址:https://www.cnblogs.com/yunyunde/p/13536659.html
Copyright © 2011-2022 走看看