Spring Cloud系列二:Ribbon

2021-02-26 15:13:35 浏览数 (1)

一、Ribbon介绍

Ribbon是Netfix开源的一款客户端负载均衡工具,GitHub地址:

https://github.com/Netflix/ribbon

概括2点:

1、主要作用是实现负载均衡

2、它实现在客户端

在微服务环境下每个服务实例少则几个,多则上百个,如何让请求均匀分布到各服务实例上是微服务架构下必须解决的一个问题,这方面有2种解决方案:

一种是在服务端即服务提供方实现,硬件如F5,软件如Nginx都是此类成熟的方案,不过这种方案也有弊端,增加运维复杂性及可能引入新的单点,因为每次请求都要经过负载均衡器;

另一方面是在客户端即调用方实现,Ribbon就是这类方案的代表,即调用方根据一定规则选择一个合适的服务提供方进行调用。

二、Ribbon使用

1、直接使用

1)、引入依赖

代码语言:javascript复制
<dependency>
      <groupId>com.netfix.ribbon</groupId>
      <artifactId>ribbon</artifactId>
      <version>2.2.5</version>
    </dependency>

2、编写测试代码

代码语言:javascript复制
public class RibbonTest {

    /**
     *
     * @param args
     */
    public static void main(String[] args){
        ribbon();
    }

    /**
     *
     */
    protected static void ribbon(){
        List<Server> serverList = new ArrayList<>();
        serverList.add(new Server("172.21.107.76", 80));
        serverList.add(new Server("172.21.107.77", 80));

        ILoadBalancer loadBalancer = LoadBalancerBuilder.newBuilder().buildFixedServerListLoadBalancer(serverList);


        for(int i=0; i<10; i  ) {
            String result = LoadBalancerCommand.<String>builder().withLoadBalancer(loadBalancer).build().submit(
                    new ServerOperation<String>() {
                        @Override
                        public Observable<String> call(Server server) {
                            String url = "http://"   server.getHost()   ":"   server.getPort()   "/test";
                            System.out.println("调用地址:"   url);

                            try {
                                HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
                                connection.setRequestMethod("GET");
                                connection.connect();

                                InputStream in = connection.getInputStream();
                                byte[] response = new byte[in.available()];
                                in.read();

                                return Observable.just(new String(response));
                            } catch (Exception ex) {
                                return Observable.just(ex.getMessage());
                            }
                        }
                    }
            ).toBlocking().first();

            System.out.println("response:"   result);
        }
    }
}

这里通过构造一个固定的Server列表,然后通过一个循环来发送Http请求,通过打印每次请求的地址就知道是否负载均衡了。

2、整合Spring Cloud使用

2.1 RestTemplate使用

在Spring Cloud中发送Http协议请求官方推荐使用RestTemplate,RestTemplate使用比较简单,如getForObject方法:

代码语言:javascript复制
@RestController
public class TestController {
  @Autowired
  private RestTemplate restTemplate;  

@GetMapping("/test")     
  public String getData(@RequestParam("name") String name) {         
     return restTemplate.getForObject(
    "http://localhost:9000/test/{name}", String.class, name);     
  }   
       

getForObject参数如下:

1:请求Url

2:返回结果的数据类型

3:参数列表(参数变长)

如果是POST请求则调用postForObject方法。

2.2 RestTemplate结合Ribbon

在RestTemplate上启用负载均衡只要加一个@LoadBalanced的注解就可以了:

代码语言:javascript复制
Configuration
public class BeanConfig {
  @Bean
  @LoadBalanced
  public RestTemplate getRestTemplate() {
    return new RestTemplate();
  }

}

还是用上面的Controller测试下,应该就可以看到效果了。

三、Ribbon原理分析

spring-cloud-common在启动的时候LoadBalancerAutoConfiguration会对RestTemplate进行拦截:

代码语言:javascript复制
public class LoadBalancerAutoConfiguration {

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

