用DiscoveryClient手寫負載均衡
-
-
- 1. 原理
-
- 1.1 DiscoveryClient類
- 2. 準備工作
-
- 2.1 先建注冊中心
- 2.2 服務提供者
- 3. 實操
-
- 3.1 寫pom
- 3.2 寫啟動類
- 3.3 application.yaml
- 3.4 定義LoadBalancer接口
- 3.5 實作接口
- 3.6 調用
- 3. 小總結
-
前戲
在分布式微服務調用的時候我們會想到一個工具叫Ribbon,關于這個Ribbon的具體實作負載均衡的原理可以參考本人這篇文章 《Ribbon原理-RestTemplate使用@LoadBalanced負載均衡源碼詳解》, 本篇内容主要講自己如何寫一個簡單的負載均衡邏輯,話不多說,直接開幹!
1. 原理
在開幹之前講一下這個負載均衡的原理,很簡單,就是拿到叢集的服務清單,然後根據某種算法拿到一個服務來進行調用,算法可以是輪詢,也可以是一緻性哈希,也可以根據你的想法去定制,花裡胡哨的都可以,Do what you think! 話不多說,來咯~
看下主角DiscoveryClient類
1.1 DiscoveryClient類
/**
* Represents read operations commonly available to discovery services such as Netflix
* Eureka or consul.io.
*
* @author Spencer Gibb
* @author Olga Maciaszek-Sharma
*/
public interface DiscoveryClient extends Ordered {
/**
* Default order of the discovery client.
*/
int DEFAULT_ORDER = 0;
/**
* A human-readable description of the implementation, used in HealthIndicator.
* @return The description.
*/
String description();
/**
* Gets all ServiceInstances associated with a particular serviceId.
* @param serviceId The serviceId to query.
* @return A List of ServiceInstance.
*/
List<ServiceInstance> getInstances(String serviceId);
/**
* @return All known service IDs.
*/
List<String> getServices();
/**
* Default implementation for getting order of discovery clients.
* @return order
*/
@Override
default int getOrder() {
return DEFAULT_ORDER;
}
}
這是個接口,主要方法是它的getServices方法和getInstances方法,能從注冊中心擷取所有的服務或服務執行個體,那誰去實作它呢,當然是springcloud家族的一些注冊中心去實作它啦,我們在建立注冊中心的時候會引入一些starter包,這些包裡面就有DiscoveryClient的實作,看下圖:
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLiAzNfRHLGZkRGZkRfJ3bs92YsYTMfVmepNHL6VEVPRzZE1EMNpHW4Z0MMBjVtJWd0ckW65UbM5WOHJWa5kHT20ESjBjUIF2X0hXZ0xCMx81dvRWYoNHLrdEZwZ1Rh5WNXp1bwNjW1ZUba9VZwlHdssmch1mclRXY39CXldWYtlWPzNXZj9mcw1ycz9WL49zZuBnL4QDN1QzNyETM3ETNwAjMwIzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
哇喔,看明白了不~,用的時候直接autowired就行了!
2. 準備工作
2.1 先建注冊中心
注冊中心用的是Eureka,很簡單就不同多說啦
- EurekaServerBoot.java
@SpringBootApplication
@EnableEurekaServer //該服務為注冊中心
public class EurekaServerBoot {
public static void main(String[] args) {
SpringApplication.run(EurekaServerBoot.class,args);
}
}
- application.yaml
server:
port: 7002
eureka:
instance:
hostname: eureka7002.com #這裡我改了主機host,将eureka7002.com指向localhost
client:
register-with-eureka: false #是否向自己注冊,作為注冊中心不需要向自己注冊
fetch-registry: false #表示自己端就是注冊中心,我的職責就是維護服務執行個體,并不需要去檢索服務
service-url:
defaultZone: http://eureka7001.com:7001/eureka/ #向另一個節點注冊
2.2 服務提供者
寫兩個服務提供者模拟叢集服務,隻是暴露的端口不同,其他都是一樣的,挑一個看裡面的配置,其他的就不介紹了
server:
port: 8002
spring:
application:
name: springcloud-payment-service #服務名,也就是放進注冊中心的名字
http:
encoding:
force: true
charset: UTF-8
enabled: true
# 一定要有端口号和服務名稱
datasource:
type: com.alibaba.druid.pool.DruidDataSource #目前資料源操作類型
driver-class-name: com.mysql.jdbc.Driver #資料庫驅動包
url: jdbc:mysql://localhost:3306/test?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&rewriteBatchedStatements=true
username: root
password: root
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.yolyn.springcloud.entities #所有entity别名所在包
eureka:
client:
register-with-eureka: true
fetch-registry: true #是否從EurekaServer擷取已有的注冊資訊,預設為ture
service-url:
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/
instance:
instance-id: payment8002 #id别名
prefer-ip-address: true # 顯示ip資訊
要着重注意的是application.name,決定注冊進注冊中心的應用名叫什麼,這個應用名可以對應多個不同的服務執行個體,後面的負載均衡也就是通過這個應用名擷取所有的服務執行個體,在springcloud-payment-service裡面實作了什麼功能暫時不用關心,後面隻是用負載均衡拿到某個服務執行個體資訊就行了。
3. 實操
3.1 寫pom
主要引入以下次幾個依賴
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--eureka-client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
3.2 寫啟動類
/**
* @author Yolyn
* @version 1.0
* @date 2020/5/3 10:35
* @project springcloud-2020
*/
@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class, DataSourceTransactionManagerAutoConfiguration.class })
@EnableEurekaClient
//@RibbonClient(configuration = MyRibbonRule.class,name = "springcloud-payment-service")
//@EnableDiscoveryClient//是否注冊本地服務,該注解用于向使用consul或者zk作為注冊中心時注冊服務
public class OrderBoot {
public static void main(String[] args) {
SpringApplication.run(OrderBoot.class,args);
}
}
啟動類就不介紹了,我這裡是選用Eureka作為注冊中心,@EnableEurekaClient 是Netflix用于注冊服務的注解,而 @EnableDiscoveryClient 是springcloud預設用于注冊服務的注解,通常選zk或者consul作為注冊中心時,可以選這個注解。
3.3 application.yaml
server:
port: 80
debug: true
spring:
application:
name: cloud-order-service
# zipkin:
# base-url: http://localhost:9411 # 監控鍊位址
# sleuth:
# sampler:
# probability: 1 #采樣率值介于 0 到 1 之間,1 則表示全部采集
eureka:
client:
register-with-eureka: true
fetch-registry: true #是否從EurekaServer擷取已有的注冊資訊,預設為ture
service-url:
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/
注釋的不用看
3.4 定義LoadBalancer接口
在這裡我們先定義一個LoadBalancer接口,在接口裡面定義一個getInstance方法。
/**
* @author Yolyn
* @version 1.0
* @date 2020/5/5 14:14
* @project springcloud-2020
*/
public interface LoadBalancer {
/**
* 擷取服務執行個體
* @param serviceInstanceList
* @return
*/
ServiceInstance getInstance(List<ServiceInstance> serviceInstanceList);
}
後面我們實作這個方法,利用某種算法從傳過來的服務清單裡面挑一個服務來進行調用,看下面:
3.5 實作接口
/**
* @author Yolyn
* @version 1.0
* @date 2020/5/5 14:15
* @project springcloud-2020
*/
@Component
public class MyRoundLB implements LoadBalancer {
private AtomicInteger invokeTimes = new AtomicInteger(0);
public final int getAndIncrement() {
int current, next;
do {
current = this.invokeTimes.get();
next = current >= Integer.MAX_VALUE ? 0 : current + 1;//防止調用次數過多
} while (!this.invokeTimes.compareAndSet(current, next));
return next;
}
@Override
public ServiceInstance getInstance(List<ServiceInstance> serviceInstanceList) {
int index= getAndIncrement()%serviceInstanceList.size();
return serviceInstanceList.get(index);
}
}
這裡getAndIncrement算法很簡單咯,調用一次對invokeTimes作+1操作,防止并發用了cas,然後在getInstance方法中用調用次數對叢集服務數取餘得到下标,而實作的一個輪詢算法。
3.6 調用
@RestController
public class OrderController {
@Autowired
private LoadBalancer loadBalancer;
@Autowired
private DiscoveryClient discoveryClient;
@GetMapping("/consumer/payment/lbService")
public ResultModel getPaymentLbService() {
List<ServiceInstance> instanceList = discoveryClient.getInstances("springcloud-payment-service");
if (null == instanceList || instanceList.size() <= 0) {
return null;
}
return new ResultModel().setSuccess(loadBalancer.getInstance(instanceList).getMetadata());
}
}
先後啟動注冊中心,兩個服務提供者,和第三節的服務消費者。
注冊中心:
3. 小總結
一句話:負載均衡無非就是拿到叢集的服務清單,然後根據某種算法挑選一個服務來進行調用,算法實作可以根據實際場景來選擇,如權重輪詢、一緻性哈希和LRU等等。