본문 바로가기
카테고리 없음

웹 서버 성능테스트 - 병목 해결을 위한 단계별 테스트

by devstep 2024. 1. 9.


회사 프로젝트 오픈 전 얼마만큼의 사용자를 처리할 수 있는지 확인하고 사업 목표 TPS를 달성하기 위해 성능테스트를 진행하였습니다. 
성능테스트를 진행하기 전 시나리오를 작성하고 API를 최적화한 작업과, 테스트를 진행하며 병목 포인트를 찾아간 방법과 해결한 과정들을 적어 보았습니다.

 


웹 서버 성능테스트 과정 

 

목차 

  1. 어떤 시나리오를 선택 했는지
  2. 시나리오 내 API 최적화 작업 사항
  3. 부하테스트 도구 사용법 : 개념, 설정방법, 결과 확인
  4. 병목 포인트 발견 방법과 해결 과정 

1 읽기용 시나리오 작성과 API 최적화 작업 

시나리오는 읽기용과 쓰기용으로 나눠서 작성

1-1  시나리오 선정 

예약하기 전 사용자가 상품을 둘러보기 전에 가장 많이 접근하는 3개의 페이지를 선정했다. 각 페이지는 여러 API로 이루어져 있다. 

  • 메인 화면 : 추천상품 API, 오픈 공지 API 
  • 카테고리 상품 목록 : 롤 배너 API, 상품 목록 API 
  • 상품 상세

1-2  API 최적화 작업 사항

1) cache 적용 

읽기용 API는 사용자가 같은 정보를 확인하기에 DB 접근 비용을 줄이는 cache를 적용하기에 알맞다.
cache는 로컬 캐시와 글로벌 캐시로 나눌 수 있는데 로컬 캐시를 선택했다. 

로컬 캐시를 선택한 이유는 

  • 별도 서버 마련하지 않아도 될만한 캐시 데이터 크기
  • 글로벌 캐시보다 빠른 로컬 캐시 사용
  • 대신 캐시 정보 실시간 업데이트는 불가하여 적절한 TTA 시간을 주어 보완. 그리고 각 화면의 API에서 정보성 데이터가 아닌 예매에 직접적으로 관련된 데이터는 캐시에서 배제 

개인화된 응답이 아니라 모든 사용자가 같은 데이터를 조회하므로 시나리오에서 선정한 API 갯수 정도의 캐시가 필요했다. 즉, 같은 API에 대해서 캐시 key가 여러 개로 파생되지 않기에 캐싱 되는 데이터가 많지 않다고 판단했다. 
상품 상세의 경우 상품 갯수만큼 캐시에 저장되어야 하므로 상품의 갯수가 많아지면 캐시 데이터를 별로 서버에 저장하는 글로벌 캐시를 고려해야겠지만 커머스가 아니라 한정된 수의 공연을 상품으로 취급하기에 로컬 캐시를 이용했다. 

2) response data size 줄이기 

API 응답 시 프론트에서 사용하지 않는 불필요한 속성들이 있어서 제거했다. 미미하지만 network IO를 줄이고,  캐싱되는 데이터의 크기도 줄일 수 있기 때문이다. 


2 쓰기용 시나리오 작성과 API 최적화 작업

2-1  시나리오 선정

사용자의 액션으로 데이터를 DB에 write하는  "티켓 발행하기" 선정. 

2-2  API 최적화 작업 사항

1) 쿼리 최적화 

예매 관련 데이터는 각 예매마다 조회할 데이터가 다르고 티켓 발행이라는 이벤트는 단 한 번 발생한다. 
그러므로 데이터 캐시가 의미없고 "티켓 발행"이라는 이벤트 시  조회되는 쿼리의 최적화를 해야 한다. 

티켓 발행 전에 예매 테이블을 조회하는데 불필요한 인덱스는 제거하고, 적용되어야 할 인덱스가 선택되도록 기존 인덱스를 rebuild 하였다. 

2) mock server 생성

선정한 시나리오에 외부 API를 요청하는 부분이 있었다. 해당 서버는 타 회사 서버이고 성능테스트 대상이 아니다. 
하지만 실제로 API 요청을 주고 받으면서 http connection 을 맺고 I/O를 발생 시키는 작업은 성능테스트 대상 서버가 하는 일이다. 

그리하여 외부 API 서버의 일을 대신하는 가짜 서버를 만들었다. mock server의 조건은 아래와 같다. 

  • 모든 요청에 기대한 결과를 반환  → 성공 응답만 반환
  • 병목이 되지 않아야 한다. → 성능테스트 대상 서버가 아닌 별도 서버 마련 

 


