Spring Tips:Spring Cloud Loadbalancer

工程 | Josh Long | 2020年3月25日 | ...

演讲者:Josh Long (@starbuxman)

大家好,Spring 粉丝!欢迎收看另一期 Spring Tips!在本期节目中,我们将介绍 Spring Cloud 的一项新功能:Spring Cloud Loadbalancer。Spring Cloud Loadbalancer 是一个通用的抽象,它可以完成我们过去使用 Netflix 的 Ribbon 项目所做的工作。Spring Cloud 仍然支持 Netflix Ribbon,但 Netflix Ribbon 的日子已经屈指可数了,就像 Netflix 微服务栈的许多其他组件一样,所以我们提供了一个抽象来支持一个替代方案。

服务注册中心

为了使用 Spring Cloud Load Balancer,我们需要有一个正在运行的服务注册中心。服务注册中心可以轻松地以编程方式查询系统中给定服务的位置。有几种流行的实现,包括 Apache Zookeeper、Netflix Eureka、Hashicorp Consul 等。您甚至可以将 Kubernetes 和 Cloud Foundry 用作服务注册中心。Spring Cloud 提供了一个抽象 DiscoveryClient,您可以使用它来通用地与这些服务注册中心进行通信。服务注册中心支持的几种模式是使用单纯的 DNS 无法实现的。我喜欢做的一件事是客户端负载均衡。客户端负载均衡要求客户端代码决定哪个节点接收请求。系统中可能存在任意数量的服务实例,它们处理特定请求的适用性由每个客户端决定。如果它能在启动可能注定失败的请求之前做出决定,那就更好了。这可以节省时间,减轻服务繁琐的流量控制要求,并使我们的系统更具动态性,因为我们可以查询其拓扑。

您可以运行任何您喜欢的服务注册中心。我喜欢为这些事情使用 Netflix Eureka,因为它设置起来更简单。让我们设置一个新实例。如果您愿意,可以下载并运行一个标准的镜像,但我希望使用 Spring Cloud 中提供的预配置实例。

转到 Spring Initializer,选择 Eureka ServerLombok。我将其命名为 eureka-service。点击 Generate

使用内置 Eureka Service 的大部分工作都在配置中,我已在此处重印。

server.port=8761
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false

然后您需要自定义 Java 类。将 @EnableEurekaServer 注释添加到您的类中。

package com.example.eurekaservice;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@SpringBootApplication
@EnableEurekaServer
public class EurekaServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(EurekaServiceApplication.class, args);
    }

}

您现在可以运行它了。它将在端口 8761 上可用,其他客户端默认将连接到该端口。

一个简单的 API

现在让我们转向 API。我们的 API 是最简单的。我们只需要一个客户端可以发出请求的端点。

转到 Spring Initializr,使用 Reactive WebLombokEureka Discovery Client 生成新项目。最后一部分是关键!您不会在以下 Java 代码中看到它的使用。它完全是自动配置,我们在 2016 年就已经介绍过了,它在应用程序启动时运行。自动配置将使用 spring.application.name 属性自动将应用程序注册到指定的注册中心(在本例中,我们使用的是 Netflix Eureka 的 DiscoveryClient 实现)。

指定以下属性。

spring.application.name=api
server.port=9000

我们的 HTTP 端点是一个“Hello, world!”处理程序,它使用我们在2017 年的另一个 Spring Tips 视频中介绍的函数式响应式 HTTP 风格。

package com.example.api;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.reactive.function.server.HandlerFunction;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;

import java.util.Map;

import static org.springframework.web.reactive.function.server.RouterFunctions.route;
import static org.springframework.web.reactive.function.server.ServerResponse.*;

@SpringBootApplication
public class ApiApplication {
    
    @Bean
    RouterFunction<ServerResponse> routes() {
        return route()
            .GET("/greetings", r -> ok().bodyValue(Map.of("greetings", "Hello, world!")))
            .build();
    }

    public static void main(String[] args) {
        SpringApplication.run(ApiApplication.class, args);
    }
} 

现在运行该应用程序,您将在 Netlfix Eureka 实例中看到它的反映。您可以将 application.properties 中的 server.port 值更改为 0。如果您运行多个实例,您将在控制台中看到它们的反映。

负载均衡客户端

好了,现在我们准备好演示负载均衡了。我们需要一个新的 Spring Boot 应用程序。转到 Spring Initializr,使用 Eureka Discovery ClientLombokCloud LoadbalancerReactive Web 生成一个新项目。点击 Generate,然后在您喜欢的 IDE 中打开该项目。

将 Caffeine Cache 添加到类路径中。它不在 Spring Initializr 上,所以我手动添加了它。它的 Maven 坐标是 com.github.ben-manes.caffeine:caffeine:${caffeine.version}。如果存在此依赖项,则负载均衡器将使用它来缓存已解析的实例。

让我们回顾一下我们想要发生的事情。我们希望调用我们的服务 api。我们知道负载均衡器中可能有一个以上的服务实例。我们可以将 API 放在负载均衡器后面,然后就完成了。但是我们想要做的是使用有关每个应用程序状态的信息来做出更明智的负载均衡决策。有很多原因我们会使用客户端负载均衡而不是 DNS。首先,Java DNS 客户端倾向于缓存已解析的 IP 信息,这意味着后续对同一已解析 IP 的调用将最终连续地集中在一个服务上。您可以禁用它,但您正在逆着 DNS 的趋势行事,DNS 是一个以缓存为中心系统。DNS 只告诉您某物哪里,而不是是否在那里。换句话说;您不知道 DNS 负载均衡器背后的某处是否会有人等待您的请求。在发起调用之前,您难道不想知道,从而避免您的客户端在调用失败之前进行漫长的超时等待吗?此外,服务对冲等某些模式——也是另一个 Spring Tips 视频的主题——只有使用服务注册中心才可能实现。

让我们看看 client 的常规配置属性。这些属性指定了 spring.applicatino.name,这没什么新颖的。第二个属性很重要。它禁用了自 2015 年 Spring Cloud 首次亮相以来一直存在的默认 Netflix Ribbon 后端负载均衡策略。毕竟,我们想使用新的 Spring Cloud Load balancer。

spring.application.name=client
spring.cloud.loadbalancer.ribbon.enabled=false

因此,让我们看看我们服务注册中心的使用情况。首先,我们的客户端需要通过 Eureka DiscoveryClient 实现与服务注册中心建立连接。Spring Cloud DiscoveryClient 抽象在类路径上,因此它将自动启动并将 client 注册到服务注册中心。

这是我们应用程序的开头,一个入口点类。

package com.example.client;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer;
import org.springframework.cloud.client.loadbalancer.reactive.ReactorLoadBalancerExchangeFilterFunction;
import org.springframework.cloud.client.loadbalancer.reactive.Response;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;

import static com.example.client.ClientApplication.call;

@SpringBootApplication
public class ClientApplication {

    public static void main(String[] args) {
        SpringApplication.run(ClientApplication.class, args);
    }
}

我们将为此添加一个 DTO 类,以在服务返回给客户端的 JSON 结构中进行传递。这个类使用了一些 Lombok 的方便的注解。

@Data
@AllArgsConstructor
@NoArgsConstructor
class Greeting {
    private String greetings;
}

现在,让我们看看负载均衡的三种不同方法,每种方法都越来越复杂。

直接使用 Loadbalancer 抽象

第一种方法是这三种中最简单但也是最冗长的。在这种方法中,我们将直接使用负载均衡抽象。该组件注入了 ReactiveLoadBalancer.Factory<ServiceInstance> 的指针,然后我们可以使用它来提供一个 ReactiveLoadBalancer<ServiceInstance>。这个 ReactiveLoadBalancer 是我们通过调用 api.choose() 来负载均衡对 api 服务的调用的接口。然后,我使用该 ServiceInstance 来构建特定 ServiceInstance 的主机和端口的 URL,然后使用我们的响应式 HTTP 客户端 WebClient 发起 HTTP 请求。

@Log4j2
@Component
class ReactiveLoadBalancerFactoryRunner {

  ReactiveLoadBalancerFactoryRunner(ReactiveLoadBalancer.Factory<ServiceInstance> serviceInstanceFactory) {
        var http = WebClient.builder().build();
        ReactiveLoadBalancer<ServiceInstance> api = serviceInstanceFactory.getInstance("api");
        Flux<Response<ServiceInstance>> chosen = Flux.from(api.choose());
        chosen
            .map(responseServiceInstance -> {
                ServiceInstance server = responseServiceInstance.getServer();
                var url = "http://" + server.getHost() + ':' + server.getPort() + "/greetings";
                log.info(url);
                return url;
            })
            .flatMap(url -> call(http, url))
            .subscribe(greeting -> log.info("manual: " + greeting.toString()));

    }
}

发起 HTTP 请求的实际工作由我放在应用程序类中的一个静态方法 call 完成。它需要一个有效的 WebClient 引用和一个 HTTP URL。


   static Flux<Greeting> call(WebClient http, String url) {
       return http.get().uri(url).retrieve().bodyToFlux(Greeting.class);
   }

这种方法有效,但要进行一次 HTTP 调用,代码量实在太大了。

使用 ReactorLoadBalancerExchangeFilterFunction

下一个方法将许多样板负载均衡逻辑隐藏在一个 WebClient 过滤器中,该过滤器属于 ExchangeFilterFunction 类型,称为 ReactorLoadBalancerExchangeFilterFunction。我们在发出请求之前插入该过滤器,之前的许多代码就消失了。

@Component
@Log4j2
class WebClientRunner {

    WebClientRunner(ReactiveLoadBalancer.Factory<ServiceInstance> serviceInstanceFactory) {

        var filter = new ReactorLoadBalancerExchangeFilterFunction(serviceInstanceFactory);

        var http = WebClient.builder()
            .filter(filter)
            .build();

        call(http, "http://api/greetings").subscribe(greeting -> log.info("filter: " + greeting.toString()));
    }
}

啊哈。好多了!但我们可以做得更好。

@LoadBalanced 注解

在最后一个示例中,我们将让 Spring Cloud 为我们配置 WebClient 实例。如果通过该共享 WebClient 实例的所有请求都需要负载均衡,那么这种方法就非常出色。只需定义一个 WebClient.Builder 的提供者方法,并用 @LoadBalanced 进行注解。然后,您可以使用该 WebClient.Builder 来定义一个将自动为我们进行负载均衡的 WebClient


    @Bean
    @LoadBalanced
    WebClient.Builder builder() {
        return WebClient.builder();
    }
    
    @Bean
    WebClient webClient(WebClient.Builder builder) {
        return builder.build();
    }

完成之后,我们的代码几乎什么都不剩了。

@Log4j2
@Component
class ConfiguredWebClientRunner {

    ConfiguredWebClientRunner(WebClient http) {
        call(http, "http://api/greetings").subscribe(greeting -> log.info("configured: " + greeting.toString()));
    }
}

现在,这才够方便。

负载均衡器使用轮询负载均衡,它会随机地将负载分配给任何一个配置的实例,使用 org.springframework.cloud.loadbalancer.core.RoundRobinLoadBalancer 策略。这个的好处是它是可插拔的。如果您愿意,还可以插入其他启发式算法。

下一步

在此 Spring Tips 的这一期中,我们才刚刚开始触及负载均衡抽象的表面,但我们已经取得了巨大的灵活性和简洁性。如果您对自定义负载均衡器更感兴趣,可以查看 @LoadBalancedClient 注解。

获取 Spring 新闻通讯

通过 Spring 新闻通讯保持联系

订阅

领先一步

VMware 提供培训和认证,助您加速进步。

了解更多

获得支持

Tanzu Spring 提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件,只需一份简单的订阅。

了解更多

即将举行的活动

查看 Spring 社区所有即将举行的活动。

查看所有