ElasticSearch TimeOutException 해결과정

Jeonghwa·2024년 3월 25일
0

서론

가상User수를 200명으로 5분동안 성능 테스트 중, 많은 부하를 주지 않았지만 500 응답이 간헐적으로 187건이 발생하였습니다. 혹시 ElasticSearch 서버의 문제일까 로그를 확인해 보았지만 별다른 이상은 없었으며, Client 서버(Application 서버) 로그를 보니 아래와 같이 httpasyncclient로 통신하려다 TimeOutException이 발생하였습니다.

2024-03-20 21:57:01.581 ERROR 1 --- [o-8080-exec-834] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.RuntimeException: error while performing request] with root cause

java.util.concurrent.TimeoutException: Connection lease request time out
	at org.apache.http.nio.pool.AbstractNIOConnPool.processPendingRequest(AbstractNIOConnPool.java:411) ~[httpcore-nio-4.4.16.jar!/:4.4.16]
	at org.apache.http.nio.pool.AbstractNIOConnPool.processNextPendingRequest(AbstractNIOConnPool.java:391) ~[httpcore-nio-4.4.16.jar!/:4.4.16]
	at org.apache.http.nio.pool.AbstractNIOConnPool.release(AbstractNIOConnPool.java:355) ~[httpcore-nio-4.4.16.jar!/:4.4.16]
	at org.apache.http.impl.nio.conn.PoolingNHttpClientConnectionManager.releaseConnection(PoolingNHttpClientConnectionManager.java:393) ~[httpasyncclient-4.1.5.jar!/:4.1.5]
	at org.apache.http.impl.nio.client.AbstractClientExchangeHandler.releaseConnection(AbstractClientExchangeHandler.java:247) ~[httpasyncclient-4.1.5.jar!/:4.1.5]
	at org.apache.http.impl.nio.client.MainClientExec.responseCompleted(MainClientExec.java:387) ~[httpasyncclient-4.1.5.jar!/:4.1.5]
	at org.apache.http.impl.nio.client.DefaultClientExchangeHandlerImpl.responseCompleted(DefaultClientExchangeHandlerImpl.java:173) ~[httpasyncclient-4.1.5.jar!/:4.1.5]
	at org.apache.http.nio.protocol.HttpAsyncRequestExecutor.processResponse(HttpAsyncRequestExecutor.java:448) ~[httpcore-nio-4.4.16.jar!/:4.4.16]
	at org.apache.http.nio.protocol.HttpAsyncRequestExecutor.inputReady(HttpAsyncRequestExecutor.java:338) ~[httpcore-nio-4.4.16.jar!/:4.4.16]
	at org.apache.http.impl.nio.DefaultNHttpClientConnection.consumeInput(DefaultNHttpClientConnection.java:265) ~[httpcore-nio-4.4.16.jar!/:4.4.16]
	at org.apache.http.impl.nio.client.InternalIODispatch.onInputReady(InternalIODispatch.java:87) ~[httpasyncclient-4.1.5.jar!/:4.1.5]
	at org.apache.http.impl.nio.client.InternalIODispatch.onInputReady(InternalIODispatch.java:40) ~[httpasyncclient-4.1.5.jar!/:4.1.5]
	at org.apache.http.impl.nio.reactor.AbstractIODispatch.inputReady(AbstractIODispatch.java:114) ~[httpcore-nio-4.4.16.jar!/:4.4.16]
	at org.apache.http.impl.nio.reactor.BaseIOReactor.readable(BaseIOReactor.java:162) ~[httpcore-nio-4.4.16.jar!/:4.4.16]
	at org.apache.http.impl.nio.reactor.AbstractIOReactor.processEvent(AbstractIOReactor.java:337) ~[httpcore-nio-4.4.16.jar!/:4.4.16]
	at org.apache.http.impl.nio.reactor.AbstractIOReactor.processEvents(AbstractIOReactor.java:315) ~[httpcore-nio-4.4.16.jar!/:4.4.16]
	at org.apache.http.impl.nio.reactor.AbstractIOReactor.execute(AbstractIOReactor.java:276) ~[httpcore-nio-4.4.16.jar!/:4.4.16]
	at org.apache.http.impl.nio.reactor.BaseIOReactor.execute(BaseIOReactor.java:104) ~[httpcore-nio-4.4.16.jar!/:4.4.16]
	at org.apache.http.impl.nio.reactor.AbstractMultiworkerIOReactor$Worker.run(AbstractMultiworkerIOReactor.java:591) ~[httpcore-nio-4.4.16.jar!/:4.4.16]
	at java.base/java.lang.Thread.run(Thread.java:829) ~[na:na]

해결 과정

1. Time out 시간 늘리기

일단 연결하려다 TimeOut이 발생하였으니, TimeOut에 대한 시간을 늘려보는 것으로 해결을 시도하였습니다.

