Skip to content

주문 결제 성능 최적화

BaekSeungYun edited this page Sep 12, 2023 · 1 revision

주문 병목 문제

주문 병목 이슈

결제 API 요청에 의한 병목

  • 주문 결제 API는 응답시간 1.42s 걸리는 무거운 작업

    스크린샷 2023-08-25 오전 10.47.29.png

  • 이 무거운 작업동안 하나의 Request 스레드가 하나의 DB Connection을 끝까지 물고 있음

문제 상황 테스트

스크린샷 2023-08-25 오전 11.02.04.png

스크린샷 2023-08-25 오전 11.02.27.png

  • product Quantity 9명 대기
  • transactional 입구 10명 대기

개선 1 - PaymentHandler에서 TransactionalEventListener를 사용하도록 변경

@TransactionalEventListener(phase = AFTER_COMMIT)

스크린샷 2023-08-25 오전 11.33.59.png

스크린샷 2023-08-25 오전 11.35.18.png

개선 2 - 결제를 비동기 방식으로 변경

스크린샷 2023-08-25 오후 2.58.53.png

  • 비동기 방식으로 변경했기 때문에 Payment 작업이 끝나지 않았는데도(결제 승인이 되지 않았어도) 사용자는 주문이 배치(place)되었다는 응답을 받게 됨(정책 변경)

  • Toss 측에 인증 요청을 보내는 로직이 blocking으로 동작하기 때문에, 핸들러를 실행하는 ThreadPool에 있는 스레드들이 하나의 결제 작업에 1초 이상의 작업을 수행하게 되고, 1초에 스레드 수만큼의 결제 작업만 가능해 병목 발생.

    스크린샷 2023-08-25 오후 3.37.33.png

SELECT o.updatedAt, p.createdAt - o.updatedAt FROM payments p LEFT JOIN orders o ON p.orderId = o.id WHERE p.id > 1064384;

스크린샷 2023-08-25 오후 3.57.33.png

스크린샷 2023-08-25 오후 3.57.41.png

  • 주문 완료 시점과 결제 완료 시점이 1.14초부터 점점 증가하여 53초 뒤 요청한 주문에 대한 결제 완료까지 걸리는 시간이 388초까지 증가.
  • 비동기 작업큐에 계속 적재가 되고있는 상황이며 특정 시점에 작업 큐가 꽉 찰 수 있는 문제가 발생함
  • 따라서 WebClient 요청을 비동기적으로 할 수 있도록 변경하여 작업 큐에서 기다리는 동안 네트워크 응답을 받을 수 있도록 변경해보자.

개선 3 - 트랜잭션 내에서 Mono를 등록, 이후

[[Spring] @Async 사용 방법](https://steady-coding.tistory.com/611)

스크린샷 2023-08-25 오후 4.54.50.png

스크린샷 2023-08-25 오후 4.54.39.png

스크린샷 2023-08-25 오후 4.54.28.png

  • 개선 결과 결제 완료까지 걸리는 시간이 전체적으로 확 줄어든 것을 확인

  • 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이 걸리지 않고 안정적으로 받아냄

스크린샷 2023-08-25 오후 5.02.23.png

https://spring.io/blog/2019/12/13/flight-of-the-flux-3-hopping-threads-and-schedulers

https://godekdls.github.io/Reactive Spring/springwebflux/

스크린샷 2023-08-27 오후 10.09.26.png

실제 주문과 결제를 비동기로 진행했을 때 비즈니스 메트릭을 붙혀 진행했다

//주문 요청 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());
}

스크린샷 2023-08-28 오전 10.27.14.png

TPS

다양한 프로덕트를 구매하면 어떨까?

2:8 법칙으로 진행한 프로덕트를 구매하면

void test() {
      단건_주문(getWeightedRandomProductId())
  }

한 프로덕트에 대해 집중적으로 구매한 테스트와 달리 다양한 프로덕트 구매를 진행함

퍼포먼스가 향상될 것이라 기대했지만

20 Vuser 테스트 - 여러 프로덕트

스크린샷 2023-08-29 오후 7.27.47.png

스크린샷 2023-08-29 오후 7.30.25.png

스크린샷 2023-08-29 오후 7.29.10.png

평균 14.6 TPS가 나왔으며 CPU usage는 0.6

조금 더 안정적인 성능이 나왔지만 큰 차이는 없었음

비관적 락이 큰 성능적 영향을 끼치지 않음

한 프로덕트 구매 성능 측정 - 20 Vuser

@Test
void test() {
    단건_주문(1)
}

스크린샷 2023-08-29 오후 7.42.25.png

스크린샷 2023-08-29 오후 7.43.23.png

스크린샷 2023-08-29 오후 7.43.38.png

14.6 TPS로 동일한 성능

99 vuser

스크린샷 2023-08-29 오후 8.11.25.png

스크린샷 2023-08-29 오후 8.11.37.png

198 vuser

스크린샷 2023-08-29 오후 8.15.38.png

최종 시나리오 검증 결과

Vuser 40 - 2P 20T

스크린샷 2023-08-30 오후 2.51.45.png

스크린샷 2023-08-30 오후 2.52.41.png

Load Average가 적절한 수치임

위 상태가 peek 타임에 버틸 수 있는 최대치라고 할 수 있음

주문하기 120 vuser - 2:8 상품 단건 주문

평균 84 TPS - 로드 average가 안정적