본문 바로가기
우아한테크코스 4기/레벨4

부하테스트(3) - WAS, Connection Pool 설정하기

by 나는후니 2022. 9. 27.

앞선 글을 보면 활성 이용자 수를 통해 VUser를 계산하고 적절한 부하테스트 툴을 설정했다면, 이제 테스트를 진행하며 성능 개선 혹은 적절한 설정을 해줄 수 있는데요. 이번 글에서는 부하테스트를 진행하고 현재 상황에 적절한 WAS, Connection Pool 설정을 진행한 과정을 설명드리고자 합니다.

0. 초기 테스트 환경

  • AWS EC2 t4g.micro
    • WS : memory 1GB, CPU 2 core
    • WAS : memory 1GB, swap 1GB, CPU 2 core
    • DB : memory 1GB, CPU 2 core
  • Hikari CP
    • default
  • 회원의 모든 받은 쿠폰 조회 -> 단일 쿠폰 조회 join , 총 5만건
  • VUser 10 ~ 20
  • Spring actuator, Prometheus, Grafana

1. max Thread 조정

server:
  tomcat:
    threads:
      max: 100

우아한테크코스 내 사용자 수를 감안해 최초에는 Thread 수를 100개로 지정하였습니다. 최초 부하테스트 결과는 아래와 같습니다.

10 -> 20으로 Ramp up 되는 과정에서 기대하는 RPS까지 오르지않습니다. VUser는 20인 반면 hikari thread pool이 10으로 고정되어 있었기 때문에, Connection을 잡기위해 대기하고 있는 스레드들이 많았습니다.

이번에는 hikari pool을 수정하여 테스트를 수행해보겠습니다.

Hikari CP github에 가면 스레드를 기준으로 데드락을 방지하기 위한 최소한의 커넥션 풀을 구하는 공식이 있습니다. 하지만 우선 VUser에 따라 Hikari maximum pool을 지정했습니다.

2. Hikari Pool 조정

server:
  tomcat:
    threads:
      max: 100
hikari:
    maximumPoolSize: 21

Connection을 잡기 위해 대기하는 스레드가 줄어들다 보니 latency가 약 3배 줄어든 것을 확인할 수 있습니다. 하지만 여전히 VUser가 오른만큼 RPS가 오르지 않는 현상이 있었습니다. 게다가 CPU 사용량도 예상했던 수준 (4 ~ 80)을 훨씬 넘어서서 동시에 실행되는 스레드의 개수를 어느정도 제한해야겠다고 판단했습니다.

사실 현재 사용하고 있는 서버 스펙은 CPU core가 두개이기 때문에 스레드를 코어 개수와 유사하게 가져가는 게 컨텍스트 스위칭 비용을 최소화할 수 있으나, 현재 테스트하고자 하는 지점이 CPU를 많이 사용하는 작업이 아니라, I/O 작업이 주로 이뤄지는 요청이기 때문에 스레드의 개수를 넉넉하게 가져갔습니다.

따라서 최종적으로 다음과 같이 설정하게 됐습니다.

server:
  tomcat:
    accept-count: 100
    max-connections: 100
    threads:
      max: 50

hikari:
    minimum-idle: 10
    maximumPoolSize: 51
    idleTimeout: 10000

부하테스트 결과를 보니 몇몇 지점을 제외하고는 크게 응답에 문제가 없는 것을 볼 수 있었습니다. 이제 이 설정을 바탕으로 JVM을 모니터링 하며 조금씩 더 적절한 설정을 찾아갔습니다.

JVM 모니터링

모니터링은 Spring Boot actuator와 micormeter 설정을 하여 prometheus 로 수집, grafana로 시각화하였습니다.

Thread 줄이기

테스트 과정 중의 Thread dump를 보면 막상 동시에 작동하는 Thread는 22개 정도이고, 모든 Thread가 http 요청으로 인해 생성되는 Thread가 아니었습니다. 결국 VUser가 20이더라도 막상 동시에 애플리케이션에 사용되는 스레드는 20개가 채 되지 않았던 것이죠.

또, 오히려 스레드를 동시에 많이 사용하는 지점에서 지표상 눈에 띄는 성능 저하를 볼 수 있었는데요. 이러한 점에서 max 스레드를 조금 더 줄여도 좋겠다는 판단을 하였습니다. 따라서 max Thread를 절반 감소시켰습니다.

