nGrinder를 이용한 성능테스트

sieun·2022년 2월 21일
2
post-thumbnail

개요

e-commerce 대용량 서버 프로젝트가 실제로 트래픽을 받았을 때 성능이 어느정도까지 도달할 수 있는지 측정해보고자 합니다. 때문에 실제와 같은 환경을 만들어 놓고 서버가 사용자를 얼마 만큼 수용할 수 있는지 nGrinder을 사용하여 테스트를 진행하기로 하였습니다.

nGrinder

: Naver에서 개발한 부하 테스트 툴

  • controller : 웹 기반 GUI 시스템으로, 작업을 전반적으로 관리하여 부하스크립트 작성 기능을 지원합니다.
  • agent : controller의 제어에 따라서 실제로 부하를 발생시킵니다.

Controller 설치 및 실행

# 서버 접속 명령어
ssh root@[서버접속용IP] -p [포트번호]

# 관리자 비밀번호 입력
root@[IP]'s password: 

# 서버 접속 
# 자바 설치
[root@ngrinder-server ~]# yum list java*jdk-devel

[root@ngrinder-server ~]# yum install java-11-openjdk-devel.x86_64

# 설치 확인
[root@ngrinder-server ~]# which javac
/usr/bin/javac
[root@ngrinder-server ~]# java -version
openjdk version "11.0.14" 2022-01-18 LTS
OpenJDK Runtime Environment 18.9 (build 11.0.14+9-LTS)
OpenJDK 64-Bit Server VM 18.9 (build 11.0.14+9-LTS, mixed mode, sharing)
[root@ngrinder-server ~]# javac -version
javac 11.0.14

[root@ngrinder-server ~]# rpm -qa java*jdk-devel
java-11-openjdk-devel-11.0.14.0.9-1.el7_9.x86_64

[root@ngrinder-server ~]# readlink /etc/alternatives/javac
/usr/lib/jvm/java-11-openjdk-11.0.14.0.9-1.el7_9.x86_64/bin/javac

[root@ngrinder-server ~]# ls -l /usr/bin/javac
lrwxrwxrwx 1 root root 23  130 14:06 /usr/bin/javac -> /etc/alternatives/javac

# 환경설정
[root@ngrinder-server ~]# vi /etc/profile

# 아래 코드 추가
export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-11.0.14.0.9-1.el7_9.x86_64
export PATH=$JAVA_HOME/bin:$PATH
export JAVA_OPTS=Dfile.encoding=UTF-8
export CLASSPATH="."


[root@ngrinder-server ~]# source /etc/profile

# 서버 재시작
[root@ngrinder-server ~]# reboot
Connection to 106.10.50.203 closed by remote host.
Connection to 106.10.50.203 closed.

# nGrinder 다운로드
[root@ngrinder-server ~]# wget https://github.com/naver/ngrinder/releases/download/ngrinder-3.5.3-20201127/ngrinder-controller-3.5.3.war

# nGrinder 실행
java -jar ngrinder-controller-3.5.3.war

Agent 설치 및 실행

[root@ngrinder-server ~]# cd /usr/local

# ngrinder 웹페이지 우측 상단에 admin 토글을 클릭하여 Download Agent를 눌러 agent tar 파일을 다운
# agent 다운로드
[root@ngrinder-server local]# wget --content-disposition http://[IP]:8080/agent/download

[root@ngrinder-server local]# ls
bin  games    lib    libexec                                  sbin   src
etc  include  lib64  ngrinder-agent-3.5.3-[IP].tar  share

[root@ngrinder-server local]# tar xvf ngrinder-agent-3.5.3-[IP].tar

[root@ngrinder-server local]# ls
bin    include  libexec                                  sbin
etc    lib      ngrinder-agent                           share
games  lib64    ngrinder-agent-3.5.3-1.[IP].tar  src

[root@ngrinder-server local]# cd ngrinder-agent
[root@ngrinder-server ngrinder-agent]# ls
__agent.conf   run_agent.sh            run_agent_internal.sh
lib            run_agent_bg.sh         stop_agent.bat
run_agent.bat  run_agent_internal.bat  stop_agent.sh

