SpringCloud系列(四):客户端负载均衡Ribbon

star2017 1年前 ⋅ 459 阅读

Ribbon 是 Netflix 开源的内置了软件负载均衡器的进程间通信(远程调用)库。支持负载均衡、容错处理、异步和响应式模型中的多协议(HTTP、TCP、UDP)支持、缓存和批处理。

目前行业常见的负载均衡方案分两种:一种是集中式负载均衡,在消费者与服务提供方中间使用独立的代理方式进行负载,有根据 IP 的硬件负载(如 F5,Array), 有软件的负载(如 Nginx,LVS等);另一种是客户端自己做负载均衡,根据自己对目标的请求做负载,Ribbon 就是属于客户端侧的负载均衡。

后续要讲到的 Rest 客户端 Feign 也是基于 Ribbon 实现的。

Spring Cloiud - Client Side Load Balancer: Ribbon 文档Netflix Ribbon 官方文档Ribbon 负载均衡文档

Netflix Ribbon

Ribbon 模块

  • ribbon:在 ribbon 模块和 Hystrix 基础之上,集成了 负载均衡、容错处理、缓存/批处理 的 APIs。
  • ribbon-loadbalancer:负载均衡模块,可以独立使用,也可和其它模块一起使用。
  • ribbon-eureka:基于 Eureka 封装的模块,可快速方便地与 Eureka 集成,为云提供动态服务器列表 APIs。
  • ribbon-transport:使用具有负载均衡功能的 RxNetty 进行客户端传输,支持 HTTP、TCP和UDP协议。
  • ribbon-httpclient:基于 Apache HttpClient 集成了负载均衡的 Rest 客户端。
  • ribbon-example:Ribbon 使用代码示例。
  • ribbon-core:Ribbon 核心功能和代码,客户端 APIs 配置和其它 APIs 定义。

Ribbon 单独使用

使用原生 Ribbon 实现客户负载均衡

  1. 添加 Ribbon 依赖

    <dependency>
        <groupId>com.netflix.ribbon</groupId>
        <artifactId>ribbon</artifactId>
        <version>2.2.2</version>
    </dependency>
    
  2. 负载均衡调用演示

    @RestController
    @RequestMapping("/consumer1")
    public class ConsumerController {
    
        /**
         * ribbon 单独使用负载均衡
         */
        @GetMapping("/ribbon")
        public String ribbon() {
    
            ArrayList<Server> serverList = Lists.newArrayList(
                    new Server("localhost", 8001),
                    new Server("localhost", 8002));
            //构建负载实例
            ILoadBalancer loadBalancer = LoadBalancerBuilder.newBuilder().buildFixedServerListLoadBalancer(serverList);
    
            //调用5次来测试结果
            for (int i = 0; i < 5; i++) {
                String result = LoadBalancerCommand.<String>builder()
                        .withLoadBalancer(loadBalancer)
                        .build()
                        .submit(new ServerOperation<String>() {
                            @Override
                            public Observable<String> call(Server server) {
    //                            String address = "http://" + server.getHost() + ":" + server.getPort();
                                String address = "http://" + server.getHost() + ":" + server.getPort() + "/service/home";
                                System.out.println("调用地址:" + address);
                                String body = "";
                                try {
                                    URL url = new URL(address);
                                    HttpURLConnection connection = (HttpURLConnection) url.openConnection();
                                    connection.connect();
                                    if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
                                        InputStream is = connection.getInputStream();
                                        StringBuffer sb = new StringBuffer();
                                        byte[] buffer = new byte[1024];
                                        int len = 0;
                                        while ((len = is.read(buffer)) != -1) {
                                            String str = new String(buffer, Charset.forName("utf-8"));
                                            sb.append(str);
                                        }
                                        body = sb.toString();
                                        is.close();
                                        connection.disconnect();
                                    }
                                    return Observable.just(body);
                                } catch (Exception e) {
                                    return Observable.error(e);
                                }
                            }
                        }).toBlocking().first();
                System.out.println("调用结果:" + result);
            }
            return null;
        }
    }
    
    //调用结果
    调用地址:http://localhost:8002/service/home
    调用结果:DESKTOP-G05G2748002SAKILA-SERVICE2                
    调用地址:http://localhost:8001/service/home
    调用结果:DESKTOP-G05G2748001SAKILA-SERVICE1              
    调用地址:http://localhost:8002/service/home
    调用结果:DESKTOP-G05G2748002SAKILA-SERVICE2     
    调用地址:http://localhost:8001/service/home
    调用结果:DESKTOP-G05G2748001SAKILA-SERVICE1
    调用地址:http://localhost:8002/service/home
    调用结果:DESKTOP-G05G2748002SAKILA-SERVICE2
    
  3. 负载均衡官方示例

    public class URLConnectionLoadBalancer {
    
        private final ILoadBalancer loadBalancer;
        // retry handler that does not retry on same server, but on a different server
        private final RetryHandler retryHandler = new DefaultLoadBalancerRetryHandler(0, 1, true);
    
        public URLConnectionLoadBalancer(List<Server> serverList) {
            loadBalancer = LoadBalancerBuilder.newBuilder().buildFixedServerListLoadBalancer(serverList);
        }
    
        public String call(final String path) throws Exception {
            return LoadBalancerCommand.<String>builder()
                    .withLoadBalancer(loadBalancer)
                    .build()
                    .submit(new ServerOperation<String>() {
                @Override
                public Observable<String> call(Server server) {
                    URL url;
                    try {
                        url = new URL("http://" + server.getHost() + ":" + server.getPort() + path);
                        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
                        return Observable.just(conn.getResponseMessage());
                    } catch (Exception e) {
                        return Observable.error(e);
                    }
                }
            }).toBlocking().first();
        }
    
        public LoadBalancerStats getLoadBalancerStats() {
            return ((BaseLoadBalancer) loadBalancer).getLoadBalancerStats();
        }
    
        public static void main(String[] args) throws Exception {
            URLConnectionLoadBalancer urlLoadBalancer = new URLConnectionLoadBalancer(Lists.newArrayList(
                    new Server("www.taobao.com", 80),
                    new Server("www.baidu.com", 80),
                    new Server("www.jd.com", 80)));
            for (int i = 0; i < 6; i++) {
                System.out.println(urlLoadBalancer.call("/"));
            }
            System.out.println("=== Load balancer stats ===");
            System.out.println(urlLoadBalancer.getLoadBalancerStats());
        }
    }
    

