-
Notifications
You must be signed in to change notification settings - Fork 3
주문 결제 성능 최적화
-
주문 결제 API는 응답시간 1.42s 걸리는 무거운 작업
-
이 무거운 작업동안 하나의 Request 스레드가 하나의 DB Connection을 끝까지 물고 있음
- product Quantity 9명 대기
- transactional 입구 10명 대기
@TransactionalEventListener(phase = AFTER_COMMIT)
-
트랜잭션은 분리했으나 분리만 처리되었을 뿐 여전히 DB 커넥션을 물고 있다.
-
Payments
객체가 저장되지 않는다.-
@EventListener
메서드 내 작업은 기존 트랜잭션에 병합되는데, 기존 트랜잭션이 이미 커밋되었기 때문에 Payment를 저장하는 작업이 발생하지 않는다. - 따라서
@Transactional(propagation=Propagation.REQUIRES_NEW)
로 새로운 트랜잭션을 시작해야 한다.
-
-
@TransactionalEventListener
를 동기 방식으로 사용하면 기존 트랜잭션 커밋 시 DB Connection을 Pool에 반환하지 않는 이슈가 있다.[TransactionalEventListener를 동기로 처리할 때의 발생가능한 문제](https://velog.io/@byeongju/TransactionalEventListener를-동기로-처리할-때의-발생가능한-문제)
-
따라서 비동기 방식으로 추가로 변경해 보자
-
비동기 방식으로 변경했기 때문에 Payment 작업이 끝나지 않았는데도(결제 승인이 되지 않았어도) 사용자는 주문이 배치(place)되었다는 응답을 받게 됨(정책 변경)
-
Toss 측에 인증 요청을 보내는 로직이 blocking으로 동작하기 때문에, 핸들러를 실행하는 ThreadPool에 있는 스레드들이 하나의 결제 작업에 1초 이상의 작업을 수행하게 되고, 1초에 스레드 수만큼의 결제 작업만 가능해 병목 발생.
SELECT o.updatedAt, p.createdAt - o.updatedAt FROM payments p LEFT JOIN orders o ON p.orderId = o.id WHERE p.id > 1064384;
- 주문 완료 시점과 결제 완료 시점이 1.14초부터 점점 증가하여 53초 뒤 요청한 주문에 대한 결제 완료까지 걸리는 시간이 388초까지 증가.
- 비동기 작업큐에 계속 적재가 되고있는 상황이며 특정 시점에 작업 큐가 꽉 찰 수 있는 문제가 발생함
- 따라서 WebClient 요청을 비동기적으로 할 수 있도록 변경하여 작업 큐에서 기다리는 동안 네트워크 응답을 받을 수 있도록 변경해보자.
[[Spring] @Async 사용 방법](https://steady-coding.tistory.com/611)
-
개선 결과 결제 완료까지 걸리는 시간이 전체적으로 확 줄어든 것을 확인
-
TPS는 이전에 비해 10정도 낮아진 23.5 TPS
-
하지만 비동기 작업큐에 계속 적재가 되고 결국 작업 큐가 터지는 상황이 발생함
org.springframework.core.task.TaskRejectedException: Executor [java.util.concurrent.ThreadPoolExecutor@296bff1a[Running, pool size = 30, active threads = 30, queued tasks = 50, completed tasks = 1996]] did not accept task: org.springframework.aop.interceptor.AsyncExecutionInterceptor$$Lambda$1844/0x0000000100ca2c40@6d3a10cb at org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor.submit(ThreadPoolTaskExecutor.java:391) at org.springframework.aop.interceptor.AsyncExecutionAspectSupport.doSubmit(AsyncExecutionAspectSupport.java:292) at org.springframework.aop.interceptor.AsyncExecutionInterceptor.invoke(AsyncExecutionInterceptor.java:129) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:763) at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:708) at com.woowa.woowakit.domain.payment.domain.PaySaveService$$EnhancerBySpringCGLIB$$69a88087.save(<generated>) at com.woowa.woowakit.domain.payment.domain.PaymentHandler.payOrder(PaymentHandler.java:31) at com.woowa.woowakit.domain.payment.domain.PaymentHandler.handle(PaymentHandler.java:22) at jdk.internal.reflect.GeneratedMethodAccessor141.invoke(Unknown Source) at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.base/java.lang.reflect.Method.invoke(Method.java:566) at org.springframework.context.event.ApplicationListenerMethodAdapter.doInvoke(ApplicationListenerMethodAdapter.java:344) at org.springframework.context.event.ApplicationListenerMethodAdapter.processEvent(ApplicationListenerMethodAdapter.java:229) at org.springframework.context.event.ApplicationListenerMethodAdapter.onApplicationEvent(ApplicationListenerMethodAdapter.java:166) at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:176) at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:169) at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:143) at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:421) at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:391) at org.springframework.data.repository.core.support.EventPublishingRepositoryProxyPostProcessor$EventPublishingMethod.publishEventsFrom(EventPublishingRepositoryProxyPostProcessor.java:204) at org.springframework.data.repository.core.support.EventPublishingRepositoryProxyPostProcessor$EventPublishingMethodInterceptor.invoke(EventPublishingRepositoryProxyPostProcessor.java:116) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:123) at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:388) at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(R
-
HikariCP Connection Pool의 Max-Size를 33으로 설정했음에도 커넥션 풀이 부족한 듯한 느낌.
Max-size를 51로 변경
Pending이 걸리지 않고 안정적으로 받아냄
https://spring.io/blog/2019/12/13/flight-of-the-flux-3-hopping-threads-and-schedulers
https://godekdls.github.io/Reactive Spring/springwebflux/
실제 주문과 결제를 비동기로 진행했을 때 비즈니스 메트릭을 붙혀 진행했다
//주문 요청 OrderService.class
@Transactional
@Counted("order.order")
public Long order(final AuthPrincipal authPrincipal, final OrderCreateRequest request) {
log.info("주문 생성 memberId: {} orderId: {} paymentKey: {}", authPrincipal.getId(),
request.getOrderId(), request.getPaymentKey());
Order order = getOrderById(authPrincipal.getId(), request.getOrderId());
order.order(request.getPaymentKey());
return orderRepository.save(order).getId();
}
--------
//비동기 결제 진행 PaymentService.class
@Counted("order.payment.success")
@Transactional
public void handlePaySuccess(final OrderCompleteEvent event) {
Order order = findOrderById(event.getOrder().getId());
Payment payment = paymentMapper.mapFrom(event);
order.pay();
paymentRepository.save(payment);
log.info("결제 완료 subscribe paymentKey: {}", event.getPaymentKey());
}
TPS
2:8 법칙으로 진행한 프로덕트를 구매하면
void test() {
단건_주문(getWeightedRandomProductId())
}
한 프로덕트에 대해 집중적으로 구매한 테스트와 달리 다양한 프로덕트 구매를 진행함
퍼포먼스가 향상될 것이라 기대했지만
20 Vuser 테스트 - 여러 프로덕트
평균 14.6 TPS가 나왔으며 CPU usage는 0.6
조금 더 안정적인 성능이 나왔지만 큰 차이는 없었음
비관적 락이 큰 성능적 영향을 끼치지 않음
@Test
void test() {
단건_주문(1)
}
14.6 TPS로 동일한 성능
99 vuser
198 vuser
Vuser 40 - 2P 20T
Load Average가 적절한 수치임
위 상태가 peek 타임에 버틸 수 있는 최대치라고 할 수 있음
주문하기 120 vuser - 2:8 상품 단건 주문
평균 84 TPS - 로드 average가 안정적