[root@ngrinder-server ngrinder-agent]# cp __agent.conf agent.conf 
[root@ngrinder-server ngrinder-agent]# ls
__agent.conf  run_agent.bat    run_agent_internal.bat  stop_agent.sh
agent.conf    run_agent.sh     run_agent_internal.sh
lib           run_agent_bg.sh  stop_agent.bat

# agent 실행
[root@ngrinder-server ngrinder-agent]# sh run_agent.sh

테스트 스크립트 작성

테스트를 진행해볼 기능

  • 상품 목록 가져오기
  • 쿠폰 발급받기

1) 상품 목록 보기 API

@RunWith(GrinderRunner)
class TestRunner {

	public static GTest test1
	public static GTest test2
	public static GTest test3
	
	public static HTTPRequest request
	public static NVPair[] headers = []
	public static NVPair[] params = []
	public static Cookie[] cookies = []
	
	public static MAX_RECORDS = 99800

	// 프로세스가 생성될 때 동작해야하는 작업 정의
	@BeforeProcess
	public static void beforeProcess() {
		HTTPPluginControl.getConnectionDefaults().timeout = 6000
		// 각 테스트 통계를 수집할 때 사용되는 GTest인스턴스 정의
		test1 = new GTest(1, "Test1")
		test2 = new GTest(2, "Test2")
		test3 = new GTest(3, "Test3")
		
		request = new HTTPRequest()
		grinder.logger.info("before process.");
	}

	// 각 쓰레드가 실행되기 전에 동작해야하는 작업 정의
	@BeforeThread 
	public void beforeThread() {
		//request 인스턴스에 대해 메소드를 호출하게 되면 테스트 별로 TPS증가시켜 기록
		test1.record(this, "test1")
		test2.record(this, "test2")
		test3.record(this, "test3")
		
		grinder.statistics.delayReports=true;
		grinder.logger.info("before thread.");
	}
	
	@Before
	public void before() {
		request.setHeaders(headers)
		cookies.each { CookieModule.addCookie(it, HTTPPluginControl.getThreadHTTPClientContext()) }
		grinder.logger.info("before. init headers and cookies");
	}

	// 테스트 동작 정의
	@Test
	public void test1(){
		String origin = "http://[public IP]:8080/products"
		String deliveryType = "ROCKET"
		int randomNum = Math.abs(new Random().nextInt() % MAX_RECORDS) + 1
		String params = "?deliveryType="+ deliveryType +"&start="+ Integer.toString(randomNum) +"&listSize="+"100"
		HTTPResponse result = request.GET(origin + params)

		if (result.statusCode == 301 || result.statusCode == 302) {
			grinder.logger.warn("Warning. The response may not be correct. The response code was {}.", result.statusCode); 
		} else {
			assertThat(result.statusCode, is(200));
		}
	}
	
	@Test
	public void test2(){
		String origin = "http://[public IP]:8080/products"
		String deliveryType = "ROCKET_FRESH"
		int randomNum = Math.abs(new Random().nextInt() % MAX_RECORDS) + 1
		String params = "?deliveryType="+ deliveryType +"&start="+ Integer.toString(randomNum) +"&listSize="+"100"
		HTTPResponse result = request.GET(origin + params)

		if (result.statusCode == 301 || result.statusCode == 302) {
			grinder.logger.warn("Warning. The response may not be correct. The response code was {}.", result.statusCode); 
		} else {
			assertThat(result.statusCode, is(200));
		}
	}
	
	@Test
	public void test3(){
		String origin = "http://[public IP]:8080/products"
		String deliveryType = "ROCKET_GLOBAL"
		int randomNum = Math.abs(new Random().nextInt() % MAX_RECORDS) + 1
		String params = "?deliveryType="+ deliveryType +"&start="+ Integer.toString(randomNum) +"&listSize="+"100"
		HTTPResponse result = request.GET(origin + params)

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

2) 쿠폰 발급받기 API

@RunWith(GrinderRunner)
class TestRunner {
	public static GTest test
	public static HTTPRequest request
	public Object cookies = []
	
