Post

애플리케이션 성능 테스트 (2)

애플리케이션의 성능 테스트 결과를 분석하고 문제를 해결해나가는 과정에 대해서 설명합니다.

튜닝 목표

문제를 해결하기 이전에 목표를 다시 한번 살펴보자.
사용자가 최초 홈페이지에 접근하면 서버에 다음과 같은 목록을 요청한다.

  1. 아이템 목록
  2. 인기 아이템 랭킹

RPS가 480인 상황에서 아이템 목록인기 아이템 랭킹 목록을 150ms 내에 응답해주는 것이 목표입니다.

테스트 진행

테스트 내용

1시간 동안 70초에 한번씩 Vuser의 수를 하나씩 늘려가며 1시간동안 부하테스트를 진행했습니다. 최대 Vuser의 수는 50입니다.

테스트 결과

  • 프로세스 CPU 사용률이 100%에 근접했습니다. 이 부분은 하드웨어적인 문제이기 때문에
    • VM 인스턴스의 스펙을 더 좋은 것으로 바꾸거나
    • WAS를 여러 개 구축하고 로드 밸런서로 트래픽을 분산하는 방법으로 해결할 수 있습니다.
  • Vuser가 10명이 되는 지점에서 TPS가 급감하고, 응답시간은 급증하는 모습을 보입니다.
  • Vuser가 27명이 되는 지점에서 사용되는 DB 커넥션의 수가 급증하였다. 이후 커넥션 갯수가 부족해 커넥션을 할당받기를 기다리는 Pending 현상이 발생하였습니다. 애플리케이션에 무리되지 않는 선에서 데이터베이스 커넥션 풀의 커넥션 수를 증가시키는 방법으로 문제를 해결할 수 있습니다.

위 이미지는 커넥션의 갯수가 부족해지는 병목지점을 콜스택으로 살펴본 것입니다. @Transactional애노테이션을 사용했기 때문에 트랜잭션을 시작하기 위해서 커넥션을 얻어와야 합니다. 여유 커넥션이 부족하기 때문에 커넥션 풀에서 커넥션을 얻어오는 getConnection() 메서드에서 병목이 발생하는 것을 확인할 수 있습니다.

DBCP 커넥션 갯수 조정

커넥션을 얻지 못하고 여유 커넥션이 생길 때까지 대기하는 작업때문에 응답속도가 느려지는 현상이 관측했습니다 커넥션 풀의 커넥션 갯수를 현재의 2배인 20개로 늘리고 다시 한번 테스트를 해보겠습니다.

Spring Boot 2.0 부터는 커넥션 풀 라이브러리로 HikariCP를 사용하고 있습니다. HikariCP 설정 정보를 변경해 커넥션의 갯수를 변경해봅시다.

1
2
3
4
5
6
# application.yml
spring:
  datasource:
    hikari:
      maximum-pool-size: 20
      minimum-idle: 20

커넥션 풀의 커넥션 최대 갯수를 기본값인 10에서 20으로 수정하였습니다.

다시 테스트를 수행한 결과 대기하는 커넥션은 없어졌지만 여전히 응답에는 지연이 발생했습니다. CPU 사용률이 계속해서 100%를 유지하면서 CPU의 처리능력 한계로 대기시간이 증가했기 때문입니다.

Scale Out

CPU 사용률이 100%에 도달하면 CPU가 추가적인 작업을 처리할 여유가 없기 때문에 도착한 요청들을 큐에 담아두고 대기시킵니다. 새로운 요청이 즉시 처리되지 않고 대기해야하기 때문에 그만큼 응답시간도 느려지는 것입니다. 이를 해결하기 위해서는 작업 부하를 줄이거나 수평적 확장(Scale Out)을 통해 더 많은 CPU 자원을 확보해야합니다. 저는 우선적으로 Scale Out을 통해서 문제를 해결해보고자 하였습니다. 작은 인스턴스를 몇개 더 만드는 것이 비용적으로 부담이 없고, 이후에 무중단 배포 작업을 위한 초석으로도 사용할 수 있기 때문입니다.

WAS로 사용할 서버 인스턴스 2개와 WAS로 트래픽을 분산할 로드밸런서 인스턴스를 새롭게 만들었습니다. 로드밸런서는 Nginx를 사용하여 구현하였습니다. 자세한 내용은 Nginx를 위한 로드밸런싱을 참고해주세요

로드밸런서를 활용한 Scale Out으로 서버의 CPU포화도가 증가해 처리량이 급격하게 감소하는 현상이 해결되었습니다.

또 두 인스턴스의 CPU 사용량도 많이 줄어든 것을 확인할 수 있습니다. 100%에 여러번 도달했던 이전과는 다르게 높아야 30%의 사용률을 보입니다. Scale Out 후 CPU 사용량

응답시간 점도표를 살펴보아도 Vuser가 30명이 넘어가면 2000~3000ms대의 응답시간을 보였던 것과도 확연한 차이를 보입니다.

Pinpoint_Scatter_Chart

성능측면에서 Scale Out 이전과 비교해보면