3 부하테스트 도구JMeter 사용법

JMeter를 처음 사용해보면서 사용법 자체에 대한 검색도 많이 하게 되었다. JMeter 공식문서가 있으나 예시가 자세하지 않아 시간이 걸린 부분이 있다.
POST 메서드 API를 테스트할 때, 각 요청마다 다른 requestBody를 설정하는 방법을 중점으로 JMeter 사용법을 작성했다. 

https://devstep.tistory.com/122

 


4 병목 포인트 발견 방법과 해결 과정 

시행착오

처음에는 시나리오대로 바로 부하테스트를 진행했다. 많은 사용자가 한꺼번에 서버에 접속하니 여러 문제들이 생겨 알고 있는 지식을 동원해 웹서버, was 서버, 로드밸런서의 설정값들을 조정하면서 진행했다.
하지만 병목 발생 위치를 찾기 위해 여러 로그를 한 번에 모두 확인 해봐야하고 설정 값 변경 후 변화를 확인하는 것도 여러 로그를 모두 확인해야 했다.
그리고 병목이 여기 였었고  이 속성을 변경해서 나아진 것이라고 “예상”을 해야만 했다.

 

단계별 부하테스트 진행

부하를 줄 포인트를 선정해 단계별로 부하테스트를 진행했다. 여기에서 핵심은 부하 포인트 이외에는 부하가 발생하지 않도록 해주는 것이다. 

[단계별 부하테스트 진행]

4-1 1단계 웹서버 

4-1-1  테스트 방법

  • 대상시스템의 “로컬”에서 “정적 파일”만 요청하는 테스트 진행
  • Thread수를 높혀가면서 서버 자원 사용량이 최대가 될 때까지 테스트 

4-1-2  테스트 목적 

  • nginx 설정 확인
  • Jmeter(부하테스트 도구) 자체의 부하 확인

테스트 방법을 로컬에서 하는 이유는 TPS 결과에 network latency 요소를 없애고자 함이었다. 
그리고 단순 html 정적파일을 응답하게 한 것은 WAS를 사용하지 않고 nginx만 사용해 부하를 주는 범위를 웹서버로 한정하기 위함이었다. 

4-1-3  테스트 과정 

  1. 자원 사용률을 높히려고 thread수를 늘림 (가상 유저수 1000 → 2000 → 3000) 
  2. 에러 발생 :  too many open files
  3. 해당 에러 해결 
  4. 결론을 내림 : 에러 해결된 상태에서 TPS가 더 이상 증가하지 않고 서버에 유휴 자원이 있을 때, 해당 Thread 수를 Jmeter의 최대 요청 수로 판단 

thread수를 늘리면서 테스트 진행시 어느 순간 nginx error log 파일에 too many open files 에러가 발생했다. 관련 에러를 해결하면서 nginx의 설정값을 조정해주었다. 그 후엔 thread 수(사용자 수)를 늘려도 에러는 발생하지 않았지만 서버의 CPU, memory 리소스가 남아도는 데도 TPS가 증가하지 않았다. 
부하테스트도 부하를 발생하는 데 한계가 있다. 왜냐하면 실제 사용자가 아닌 하나의 툴에서 여러 사용자가 요청을 주고 받는 것처럼 작동하기 때문에 제약이 있다고 한다. 

서버사양 2vCPU 4GB Memory로 테스트 했을 때 JMeter가 부하를 줄 수 있는 범위는 10초당 1000유저가 한계였다. 

4-1-4  테스트 결과  

nginx에만 부하를 주는 테스트를 하면서 부하테스트 도구의 한계를 발견했다.
부하테스트 도구의 한계를 해결하기 위해 JMeter를 서버 여러 대에 설치하고 부하를 주는 방법이 있는데, JMeter master slave 키워드로 방법을 찾을 수 있다. 

“JMeter master slave”

  • 서버에 병목이 발생할 정도로 가상 사용자를 충분히 증가하려면 Jmeter를 여러 대 구성 후 테스트
  • JMeter Master는 사용자가 부하테스트를 실행하는 용도이고, Slave는 실제 대상 서버에 부하를 주는 서버로 여러 대로 구성한다. JMeter 마스터에 Slave를 등록하고, Slave에는 기존 설정대로 진행하면 된다. 
  • 공식문서 : https://jmeter.apache.org/usermanual/jmeter_distributed_testing_step_by_step.pdf
  • 제약 사항 : JMeter slave는 같은 네트워크 대역 이용해야 한다.

 