Spring Cloud Ribbon

Spring Cloud 根据 RibbonClientConfiguration 为每个命名的客户端创建了一个新的集成作为 ApplicationContext 。它包含 ILoadBalancer、一个 RestClient、一个 ServerListFilter。

引入 Ribbon 依赖

在 pom.xm 文件添加 spring-cloud-starter-netflix-ribbon 依赖包

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>

在 Eureka 中使用 Ribbon

EurekaRibbon 一起使用时(即两者都在类路径上,即都引入了两者的依赖包),ribbonServerList 将被DiscoveryEnabledNIWSServerList 的扩展覆盖,该扩展为 Eureka 的服务器列表。

还用 NIWSDiscoveryPing 替换 IPing 接口,NIWSDiscoveryPing 委托 Eureka 确定服务是否启动。

默认情况下安装的 ServerList 是 DomainExtractingServerList,DomainExtractingServerList 实现的是 Ribbon 负载均衡模块 loadbalancer 里的 ServerList<DiscoveryEnabledServer>接口,目的是在不使用 AWS AMI 元数据(这是 Netflix 所依赖的) 的情况下为负载均衡器提供元数据。

默认情况下,Server List 使用实例元数据提供的 zone 信息构建(在远程客户端上,设置 eureka.instance.metadatamap.zone)。如果缺少该标志,但设置开启了 approximateZoneFromHostname ,则可以使用服务器主机名的域名作为 zone 的代理(映射)。一旦 zone 信息可用,就可以 ServletListFilter 中使用它。

默认情况下,这些 zone 元数据用于定位与客户端在相同区域的服务器,默认是在 ZonePreferenceServerListFilter 过滤器中处理 zone 信息。默认情况下,确定客户端所在的区域与远程实例的方式相同,即通过 eureka.instance.metadataMap.zone 确定。(此两条:即根据客户端定位服务器和根据服务器定位客户端所在的区域)。

注意:如何使用了外部配置:archaius ,通过 @zone 注解属性设置客户端所在区域。如果配置可用,Spring Cloud 会优先使用它。

RestTemplate 集成 Ribbon

