[Spring Cloud AWS] IdleConnectionReaper 메모리 누수 이슈

mrcocoball·2025년 9월 29일
0

Spring Cloud

목록 보기
11/11

개요

Spring Cloud AWS를 사용하는 배치에서 지속적인 메모리 누수 이슈가 발생하고 있었다는 것을 최근에 알게 되었다.
S3를 통해 CSV 오브젝트를 가져오고 Spring Batch Excel을 통해 아이템을 read하는 배치인데, 이 배치가 실행되는 시간에만 메모리 누수가 발생하고 있었다.

확인 결과

초기 대응

처음에는 메모리 누수 발생 원인을 2가지 중 하나로 추측하였다.

1. PoiItemReader를 Delegating Item Reader로 사용하고 있는 ItemStreamReader의 문제
대부분의 배치가 JdbcPagingItemReaderPoiItemReader를 직접 사용하는 것이 아닌, ItemStreamReader의 구현체를 사용하고 그 구현체가 실제 ItemReader에게 작업을 위임하는 형태로 설계가 되었는데 close() 가 실행이 되지 않아서 메모리 누수가 생긴 것이 아닌가 하는 추측을 했었다.

그래서StepExecutionListener를 확장해서 해당 Item Reader를 사용하고 있는 Step이 끝날 때 명시적으로 Step을 close() 하게끔 처리하였다. 그러나 메모리 누수는 여전했다.

2. Resource를 어딘가에서 계속 점유하고 있다
S3 Object를 가져와서 파일 확장자 여부에 따라 파일을 읽어들여 ByteArrayResource[] 로 변환해주는 컴포넌트가 있었는데 여기서 Resource를 해제시키지 않고 계속 가지고 있는 것이 아닌가 하는 생각이 들었다. 그러나 해당 컴포넌트는 파일을 읽어들이는 과정에서 try-with-resources 구문을 사용하고 있었고, 혹시나 싶어서 S3 Object 객체를 명시적으로 close() 처리하였지만 역시나 메모리 누수는 여전했다.

2차 대응

결국 겉으로 드러나는 코드상으로는 메모리 누수의 원인을 확인할 수 없었고 ECS 컨테이너에 들어가서 메모리 덤프를 한 뒤 Memory Analyzer를 통해 원인을 분석했다.

Leak Suspect에서는 LaunchedURLClassLoader 내에 IdleConnectionReaper가 지역 변수로 계속해서 남아 있다는 분석 결과를 알려주고 있었다.

IdleConnectionReaper는 데몬 스레드로서 유휴 연결이 있는지 연결 풀을 주기적으로 확인하는 클래스라고 한다.
https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/http/IdleConnectionReaper.html

레퍼런스를 조사해보니 이 IdleConnectionReaper에 대한 메모리 누수 이슈가 생각보다 많이 발생하는 것으로 보였으며, AWS SDK V2에서 어느 정도 해결이 된 것으로 보이지만 우리 프로젝트에서는 Spring Cloud AWS의 버전이 AWS SDK V2가 적용된 버전보다 낮아서 (Spring Boot 버전 호환성 문제) 메모리 누수 이슈 취약점이 있던 것으로 추측된다.

ChatGPT, Claude를 통해 확인해본 결과 이 IdleConnectionReaper가 배치가 종료되었음에도 계속 남아 있어 메모리 누수를 일으킨 것으로 추측되었는데, 사실 해당 배치가 단독으로 jar 형태로 실행되는 것이 아닌, Quartz Scheduler에서 Job으로 실행되는 형태였기 때문에 배치가 종료되어도 스케줄러 어플리케이션 상에서 계속 살아있어서 메모리 누수가 발생된 것이 아닌가 하는 생각이 들었다.

대응 및 결과

해당 배치가 종료될 때 @PreDestroy 훅으로 IdleConnectionReaper를 명시적으로 셧다운하게끔 처리하였다.

@Slf4j
@SpringBootApplication
public class BatchApplication {

    public static void main(String[] args) {
        System.exit(SpringApplication.exit(SpringApplication.run(BatchApplication.class, args)));
    }

	...

    @PreDestroy
    public void cleanup() {
        try {
            IdleConnectionReaper.shutdown();
            log.info("IdleConnectionReaper shutdown completed.");
        } catch (Exception e) {
            log.error("Failed to shutdown IdleConnectionReaper: ", e);
        }
    }

}

그리고 3일간 모니터링해본 결과 메모리가 소폭 오르긴 했지만 시간이 지나면서 감소하였고, 그 다음번 배치에서는 메모리가 증가하지 않음에 따라 메모리 누수 이슈가 해결된 것으로 보인다.

사실 진작에 메모리 덤프를 떴으면 삽질을 하지 않았을텐데 이번 이슈를 계기로 메모리 덤프를 먼저 떠보고 분석을 해야겠다고 다짐했다...

profile
Backend Developer

0개의 댓글