Test

nGrinder를 활용한 API 서버 부하 테스트

KAispread 2023. 10. 29. 18:42
728x90
반응형

📌 개요

개발을 하다 보면 단순히 어떤 기능을 구현하는 것에만 집중할 때가 있다. 필자도 이전까지는 주어진 요구사항에 맞춰 기능을 구현하는 것에만 집중했었다. 하지만 서비스를 운영하다 보니 기능을 제공하는 것만큼이나 중요한 것이 제품 퀄리티라고 느껴졌다. 내가 개발한 기능이 요구사항대로 정확하게 동작하는지, 응답시간이 너무 길어서 사용자가 불편해하진 않는지 등등 실제 사용자 입장에서 생각해보며 지속적으로 개선해 나가는 과정이 필요하다고 생각하게 되었다. 이와 같은 부분들을 검증할 수 있는 도구가 '테스트'이고 이것이 테스트가 중요한 이유이다.


요구사항을 충족하는지에 대한 부분은 단위, 통합, 인수 테스트 등을 통해 검증할 수 있다. 테스트는 요구사항을 작성한 하나의 '문서'라고 불리는 만큼 다양한 상황에 대해 요구사항대로 코드가 동작하는지 검증하여 코드 신뢰성을 높일 수 있다.
요구사항을 충족하는 것 만큼이나 중요한 것이 성능이다. 서비스를 이용하는데 화면을 이동할 때마다 너무 오랜 시간이 걸린다면 서비스 만족도는 떨어질 수밖에 없다. 서비스 이용에 불편함이 없도록 성능을 개선하는 과정이 필요하고 이를 돕기 위한 여러 성능 테스트 도구들이 있다.
 

📌 성능 테스트 도구

다양한 성능 테스트 도구

API의 성능을 테스트하기 위한 다양한 테스트 도구들이 있다. 각 도구들에 대한 간단한 특징은 다음과 같다.

도구 스크립트 언어 특징
nGrinder Groovy, Jython  오픈소스
한국어 지원
완성도 높은 뼈대 스크립트 코드 지원
 가독성 좋은 UI
 수많은 레퍼런스 존재
스크립트 작성시 IDE의 지원을 받을 수 없음
JMeter Java  오픈소스
• 많은 릴리즈와 개선 사항을 통해 고도로 유지, 관리
다양한 프로토콜 제공
다양한 플러그인 제공 (특히 Intellij)
낮은 그래프 가독성
GUI 모드에서 많은 메모리 사용
복잡한 스크립트
Artillery JS, Json, Yaml 오픈소스
테스트 시나리오를 통한 단계적 테스트 가능
쉽고 빠르게 yaml 또는 json 파일로 테스트 파일 작성 가능
식별하기 용이한 Report Graph
대규모 분산 테스트 수행가능
k6 JS Grafana 대시보드와 연동하여 깔끔한 GUI 제공
깔끔한 공식 문서 제공
JS를 사용한 간단한 스크립트 작성
유료 클라우드 서비스 필요
부하 분산 테스트를 제공하지 않음

앞서 언급했던 도구들 말고도 Loadninja, loadview 등 성능 테스트를 위한 도구는 아주 많다. 
필자는 깔끔하고 가독성 좋은 GUI와 관련된 레퍼런스가 많다는 점이 마음에 들어 nGrinder를 선택하게 되었다.
 
 

📌 nGrinder ?

nGrinder는 네이버에서 제공하는 서버 부하 테스트 오픈 소스 프로젝트이다. nGrinder를 통해 다양한 가상 시나리오에서 트래픽이 발생했을 때 성능을 측정할 수 있도록 다양한 기능을 제공한다.
nGrinder는 ControllerAgent로 구성되어 있다.

✔️ Controller
  ├── 성능 측정을 위한 웹 인터페이스 제공
  ├── 테스트 프로세스 조정
  ├── 테스트 통계를 수집하고 표시
  └── 스크립트 수정 기능 제공

✔️ Agent
  ├── 에이전트 모드에서 실행할 때 대상 시스템에 부하를 주는 프로세스 및 스레드를 실행
  └── 모니터 모드에서 실행 시 대상 시스템 성능(CPU/메모리) 모니터링

Controller에서 제공하는 GUI를 통해 테스트를 실행하고 연동된 여러 개의 Agent가 대상 시스템에 부하를 발생시켜 서버가 어떻게 동작하는지 확인하는 구조이다. 
 

📌 nGrinder 실습

본 포스팅에서는 로컬에 성능 테스트를 위한 환경을 구축하고 간단한 테스트를 진행한다. 다음과 같이 5단계로 진행한다. 필자는 Mac OS 환경에서 진행하지만, Java 설치 및 버전 변경을 제외하고는 실습에서 크게 다른 점이 없다. 따라서, Window 사용자는 Terminal이나 Powershell 같은 도구를 사용하여 동일한 방식으로 진행하면 된다.

  1. Java 설치 & 버전 변경
  2. Controler 다운로드 및 실행
  3. 스크립트 작성
  4. Agent 다운로드 및 실행
  5. 부하 테스트 진행

 