RestTemplate 实际上是在 Eureak 基础上整合 Ribbon;应用作为 eureka-client 注册到 eureka-server 服务器。

  1. 客户端应用引入Spring Cloud eureka-client 和 ribbon 包。

  2. 客户端应用使用 RestTemplate 调用远程服务,在 RestTemplate Bean 上添加负载均衡注解 @LoadBalanced。

    /**
     * RestTemplate配置类
     */
    @Configuration
    public class RestTemplateConfig {
    
        @Bean(name = "restTemplateOne")
        public RestTemplate restTemplateOne() {
            //设置超时时间,毫秒
            return new RestTemplateBuilder().setConnectTimeout(Duration.ofMillis(1000)).setReadTimeout(Duration.ofMillis(1000)).build();
        }
    
        @Bean(name = "restTemplateTwo")
        @LoadBalanced
        public RestTemplate restTemplateTwo(){
            //设置超时时间,毫秒
            return new RestTemplateBuilder().setConnectTimeout(Duration.ofMillis(1000)).setReadTimeout(Duration.ofMillis(1000)).build();
        }
    }
    
  3. 远程服务创建接口,并使用不同端口运行多个实例。

    spring.application.name=sakila-service1
    
    @RestController
    @RequestMapping("/service")
    public class HomeController {
    
        @Value("${server.port}")
        private String port;
    
        @GetMapping("/port")
        public String serverPort(){
            return port;
        }
    }
    
  4. 客户端应用通过 Eureka 调用远程服务接口。
    实际是通过 Eureka 服务发现,使用 Eureka 实例名的链接来调用,而不是调用指定端口的服务。

     @RestController
     @RequestMapping("/consumer1")
     public class ConsumerController {
    
        @Autowired
        private RestTemplate restTemplateOne;
    
        @Autowired
        private RestTemplate restTemplateTwo;
    
        @GetMapping("/home")
        public String callHome() {
    
            //直接调用服务接口
            String url1 = "http://localhost:8001/service/home";
            String str1 = restTemplateOne.getForObject(url1, String.class);
    
            //通过Eureka来调用服务接口
            String url2 = "http://sakila-service1/service/port";
            String str2 = restTemplateTwo.getForObject(url2, String.class);
            System.out.println("调用结果:" + str2);
            return "调用结果:" + str2;
        }
     }
    
  5. 多次调用远程服务接口的结果:

    调用结果:8001
    调用结果:8002
    调用结果:8001
    调用结果:8002
    调用结果:8001
    调用结果:8002
    

@LoadBalanced 原理

在 RestTemplate 上加了一个 @LoadBalanced 注解就可以负载均衡。这是因为 Spring Cloud 做了大量的底层封装,做了很多简化。

内部的主要逻辑就是给 RestTemplate 增加拦截器,在请求之前对请求的地址进行了替换,或者根据具体的负载策略选择服务地址,然后去调用,这就是 @LoadBalanced 的原理。

Spring Web 为 HttpClient 提供了 Request 拦载器 ClientHttpRequestInterceptor,位于 spring-web jar 包下。

在 spring-cloud-commons 包中提供了负载均衡自动配置类 LoadBalancerAutoConfiguration ,里面维护了一个 @LoadBalanced 注解的 RestTemplate 列表,里面的静态类 LoadBalancerInterceptorConfig 注册了 负载均衡拦截器 LoadBalancerInterceptor,RestTemplateCustomizer 来添加拦截器列表。

负载均衡拦截器 LoadBalancerInterceptor 实现了 ClientHttpRequestInterceptor,主要逻辑在 intercept() 方法中,执行交给了 LoadBalancerClient,通过 LoadBalancerRequestFactory 来构建一个 LoadBalancerRequest 对象,createRequest 方法中通过 ServiceRequestWrapper 来执行替换 URI 的逻辑,核心是通过 reconstructURI() 方法实现,该方法的业务实现是在 RibbonLoadBalancerClient 类中 。

Spring Cloud Ribbon 配置

Ribbon 饥饿加载

在进行服务调用时,如果网络不好,第一次调用会超时,这是因为 Ribbon 客户端在第一次请求的时候初始化,如果超时时间比较短的话,初始化 Client 的时间再加上请求接口的时间,就会导致第一次请求超时。

spring-cloud-netflix-ribbon 提供了饥饿加载模式,在服务启动时就执行 Ribbon 客户端初始化,配置如下:

#开启饥饿加载模式
ribbon.eager-load.enabled=true
#指定需要饥饿加载的服务名,允许多个,用逗号隔开
ribbon.eager-load.clients=

负载均衡策略

