Spring Cloud升級之路 - Hoxton - 7. 後續更新(WebFlux等)

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來實現實例級別的斷路器還有重試。

源碼:WebClientConfig.java

實現重試:

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<>() {
                });
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章