상황
WebClient를 사용해서 통신을 하고 있는데, 모종의 이유로 통신이 전혀 되지 않는 상황. 이로 인해 외부 서버와 통신하는 어플리케이션에서 장애가 발생함. 장애가 발생한 어플리케이션에서는 타임 아웃등의 에러가 떨어지는 중.
원인
WebClient는 API 요청 시 Connection Pool을 사용한다. 이미 사용 중이지 않은 Connection이 있다면 이를 재사용하고, 그렇지 않다면 새로운 Connection을 맺어 요청을 처리한다. 요청들은 대기열(Event Queue)에 대기하다가 순차적으로 처리된다.
여러 WebClient 인스턴스를 별도로 선언해 사용하더라도, 각 WebClient에 별도의 Connection Pool을 설정하지 않으면 글로벌 Queue를 공용으로 사용하게 된다. 따라서, 하나의 대기열이 꽉 차면 모든 WebClient에서 요청을 처리할 수 없게 된다.
확인 결과 특정 서버와 SSL HandShake가 되지 않고 10초간 행이 걸리면서 다른 요청들이 대기열에 쌓인것으로 추정된다.
해결 방안
각 WebClient 인스턴스에 별도의 스레드 풀을 할당하여 하나의 큐에 대기열이 걸리더라도 다른 WebClient 인스턴스와 디커플링을 하여 문제가 없도록 한다.
public static ClientHttpConnector connector() {
var provider = ConnectionProvider.builder(커넥터 아이디)
.maxConnections(최대 커넥션 수)
.pendingAcquireTimeout(ofMillis(Connection Pool에서 사용할 수 있는 Connection 이 없을때 (모두 사용중일때) Connection을 얻기 위해 대기하는 시간))
.pendingAcquireMaxCount(최대 연결 대기 수)
.maxIdleTime(ofMillis(사용하지 않는 상태(idle)의 Connection이 유지되는 시간.))
.maxLifeTime(ofMillis(Connection Pool 에서의 최대 수명 시간)
.evictInBackground(ofMillis(백그라운드에서 만료된 connection을 제거하는 주기
))
.build();
ReactorResourceFactory factory = new ReactorResourceFactory();
factory.setConnectionProvider(provider);
factory.setUseGlobalResources(false);
// 이벤트 루프 그룹 관리, 위에서 사용한 커넥터 아이디와 동일해야 한다.
factory.setLoopResources(LoopResources.create(커넥터 아이디));
return new ReactorClientHttpConnector(factory, httpClient ->
httpClient.option(CONNECT_TIMEOUT_MILLIS, 연결 타임아웃)
.doOnConnected(connection ->
connection.addHandlerLast(new ReadTimeoutHandler(연결 타임아웃 초, MILLISECONDS))
.addHandlerLast(new WriteTimeoutHandler(쓰기 타임아웃 초, MILLISECONDS))
));
}
참고
https://www.baeldung.com/spring-webflux-concurrency