詳解SpringCloud LoadBalancer 新一代負載均衡器
前言
工作中使用 OpenFeign 進行跨服務調(diào)用,最近發(fā)現(xiàn)線上經(jīng)常會遇到請求失敗。
java.net.ConnectException: Connection refused: connect
通過排查我們發(fā)現(xiàn)不是接口超時,而是有時候會請求到已經(jīng)下線的服務導致報錯。這多發(fā)生在服務提供者系統(tǒng)部署的時候,因為系統(tǒng)部署的時候會調(diào)用 Spring 容器 的 shutdown() 方法, Eureka Server 那里能夠及時的剔除下線服務,但是我們上一篇文章中已經(jīng)知道 readOnlyCacheMap 和 readWriteCacheMap 同步間隔是 30S,Client 端拉取實例信息的間隔也是 30S,這就導致 Eureka Client 端存儲的實例信息數(shù)據(jù)在一個臨界時間范圍內(nèi)都是臟數(shù)據(jù)。
調(diào)整 Eureka 參數(shù)
既然由于 Eureka 本身的設計導致會存在服務實例信息延遲更新,那么我們嘗試去修改幾個參數(shù)來降低延遲
- Client 端設置服務拉取間隔3S,
eureka.client.registry-fetch-interval-seconds = 3 - Server 端設置讀寫緩存同步間隔 3S,
eureka.server.response-cache-update-interval-ms=3000
這樣設置之后經(jīng)過一段時間的觀察發(fā)現(xiàn)情況有所改善,但還是存在這個問題,而且并沒有改善多少。
LoadBalancer 如何獲取實例信息
在 Eureka 和 OpenFeign 的文章中都有提到,OpenFeign 進行遠程調(diào)用的時候會通過負載均衡器選取一個實例發(fā)起 Http 請求。我們 SpringCloud 版本是 2020,已經(jīng)移除了 ribbon,使用的是 LoadBalancer。
通過 debug OpenFeign 調(diào)用的源碼發(fā)現(xiàn)它是從 DiscoveryClientServiceInstanceListSupplier的構造方法獲取實例信息集合 List<ServiceInstance> 的,內(nèi)部調(diào)用到 CachingServiceInstanceListSupplier 構造方法,重點看 CacheFlux.lookup()
public CachingServiceInstanceListSupplier(ServiceInstanceListSupplier delegate, CacheManager cacheManager) {
super(delegate);
this.serviceInstances = CacheFlux.lookup(key -> {
// TODO: configurable cache name
Cache cache = cacheManager.getCache(SERVICE_INSTANCE_CACHE_NAME);
if (cache == null) {
if (log.isErrorEnabled()) {
log.error("Unable to find cache: " + SERVICE_INSTANCE_CACHE_NAME);
}
return Mono.empty();
}
List<ServiceInstance> list = cache.get(key, List.class);
if (list == null || list.isEmpty()) {
return Mono.empty();
}
return Flux.just(list).materialize().collectList();
}, delegate.getServiceId()).onCacheMissResume(delegate.get().take(1))
.andWriteWith((key, signals) -> Flux.fromIterable(signals).dematerialize().doOnNext(instances -> {
Cache cache = cacheManager.getCache(SERVICE_INSTANCE_CACHE_NAME);
if (cache == null) {
if (log.isErrorEnabled()) {
log.error("Unable to find cache for writing: " + SERVICE_INSTANCE_CACHE_NAME);
}
}
else {
cache.put(key, instances);
}
}).then());
}
這里先去查緩存,緩存有就直接返回,緩存沒有就去 CompositeDiscoveryClient.getInstances() 查詢。查詢完畢之后會回調(diào)到 CacheFlux.lookup(param,param2) 第二個參數(shù)的代碼塊,將結果放進緩存。
@Override
public List<ServiceInstance> getInstances(String serviceId) {
if (this.discoveryClients != null) {
for (DiscoveryClient discoveryClient : this.discoveryClients) {
List<ServiceInstance> instances = discoveryClient.getInstances(serviceId);
if (instances != null && !instances.isEmpty()) {
return instances;
}
}
}
return Collections.emptyList();
}
重點看這個方法,由于我們使用的是 Eureka 作為注冊中心。所以這里會調(diào)用 EurekaDiscoveryClient 的getInstances(), 最終我們發(fā)現(xiàn)底層其實就是從 DiscoveryClient.localRegionApps 獲取的服務實例信息。
現(xiàn)在我們清楚了,OpenFeign 調(diào)用時,負載均衡策略還不是從 DiscoveryClient.localRegionApps 直接拿的實例信息,是自己緩存了一份。這樣一來,不僅要計算 Eureka 本身的延遲,還要算上緩存時間。
SpringCloud 中有很多內(nèi)存緩存的實現(xiàn),這里我們選擇的是 Caffine
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.0.5</version>
</dependency>
引入依賴即可自動配置,從 LoadBalancerCacheProperties 中我們能夠發(fā)現(xiàn)默認的緩存時間是 35S,所以要解決我們的問題還需要降低緩存時間,也可以直接不使用內(nèi)存緩存,每次都從 EurekaClient 拉取過來的實例信息讀取即可。
通過上面的分析我們可以發(fā)現(xiàn)使用 OpenFeign 內(nèi)部調(diào)用是無法根治這個問題的,因為 Eureka 的延遲是無法根治的,只能說在維持機器性能等各方面的前提下盡可能的縮短數(shù)據(jù)同步定時任務的時間間隔。所以我們可以換個角度,讓調(diào)用失敗的請求進行重試。
LoadBalancer 的兩種負載均衡策略
通過源碼調(diào)試,發(fā)現(xiàn)它有兩種負載均衡策略 RoundRobinLoadBalancer、RandomLoadBalancer,輪詢和隨機,默認的策略是輪詢
LoadBalancerClientConfiguration 類
@Bean
@ConditionalOnMissingBean
public ReactorLoadBalancer<ServiceInstance> reactorServiceInstanceLoadBalancer(Environment environment,
LoadBalancerClientFactory loadBalancerClientFactory) {
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new RoundRobinLoadBalancer(
loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
}
這兩種策略都比較簡單,沒什么好說的。
輪詢策略存在的問題
我們可以觀察下輪詢策略的實現(xiàn),它有一個原子類型的成員變量,用來記錄下一次請求要落到哪一個實例
final AtomicInteger position;
核心邏輯
private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances) {
if (instances.isEmpty()) {
if (log.isWarnEnabled()) {
log.warn("No servers available for service: " + serviceId);
}
return new EmptyResponse();
}
// TODO: enforce order?
int pos = Math.abs(this.position.incrementAndGet());
ServiceInstance instance = instances.get(pos % instances.size());
return new DefaultResponse(instance);
}
可以看到實現(xiàn)邏輯很簡單,用 position 自增,然后實例數(shù)量進行求余,達到輪詢的效果。乍一看好像沒問題,但是它存在這樣一種情況?,F(xiàn)在我們有兩個實例 192.168.1.121、192.168.1.122,這時候兩個請求 A、B 過來,A 請求了 121 的,B 請求了 122 的,然后 A 請求失敗了觸發(fā)重試,由于輪詢機制 A 重試的實例又回到了 121 ,這樣就有問題了,因為還是失敗,我們要讓重試的請求一定能重試到其他的服務實例。
使用 TraceId 實現(xiàn)自定義負載均衡策略
因為重試的時候是在 OpenFeign 內(nèi)部重新發(fā)起了一次 HTTP 請求,所以 traceId 并沒有變,我們可以先從 MDC 上下文獲取 traceId,再從緩存中獲取 traceId 對應的值,如果沒有就隨機生成一個數(shù)字然后和 RoundRobinLoadBalancer 一樣自增求余,如果緩存中已經(jīng)有了就直接自增求余,這樣就一定能重試到不同的實例。
這里我們緩存組件還是使用 Caffeine
private final LoadingCache<String, AtomicInteger> positionCache = Caffeine.newBuilder().expireAfterWrite(1, TimeUnit.MINUTES)
.build(k -> new AtomicInteger(ThreadLocalRandom.current().nextInt(0, 1000)));
private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> serviceInstances) {
if (serviceInstances.isEmpty()) {
log.warn("No servers available for service: " + serviceId);
return new EmptyResponse();
}
String traceId = MDC.get("traceId");
if (traceId == null) {
traceId = UUID.randomUUID().toString();
}
AtomicInteger seed = positionCache.get(traceId);
int s = seed.getAndIncrement();
int pos = s % serviceInstances.size();
return new DefaultResponse(serviceInstances.stream()
.sorted(Comparator.comparing(ServiceInstance::getInstanceId))
.collect(Collectors.toList()).get(pos));
}
這個方法是從哈希哥那里學到的,他的主頁 juejin.cn/user/501033… 。
完了之后聲明我們自己的負載均衡器的 Bean
public class FeignLoadBalancerConfiguration {
@Bean
public ReactorLoadBalancer<ServiceInstance> reactorServiceInstanceLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSuppliers, Environment environment) {
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new RoundRobinRetryDifferentInstanceLoadBalancer(serviceInstanceListSuppliers,name);
}
}
之后在主啟動類上使用 @LoadBalancerClient 指定我們自定義的負載均衡器
@LoadBalancerClient(name = "feign-test-product", configuration = FeignLoadBalancerConfiguration.class)
設置 LoadBalancer Zone
還記得之前 Eureka 我們?yōu)榱私鉀Q本機調(diào)用的時候會通過負載均衡調(diào)用到開發(fā)環(huán)境的機器設置了 zone,SpringCloud LoadBalancer 也提供了這個配置,并且從源碼中我們可以發(fā)現(xiàn),最終會以 LoadBalancer 設置的為準,如果沒有為它設置,那么會使用 Eureka 中的 zone 配置,如果設置了就會覆蓋 Eureka 的 zone 設置
EurekaLoadBalancerClientConfiguration.postprocess()
@PostConstruct
public void postprocess() {
if (!StringUtils.isEmpty(zoneConfig.getZone())) {
return;
}
String zone = getZoneFromEureka();
if (!StringUtils.isEmpty(zone)) {
if (LOG.isDebugEnabled()) {
LOG.debug("Setting the value of '" + LOADBALANCER_ZONE + "' to " + zone);
}
zoneConfig.setZone(zone);
}
}以上就是詳解SpringCloud LoadBalancer 新一代負載均衡器的詳細內(nèi)容,更多關于SpringCloud LoadBalancer負載均衡器的資料請關注腳本之家其它相關文章!
相關文章
Caused by: java.lang.ClassNotFoundException: org.objectweb.a
這篇文章主要介紹了Caused by: java.lang.ClassNotFoundException: org.objectweb.asm.Type異常,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-07-07
JAVA Comparator 和 Comparable接口使用方法
本文介紹了Java中Comparable和Comparator接口的使用,包括它們的定義、方法和應用場景,Comparable用于定義類的自然排序規(guī)則,而Comparator提供了一種靈活的方式來定義對象之間的排序規(guī)則,無需修改類本身,感興趣的朋友一起看看吧2025-03-03
詳解基于java的Socket聊天程序——客戶端(附demo)
這篇文章主要介紹了詳解基于java的Socket聊天程序——客戶端(附demo),客戶端設計主要分成兩個部分,分別是socket通訊模塊設計和UI相關設計。有興趣的可以了解一下。2016-12-12
Java網(wǎng)絡通信中ServerSocket的設計優(yōu)化方案
今天小編就為大家分享一篇關于Java網(wǎng)絡通信中ServerSocket的設計優(yōu)化方案,小編覺得內(nèi)容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧2019-04-04

