Spring Cloud 客户端负载工具 Ribbon

Ribbon是Netflix公司提供的一个客户端负载工具,Spring Cloud也对其进行了集成支持。使用Ribbon需要在pom.xml中添加如下依赖。

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

为了使用LoadBalancerClient,还需要在Classpath下存在RestTemplate,为此引入如下依赖。

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-web</artifactId>
</dependency>

然后在@Configuration类上使用@org.springframework.cloud.netflix.ribbon.RibbonClient声明一个Ribbon Client,指定Client的名称。如下代码声明了一个名称为hello的Ribbon Client。

@SpringBootApplication
@RibbonClient("hello")
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

然后可以通过下面的方式指定服务端的地址,其中hello是上面声明的Ribbon Client的名称。

hello.ribbon.listOfServers=http://localhost:8900,http://localhost:8901

然后可以在应用中注入org.springframework.cloud.client.loadbalancer.LoadBalancerClient,并通过它来获取服务端地址了。

@SpringBootTest(classes=Application.class)
@RunWith(SpringRunner.class)
public class RibbonTest {
    @Autowired
    private LoadBalancerClient loadBalancerClient;
  
    @Test
    public void test() {
        String serviceId = "hello";
        for (int i=0; i<5; i++) {
            ServiceInstance instance = this.loadBalancerClient.choose(serviceId);
            System.out.println(i + ". " + instance.getUri());
        }
    }
  
}

上面的代码就通过LoadBalancerClient连续获取了5次服务端地址,运行代码你会看到如下这样的输出。

0. http://localhost:8900
1. http://localhost:8901
2. http://localhost:8900
3. http://localhost:8901
4. http://localhost:8900

Spring Cloud会注册Ribbon Client需要的IRule、IPing和ILoadBalancer等bean,它们由org.springframework.cloud.netflix.ribbon.RibbonClientConfiguration负责注册。我们可以通过注册自己的相关类型的bean来覆盖默认的bean定义,比如下面的代码就指定了使用的IRule是基于轮询的RoundRobinRule实现。

@Configuration
public class RibbonConfiguration {
    @Bean
    public IRule rule() {
        return new RoundRobinRule();
    }
  
}

也可以通过@RibbonClients的defaultConfiguration指定通用的配置信息。

@SpringBootApplication
@RibbonClient(value="hello")
@RibbonClients(defaultConfiguration=RibbonConfiguration.class)
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
  
}

也可以专门为某个Ribbon Client定义特殊配置信息,此时可以通过@RibbonClient的configuration属性指定需要应用的@Configuration配置类。

@SpringBootApplication
@RibbonClient(value="hello", configuration=RibbonConfiguration.class)
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
  
}

Ribbon Client的配置参数也可以通过application.properties进行配置,属性名格式是clientName.ribbon.property。比如下面的配置指定了名称为hello的Ribbon Client的服务地址和使用的IRule。更多的配置参数可以参考com.netflix.client.config.CommonClientConfigKey的API文档,这些参数的默认值可以参考com.netflix.client.config.DefaultClientConfigImpl

hello.ribbon.listOfServers=http://localhost:8900,http://localhost:8901
hello.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RoundRobinRule

使用配置文件指定的参数比使用@RibbonClient(value="hello", configuration=RibbonConfiguration.class)指定的配置拥有更高的优先级。

Ribbon和Eureka一起使用

当应用中存在Eureka Client时,可以不通过hello.ribbon.listOfServers=http://localhost:8900,http://localhost:8901写死hello客户端对应的服务地址。此时可以通过Eureka Client从Eureka Server获取服务地址,从而达到动态获取服务地址的目的。此时如果声明了名为hello的Ribbon Client,则会从Eureka Server获取serviceId为hello的服务对应的地址。

eureka.client.registerWithEureka=false
eureka.client.serviceUrl.defaultZone=http://localhost:8089/eureka/

如果应用中存在Eureka Client,但不希望通过Eureka Client来获取Ribbon Client对应的服务地址,可以指定ribbon.eureka.enabled=false

hello.ribbon.listOfServers=http://localhost:8900,http://localhost:8901
ribbon.eureka.enabled=false
eureka.client.registerWithEureka=false
eureka.client.serviceUrl.defaultZone=http://localhost:8089/eureka/

RestTemplate负载

RestTemplate可以和Ribbon一起使用,使其具备负载能力。在对应的RestTemplate对应的bean上加上@LoadBalanced可以使其拥有负载能力。

