深入研究Spring Cloud負載平衡器 – Piotr
Spring Cloud當前即將發生大的變化。雖然幾乎所有的Spring Cloud Netflix元件都將在下一版本中刪除,但最大的變化似乎是將Ribbon客戶端替換為Spring Cloud Load Balancer。當前,關於Spring Cloud Load Balancer的線上文章很少。實際上,該元件仍在積極開發中,因此我們可以在不久的將來期待一些新功能。Netflix Ribbon客戶端是穩定的解決方案,但不幸的是它不再開發。但是,它仍被用作所有Spring Cloud專案中的預設負載均衡器,並具有許多有趣的功能,例如與斷路器整合或根據來自服務例項的平均響應時間進行負載均衡。目前,Spring Cloud Load Balancer尚不提供此類功能,但是我們可以建立一些自定義程式碼來實現它們。在本文中,我將向您展示如何將spring-cloud-loadbalancer模組與RestTemplate 對於應用程式之間的通訊,如何基於平均響應時間實現自定義負載均衡器,最後如何提供服務地址的靜態列表。
您可以在我的GitHub儲存庫https://github.com/piomin/course-spring-microservices.git中找到與本文相關的原始碼片段。該儲存庫也用於我的線上課程,因此我決定透過新示例對其進行擴充套件。所有必需的更改都在該儲存庫中的目錄內部通訊/內部呼叫者服務中執行。該程式碼用Kotlin編寫。這裡有三個應用程式,它們是示例系統的一部分:discovery-server(Spring Cloud Netflix Eureka),inter-callme-service(公開REST API的Spring Boot應用程式),最後是inter-caller-service(呼叫公開的端點的Spring Boot應用程式inter-callme-service)。
如何開始
為了為我們的應用程式啟用Spring Cloud Load Balancer,我們首先需要包括以下啟動器對Maven的依賴關係(此模組也可以與其他一些Spring Cloud啟動器一起包含在內)。
dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-loadbalancer</artifactId> </dependency> |
由於Ribbon仍用作應用程式之間基於REST的通訊的預設客戶端負載平衡器,因此我們需要在應用程式屬性中將其禁用。這是application.yml檔案的片段。
spring: application: name: inter-caller-service cloud: loadbalancer: ribbon: enabled: false |
對於發現整合,我們還需要包含spring-cloud-starter-netflix-eureka-client。要RestTemplate與客戶端負載平衡器一起使用,我們應該定義此類bean並使用進行註釋@LoadBalanced。正如你在下面的程式碼我還設定攔截上RestTemplate,但更多的是在接下來的一節。
@Bean @LoadBalanced fun template(): RestTemplate = RestTemplateBuilder() .interceptors(responseTimeInterceptor()) .build() |
使流量適應平均響應時間
Spring Cloud Load Balancer提供了簡單的迴圈規則,可在單個服務的多個例項之間實現負載平衡。我們的目標是實施一個規則,該規則可測量每個應用程式的響應時間並根據該時間給出權重。響應時間越長,重量就越少。該規則應隨機選擇一個可能性,該可能性由其權重決定。要記錄每個呼叫的響應時間,我們需要設定已經提到的實現的攔截器ClientHttpRequestInterceptor。攔截器在每個請求上執行。由於實施非常典型,因此需要一行解釋。我從Slf4J中存在的執行緒作用域變數獲取目標應用程式的地址MDC。當然,我也可以基於實現一個簡單的執行緒範圍上下文ThreadLocal,但是MDC 此處僅用於簡化。
class ResponseTimeInterceptor(private val responseTimeHistory: ResponseTimeHistory) : ClientHttpRequestInterceptor { private val logger: Logger = LoggerFactory.getLogger(ResponseTimeInterceptor::class.java) override fun intercept(request: HttpRequest, array: ByteArray, execution: ClientHttpRequestExecution): ClientHttpResponse { val startTime: Long = System.currentTimeMillis() val response: ClientHttpResponse = execution.execute(request, array) // 1 val endTime: Long = System.currentTimeMillis() val responseTime: Long = endTime - startTime logger.info("Response time: instance->{}, time->{}", MDC.get("address"), responseTime) responseTimeHistory.addNewMeasure(MDC.get("address"), responseTime) // 2 return response } } |
當然,計算平均響應時間只是我們工作的一部分。最重要的是自定義負載均衡器的實現,如下所示。它應該實現interface ReactorServiceInstanceLoadBalancer。它需要注入ServiceInstanceListSupplierbean來以重寫方法獲取給定服務的可用例項列表choose。選擇正確的例項時,我們正在分析ResponseTimeHistoryby 儲存的每個例項的平均響應時間ResponseTimeInterceptor。首先,我們的負載均衡器的作用就像簡單的迴圈輪詢。
class WeightedTimeResponseLoadBalancer( private val serviceInstanceListSupplierProvider: ObjectProvider<ServiceInstanceListSupplier>, private val serviceId: String, private val responseTimeHistory: ResponseTimeHistory) : ReactorServiceInstanceLoadBalancer { private val logger: Logger = LoggerFactory.getLogger(WeightedTimeResponseLoadBalancer::class.java) private val position: AtomicInteger = AtomicInteger() override fun choose(request: Request<*>?): Mono<Response<ServiceInstance>> { val supplier: ServiceInstanceListSupplier = serviceInstanceListSupplierProvider .getIfAvailable { NoopServiceInstanceListSupplier() } return supplier.get().next() .map { serviceInstances: List<ServiceInstance> -> getInstanceResponse(serviceInstances) } } private fun getInstanceResponse(instances: List<ServiceInstance>): Response<ServiceInstance> { return if (instances.isEmpty()) { EmptyResponse() } else { val address: String? = responseTimeHistory.getAddress(instances.size) val pos: Int = position.incrementAndGet() var instance: ServiceInstance = instances[pos % instances.size] if (address != null) { val found: ServiceInstance? = instances.find { "${it.host}:${it.port}" == address } if (found != null) instance = found } logger.info("Current instance: [address->{}:{}, stats->{}ms]", instance.host, instance.port, responseTimeHistory.stats["${instance.host}:${instance.port}"]) MDC.put("address", "${instance.host}:${instance.port}") DefaultResponse(instance) } } } |
這是ResponseTimeHistorybean 的實現,它負責儲存度量並根據計算的權重選擇服務例項。
class ResponseTimeHistory(private val history: MutableMap<String, Queue<Long>> = mutableMapOf(), val stats: MutableMap<String, Long> = mutableMapOf()) { private val logger: Logger = LoggerFactory.getLogger(ResponseTimeHistory::class.java) fun addNewMeasure(address: String, measure: Long) { var list: Queue<Long>? = history[address] if (list == null) { history[address] = LinkedList<Long>() list = history[address] } logger.info("Adding new measure for->{}, measure->{}", address, measure) if (measure == 0L) list!!.add(1L) else list!!.add(measure) if (list.size > 9) list.remove() stats[address] = countAvg(address) logger.info("Counting avg for->{}, stat->{}", address, stats[address]) } private fun countAvg(address: String): Long { val list: Queue<Long>? = history[address] return list?.sum()?.div(list.size) ?: 0 } fun getAddress(numberOfInstances: Int): String? { if (stats.size < numberOfInstances) return null var sum: Long = 0 stats.forEach { sum += it.value } var r: Long = Random.nextLong(100) var current: Long = 0 stats.forEach { val weight: Long = (sum - it.value)*100 / sum logger.info("Weight for->{}, value->{}, random->{}", it.key, weight, r) current += weight if (r <= current) return it.key } return null } } |
自定義LOADBALANCER
我們的加權響應時間規則機制的實現已經準備就緒,因此最後一步是將其應用於Spring Cloud Load Balancer。為此,我們需要使用ReactorLoadBalancerbean宣告建立一個專用的配置類,如下所示。
class CustomCallmeClientLoadBalancerConfiguration(private val responseTimeHistory: ResponseTimeHistory) { @Bean fun loadBalancer(environment: Environment, loadBalancerClientFactory: LoadBalancerClientFactory): ReactorLoadBalancer<ServiceInstance> { val name: String? = environment.getProperty("loadbalancer.client.name") return WeightedTimeResponseLoadBalancer( loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier::class.java), name!!, responseTimeHistory) } } |
可以使用註釋將自定義配置傳遞給負載均衡器@LoadBalancerClient。客戶端名稱應與發現中註冊的名稱相同。目前,這部分程式碼已在GitHub儲存庫中註釋掉,因此,如果要啟用它以進行測試,則只需取消註釋即可。
@SpringBootApplication @LoadBalancerClient(value = "inter-callme-service", configuration = [CustomCallmeClientLoadBalancerConfiguration::class]) class InterCallerServiceApplication { @Bean fun responseTimeHistory(): ResponseTimeHistory = ResponseTimeHistory() @Bean fun responseTimeInterceptor(): ResponseTimeInterceptor = ResponseTimeInterceptor(responseTimeHistory()) // THE REST OF IMPLEMENTATION... } |
定製提供者例項列表
當前,Spring Cloud Load Balancer不支援在配置屬性中設定的靜態例項列表(與Netflix Ribbon不同)。我們可以輕鬆新增這樣的機制。如下所示,將定義每個服務的例項的靜態列表。
spring: application: name: inter-caller-service cloud: loadbalancer: ribbon: enabled: false instances: - name: inter-callme-service servers: localhost:59600, localhost:59800 |
第一步,我們應該定義一個實現介面ServiceInstanceListSupplier並覆蓋兩個方法的類:getServiceId()和get()。的以下實現ServiceInstanceListSupplier從應用程式屬性到獲取服務地址列表@ConfigurationProperties。
class StaticServiceInstanceListSupplier(private val properties: LoadBalancerConfigurationProperties, private val environment: Environment) : ServiceInstanceListSupplier { override fun getServiceId(): String = environment.getProperty("loadbalancer.client.name")!! override fun get(): Flux<MutableList<ServiceInstance>> { val serviceConfig: LoadBalancerConfigurationProperties.ServiceConfig? = properties.instances.find { it.name == serviceId } val list: MutableList<ServiceInstance> = serviceConfig!!.servers.split(",", ignoreCase = false, limit = 0) .map { StaticServiceInstance(serviceId, it) }.toMutableList() return Flux.just(list) } } |
這是帶有屬性的配置類的實現。
@Configuration @ConfigurationProperties("spring.cloud.loadbalancer") class LoadBalancerConfigurationProperties { val instances: MutableList<ServiceConfig> = mutableListOf() class ServiceConfig { var name: String = "" var servers: String = "" } } |
與前面的示例相同,我們還應該ServiceInstanceListSupplier在自定義配置類中將Bean的實現註冊為Bean。
class CustomCallmeClientLoadBalancerConfiguration) { @Bean fun discoveryClientServiceInstanceListSupplier(discoveryClient: ReactiveDiscoveryClient, environment: Environment, zoneConfig: LoadBalancerZoneConfig, context: ApplicationContext, properties: LoadBalancerConfigurationProperties): ServiceInstanceListSupplier { val delegate = StaticServiceInstanceListSupplier(properties, environment) val cacheManagerProvider = context.getBeanProvider(LoadBalancerCacheManager::class.java) return if (cacheManagerProvider.ifAvailable != null) { CachingServiceInstanceListSupplier(delegate, cacheManagerProvider.ifAvailable) } else delegate } } |
測試中
要測試針對本文目的實現的解決方案,您應該:
- 執行發現伺服器例項(僅在StaticServiceInstanceListSupplier禁用時)
- 執行兩個例項inter-callme-service(對於一個選定的例項,使用VM引數啟用隨機延遲-Dspring.profiles.active=delay)
- 執行的例項inter-caller-service,該例項在埠上可用8080
- 例如,使用命令將一些測試請求傳送到呼叫者間服務 curl -X POST http://localhost:8080/caller/random-send/12345
下圖顯示了我們的測試場景。
結論
當前,Spring Cloud Load Balancer並沒有提供像Netflix Ribbon客戶端那樣的用於服務間通訊的有趣功能。當然,Spring Team仍在積極開發它。好訊息是我們可以輕鬆自定義Spring Cloud Load Balancer來新增一些自定義功能。在本文中,我演示瞭如何提供更高階的負載平衡演算法或如何建立自定義例項列表供應商。
相關文章
- Spring Cloud Netflix—客戶端負載平衡器:RibbonSpringCloud客戶端負載
- Spring RestTemplate作為負載平衡器客戶端SpringREST負載客戶端
- Spring Cloud Ribbon負載均衡SpringCloud負載
- Spring Cloud Gateway之負載均衡SpringCloudGateway負載
- Spring cloud(3)-負載均衡(Feign,Ribbon)SpringCloud負載
- 客服端負載均衡:Spring Cloud Ribbon負載SpringCloud
- Spring Cloud:自定義 Ribbon 負載均衡策略SpringCloud負載
- Spring Cloud Ribbon 客戶端負載均衡SpringCloud客戶端負載
- Spring Cloud之負載均衡元件Ribbon原理分析SpringCloud負載元件
- Spring Cloud:使用 Feign 實現負載均衡詳解SpringCloud負載
- Spring Cloud:使用Ribbon實現負載均衡詳解(上)SpringCloud負載
- Spring Cloud:使用Ribbon實現負載均衡詳解(下)SpringCloud負載
- 微服務Spring Cloud17_負載均衡Ribbon6微服務SpringCloud負載
- Spring Cloud負載均衡神器——Ribbon簡介與基本使用SpringCloud負載
- Spring Cloud入門教程-Ribbon實現客戶端負載均衡SpringCloud客戶端負載
- 4. Spring Cloud Ribbon 實現“負載均衡”的詳細配置說明SpringCloud負載
- 通過Spring Boot,Spring Cloud Gateway構建基於Consul叢集的微服務案例演示 – Piotr的TechBlogSpring BootCloudGateway微服務
- 透過Spring Boot,Spring Cloud Gateway構建基於Consul叢集的微服務案例演示 – Piotr的TechBlogSpring BootCloudGateway微服務
- spring cloud 上雲的情況下,Ribbon 客戶端負載均衡 與 ALB 服務端負載均衡的選擇SpringCloud客戶端負載服務端
- Spring Cloud Feign設計原理(轉載)SpringCloud
- java Spring Cloud企業快速開發架構之Ribbon結合RestTemplate實現負載均衡JavaSpringCloud架構REST負載
- Spring Cloud(二):Spring Cloud ConfigSpringCloud
- (20)java Spring Cloud企業快速開發架構之SpringCloud-Ribbon自定義負載均衡策略JavaSpringCloud架構GC負載
- Spring Cloud 關於:Spring Cloud Netflix HystrixSpringCloud
- 【VMware VCF】VMware Cloud Foundation Part 06:部署 VI 工作負載域。Cloud負載
- spring cloud分散式微服務:Spring Cloud ConfigSpringCloud分散式微服務
- spring cloud 和 阿里微服務spring cloud AlibabaSpringCloud阿里微服務
- 使用Spring Boot和GraalVM在Knative上構建微服務 - piotrSpring BootLVM微服務
- 微服務Spring Cloud17_Spring Cloud概述3微服務SpringCloud
- Spring Cloud 整合SpringCloud
- Spring Cloud (十四):Spring Cloud 開源軟體都有哪些?SpringCloud
- 在Kubernetes上使用Spring Boot實現Hazelcast分散式快取 – PiotrSpring BootAST分散式快取
- spring cloud feign 檔案上傳和檔案下載SpringCloud
- spring cloud微服務分散式雲架構Spring Cloud ZuulSpringCloud微服務分散式架構Zuul
- 微服務 | Spring Cloud(一):從單體SSM 到 Spring Cloud微服務SpringCloudSSM
- spring cloud微服務分散式雲架構-Spring Cloud NetflixSpringCloud微服務分散式架構
- spring cloud微服務分散式雲架構-Spring Cloud BusSpringCloud微服務分散式架構
- 詳細剖析Spring Cloud 和Spring Cloud Alibaba的前世今生SpringCloud