티스토리 뷰

HTTP API 성능 테스트 중에 서버 자원은 남아 있지만 HTTP 요청 수를 늘려도 성능이 선형적으로 늘어나지 않는 문제가 있었고, 이것을 어떤 과정으로 해결했는지 기록을 남겨두고자 한다.

 

원인은 이런 느낌이었을까?

 

결론

결론부터 얘기하자면 서비스에 트랜잭션 락이 걸리면서, HTTP 쓰레드의 상태가 대부분 TIMED_WAITING이 되어 성능이 일정 수준에 머무르게 되었다 😓

 

다행히 테스트가 필요한 API는 DB access를 하지 않고 있어 트랜잭션 처리를 하지 않는 것으로 수정하여, TPS 기준으로 성능이 약 35% 증가되었다 😲

 

 

분석 과정

request thread 수를 조정하여 테스트

여담이지만 마침 최근에 구매한 책이 JVM 성능 분석과 관련된 책이었고, 해당 책에서 JMC 사용법에 대한 가이드가 있어 활용해보았다.

 

스프링 부트에서 사용되고 있는 WAS는 톰캣이었고 thread 갯수를 별도로 조정(server.tomcat.threads.max)하지 않으면 기본 값이 200개이므로 처음에는 클라이언트 요청이 HTTP API 처리 속도에 비해 적을 수 있겠다고 생각했다.

 

하지만, 요청 쓰레드를 두배로 늘려도(30→60) TPS와 CPU 사용률에 큰 변화는 없었다.

 

 

요청 쓰레드 수를 두 배로 늘려도 CPU 자원이 늘어나지 않음

 

 

쓰레드 분석

원인 파악을 위해 쓰레드 상태를 살펴보았는데, http 요청과 관련된 쓰레드들의 상태가 대부분 TIMED_WAITING였고, 원인은 트랜잭션 락으로 파악 되었다.

 

 

쓰레드 stacktrace

Stack Trace
http-nio-41995-exec-158 [230] (TIMED_WAITING)
   sun.misc.Unsafe.park line: not available [native method]
   java.util.concurrent.locks.LockSupport.parkNanos line: 215 
   java.util.concurrent.SynchronousQueue$TransferQueue.awaitFulfill line: 764 
   java.util.concurrent.SynchronousQueue$TransferQueue.transfer line: 695 
   java.util.concurrent.SynchronousQueue.poll line: 941 
   com.zaxxer.hikari.util.ConcurrentBag.borrow line: 151 
   com.zaxxer.hikari.pool.HikariPool.getConnection line: 180 
   com.zaxxer.hikari.pool.HikariPool.getConnection line: 162 
   com.zaxxer.hikari.HikariDataSource.getConnection line: 128 
   org.hibernate.engine.jdbc.connections.internal.DatasourceConnectionProviderImpl.getConnection line: 122 
   org.hibernate.internal.NonContextualJdbcConnectionAccess.obtainConnection line: 38 
   org.hibernate.resource.jdbc.internal.LogicalConnectionManagedImpl.acquireConnectionIfNeeded line: 108 
   org.hibernate.resource.jdbc.internal.LogicalConnectionManagedImpl.getPhysicalConnection line: 138 
   org.hibernate.resource.jdbc.internal.LogicalConnectionManagedImpl.getConnectionForTransactionManagement line: 276 
   org.hibernate.resource.jdbc.internal.LogicalConnectionManagedImpl.begin line: 284 
   org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl$TransactionDriverControlImpl.begin line: 246 
   org.hibernate.engine.transaction.internal.TransactionImpl.begin line: 83 
   org.springframework.orm.jpa.vendor.HibernateJpaDialect.beginTransaction line: 164 
   org.springframework.orm.jpa.JpaTransactionManager.doBegin line: 421 
   org.springframework.transaction.support.AbstractPlatformTransactionManager.startTransaction line: 400 
   org.springframework.transaction.support.AbstractPlatformTransactionManager.getTransaction line: 373 
   org.springframework.transaction.interceptor.TransactionAspectSupport.createTransactionIfNecessary line: 595 
   org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction line: 382 
   org.springframework.transaction.interceptor.TransactionInterceptor.invoke line: 119 
   org.springframework.aop.framework.ReflectiveMethodInvocation.proceed line: 186 
   org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed line: 750 
   org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept line: 692 
   [@Service]$$EnhancerBySpringCGLIB$$5f4fb8fc.[method] line: not available 
   [@Controller] line: 245

 

 

해결

DB access를 하지 않는 서비스 메소드라 트랜잭션을 사용하지 않도록 수정했고, TPS 기준으로 성능이 약 35% 증가되었다. → 진행중인 부모 트랜잭션이 있으면 예외가 발생할 수 있지만, 다른 트랜잭션이 해당 메소드를 호출할 일이 없다고 판단했다.

 

@Transactional(transactionManager = "~", propagation = Propagation.NEVER)
public Return method(Foo foo, Bar bar) {
	...
}

 

 

클래스 레벨 트랜잭션 🤔

해당 서비스를 작성할 때 클래스 레벨에서 트랜잭션을 걸어두고 메소드 단위에서 세부 조정(ex. noRollbackFor)을 했었는데, 메소드 단위로 트랜잭션 설정에 대한 고민이 제대로 되지 않으면 비슷한 이슈가 발생할 수 있을 것 같다.

 

 

 

참고 자료  🙇‍♂️

댓글