해당 어플리케이션은 내부 네트워크가 아닌, 외부 네트워크환경에서 구동되고 있는 ElasticSearch 서버와 통신하는 것이기 때문에 원래 ConnectTimeout을 1초에서 조금 더 넉넉하게 10초 정도 잡고 수정하였습니다.

@Configuration
public class RestClientConfig extends AbstractElasticsearchConfiguration {
    @Value("${spring.elasticsearch.host}")
    private String host;
    @Value("${spring.elasticsearch.port}")
    private int port;
    @Value("${spring.elasticsearch.username}")
    private String username;
    @Value("${spring.elasticsearch.password}")
    private String password;
    @Override
    public RestHighLevelClient elasticsearchClient() {
        final ClientConfiguration clientConfiguration = ClientConfiguration.builder()
                .connectedTo(host + ":" + port)
                .withBasicAuth(username, password)
                .withConnectTimeout(Duration.ofSeconds(10)) // 10초 설정
                .build();

        return RestClients.create(clientConfiguration).rest();
    }
}

가상User수를 200명으로 5분동안 테스트를 수행하였더니 187건에서 25건으로 줄었지만 Error는 여전히 존재합니다.

추가: 위 방법은 실제 운영되는 서비스에서는 위험한 방법입니다. 1초면 성공이던 오류던 사용자에게 바로바로 응답을 줄 수 있는것을 최대 10초까지 끌고갈 수 있기때문에 만약 트래픽이 늘어나게 되면 심각한 장애를 유발할 수 있습니다.. ThreadPool과 ConnectionPool을 조절하여 튜닝합시다!

2. Connection Pool 늘리기

TimeOut을 늘렸음에도 TimeOut에러가 난다는 것은 정말로 실제 Connection이 부족해서일 가능성이 큽니다.

따라서 가상User수를 200명으로 가정했고 한 프로세스당 50개의 thread를 돌도록 하였으므로, ElasticSearch 동시 접속자 수를 50명으로 잡고 Connection Pool을 늘려주는 작업을 시도해보겠습니다.

@Configuration
public class RestClientConfig extends AbstractElasticsearchConfiguration {

    @Value("${spring.elasticsearch.host}")
    private String host;
    @Value("${spring.elasticsearch.port}")
    private int port;
    @Value("${spring.elasticsearch.username}")
    private String username;
    @Value("${spring.elasticsearch.password}")
    private String password;

    @Override
    public RestHighLevelClient elasticsearchClient() {
        final BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
        credentialsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(username, password));

        RestClientBuilder builder = RestClient.builder(new HttpHost(host, port));
        builder.setRequestConfigCallback(
                requestConfigBuilder -> requestConfigBuilder.setConnectTimeout(10000) // 10초
        );
        builder.setHttpClientConfigCallback(httpClientBuilder ->
                httpClientBuilder
                        .setDefaultCredentialsProvider(credentialsProvider)
                        .setMaxConnTotal(100) // 전체 최대 연결 수를 100개로 설정
                        .setMaxConnPerRoute(50) // 단일 라우트(호스트) 당 최대 연결 수
        );
        return new RestHighLevelClient(builder);
    }
}

ConnectionPool 조절을 위해선 HttpClientConfig 설정이 필요하므로 설정 방식을 변경하였습니다.

참고: https://www.elastic.co/guide/en/elasticsearch/client/java-api-client/7.17/_basic_authentication.html

만약 설정을 바꾸지 않으면, 기본적으로 MaxConnTotal(전체 최대 연결 수)는 20, MaxConnPerRoute(단일 호스트 당 최대 연결 수)는 2라는 default값을 갖습니다.

현재 HttpClient로 접속하는 Host는 ElasticSearch 서버 단 1개이므로 MaxConnPerRoute로 전체 커넥션을 조절한다고 봐도 무방합니다.

HttpAsyncClientBuilder를 통해 NHttpClientConnectionManager의 설정을 변경하는 것이며 아래 공식문서를 참고하면 기본 설정값이 나와있습니다.
참고: https://hc.apache.org/httpcomponents-asyncclient-4.1.x/current/httpasyncclient/apidocs/org/apache/http/impl/nio/conn/PoolingNHttpClientConnectionManager.html

설정을 변경하고 가상User수를 200명으로 5분동안 테스트를 수행해보면 Error가 사라진 것을 볼 수 있습니다.

가상User수를 1000으로 늘리고 10분동안 테스트해봐도 Error는 발생하지 않습니다.


결론

Connection Pool을 얼마나 잡을지, TimeOut 시간을 어느정도로 제한을 둘지에대한 정답은 없습니다. 부하테스트를 통해 어플리케이션에 적합한 값을 찾는 것이 중요합니다.

profile
backend-developer🔥

0개의 댓글

관련 채용 정보