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最大连接数

原文地址:https://www.cnblogs.com/yunyunde/p/13536659.html