Spring Cloud(四):客户端负载均衡 Ribbon

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 依赖

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    @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. 负载均衡官方示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    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 依赖包

1
2
3
4
<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。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    /**
    * 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. 远程服务创建接口,并使用不同端口运行多个实例。

    1
    spring.application.name=sakila-service1
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @RestController
    @RequestMapping("/service")
    public class HomeController {

    @Value("${server.port}")
    private String port;

    @GetMapping("/port")
    public String serverPort(){
    return port;
    }
    }
  4. 客户端应用通过 Eureka 调用远程服务接口。

    实际是通过 Eureka 服务发现,使用 Eureka 实例名的链接来调用,而不是调用指定端口的服务。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    @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. 多次调用远程服务接口的结果:

    1
    2
    3
    4
    5
    6
    调用结果: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 客户端初始化,配置如下:

1
2
3
4
#开启饥饿加载模式
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() 方法,在方法内部定义服务选择逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 是调用的服务名称,如下,

1
2
3
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 上)来完全控制客户端。如下示例:

1
2
3
4
@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 注解的配置类中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@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 客户端提供默认配置,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@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客户端。

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

支持的属性列表:

1
2
3
4
5
<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,可以设置如下属性:

1
2
3
4
5
#application.yml
users:
ribbon:
NIWSServerListClassName: com.netflix.loadbalancer.ConfigurationBasedServerList
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.WeightedResponseTimeRule

在 Ribbon 中禁用 Eureka

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

1
ribbon.eureka.enabled=false

直接使用 Ribbon API

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

1
2
3
4
5
6
7
8
9
10
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 客户端默认为已配置的服务器列表。可使用如下配置:

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

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

客户端请求重试机制

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

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

  1. RetryRule 重试

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

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

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

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

Ribbon 超时设置

  1. 客户端默认配置在 com.netflix.client.config.DefaultClientConfigImpl 配置类上。
  2. 对配置类的调用大多是在 com.netflix.loadbalancer.LoadBalancerContext 类上。
1
2
3
4
5
# 超时时间
## 请求连接超时时间
ribbon.connectTimeout=2000
## 请求处理超时时间
ribbon.readTimeout=5000
作者

光星

发布于

2019-03-03

更新于

2022-06-17

许可协议

评论