1. Java 설치 & 버전 변경

nGrinder는 JDK 8이나 11에서 정상적으로 동작한다. Java 버전을 확인하고 8이나 11로 버전을 변경해 주자. JDK 8, 11 이 설치되어있지 않다면 설치해 주는 과정이 필요하다.

# Java 버전 확인
$ java -version

# 설치된 Java 버전 확인
$ /usr/libexec/java_home -VCopy

# 자바 변경 (8)
$ export JAVA_HOME=$(/usr/libexec/java_home -v 1.8)

# bash shell 사용할 경우 - 변경 사항 적용
$ source ~/.bash_profile

# zsh shell 사용할 경우 - 변경 사항 적용
$ source ~/.zshrc

 
 
 

2. Controller 다운로드 및 실행

23.10.29 기준 그리고 최신 버전 (3.5.8)에서는 스크립트를 작성하고 실행하는 데 있어 여러 문제가 발생한다. 이보다 낮은 버전인 3.5.6 버전을 다운로드하여 실행한다.

# nGrinder 다운로드
wget https://github.com/naver/ngrinder/releases/download/ngrinder-3.5.6-20221007/ngrinder-controller-3.5.6.war

# 실행
java -Djava.io.tmpdir=/Users/kai/ngrinder -jar ngrinder-controller-3.5.6.war --port=8300

jar 파일을 다운로드하고 8300 포트로 애플리케이션을 실행한다.
 
주소창에 localhost:8300 을 입력하여 nGrinder 페이지에 접속한다.

default 아이디 비밀번호인 admin / admin 을 입력하여 로그인하자.
 

User Management 메뉴를 통해 계정을 관리할 수 있다. 이곳에서 아이디 비밀번호를 변경해 줄 수 있다.
 
 

3. 스크립트 작성

상단 Script 메뉴에서 Create a script를 클릭하여 테스트를 작성할 Script를 생성할 수 있다.
 

nGrinder는 완성도 높은 뼈대 코드를 지원한다. 다음과 같이 요청을 전송할 url과 header, Body 등을 적어주면 요구사항에 따라 기본 스크립트를 작성해 준다.
각 필드에 대한 설명은 다음과 같다.

  1. Script Name 필드다. 스크립트 식별을 위해 테스트 목적에 맞게 이름을 작성한다
  2. 테스트 대상이 되는 Http Method와 URL을 입력한다
  3. 요청에 필요한 데이터를 추가한다.

 
필자는 뼈대 코드를 수정하여 다음과 같이 스크립트를 작성했다.

/**
* A simple example using the HTTP plugin that shows the retrieval of a single page via HTTP.
*
* This script is automatically generated by ngrinder.
*
* @author admin
*/
@RunWith(GrinderRunner)
class TestRunner {

	public static GTest test
	public static HTTPRequest request
	public static Map<String, String> headers = [:]
	public static String body;
	
	public static List<Cookie> cookies = []
	public static int number = 1;

	@BeforeProcess
	public static void beforeProcess() {
		HTTPRequestControl.setConnectionTimeout(300000)
		test = new GTest(1, "127.0.0.1")
		request = new HTTPRequest()

		// Set header data
		headers.put("Content-Type", "application/json")
		grinder.logger.info("before process.")
	}

	// set data
	@BeforeThread
	public void beforeThread() {
		test.record(this, "test")
		grinder.statistics.delayReports = true
		
		number += 1;
		String nickname = "Test User " + number;
		int increasePhone = 10000000 + number;
		String phoneNumber = "+8210" + increasePhone;
        
        // 요청 body 데이터
		body = '{"nickname":"'+nickname+'","gender":"MAN","phoneNumber":"'+phoneNumber+'","collegeInfo":{"collegeCode":"CE-001","collegeType":"ENGINEERING","admissionYear":"19"},"mbti":"ESTJ","allowMarketing":false}'
	}

	@Before
	public void before() {
		request.setHeaders(headers)
		grinder.logger.info("before. init headers and cookies")
	}

	@Test
	public void test() {
		HTTPResponse response = request.POST("http://127.0.0.1:8080/v1/member", body.getBytes())

		if (response.statusCode == 301 || response.statusCode == 302) {
			grinder.logger.warn("Warning. The response may not be correct. The response code was {}.", response.statusCode)
		} else {
			assertThat(response.statusCode, is(200))
		}
	}
}

 

각 테스트 어노테이션은 다음과 같은 단계에서 실행된다. 각 단계에서 요청에 필요한 작업들을 수행해 주면 된다.
 

