Spring 小貼士:Spring Cloud Loadbalancer

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

講者:Josh Long (@starbuxman)

嗨,Spring 的粉絲們!歡迎來到 Spring 小貼士的另一個單元!在本單元中,我們將了解 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 Initializr,選擇 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!」處理程序,它使用我們在 另一個 Spring 小貼士影片中於 2017 年介紹的功能性反應式 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。如果您執行多個實例,您將在控制台中看到它們的反映。

負載平衡客戶端

好的,現在我們準備好展示 laod balancing 的作用。我們需要一個新的 spring boot 應用程式。前往 Spring Intiialzir 並使用 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(一種以快取為中心的系統)的 grain 作鬥爭。DNS 只告訴您某個東西在哪裡,而不是是否在那裡。換句話說;您不知道在基於 DNS 的負載平衡器的另一側是否有任何東西在等待您的請求。您是否想在發出呼叫之前知道,從而避免客戶端在呼叫失敗之前經歷繁瑣的逾時時間?此外,某些模式(例如服務對沖)-也是另一個 Spring 小貼士影片的主題-只有透過服務註冊中心才能實現。

讓我們看看 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 定義一個 provider 方法,並使用 @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 社群中所有即將到來的活動。

查看全部