@Configuration
public class RibbonConfiguration {
    @Bean
    public IRule rule() {
        return new RoundRobinRule();
    }
  
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate(RestTemplateBuilder builder) {
        return builder.build();
    }
  
}

在使用的时候需要把host改为对应的serviceId,比如有一个服务hello,我们想访问它提供的/api/abc服务,即可以访问http://hello/api/abc。内部在访问的时候会自动把hello替换为服务hello的一个具体地址(它可能是通过ribbon.listOfServers指定的,也可能是直接从Eureka获取的)。

@Autowired
private RestTemplate restTemplate;
@GetMapping("hello")
public String hello() {
    String result = this.restTemplate.getForObject("http://hello/api/hello/abc", String.class);
    return result;
}

如果你的应用中需要同时有多个RestTemplate,有的需要有负载均衡功能,有的不需要有,则可以在应用中创建多个RestTemplate类型的bean,然后根据需要用@Primary指定一个为自动注入时默认使用的。比如下面我们定义了默认注入的是拥有负载均衡功能的RestTemplate。

@Bean
@LoadBalanced
@Primary
public RestTemplate restTemplate(RestTemplateBuilder builder) {
    return builder.build();
}
@Bean
public RestTemplate commonRestTemplate(RestTemplateBuilder builder) {
    return builder.build();
}

WebClient负载

方式一

WebClient也是可以负载的,与RestTemplate类似,它需要定义一个WebClient.Builder类型的bean,并使用@LoadBalanced标注。

@Bean
@LoadBalanced
public WebClient.Builder webClientBuilder() {
    return WebClient.builder();
}

然后使用的时候就注入WebClient.Builder,通过它创建一个WebClient对象。WebClient进行访问时URI中的host也是需要使用服务名。

@Autowired
private WebClient.Builder webClientBuilder;
@GetMapping("webClient/hello")
public Mono<String> hello() {
  return this.webClientBuilder.build().get().uri("http://hello/api/hello/abc").retrieve().bodyToMono(String.class);
}

方式二

方式二是注入一个LoadBalancerExchangeFilterFunction类型的bean,该bean将由Spring Cloud自动创建。然后在通过WebClient.Builder创建WebClient时指定该ExchangeFilterFunction,这样创建出来的WebClient也是具有负载功能的。

@Autowired
private LoadBalancerExchangeFilterFunction lbFunction;
@GetMapping("webClient/lbFunction")
public Mono<String> lbFunction() {
    return WebClient.builder().baseUrl("http://hello")
        .filter(lbFunction)
        .build()
        .get()
        .uri("/api/hello/abc")
        .retrieve()
        .bodyToMono(String.class);
}

自动重试

Ribbon和Spring Retry一起使用的时候可以在调用远程服务失败时发起重试。需要先加入Spring retry依赖。

<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
</dependency>

然后可以通过MaxAutoRetries指定最多重试次数,比如ribbon.MaxAutoRetries=2指定所有的Ribbon客户端在发起请求时最多重试两次,第一次调用不算在重试次数中。可以通过MaxAutoRetriesNextServer配置最多重试的服务器数量,第一个服务器是不算的。比如MaxAutoRetries=2,MaxAutoRetriesNextServer=1,那么会在第一台服务器上最多调用3次,在第二台服务器上也最多调用3次,如果还有第三台服务器,则第三台服务器不会再调用了。Ribbon默认只对GET请求进行重试,如果需要对POST请求也进行重试,则可以配置ribbon.OkToRetryOnAllOperations=true。还可以通过retryableStatusCodes来指定需要进行重试的Http状态码,比如只希望在状态码为500或502时进行重试,则配置ribbon.retryableStatusCodes=500,502。默认情况只要服务器通讯正常都不会重试,即状态码不管是404还是502等都不会发起重试,只有建立连接失败或者请求超时会重试。所以如果我们需要在状态码为502的时候也能发起重试则需要指定retryableStatusCodes。

ribbon.MaxAutoRetries=2
ribbon.MaxAutoRetriesNextServer=2
ribbon.OkToRetryOnAllOperations=true
ribbon.retryableStatusCodes=404,502

示例代码如下。

@GetMapping("retry/{sub}")
public String retryAny(@PathVariable("sub") String sub) {
    String result = this.restTemplate.getForObject("http://hello/{sub}", String.class, sub);
    return result;
}

如果Classpath下存在Spring Retry的相关jar包,但是又不希望使用它,则可以指定spring.cloud.loadbalancer.retry.enabled=false

参考文档