4-2  2단계 WAS  

4-2-1  테스트 방법

  • WAS까지 확인하기 위해 tomcat을 기동
  • API 1개로만 테스트
  • DB 연결을 하지 않은 단순한 로직의 API 로 테스트 

4-2-2  테스트 목적 

  • tomcat 설정 값 최적화 (server.xml)
  • JVM 메모리 확인 (catalina.sh)

DB 연결을 하지 않은 API를 해당 단계의 시나리오로 선정한 이유는 tomcat범위만 확인하기 위해서이다.   

4-2-3  테스트 과정 

  1. 테스트 진행. thread수(유저수) 1000부터 시작으로 에러가 발생했을 때 적게 변경 
  2. tomcat이 기동되어 있는 상태였으나,  nginx error log 에 살아있는 upstream이 없다는 에러 메시지 발견.
    no live upstreams while connecting to upstream
  3. tomcat 로그 확인
    java.lang.OutOfMemoryError: GC overhead limit exceeded 에러 발생
  4. 에러 해결
    1) JVM 메모리 크기 증가 (catalina.sh)
    2) upstream 연결을 위한 port 수 증가 
  5. tomcat 설정파일인 server.xml 에서 maxThreads, minSpareThreads, acceptCount값 최적화하기

 

4-2-4  테스트 결과 

1) 리눅스 서버 설정 

tomcat 자체의 설정도 중요하지만, 웹서버 nginx에서 WAS(upstream)로 사용자 요청을 serve하는데 이상이 없도록 하는 것도 중요하다. 웹서버에서 WAS에 요청을 보낼 때 TCP연결을 하는데 리눅스 서버에서 upstream으로 TCP 연결을 할 수 있도록 사용할 수 있는 port를 증가시켜 두어야 한다. 
해당 내용은 아래 키워드로 해결할 수 있다. 

  • 로컬 포트 범위 증가 설정
  • port 재사용 설정
 

2) JVM 메모리 값 최적화

JVM 메모리를 기본 설정값으로 두고 사용하고 있었는데 heap 메모리 크기를 증가해주어 해결하였다. 서버의 메모리 사용량을 확인하고 유휴 메모리가 많이 남은 상황이라면 JVM 메모리 크기를 조절해보자. 일정 크기까지는 JVM 메모리를 늘려주었을 때 TPS가 향상됨을 느낄 수 있다. 
JVM메모리 설정은 catalina.sh에 아래 내용을 추가 해주었다. 
JAVA_OPTS="-Djava.awt.headless=true -Dfile.encoding=UTF-8 -server -Xms1024M -Xmx1024M -XX:NewSize=512m -XX:MaxNewSize=512m -XX:+DisableExplicitGC"

3) tomcat server.xml 주요 값 최적화

아래 값들을 최적화하는 부분은 길고 지루한 과정이 될 수 있다. 일단 "보통 어느 정도로 설정한다"라는 값으로 설정한 후에 증가/감소를 반복해 최적의 값을 찾아간다. tomcat의 server.xml  주요 값은 아래와 같다.

maxThreads 주어진 시간에 실행할 수있는 최대 스레드 수(기본값 200)
minSpareThreads 항상 실행되어야 하는 최소 스레드 수(기본값 10)
acceptCount 사용 가능한 작업자 스레드가 없을 때 
OS 수준의 대기열에서 대기 할 수 있는 최대 TCP 요청 수(기본값 100)
maxConnections 서버가 수락하고 처리 할 총 동시 연결 수(기본값 8192 ~ 10,000)

 


포스트모텀

성능테스트 자동화 

시나리오를 작성하고 JMeter같은 부하테스트 도구 설정을 마치고 나면 그 이후에는 설정 값들을 적용하면서 최적 값을 찾아가는 지루한 과정의 반복이 시작된다. 그리고 큰 문제들을 해결할 때 조차도 어떻게 했고 어떤 결과가 나왔는지 기록을 해두어야 병목을 찾고 해당 서버의 적정 TPS를 발견해 나갈 수 있다. 
이번 경우에는 문서에 조건, 서버사용량, TPS 를 캡쳐해두고 히스토리를 확인했었다. 다음에는 성능테스트 자동화를 해보는 것이 좋겠다. 


 

성능 테스트 진행 시 도움이 된 자료

댓글