用DiscoveryClient手寫負載均衡


前戲
在分佈式微服務調用的時候我們會想到一個工具叫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的實現,看下圖:
在這裏插入圖片描述
哇喔,看明白了不~,用的時候直接autowired就行了!

2. 準備工作

2.1 先建註冊中心

在這裏插入圖片描述
註冊中心用的是Eureka,很簡單就不同多說啦

  1. EurekaServerBoot.java
@SpringBootApplication
@EnableEurekaServer //該服務爲註冊中心
public class EurekaServerBoot {
    public static void main(String[] args) {
        SpringApplication.run(EurekaServerBoot.class,args);
    }

}

  1. 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等等。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章