文章目錄
簡介
一般情況下我們所說的負載均衡通常都是指服務端負載均衡,負載均衡器會維護一個可用的後端服務器清單,然後通過心跳機制來刪除故障的服務端節點以保證清單中都是可以正常訪問的服務端節點,此時當客戶端的請求到達負載均衡服務器時,負載均衡器按照某種配置好的規則從可用服務端清單中選出一臺服務器去處理客戶端的請求。
客戶端負載均衡和服務端負載均衡最大的區別在於服務清單所存儲的位置。在客戶端負載均衡中,所有的客戶端節點都有一份自己要訪問的服務端清單,這些清單統統都是從Eureka服務註冊中心獲取的。在Spring Cloud中我們如果想要使用客戶端負載均衡,方法很簡單,開啓@LoadBalanced
註解即可,這樣客戶端在發起請求的時候會先自行選擇一個服務端,向該服務端發起請求,從而實現負載均衡。
負載均衡的工作原理如下圖:
SpringCloud原有的客戶端負載均衡方案Ribbon已經被廢棄,取而代之的是SpringCloud LoadBalancer
。本文介紹SpringCloud LoadBalancer
的搭建和測試驗證過程。
步驟
我們首先需要先創建幾個後端應用實例,然後創建一個應用使用客戶端負載均衡器SpringCloud LoadBalancer
將用戶的請求分發到這些後端實例上。
創建後端服務實例
創建一個普通的Web應用
利用Spring Initializr初始化我們的應用,在這裏後端服務只要是一個普通的Web服務就可以了,所以添加一個Spring Web
依賴即可:
將得到的壓縮包解壓後導入到idea中。
主程序
後端實例對外暴露了/greeting
和/
訪問點(endpoint),greeting會從3個字符串中隨機返回一個,具體代碼如下:
package com.example.loadbalancerserversayhello;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@SpringBootApplication
public class LoadbalancerServerSayHelloApplication {
private static Logger log = LoggerFactory.getLogger(LoadbalancerServerSayHelloApplication.class);
public static void main(String[] args) {
SpringApplication.run(LoadbalancerServerSayHelloApplication.class, args);
}
@GetMapping("/greeting")
public String greet() {
log.info("Access /greeting");
List<String> greetings = Arrays.asList("Hi there", "Greetings", "Salutations");
Random rand = new Random();
int randomNum = rand.nextInt(greetings.size());
return greetings.get(randomNum);
}
@GetMapping("/")
public String home() {
log.info("Access /");
return "Hi!";
}
}
應用配置
在src/main/resources/application.yml
加入以下配置,指定服務的名稱和運行的端口:
spring:
application:
name: say-hello
server:
port: 8100
運行多個服務實例
我們首先在idea上直接運行,該服務實例會根據src/main/resources/application.yml
中的配置運行在8100端口上。
但我們還需要再運行多幾個實例,才能看出負載均衡的效果,並且各個實例之間不能出現端口衝突,我們可以將應用打成jar包,通過多次運行jar包並指定不同端口來在一臺機器上運行同個應用的多個實例。
打開終端,將該應用打成jar包:
mvn clean package
使用jar包指定端口運行實例:
java -jar .\loadbalancer-server-say-hello-0.0.1-SNAPSHOT.jar --server.port=8101
可以再打開幾個終端,運行多幾個實例,在這裏我們再運行一個實例(需要打開一個新的終端):
java -jar .\loadbalancer-server-say-hello-0.0.1-SNAPSHOT.jar --server.port=8102
訪問後端服務
分別訪問我們創建的3個服務實例,確認服務正常運行:
創建LoadBalancerClient應用
初始化應用
利用Spring Initializr初始化我們的應用,這裏我們添加如下兩個依賴:
主程序
主程序ClientSideLoadbalancerUserApplication.java的具體代碼如下:
package com.example.clientsideloadbalanceruser;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.reactive.ReactorLoadBalancerExchangeFilterFunction;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
@RestController
@SpringBootApplication
public class ClientSideLoadbalancerUserApplication {
private final WebClient.Builder loadBalancedWebClientBuilder;
private final ReactorLoadBalancerExchangeFilterFunction lbFunction;
public ClientSideLoadbalancerUserApplication(WebClient.Builder webClientBuilder,
ReactorLoadBalancerExchangeFilterFunction lbFunction) {
this.loadBalancedWebClientBuilder = webClientBuilder;
this.lbFunction = lbFunction;
}
public static void main(String[] args) {
SpringApplication.run(ClientSideLoadbalancerUserApplication.class, args);
}
@RequestMapping("/hi")
public Mono<String> hi(@RequestParam(value = "name", defaultValue = "Mary") String name) {
return loadBalancedWebClientBuilder.build().get().uri("http://say-hello/greeting")
.retrieve().bodyToMono(String.class)
.map(greeting -> String.format("%s, %s!", greeting, name));
}
@RequestMapping("/hello")
public Mono<String> hello(@RequestParam(value = "name", defaultValue = "John") String name) {
return WebClient.builder().filter(lbFunction).build().get().uri("http://say-hello/greeting").retrieve().bodyToMono(String.class)
.map(greeting -> String.format("%s, %s!", greeting, name));
}
}
其中,loadBalancedWebClientBuilder是注入進去的,具體該bean的配置見下文。
loadBalancedWebClientBuilder.build()
會構建出一個WebClient
對象,表示某個後端實例。
WebClientConfig.java
package com.example.clientsideloadbalanceruser;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;
@Configuration
@LoadBalancerClient(name = "say-hello", configuration = SayHelloConfiguration.class)
public class WebClientConfig {
@LoadBalanced
@Bean
WebClient.Builder webClientBuilder() {
return WebClient.builder();
}
}
提供了WebClient.Builder實例,當用戶訪問/hi時,我們使用這個builder來創建一個WebClient實例,這個實例會被用來向Say Hello服務發送一個Get請求,並把結果作爲一個String返回給我們。
配置LoadBalancer的後端服務實例
我們需要一個實現ServiceInstanceListSupplier接口的類來配置LoadBalancer的後端服務實例,完整代碼如下:
package com.example.clientsideloadbalanceruser;
import java.util.Arrays;
import java.util.List;
import reactor.core.publisher.Flux;
import org.springframework.cloud.client.DefaultServiceInstance;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
@Configuration
public class SayHelloConfiguration {
@Bean
@Primary
ServiceInstanceListSupplier serviceInstanceListSupplier() {
return new DemoServiceInstanceListSuppler("say-hello");
}
}
class DemoServiceInstanceListSuppler implements ServiceInstanceListSupplier {
private final String serviceId;
DemoServiceInstanceListSuppler(String serviceId) {
this.serviceId = serviceId;
}
@Override
public String getServiceId() {
return serviceId;
}
@Override
public Flux<List<ServiceInstance>> get() {
return Flux.just(Arrays
.asList(new DefaultServiceInstance(serviceId + "1", serviceId, "localhost", 8100, false),
new DefaultServiceInstance(serviceId + "2", serviceId, "localhost", 8101, false),
new DefaultServiceInstance(serviceId + "3", serviceId, "localhost", 8102, false)));
}
}
上述代碼定義了負載均衡的後端實例的地址,在這裏我們指定了我們前面創建的3個後端實例localhost:8100
、localhost:8101
、localhost:8102
。
注意事項
當一個應用作爲DiscoveryClient
註冊到服務發現中心時,就不需要使用@LoadBalancerClient
並且手動爲這個LoadBalancer創建配置了,這個應用會使用默認的Spring Cloud LoadBalancer配置來找到服務的實例,訪問這些實例時只會選擇那些正常運行中的實例。
參考資料
什麼是客戶端負載均衡
https://spring.io/guides/gs/spring-cloud-loadbalancer/