메모리 누수 해결하기 -(1) DB 커넥션 풀을 조정하자(메모리 사용량 절반으로 절감)
에서
DB 커넥션이 이상하게 많이 생성돼 있어서 이를 줄였다.
이렇게 커넥션을 줄이게 된 이유는 99%가 모른다는 DB Connection 누수 문제이 글을 보고서 DB 커넥션이 메모리 사용량의 범인일 수 있겠다는 추측을 했기 때문이다.
heap dump 파일을 타고타고 들어가면 AbandonedConnectionCleanupThread와 관련된 클래스들이 나온다.
이 클래스는 버려진 MySQL connection, 즉 명시적으로 닫히지 않은 connection을 닫는 작업을 담당하는 스레드를 구현합니다. 이 클래스의 인스턴스는 단 하나이며 이 작업을 수행하는 단일 스레드가 있습니다. 이 스레드의 실행자는 동일한 클래스에서 정적으로 참조됩니다.
해당 클래스에 이런 내용이 있었다고 하는데 지금은 안보인다.
즉, Mysql Connection을 닫는 클래스인 것이다.
이제부터 마켓컬리의 글을 좀 참고해보자.
우선, HikariPool에 의해서 커넥션들이 생성된다.
커넥션이 생성되면 AbandonedConnectionCleanupThread에 의해 PhantomReference 인스턴스가 생성되어 connectionFinalizerPhantomRefs에 보관한다.
PhantomReference를 생성하는 이유는, AbandonedConnectionCleanupThread에서 Connection이 GC에게 수거되기 전에 네트워크 리소스를 지워주기 위해서라고 한다.
AbandonedConnectionCleanupThread의 trackConnection() 메서드에서 ConnectionFinalizerPhantomReference를 생성하는 부분을 보면 conn(Connection), io(네트워크 리소스), referenceQueue(레퍼런스 큐)를 넣어주며 생성한다.
connectionFinalizerPhantomRefs -> ConnectionFinalizerPhantomReference -> referenceQueue 로 이어지는 의존관계를 확인할 수 있다.
이 커넥션의 수명이 다하면, HikariCP에서 직접 Connection을 참조하던 부분이 끊기게 되고 Connection 객체는 Phantomly Reachable 상태가 된다.
(Phantomly Reachable 상태는 흐름을 위해 'GC가 Root에서 직접 접근하지 못하고 PhantomReference 로 접근가능한 상태'로 이해할 수있다)
GC가 동작하면 Phantomly Reachable 객체를 탐지한 후에 finalize() 하고 PhantomReference를 생성할 때 같이 넣어주었던 ReferenceQueue 에 enqueue() 한다.
AbandonedConnectionCleanupThread 는 내부에서 실행되는 백그라운드 스레드이다. ReferenceQueue를 계속해서 polling(remove()) 하며 enqueue()된 PhantomReference 를 가져오고, 직접 finalizeResource() 를 해준다.
PhantomReference 내부의 finalizeResource()는 명시적으로 networkResources를 닫아주는 역할을 한다. 만약 HikariCP를 사용하지 않고 Connection을 직접 관리했다면 놓쳤을 수 있는 networkResource를 이곳에서 종료해 주는 것이다. AbandonedConnectionCleanupThread 의 존재이유 이기도 하다.
static 영역을 확인하면, AbandonedConnectionCleanupThread는 newSingleThreadExecutor에 의해 단일 스레드로 실행된다
그리고 한 번에 한 개의 Reference 를 꺼내와 네트워크 자원을 종료해준다.
그리고 네트워크 자원 종료는 클라이언트와 서버 간의 TCP/IP 소켓 연결을 확인하기 때문에 네트워크 환경에 따라 병목이 생길 수 있다.
이렇게 AbandonedConnectionCleanupThread는 단일 스레드에서 한 번에 한 개의 네트워크 종료를 한다는 구조적인 문제로 인해 병목이 발생할 수 있는 상황이다.
마켓컬리에서 겪었던 문제는 나와는 조금 다른 상황인것으로 보인다.
나는 max-lifetime을 별도로 설정하지 않았지만, 마켓컬리는 이걸 50초로 설정했다.
그래서 AbandonedConnectionCleanupThread 에서 버려진 Connection을 처리하는 속도가 재생성되는 속도를 따라잡지 못하고 계속해서 AbandonedConnectionCleanupThread 에 Connection이 쌓이는 현상이 발생하고 있던 것이다.
나는 그보다는 커넥션 수가 너무 많아서 AbandonedConnectionCleanupThread이게 많이 생긴 것으로 보인다.
대신AbandonedConnectionCleanupThread를 비활성화해주는 게 좋다고 한다. 일반적인 서비스는 개발자가 커넥션을 직접 얻어와 networkResource를 열거나 닫는 등의 행위를 하지 않기 때문이라고 한다.
-Dcom.mysql.cj.disableAbandonedConnectionCleanup=true
자바 실행옵션이 이를 추가해주며 된다.
참고로 이 설정은
라고 한다.