Ribbon 默认的负载策略是轮询,同时也提供了很多其他的策略能够让用户根据业务需求来选择。负载均衡策略的根接口是 com.netflix.loadbalancer.IRule。
负载均衡策略

  1. BestAvailableRule
    选择最小并发请求的服务器,每个客户端都会获得一个随机的服务器列表,如果服务器被标记为错误,则跳过。

  2. AvailabilityFilteringRule
    用于过滤连接一直失败或读取失败而被标记为 circuit breaker tripped 状态的服务;或持有超过可配置限制的活动连接(默认值为 Integer.MAX_VALUE),即过滤掉高并发的后端服务。实际就是检查获取到的服务列表里,各个 Server 的 Status 。

  3. ZoneAvoidanceRule
    根据区域和可用性来过滤服务器。使用 ZoneAvoidancePredicate 来判断 Zone 的使用是否达到阀值,过滤出最差 Zone 中的所有服务器; AvailabilityPredicate 用于过滤出并发连接过多的服务。

  4. RandomRule
    随机分配流量,即随机选择一个 Server。

  5. RetryRule
    向现有负载均衡策略添加重试机制。

  6. ResponseTimeWeightedRule
    该策略已过期,同见 WeightedResponseTimeRule。

  7. WeightedResponseTimeRule
    根据响应时间为每个服务器动态分配权重(Weight)分,然后台加权循环的方式使用该策略。响应时间越长,权重越低,被选中可能性越低。

自定义负载均衡策略

创建一个实现负载均衡策略 IRule 接口的类,重写 choose() 方法,在方法内部定义服务选择逻辑。

public class MyLoadBalancerRule implements IRule {

    private ILoadBalancer iLoadBalancer;

    @Override
    public Server choose(Object key) {
        List<Server> serverList = iLoadBalancer.getAllServers();
        for (Server server : serverList) {
            //这里定义选择服务策略
            System.out.println(server.getHostPort());
        }
        return serverList.get(0);
    }

    @Override
    public void setLoadBalancer(ILoadBalancer lb) {
    }

    @Override
    public ILoadBalancer getLoadBalancer() {
        return null;
    }
}

在 Spring Cloud 中,可通过配置的方式使用自定义的负载策略,sakila-service 是调用的服务名称,如下,

sakila-service.ribbon.NFLoadBalancerRuleClassName = com.xxx.xxx.ILoadBalancer
//或使用已提供的其它策略
sakila-service.ribbon.NFLoadBalancerRuleClassName = com.netflix.loadbalancer.RandomRule

或者自定义配置,将其注册为 IRule 类型的 Bean。

自定义 Ribbon 客户端

可以在 Spring Boot 配置文件中,使用 <client>.ribbon.* 来配置 Ribbon 客户端。Spring Cloud 还允许使用 @RibbonClient 声明其它配置(在 RibbonClientConfiguration 上)来完全控制客户端。如下示例:

@Configuration
@RibbonClient(name = "custom", configuration = CustomConfiguration.class)
public class TestConfiguration {
}

这样,客户端由 RibbonClientConfiguration 中已存在的组件和 CustomConfiguration(通常会覆盖前者) 中的任何组件组成。

注意: CustomConfiguration 类必须是 @Configuration 注解的类;不能存在于 主 application context 的 @ComponentScan 中。否则,将由所有 @RibbonClients 共享。

Spring Cloud Netflix 默认为 Ribbon 提供的 Bean 包含 :

Bean Type Bean Name Class Name
IClientConfig ribbonClientConfig DefaultClientConfigImpl
IRule ribbonRule ZoneAvoidanceRule
IPing ribbonPing DummyPing
ServerList<Server> ribbonServerList ConfigurationBasedServerList
ServerListFilter<Server> ribbonServerListFilter ZonePreferenceServerListFilter
ILoadBalancer ribbonLoadBalancer ZoneAwareLoadBalancer
ServerListUpdater ribbonServerListUpdater PollingServerListUpdater

也可以自定义这些 Bean 来覆盖默认的,并将这些自定义的 Bean 放置在 @RibbonClient 注解的配置类中。

@Configuration
protected static class FooConfiguration {
    @Bean
    public ZonePreferenceServerListFilter serverListFilter() {
        ZonePreferenceServerListFilter filter = new ZonePreferenceServerListFilter();
        filter.setZone("myTestZone");
        return filter;
    }

    @Bean
    public IPing ribbonPing() {
        return new PingUrl();
    }
}

自定义所有默认配置

为使用 @RibbonClients 注解并注册默认配置的所有 Ribbon 客户端提供默认配置,如下所示:

@RibbonClients(defaultConfiguration = DefaultRibbonConfig.class)
public class RibbonClientDefaultConfigurationTestsConfig {

    public static class BazServiceList extends ConfigurationBasedServerList {
        public BazServiceList(IClientConfig config) {
            super.initWithNiwsConfig(config);
        }
    }
}

@Configuration
class DefaultRibbonConfig {

