티스토리 뷰
HTTP API 성능 테스트 중에 서버 자원은 남아 있지만 HTTP 요청 수를 늘려도 성능이 선형적으로 늘어나지 않는 문제가 있었고, 이것을 어떤 과정으로 해결했는지 기록을 남겨두고자 한다.
결론
결론부터 얘기하자면 서비스에 트랜잭션 락
이 걸리면서, HTTP 쓰레드의 상태가 대부분 TIMED_WAITING
이 되어 성능이 일정 수준에 머무르게 되었다 😓
다행히 테스트가 필요한 API는 DB access를 하지 않고 있어 트랜잭션 처리를 하지 않는 것으로 수정하여, TPS 기준으로 성능이 약 35% 증가되었다 😲
분석 과정
request thread 수를 조정하여 테스트
스프링 부트에서 사용되고 있는 WAS
는 톰캣이었고 thread 갯수를 별도로 조정(server.tomcat.threads.max
)하지 않으면 기본 값이 200개이므로 처음에는 클라이언트 요청이 HTTP API 처리 속도에 비해 적을 수 있겠다고 생각했다.
하지만, 요청 쓰레드를 두배로 늘려도(30→60) TPS와 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
)을 했었는데, 메소드 단위로 트랜잭션 설정에 대한 고민이 제대로 되지 않으면 비슷한 이슈가 발생할 수 있을 것 같다.