	// map에서 NVPair로 convert하는 함수 
	def nvs(def map) {
		def nvs = []
		map.each {
			key, value ->  nvs.add(new NVPair(key, value))
		}
		return nvs as NVPair[]
	}

	// 프로세스가 생성될 때 동작해야하는 작업 정의
	@BeforeProcess
	public static void beforeProcess() {
		HTTPPluginControl.getConnectionDefaults().timeout = 6000
		// 각 테스트 통계를 수집할 때 사용되는 GTest인스턴스 정의
		test = new GTest(1, "Test1") 
		request = new HTTPRequest()
		//request 인스턴스에 대해 메소드를 호출하게 되면 테스트 별로 TPS증가시켜 기록
		test.record(request); 
		grinder.logger.info("before process.");
	}

	// 각 쓰레드가 실행되기 전에 동작해야하는 작업 정의
	@BeforeThread 
	public void beforeThread() {
		// reset to the all cookies
        def threadContext = HTTPPluginControl.getThreadHTTPClientContext()
        cookies = CookieModule.listAllCookies(threadContext)
        cookies.each {
            CookieModule.removeCookie(it, threadContext)
        }
        
		// 테스트 전에 사전 작업으로 로그인 처리
		int randomNum = Math.abs(new Random().nextInt() % 50000) + 1 //
		String email = Integer.toString(randomNum) + "@naver.com" //
		HTTPResponse result = request.POST("http://[public IP]:8080/users/login", nvs(["email":email, "password":"1234"])) //
		
		// HTTPResponse result = request.POST("http://101.101.209.54:8080/users/login", nvs(["email":"1@naver.com", "password":"1234"]))
		cookies = CookieModule.listAllCookies(threadContext)
		grinder.statistics.delayReports=true;
		grinder.logger.info("before thread.");
	}
	
	@Before
    public void before() {
        // set cookies for login state
        def threadContext = HTTPPluginControl.getThreadHTTPClientContext()
        cookies.each {
            CookieModule.addCookie(it ,threadContext)
            net.grinder.script.Grinder.grinder.logger.info("{}", it)
        }
    }
	
	// 테스트 동작 정의
	@Test
	public void couponTest() {
		int randomNum = Math.abs(new Random().nextInt() % 20000) + 1
		String couponId = Integer.toString(randomNum)
		request.POST("http://[public IP]:8080/available-coupons/" + couponId)
	}
	
}

부하테스트 실행

1) 상품 목록 보기 - VUser 500

2) 쿠폰 발급받기 - VUser 500


테스트 결과 분석

1) 상품 목록 보기 - VUser 500

VUser을 500으로 설정한 후 10분동안 테스트를 진행한 결과는 다음과 같습니다.

지표

평균 TPS : 662

피크 TPS : 921

CPU Usage : 100%

Errors : 2,674


2) 쿠폰 발급받기 - VUser 500

VUser을 500으로 설정한 후 10분동안 테스트를 진행해보니, 아래처럼 결과가 확인되었습니다.

지표

평균 TPS : 426

피크 TPS : 538

CPU Usage : 100%

Errors : 0


후기

성능측정을 처음 해보았는데, nGrinder의 사용방법이 공식문서나 기술 블로그에 자료가 많았기 때문에 비교적 편하게 할 수 있었다. 다만, script에 오류가 있는 경우 무엇이 문제인지 찾기가 조금 어려웠지만 nGrinder에서 로그파일을 제공해주어서 참고하여 해결하는데 도움을 받았습니다. nGrinder말고도 JMeter과 같은 다른 성능테스트 툴이 있는데, 다음번엔 다른 툴을 사용하고 비교해볼까 합니다.


📕 Reference

공식문서 - Installation-Guide
공식문서 - User-Guide
Groovy Script 작성법

profile
열심히 공부중입니다😇

0개의 댓글