    @Bean
    public IRule ribbonRule() {
        return new BestAvailableRule();
    }

    @Bean
    public IPing ribbonPing() {
        return new PingUrl();
    }

    @Bean
    public ServerList<Server> ribbonServerList(IClientConfig config) {
        return new RibbonClientDefaultConfigurationTestsConfig.BazServiceList(config);
    }

    @Bean
    public ServerListSubsetFilter serverListFilter() {
        ServerListSubsetFilter filter = new ServerListSubsetFilter();
        return filter;
    }
}

属性自定义 Ribbon 客户端

从版本1.2.0开始,Spring Cloud Netflix 现在支持设置与 Ribbon 文档兼容的属性来自定义Ribbon客户端。

这样可以在不同的启动环境中使用相应的配置。

支持的属性列表:

<clientName>.ribbon.NFLoadBalancerClassName: Should implement ILoadBalancer
<clientName>.ribbon.NFLoadBalancerRuleClassName: Should implement IRule
<clientName>.ribbon.NFLoadBalancerPingClassName: Should implement IPing
<clientName>.ribbon.NIWSServerListClassName: Should implement ServerList
<clientName>.ribbon.NIWSServerListFilterClassName: Should implement ServerListFilter

注意: 这些属性中定义的类的使用优先于 @RibbonClient(configuration = MyRibbonConfig.class) 定义的 bean 以及 Spring Cloud Netflix 提供的默认值。

要为名称为 users 的服务的设置 IRule,可以设置如下属性:

#application.yml
users:
  ribbon:
    NIWSServerListClassName: com.netflix.loadbalancer.ConfigurationBasedServerList
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.WeightedResponseTimeRule

在 Ribbon 中禁用 Eureka

将 Ribbon.eureka.enabled 属性设置为 false 将显式禁用在 Ribbon 中使用 eureka,就不能使用服务名去调用接口了,必须硬编码指定服务器地址。

ribbon.eureka.enabled=false

直接使用 Ribbon API

也可以直接使用 LoadBalancerClient。Ribbon 自动配置类 RibbonAutoConfiguration 注册了 LoadBalancerClient Bean。

public class MyClass {
    @Autowired
    private LoadBalancerClient loadBalancer;

    public void doStuff() {
        ServiceInstance instance = loadBalancer.choose("stores");
        URI storesUri = URI.create(String.format("http://%s:%s", instance.getHost(), instance.getPort()));
        // ... do something with the URI
    }
}

无 Eureka 使用 Ribbon

Eureka 是一种抽象远程服务发现的便捷方式(组件),这样就无需在客户端对远程服务的 URL 进行硬编码。

如果不想使用 Eureka ,Ribbon 和 Feign 仍可使用。假设已为 stores 声明了 @RibbonClient,并且未使用 Eureka (甚至未引入 Eureka 包),Ribbon 客户端默认为已配置的服务器列表。可使用如下配置:

stores.ribbon.listOfServers=localhost:8081,localhost:8082,example.com,google.com

无 Eureka 作为服务发现,该配置的前缀是服务名称,配置后即可使用服务名称来调用接口。

客户端请求重试机制

集群环境,是多个节点提供相同的服务,若某个节点故障,该节点就无法提供服务,但 Eureka 服务列表还没更新,客户端拿到的可能是已经故障的服务信息,就发导致请求失败。

重试机制就是当 Ribbon 发现请求的服务不可到达时,重新请求另外的服务。

  1. RetryRule 重试

    利用 Ribbon 自带的重试策略进行重试,只需要指定某个服务的负载策略为重试策略即可:

    <clientName>.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RetryRule
    
  2. Spring Retry 重试

    还可通过基础 Spring Retry 来执行重试操作,需要引入 spring-retry 和 spring-boot-starter-aop 依赖包。

    # 请求重试
    ## 对当前实例重试次数,默认0
    ribbon.maxAutoRetries=1
    ## 切换实例的重试次数,默认1
    ribbon.maxAutoRetriesNextServer=1
    ## 对所有操作请求都进行重试,默认false
    ribbon.okToRetryOnAllOperations=true
    

Ribbon 超时设置

  1. 客户端默认配置在 com.netflix.client.config.DefaultClientConfigImpl 配置类上。
  2. 对配置类的调用大多是在 com.netflix.loadbalancer.LoadBalancerContext 类上。
# 超时时间
## 请求连接超时时间
ribbon.connectTimeout=2000
## 请求处理超时时间
ribbon.readTimeout=5000
更多内容请访问:IT源点

相关文章推荐

全部评论: 0

    我有话说: