Spring 提示: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。点击生成

使用内置 Eureka 服务的大部分工作都在配置中,我已经在这里重新打印了。

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 Initializer,使用Reactive WebLombok 以及 Eureka 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 Intiialzir 并使用Eureka Discovery ClientLombokCloud LoadbalancerReactive Web 生成一个新项目。单击生成并在您喜欢的 IDE 中打开项目。

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

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

让我们看看client 的常用配置属性。这些属性指定了spring.applicatino.name,这没什么新意。第二个属性很重要。它禁用了自 Spring Cloud 于 2015 年首次亮相以来一直存在的默认基于 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服务的接口,通过调用api.choose()。然后我使用该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提示中,我们仅仅触及了负载均衡抽象的表面,但我们已经获得了极大的灵活性和简洁性。如果您对自定义负载均衡器更感兴趣,可以查看@LoadBalancedClient注解。

获取Spring新闻

关注Spring新闻

订阅

领先一步

VMware提供培训和认证,帮助您快速提升技能。

了解更多

获取支持

Tanzu Spring在一个简单的订阅中提供OpenJDK™、Spring和Apache Tomcat®的支持和二进制文件。

了解更多

即将举行的活动

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

查看全部