java.lang.Exception: **Apparent connection leak detected**
at com.zaxxer.hikari.HikariDataSource.getConnection(HikariDataSource.java:128) ~[HikariCP-4.0.3.jar:na]
at org.hibernate.engine.jdbc.connections.internal.DatasourceConnectionProviderImpl.getConnection(DatasourceConnectionProviderImpl.java:122) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final]
at org.hibernate.internal.NonContextualJdbcConnectionAccess.obtainConnection(NonContextualJdbcConnectionAccess.java:38) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final]
at org.hibernate.resource.jdbc.internal.LogicalConnectionManagedImpl.acquireConnectionIfNeeded(LogicalConnectionManagedImpl.java:108) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final]
at org.hibernate.resource.jdbc.internal.LogicalConnectionManagedImpl.getPhysicalConnection(LogicalConnectionManagedImpl.java:138) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final]
at org.hibernate.resource.jdbc.internal.LogicalConnectionManagedImpl.getConnectionForTransactionManagement(LogicalConnectionManagedImpl.java:276) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final]
at org.hibernate.resource.jdbc.internal.LogicalConnectionManagedImpl.begin(LogicalConnectionManagedImpl.java:284) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final]
at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl$TransactionDriverControlImpl.begin(JdbcResourceLocalTransactionCoordinatorImpl.java:246) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final]
at org.hibernate.engine.transaction.internal.TransactionImpl.begin(TransactionImpl.java:83) ~[hibernate-core-5.6.15.Final.jar:5.6.15.Final]
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.beginTransaction(HibernateJpaDialect.java:164) ~[spring-orm-5.3.29.jar:5.3.29]
at org.springframework.orm.jpa.JpaTransactionManager.doBegin(JpaTransactionManager.java:421) ~[spring-orm-5.3.29.jar:5.3.29]
at org.springframework.transaction.support.AbstractPlatformTransactionManager.startTransaction(AbstractPlatformTransactionManager.java:400) ~[spring-tx-5.3.29.jar:5.3.29]
at org.springframework.transaction.support.AbstractPlatformTransactionManager.getTransaction(AbstractPlatformTransactionManager.java:373) ~[spring-tx-5.3.29.jar:5.3.29]
at sopt.org.umbba.api.service.scheduler.ScheduleService.lambda$schedulePushAlarm$1(**ScheduleService.java:62**) ~[main/:na]
at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54) ~[spring-context-5.3.29.jar:5.3.29]
at org.springframework.scheduling.concurrent.ReschedulingRunnable.run(ReschedulingRunnable.java:95) ~[spring-context-5.3.29.jar:5.3.29]
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515) ~[na:na]
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264) ~[na:na]
at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304) ~[na:na]
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) ~[na:na]
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) ~[na:na]
at java.base/java.lang.Thread.run(Thread.java:829) ~[na:na]
데이터베이스와의 연결이 끊긴 누수 문제 → 과연 Dead Lock이 발생한 것일까?
메모리 한계치를 초과하여 SELECT 쿼리를 보낸 경우 - https://cocococo331.tistory.com/29
하나의 쓰레드 내에서 같은 엔티티 매니저를 공유하는 경우
*하나의 EntityManager는 하나의 쓰레드에서만 사용가능하다! EntityManager 자체가 데이터베이스 연결과의 트랜잭션 상태를 유지하며 공유나 재사용이 불가능한 리소스이기 때문.
→ 쓰레드 풀 내에서 병렬로 이루어지는 작업들이 공유할 확률이 있나..?
답변은 NO!
위에서 엔티티 매니저를 주입받을 때 @PersistenceContext
어노테이션을 사용한다. 이는 각각의 쓰레드에 대해 별도의 엔티티 매니저가 생성됨을 보장한다.
또한 쓰레드에서 이루어지는 작업은 엔티티 매니저 내부에서 독립적으로 처리되기 떄문에 쓰레드 풀 내에서 병렬로 이루어지는 다른 작업들과 공유될 확률은 거의 없다고 보면 된다.
위에서 검출된 바로는 역시 스케줄링 작업 곳곳에서 누수가 발생하는 듯 보였고, EntityManager를 따로 주입받지 않는 부분에서도 같은 에러가 발생되어 명시적으로 예외가 발생되면 커넥션을 종료하도록 로직을 추가하였다.
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import java.sql.Connection;
@Aspect
@Component
public class ConnectionClosingAspect {
@AfterThrowing(pointcut = "execution(* sopt.org.umbba.notification.service.*.*(..))", throwing = "ex")
public void closeConnectionOnException(Exception ex) {
Connection conn = // DB 연결 정보로 connection 객체 가져오기
if (conn != null && !conn.isClosed()) {
conn.close();
}
}
}
하지만.. 일부 커넥션에만 누수가 발생한 것이라 새로운 Connection 객체를 만들었다가 종료시키는 건 크게 의미가 없었다고 한다..
스케줄링 작업 내에서 Thread.sleep(1000)
사용
쓰레드를 1초동안 잠시 멈추게 하는 sleep() 메서드를 호출하는 것은 불필요한 블로킹 작업일 수 있다. 잠시 멈추게 한다는 행위 자체가 쓰레드 풀의 리소스를 잠깐동안 차지하는 것이기 떄문에, 특히 여러 번 동시에 실행되는 이 스케줄링 작업에서는 꼭 필요하지 않다면 빼는 것을 권장한다.
java.util.concurrent.TimeoutException: null
at java.base/java.util.concurrent.FutureTask.get(FutureTask.java:204) ~[na:na]
at org.springframework.cloud.aws.messaging.listener.SimpleMessageListenerContainer.waitForRunningQueuesToStop(SimpleMessageListenerContainer.java:162) ~[spring-cloud-aws-messaging-2.2.6.RELEASE.jar:2.2.6.RELEASE]
at org.springframework.cloud.aws.messaging.listener.SimpleMessageListenerContainer.doStop(SimpleMessageListenerContainer.java:141) ~[spring-cloud-aws-messaging-2.2.6.RELEASE.jar:2.2.6.RELEASE]
at org.springframework.cloud.aws.messaging.listener.AbstractMessageListenerContainer.stop(AbstractMessageListenerContainer.java:352) ~[spring-cloud-aws-messaging-2.2.6.RELEASE.jar:2.2.6.RELEASE]
at org.springframework.cloud.aws.messaging.listener.SimpleMessageListenerContainer.stop(SimpleMessageListenerContainer.java:46) ~[spring-cloud-aws-messaging-2.2.6.RELEASE.jar:2.2.6.RELEASE]
at org.springframework.cloud.aws.messaging.listener.AbstractMessageListenerContainer.destroy(AbstractMessageListenerContainer.java:365) ~[spring-cloud-aws-messaging-2.2.6.RELEASE.jar:2.2.6.RELEASE]
at org.springframework.cloud.aws.messaging.listener.SimpleMessageListenerContainer.destroy(SimpleMessageListenerContainer.java:46) ~[spring-cloud-aws-messaging-2.2.6.RELEASE.jar:2.2.6.RELEASE]
at org.springframework.beans.factory.support.DisposableBeanAdapter.destroy(DisposableBeanAdapter.java:213) ~[spring-beans-5.3.29.jar:5.3.29]
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.destroyBean(DefaultSingletonBeanRegistry.java:587) ~[spring-beans-5.3.29.jar:5.3.29]
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.destroySingleton(DefaultSingletonBeanRegistry.java:559) ~[spring-beans-5.3.29.jar:5.3.29]
at org.springframework.beans.factory.support.DefaultListableBeanFactory.destroySingleton(DefaultListableBeanFactory.java:1163) ~[spring-beans-5.3.29.jar:5.3.29]
at sopt.org.umbba.notification.config.GracefulShutdown.lambda$stopSqsListeners$0(GracefulShutdown.java:54) ~[main/:na]
at java.base/java.util.LinkedHashMap$LinkedKeySet.forEach(LinkedHashMap.java:559) ~[na:na]
at sopt.org.umbba.notification.config.GracefulShutdown.stopSqsListeners(GracefulShutdown.java:54) ~[main/:na]
at sopt.org.umbba.notification.config.GracefulShutdown.stop(GracefulShutdown.java:48) ~[main/:na]
at sopt.org.umbba.notification.config.GracefulShutdown.stop(GracefulShutdown.java:37) ~[main/:na]
at org.springframework.context.support.DefaultLifecycleProcessor.doStop(DefaultLifecycleProcessor.java:235) ~[spring-context-5.3.29.jar:5.3.29]
at org.springframework.context.support.DefaultLifecycleProcessor.access$300(DefaultLifecycleProcessor.java:54) ~[spring-context-5.3.29.jar:5.3.29]
at org.springframework.context.support.DefaultLifecycleProcessor$LifecycleGroup.stop(DefaultLifecycleProcessor.java:374) ~[spring-context-5.3.29.jar:5.3.29]
at org.springframework.context.support.DefaultLifecycleProcessor.stopBeans(DefaultLifecycleProcessor.java:207) ~[spring-context-5.3.29.jar:5.3.29]
at org.springframework.context.support.DefaultLifecycleProcessor.onClose(DefaultLifecycleProcessor.java:130) ~[spring-context-5.3.29.jar:5.3.29]
at org.springframework.context.support.AbstractApplicationContext.doClose(AbstractApplicationContext.java:1070) ~[spring-context-5.3.29.jar:5.3.29]
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.doClose(ServletWebServerApplicationContext.java:174) ~[spring-boot-2.7.14.jar:2.7.14]
at org.springframework.context.support.AbstractApplicationContext.close(AbstractApplicationContext.java:1024) ~[spring-context-5.3.29.jar:5.3.29]
at org.springframework.boot.SpringApplicationShutdownHook.closeAndWait(SpringApplicationShutdownHook.java:145) ~[spring-boot-2.7.14.jar:2.7.14]
at java.base/java.lang.Iterable.forEach(Iterable.java:75) ~[na:na]
at org.springframework.boot.SpringApplicationShutdownHook.run(SpringApplicationShutdownHook.java:114) ~[spring-boot-2.7.14.jar:2.7.14]
at java.base/java.lang.Thread.run(Thread.java:829) ~[na:na]
누수 검출 옵션 ON 으로 원인 추적하기
spring:
datasource:
hikari: leak-detection-threshold: 2000 *# default: 0(이용X)*
→ 로그에서 “ProxyLeakTask”를 찾아보면, 어디서 누수가 발생하는지 검출할 수 있다!
배포 서버 로그를 뒤져보니.. 매번 다른 데서 발생해서 일관되지 않다는 것을 알았다..!
그렇다면, 검출 시간이 너무 짧아서 예민하게 반응하는 건 아닐까?
2000 → 60000으로 확 늘려줘보니까 바로 누수 관련 에러 및 경고는 뜨지 않았다..
⇒ [결론] 처음에 hikariCP 관련 옵션 설정에서 누수 검출 시간을 초과할 때까지 연결이 되지 않으면 누수로 감지하고 WARN을 날리는 시간을 2000 (2초)로 설정해뒀는데, 이게 아무래도 너무 짧았던 모양이다. 또 이렇게 연결에 지연이 발생한 부분은 아무래도 트랜잭션 관리를 수동으로 해주는 부분이지 않을까 강한 의심이 든다. 계속 누수를 검출했다고 뜨는 부분도 트랜잭션 매니저를 가져오는 부분이었으니까!
TransactionDefinition transactionDefinition = new DefaultTransactionDefinition();
TransactionStatus transactionStatus = transactionManager.getTransaction(transactionDefinition);