1. 修正實例列表亂序導致的負載均衡重試相同實例的問題
雖然之前考慮了通過每個請求的traceId
隔離負載均衡的position
來實現重試不會重試相同實例的問題,但是沒有考慮在負載均衡過程中,實例列表的更新。
例如:
- 請求第一次調用負載均衡,實例列表是:[實例1,實例2],position爲1,對2取餘=1,所以請求發送到實例2上面了
- 請求失敗,觸發重試,實例列表緩存失效,更新後變成了:[實例2,實例1],position爲2,對2取餘=0,所以請求又發送到實例2上面了
private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> serviceInstances) {
if (serviceInstances.isEmpty()) {
log.warn("No servers available for service: " + this.serviceId);
return new EmptyResponse();
}
Span currentSpan = tracer.currentSpan();
//如果沒有 traceId,就生成一個新的,但是最好檢查下爲啥會沒有
//是不是 MQ 消費這種沒有主動生成 traceId 的情況,最好主動生成下
if (currentSpan == null) {
currentSpan = tracer.newTrace();
}
long l = currentSpan.context().traceId();
int seed = positionCache.get(l).getAndIncrement();
//這裏,serviceInstances可能與上次的內容不同
//例如上次是實例1,實例2
//這次是實例2,實例1
return new DefaultResponse(serviceInstances.get(seed % serviceInstances.size()));
}
所以,在這裏追加排序,保證實例有序,從而進一步不會重試相同的實例。
private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> serviceInstances) {
if (serviceInstances.isEmpty()) {
log.warn("No servers available for service: " + this.serviceId);
return new EmptyResponse();
}
Span currentSpan = tracer.currentSpan();
//如果沒有 traceId,就生成一個新的,但是最好檢查下爲啥會沒有
//是不是 MQ 消費這種沒有主動生成 traceId 的情況,最好主動生成下
if (currentSpan == null) {
currentSpan = tracer.newTrace();
}
long l = currentSpan.context().traceId();
int seed = positionCache.get(l).getAndIncrement();
return new DefaultResponse(serviceInstances.stream().sorted(Comparator.comparing(ServiceInstance::getInstanceId)).collect(Collectors.toList()).get(seed % serviceInstances.size()));
}
2. WebFlux環境兼容與WebClient實現相同功能
maven依賴:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.7.RELEASE</version>
</parent>
<properties>
<disruptor.version>3.4.2</disruptor.version>
<resilience4j-spring-cloud2.version>1.1.0</resilience4j-spring-cloud2.version>
</properties>
<dependencies>
<!--內部緩存框架統一採用caffeine-->
<!--這樣Spring cloud loadbalancer用的本地實例緩存也是基於Caffeine-->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<!--日誌需要用log4j2-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<!--lombok簡化代碼-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--註冊到eureka-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--spring cloud rpc相關-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--調用路徑記錄-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<!--暴露actuator相關端口-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId>
</dependency>
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-cloud2</artifactId>
<version>${resilience4j-spring-cloud2.version}</version>
</dependency>
<!--log4j2異步日誌需要的依賴,所有項目都必須用log4j2和異步日誌配置-->
<dependency>
<groupId>com.lmax</groupId>
<artifactId>disruptor</artifactId>
<version>${disruptor.version}</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR4</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
其他的配置是一樣的,重點在於,如何使用WebClient
調用其他微服務,並且實現針對Get
請求重試或者是所有請求的網絡 IO 異常,例如connect timeout
等等,或者是斷路器異常(因爲請求還沒發出)。
WebClient
可以加入各種Filter
,通過實現這些Filter
來實現實例級別的斷路器還有重試。
實現重試:
private static class RetryFilter implements ExchangeFilterFunction {
private final String serviceName;
private RetryFilter(String serviceName) {
this.serviceName = serviceName;
}
@Override
public Mono<ClientResponse> filter(ClientRequest clientRequest, ExchangeFunction exchangeFunction) {
return exchangeFunction.exchange(clientRequest).retryWhen(Retry.onlyIf(retryContext -> {
//get請求一定重試
return clientRequest.method().equals(HttpMethod.GET)
//connect Timeout 是一種 IOException
|| retryContext.exception() instanceof IOException
//實例級別的斷路器的Exception
|| retryContext.exception() instanceof CallNotPermittedException;
}).retryMax(1).exponentialBackoff(Duration.ofMillis(100), Duration.ofMillis(1000)));
}
}
實例級別的斷路器:
private static class InstanceCircuitBreakerFilter implements ExchangeFilterFunction {
private final String serviceName;
private final CircuitBreakerRegistry circuitBreakerRegistry;
;
private InstanceCircuitBreakerFilter(String serviceName, CircuitBreakerRegistry circuitBreakerRegistry) {
this.serviceName = serviceName;
this.circuitBreakerRegistry = circuitBreakerRegistry;
}
@Override
public Mono<ClientResponse> filter(ClientRequest clientRequest, ExchangeFunction exchangeFunction) {
CircuitBreaker circuitBreaker;
//這時候的url是經過負載均衡器的,是實例的url
String instancId = clientRequest.url().getHost() + ":" + clientRequest.url().getPort();
try {
//使用實例id新建或者獲取現有的CircuitBreaker,使用serviceName獲取配置
circuitBreaker = circuitBreakerRegistry.circuitBreaker(instancId, serviceName);
} catch (ConfigurationNotFoundException e) {
circuitBreaker = circuitBreakerRegistry.circuitBreaker(instancId);
}
return exchangeFunction.exchange(clientRequest).transform(CircuitBreakerOperator.of(circuitBreaker));
}
}
組裝調用某個微服務(這裏是service-provider
)的WebClient
:
public static final String SERVICE_PROVIDER = "service-provider";
@Autowired
private ReactorLoadBalancerExchangeFilterFunction lbFunction;
@Bean(SERVICE_PROVIDER)
public WebClient getWebClient(CircuitBreakerRegistry circuitBreakerRegistry) {
ConnectionProvider provider = ConnectionProvider.builder(SERVICE_PROVIDER)
.maxConnections(50).pendingAcquireTimeout(Duration.ofSeconds(5)).build();
HttpClient httpClient = HttpClient.create(provider)
.tcpConfiguration(client ->
//鏈接超時
client.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 500)
.doOnConnected(conn -> conn
//讀取超時
.addHandlerLast(new ReadTimeoutHandler(1))
.addHandlerLast(new WriteTimeoutHandler(1))
)
);
return WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
//Retry在負載均衡前
.filter(new RetryFilter(SERVICE_PROVIDER))
//負載均衡器,改寫url
.filter(lbFunction)
//實例級別的斷路器需要在負載均衡獲取真正地址之後
.filter(new InstanceCircuitBreakerFilter(SERVICE_PROVIDER, circuitBreakerRegistry))
.baseUrl("http://" + SERVICE_PROVIDER)
.build();
}
這樣,我們就可以實現和之前feign
一樣的微服務調用了。
@Log4j2
@RestController
public class TestController {
@Resource(name = WebClientConfig.SERVICE_PROVIDER)
private WebClient webClient;
@RequestMapping("/testGetTimeOut")
public Mono<String> testGetTimeOut() {
return webClient.get().uri("/test-read-time-out")
.retrieve()
.bodyToMono(new ParameterizedTypeReference<>() {
});
}
@RequestMapping("/testPostTimeOut")
public Mono<String> testPostTimeOut() {
return webClient.post().uri("/test-read-time-out")
.retrieve()
.bodyToMono(new ParameterizedTypeReference<>() {
});
}
}