Hikari CP 줄이기

스레드를 줄이니, Hikari CP도 21 이상으로 상승하지 않았습니다. 따라서 Hikari CP의 max 개수도 21로 감소시킬 수 있었습니다. Connection을 획득하기 위해 대기하지 않으면서 적절한 스레드 풀을 지정했다고 생각합니다.

물론 예상한 사용자보다 훨씬 많은 사용자가 유입된다면 지금 설정이 이야기가 달라지겠지만, 현실적으로 그럴 일이 없다고 판단하였습니다. (예상 사용자가 이미 지금 사용자보다 훨씬 많은 사용자임)

따라서 JVM 을 모니터링하며 최종적으로 텀캣 및 Hikari CP 설정을 마무리할 수 있었습니다. (Max기준 TPS : 721)

server:
  tomcat:
    max-connections: 100
    threads:
      max: 25

hikari:
    minimum-idle: 10
    maximumPoolSize: 21
    idleTimeout: 10000

어려웠던 점

GC

현재 Java 11을 사용하고 있어 G1GC를 쓰고 있습니다. 그런데, 특정 지점에서 major gc의 STW가 400ms 정도 발생하는 순간이 있었습니다. 모든 스레드가 멈추기 때문에 해당 지점에서 유독 RPS가 떨어지는 것을 볼 수 있었습니다.

예상할 수 있는 문제가 몇 가지 있었는데요,

  • Tenured Generation이 JVM이 사용할 수 있도록 지정한 commited 메모리에 도달했을 때, GC가 특정 이유로 진행되지 않고 Commited 메모리를 늘려 GC해야하는 객체의 양이 많아졌습니다.
  1. 더불어 에덴 영역이 GC되는 주기도 길어지면서 GC가 원활하게 이뤄지지 않아 서바이벌 영역 -> Tenured로 이동하는 양이 많아졌습니다.

물론 위의 이유가 확실하지는 않지만 지표를 통해 이정도는 살펴볼 수 있었습니다. 현재 사용 중인 서버의 스펙이 낮아 힙 메모리 자체가 적기 때문에 stw의 시간을 parelall GC를 사용하여 줄이는 방법도 고려해볼 수 있을 것 같습니다만, G1GC가 안정적이고, 꾸준히 발전 중이라는 측면에서 유지해야겠다는 선택을 했습니다. 또, 특이 케이스가 아닌이상 STW가 길게 형성되는 경우가 거의 없었기 때문에 단순히 GC만의 문제라고 판단할 수 없었습니다.

동시 요청이 늘어나는 경우

동시 요청이 설정해놓은 max Thread보다 높아지는 경우 모든 thread의 작동 + connection 대기 등으로 인해 CPU 사용량이 급격히 올라갔습니다. 또한 위의 내용과 같이 Commited 메모리가 늘어 Heap 메모리가 사용되는 양이 매우 늘었습니다.

단순히 조회를 하는 요청이기 때문에, application 문제가 아니라 OS / 하드웨어 스펙의 부족이 원인이라고 판단했습니다. 이중화를 해보면 아래와 같이 처리할 수 있는 RPS도 높아지고 CPU 사용량도 50 ~ 70 수준으로 유지될 수 있었습니다. (현재 제약사항으로 인해 스케일 업을 할 수 없는 상태)

하지만 문제는 이중화에 따라 고려해야할 내용들이 너무 많았습니다. 또, 현재 사용자를 훨씬 상회하는 숫자의 사용자를 받기 위해 서버를 증설하는 것 자체가 현재 적절하지 않는 판단이라고 생각했습니다. 따라서 단일 서버를 유지하는 형태로 선택했습니다.

정리

적절한 설정을 찾기 위해서는 부하테스트, 모니터링, 하드웨어 스펙 등 고려해야할 점이 참 많습니다. 특히 JVM 모니터링을 보면 예상하는 것과는 정말 다른 지표를 보일 때가 종종 있었습니다. 하지만 OS나 JVM, GC등을 생활형으로 학습하는 측면에서는 정말 좋은 경험이었습니다.

 

위 글은 순수히 제 경험에 의한 글이고, 다른 사실들과는 조금 차이가 있을 수 있다는 점 참고해주세요.

 

감사합니다.