우측 상단에 Validate 를 클릭하여 의도했던 대로 스크립트가 잘 동작하는지 검증할 수 있다.
 
 

4. Agent 다운로드 및 실행

nGrinder 우측 상단 토글 메뉴에서 Agent를 다운로드한다. 

tar -xvf ngrinder-agent-3.5.8-localhost.tar

다운로드한 tar 파일 압축을 풀어주자.
 
__agent.conf 파일에서 controller과 연동하기 위한 설정을 입력할 수 있다. 로컬에서 진행하기 때문에 따로 건들건 없다.

 

./run_agent.sh

Agent를 실행시켜 준다.
 

Agent Management에서 다음과 같이 Agent가 등록되었다면 성공이다.
 
 

5. 부하 테스트 진행

nGrinder 왼쪽 상단 Performance Test -> Create Test 를 클릭하여 테스트를 생성한다.
 

  • Agent: Agent의 개수를 지정한다. 여러 대의 Agent를 구성하였다면 구성한 Agent 수만큼 지정할 수 있다.
  • VUser: 테스트를 수행할 가상 유저의 수를 나타낸다. 프로세스와 쓰레드 개수를 지정할 수 있다.
  • Script: 테스트를 수행할 스크립트를 지정한다. 
  • Duration: 테스트 수행 시간을 지정한다. Run Count 옵션 사용 시, 정해진 횟수만큼 요청을 전송한다.
  • Ramp-Up: 사용자의 수를 점점 늘려가며 테스트하기 위해 선택한다. 스트레스 테스트에 사용된다.

필자는 100명의 사용자가 5분 동안 스크립트에 지정된 요청을 전송하는 상황을 가정하고 테스트를 진행했다.
[Think Time은 따로 지정하지 않았다.]
 
그라파나를 사용하여 모니터링 대시보드를 구축하고 테스트를 진행했다. Linux 환경이라면 htop 같은 도구를 사용하면 된다.

 


Clone and Start 를 클릭하여 테스트를 실행하고 이에 대한 테스트 결과를 다음과 같이 대시보드 형태로 확인할 수 있다.

성능은 Throughput과 Latency를 보면 알 수 있다. Throughput은 시간당 처리량을 의미하며 TPS, RPS 등이 이에 해당한다. 1초에 처리하는 단위 작업의 수를 나타내며 값이 클수록 더 많은 작업을 처리할 수 있다는 뜻이므로 성능이 좋다는 것을 의미한다.

Latency는 서버가 클라이언트의 요청을 받고 응답을 보내주기까지 걸리는 시간을 의미한다. 즉 시간이 낮을 수록 성능이 좋다는 것을 의미하며 Mean Test Time으로 Latency를 측정할 수 있다.

앞서 진행했던 테스트에 대한 수행 결과로 TPS는 1650, MTT는 0.05초가 나온 모습이다. 시간별 응답 시간도 그래프형태로 제공하는데, 만약 그래프가 갑자기 뚝 떨어지는 구간이 식별된다면 병목 구간이 있음을 파악할 수 있다. 병목에 대한 원인은 수행 시간별 시스템 메트릭을 확인하거나 쓰레드 덤프를 분석하여 알아낼 수 있다. 


결과에 대한 더 자세한 내용은 그래프 상단에 위치한 Details Report 를 클릭하여 확인할 수 있다.
 
 

정리

nGrinder를 통해 간단한 API 성능 테스트를 진행했다. 이에 대한 결과로 TPS, MTT, 그래프 등 성능을 파악할 수 있는 여러 지표들을 확인할 수 있었다. 본 포스팅에서는 로컬에서 성능 테스트를 진행했지만 더 정확한 테스트를 위해서는 실제 서버가 구동되는 환경과 최대한 유사하게 환경을 구성하여 테스트를 진행해야 한다. 그래야 실제 서비스에서 어느 정도의 성능이 나오는지 확인할 수 있기 때문이다.


성능 테스트를 공부해 보며 문득, TPS가 무조건 높을수록 좋은 것인지에 대해 생각해 보게 되었다. 고민 끝에 내린 결론은 무조건 높다고 좋은 것은 아니라는 것이다. TPS가 지나치게 높다면 오히려 필요 이상의 오버스펙으로 비용 낭비를 뜻하는 것일 수 있기 때문에 적절한 목표를 설정하고 이에 맞춰나가는 과정이 필요하다고 생각한다.


또한, 성능 테스트를 진행해 보면서 VUser나 목표 TPS를 어떻게 설정할지에 대해 고민이 많았는데 이 부분은 Little's law라는 계산 공식을 통해 도출해 낼 수 있다고 한다. 인터넷에 이 부분에 대한 자료가 많기 때문에 성능 테스트에 대해 관심 있는 분들은 찾아보면 좋을 것 같다.
 

728x90
반응형