목록단일 서버Scale Out(서버 2개)
한계점Vuser 8명Vuser 12명
경합지점Vuser 10명Vuser 30명
평균 응답시간333ms185ms
최대 응답시간3200ms209ms
평균 TPS120100 + 100 = 200
  • 평균 응답시간에서도 2배 정도의 성능 향상을, 최대 응답시간관점에서는 극단적으로 응답시간이 느려지는 현상이 사라졌습니다.
  • 서버 성능의 척도를 나타내는 TPS도 2배가량 성능 향상이 이루어졌습니다.

한계점

위의 결과에서도 알 수 있듯이 한계점에 도달한 시점에도 CPU의 사용량은 20~30%에 불과한 것을 볼 수 있습니다. 이는 성능에 안좋은 영향을 미치는 다른 요인이 있다는 것을 의미합니다.

캐시 추가

커넥션이 필요한 이유는 DB에 요청 쿼리를 보내고 응답 데이터를 받기 위함입니다. 그런데 만약 자주 조회되는 데이터가 캐시에 저장되어 있다면 DB 요청을 할 필요도 없어지기 때문에 커넥션을 얻어올 필요도 없어지고 그만큼 응답 시간을 줄일 수 있습니다.

캐시는 크게 로컬 캐시리모트 캐시로 나눌 수 있습니다. 로컬 캐시는 서버 내 메모리를 캐시로 활용하는 방법이고, 리모트 캐시는 WAS 서버가 아닌 다른 서버에 캐시 저장소를 두고 필요한 데이터를 활용하는 방법입니다.

로컬 캐시의 경우에는 WAS의 메모리에 저장하는 것이기 때문에 따로 캐시서버를 두는 것보다 여유공간이 적습니다. 너무 많은 공간을 차지하게 되면 애플리케이션이 사용할 수 있는 공간이 줄어들고, 그만큼 GC의 빈도가 늘어나 성능에 안좋은 영향을 주기도 합니다. 또 여러대의 WAS로 운영되는 상황에서는 WAS간의 캐시 동기화를 위한 작업이 필요합니다.

서비스 내에서 캐시를 활용하면 성능을 높일 수 있는 사례가 약 3개 있었습니다.

  • 금칙어 목록 관리
  • 차단 IP 관리
  • 자주 조회되는 데이터 캐시

금칙어 목록, 차단 IP

금칙어 목록과 차단 IP 목록은 데이터가 자주 변경되는 특성이 있어, 여러 대의 WAS로 운영되는 현재 아키텍처에서는 리모트 캐시 사용이 적절해 보입니다. 하지만 현재는 금칙어나 차단된 IP의 수가 많지 않아, 메모리에 저장해도 큰 부담이 되지 않는다 판단하였습니다. 또한 Redis와 같은 외부 메모리 저장소를 사용할 경우 네트워크 통신이 발생하여 성능에 영향을 줄 수 있습니다.

WAS 간 데이터 동기화 문제는 일정 시간마다 관계형 데이터베이스에서 데이터를 가져오는 방식으로 해결했습니다. 물론 이 해결책은 데이터가 적은 상황에서 유효하며, 데이터가 100만 개, 1000만 개 이상으로 증가하면 애플리케이션 성능에 영향을 줄 수 있습니다. 그때는 Redis와 같은 외부 캐시 저장소를 사용해 조회 성능을 개선하는 방법을 고려하고 있습니다.

현재 상황에서는 데이터의 크기와 성능을 고려해 로컬 캐시를 사용하고 있으며, 추후 확장성을 위해 리모트 캐시 도입을 염두에 두고 있습니다.

자주 조회되는 데이터 캐시

자주 조회되는 데이터를 캐시해두는 방법도 사용하였습니다. 사용자가 홈페이지에 접근하면 아이템 목록을 서버에 요청해야 합니다. 누구나 웹 페이지에 처음 접근하면 아이템 목록의 첫번째 페이지를 요청하게 되는 것입니다. 거기다 처음 보여주는 아이템 목록은 이미 등록된 아이템 번호의 오름차순으로 조회되는 것이기 때문에 한번 설정되면 변경될 가능성이 거의 없는 것들입니다. 거의 변경되지 않는 데이터를 반환하기 위해서 애플리케이션이 매번 데이터베이스에 쿼리를 전달해야 하기 때문에 지연이 발생합니다.

따라서 첫번째 페이지에 한해서 캐시된 아이템의 목록을 제공하도록 했습니다.

테스트 결과

캐시 적용 이전과 이후를 몇번의 요청으로 단순 비교해보면 그 차이가 단적으로 드러납니다.

nGrinder를 이용한 부하테스트에서도 응답속도가 향상된 것을 확인할 수 있습니다.

vUser가 10개 일 때 30분동안 부하를 가한 결과 평균 응답속도를 보여주고 있습니다. 캐시 적용 이전의 평균 응답속도는 약 198ms, 그리고 캐시 적용 이후의 평균 응답속도는 약 45ms로 약 77% 정도의 성능향상이 이루어졌다.

This post is licensed under CC BY 4.0 by the author.