  @Bean
  public SmartInitializingSingleton loadBalancedRestTemplateInitializerDeprecated(
      final ObjectProvider<List<RestTemplateCustomizer>> restTemplateCustomizers) {
    return () -> restTemplateCustomizers.ifAvailable(customizers -> {
            for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {
                for (RestTemplateCustomizer customizer : customizers) {
                    customizer.customize(restTemplate);
                }
            }
        });
  }
}

这个customizer会把LoadBalancerInterceptor加进去:

代码语言:javascript复制
public RestTemplateCustomizer restTemplateCustomizer(
        final LoadBalancerInterceptor loadBalancerInterceptor) {
      return restTemplate -> {
                List<ClientHttpRequestInterceptor> list = new ArrayList<>(
                        restTemplate.getInterceptors());
                list.add(loadBalancerInterceptor);
                restTemplate.setInterceptors(list);
            };
    }

LoadBalancerInterceptor调用loadBalancer进行处理:

代码语言:javascript复制
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);
    return this.loadBalancer.execute(serviceName, requestFactory.createRequest(request, body, execution));
  }

loadBalancer的类型为LoadBalancerClient,execute的核心逻辑是getServer:

代码语言:javascript复制
public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
    ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
    Server server = getServer(loadBalancer);
    if (server == null) {
      throw new IllegalStateException("No instances available for "   serviceId);
    }
    RibbonServer ribbonServer = new RibbonServer(serviceId, server, isSecure(server,
        serviceId), serverIntrospector(serviceId).getMetadata(server));

    return execute(serviceId, ribbonServer, request);
  }

它又交给ILoadBalancer处理:

代码语言:javascript复制
  protected Server getServer(ILoadBalancer loadBalancer) {
    if (loadBalancer == null) {
      return null;
    }
    return loadBalancer.chooseServer("default"); // TODO: better handling of key
  }

这里有不同的实现:BaseLoadBalancer和ZoneAwareLoadBalancer

我们看下BaseLoadBalancer:

代码语言:javascript复制
 public Server chooseServer(Object key) {
        if (counter == null) {
            counter = createCounter();
        }
        counter.increment();
        if (rule == null) {
            return null;
        } else {
            try {
                return rule.choose(key);
            } catch (Exception e) {
                logger.warn("LoadBalancer [{}]:  Error choosing server for key {}", name, key, e);
                return null;
            }
        }
    }

它交给rule处理了,rule的类型是IRule:

代码语言:javascript复制
public interface IRule{
    /*
     * choose one alive server from lb.allServers or
     * lb.upServers according to key
     * 
     * @return choosen Server object. NULL is returned if none
     *  server is available 
     */

    public Server choose(Object key);
    
    public void setLoadBalancer(ILoadBalancer lb);
    
    public ILoadBalancer getLoadBalancer();    
}

它的核心函数就是choose,即根据域名选择服务器,实现有:

看下随机的实现RandomRule:

代码语言:javascript复制
public Server choose(ILoadBalancer lb, Object key) {
        if (lb == null) {
            return null;
        }
        Server server = null;

        while (server == null) {
            if (Thread.interrupted()) {
                return null;
            }
            List<Server> upList = lb.getReachableServers();
            List<Server> allList = lb.getAllServers();

            int serverCount = allList.size();
            if (serverCount == 0) {
                /*
                 * No servers. End regardless of pass, because subsequent passes
                 * only get more restrictive.
                 */
                return null;
            }

            int index = rand.nextInt(serverCount);
            server = upList.get(index);

            if (server == null) {
                /*
                 * The only time this should happen is if the server list were
                 * somehow trimmed. This is a transient condition. Retry after
                 * yielding.
                 */
                Thread.yield();
                continue;
            }

            if (server.isAlive()) {
                return (server);
            }

            // Shouldn't actually happen.. but must be transient or a bug.
            server = null;
            Thread.yield();
        }

        return server;

    }

先获取所有可达服务器(现在能连通的),再获取所有服务器(不管是否可用),然后生成一个随机数,再用这个随机数作为索引拿到相应服务器看是否可用,如果可用直接返回,反之退出时间分片后继续循环。

四、总结

1、spring-cloud-common包中的LoadBalancerAutoConfiguration在框架启动时会对所有RestTemplate进行拦截,所有请求转发给LoadBalancerInterceptor处理;

2、LoadBalancerInterceptor拦截请求调用,并转发给LoadBalancerClient处理;

3、LoadBalancerClient转发给ILoadBalancer选择服务器;

4、ILoadBalancer的实现主要有BaseLoadBalancer和ZoneAwareLoadBalancer,BaseLoadBalancer再调用IRule选择服务器;

5、IRule实现具体选择服务器的逻辑,如果我们要实现自己的负载均衡策略就可以实现IRule就可以了。

0 人点赞