1. 문제 정의

업브렐라 서비스를 출시하기 전, 업브렐라 서비스의 안정성을 확인하기 위해 QA팀을 통해 부하 테스트를 했습니다. 하지만, 전문적인 QA팀이 없는 업브렐라 팀에서 새로운 버전이 나올때 마다 부하테스트를 직접 하기에는 어려움이 있기에, 업브렐라 개발팀은 부하테스트를 도입하기로 결정했습니다.

2. nGrinder 도입

2 - 1. 부하 테스트란?

부하 테스트는 시스템의 성능을 검증하기 위한 테스트 방법 중 하나로, 시스템이 예상되는 사용자 부하 혹은 트래픽 하에서 제대로 작동하는지, 그리고 기대하는 성능 지표를 충족하는지 확인하는 것을 주 목적으로 합니다.

부하 테스트의 주요 특징과 목적은 다음과 같습니다:

  1. 성능 지표 확인: 부하 테스트는 시스템의 응답 시간, 처리량, 거래 당 시간, 동시 사용자 수 등과 같은 성능 지표를 확인합니다.
  2. 시스템 한계 탐색: 부하 테스트는 시스템이 제대로 작동하는 최대 사용자 수나 트래픽을 파악하는 데 도움을 줍니다.
  3. 자원 사용률 모니터링: 서버의 CPU 사용률, 메모리 사용량, 네트워크 대역폭 사용량, 데이터베이스 쿼리 성능 등의 자원 사용률을 모니터링합니다.
  4. 결함 및 약점 탐색: 예상 사용자 부하에서 시스템의 약점이나 결함을 찾아냅니다. 예를 들어, 메모리 누수, 데이터베이스 병목 현상 등의 문제를 파악할 수 있습니다.

부하 테스트를 수행할 때의 일반적인 절차는 다음과 같습니다:

  1. 목표 설정: 어떤 성능 지표를 중점적으로 검증할 것인지, 최대 몇 명의 동시 사용자를 지원해야 하는지 등의 목표를 설정합니다.
  2. 테스트 환경 설정: 실제 운영 환경과 유사한 테스트 환경을 구성합니다.
  3. 테스트 시나리오 생성: 사용자의 실제 행동 패턴을 모방하는 테스트 시나리오를 작성합니다.
  4. 테스트 실행: 부하를 점진적으로 증가시키며 테스트를 실행합니다.
  5. 결과 분석: 테스트 결과를 분석하여 시스템의 성능 지표를 확인하고, 문제점이나 약점을 파악합니다.

이러한 부하 테스트를 지원하는 도구로는 JMeter, nGrinder 외에도 다양한 도구들이 있지만, 업브렐라 개발팀은 관련 문서가 풍부한 nGrinder를 선택하였습니다.

2 - 2. 시나리오 설정

시나리오 A : 갑작스러운 소나기에 기존 유저 50여명이 동시에 우산 대여

시나리오 B : 갑작스러운 소나기에 신규 유저 50여명이 동시에 회원가입 후 우산 대여

시나리오 C : 우산을 대여했는데, 곧바로 비가 그쳐서 동시에 30여명이 우산 반납

시나리오 D : 우산을 대여하기 위해 100여명의 유저가 우산의 위치를 조회하기 위해, 지도 조회

3. nGrinder 설정

3 - 1. nGrinder 설치

https://github.com/naver/ngrinder/releases

  1. nGrinder 공식 GitHub에 접속하여, ngrinder-controller-3.5.8.war 을 다운로드 받습니다.

  2. 다운로드 받은 ngrinder-controller-war 파일을 실행합니다.

java -jar ngrinder-controller-3.5.8.war --port 7070

포트번호를 매핑 후 접근해주기 위해 임의로 7070으로 매핑해줍니다.

image

아래의 아이디와 비밀번호로 접속해줍니다.

User ID : admin 
Password : admin

이제 에이전트를 설치해줍니다. image

메인 페이지에서 Download Agent를 클릭합니다.

// 압축을 풀어줍니다. 
tar -xvf ngrinder-agent-3.5.8-localhost.tar

// nGrinder 폴더로 이동합니다. 
cd ngrinder-agent

폴더 내에 있는 agent를 실행해줍니다.

./run_agent.sh

에이전트까지 실행해주면 Agent Management에서 확인할 수 있습니다.

image

image

이와 같이 local에서 실행중인 agent를 확인할 수 있습니다.

실제 서비스 환경에서는 Read 작업이 가장 많이 일어날 것이기 때문에, 가장 복잡한 쿼리를 가진 모든 협업지점 조회로 임시 스크립트를 작성해보겠습니다.

import static net.grinder.script.Grinder.grinder
import static org.junit.Assert.*
import static org.hamcrest.Matchers.*
import net.grinder.script.GTest
import net.grinder.script.Grinder
import net.grinder.scriptengine.groovy.junit.GrinderRunner
import net.grinder.scriptengine.groovy.junit.annotation.BeforeProcess
import net.grinder.scriptengine.groovy.junit.annotation.BeforeThread
// import static net.grinder.util.GrinderUtils.* // You can use this if you're using nGrinder after 3.2.3
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Test
import org.junit.runner.RunWith

import org.ngrinder.http.HTTPRequest
import org.ngrinder.http.HTTPRequestControl
import org.ngrinder.http.HTTPResponse
import org.ngrinder.http.cookie.Cookie
import org.ngrinder.http.cookie.CookieManager

/**
* 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 Map<String, Object> params = [:]
	public static List<Cookie> cookies = []

	@BeforeProcess
	public static void beforeProcess() {
		HTTPRequestControl.setConnectionTimeout(300000)
		test = new GTest(1, "서버의 IP")
		request = new HTTPRequest()
		grinder.logger.info("before process.")
	}

	@BeforeThread
	public void beforeThread() {
		test.record(this, "test")
		grinder.statistics.delayReports = true
		grinder.logger.info("before thread.")
	}

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

	@Test
	public void test() {
		HTTPResponse response = request.GET("http://서버의 IP", params)

		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))
		}
	}
}

이제 준비가 완료되었으니, 실제 시나리오를 구성해보겠습니다.

3 - 2. 시나리오 설정

비가 오는 날 동시에 접속하는 유저가 많은 업브렐라의 특성 상, 한 번에 100명의 유저가 접근하는 상황을 가정해보겠습니다.

  • 사용자 로그인
  • 주변 협업지점 조회
  • 우산 대여 신청
  • 우산 반납 신청

이에 맞는 스크립트를 작성해보겠습니다.

import static net.grinder.script.Grinder.grinder
import static org.junit.Assert.*
import static org.hamcrest.Matchers.*
import net.grinder.script.GTest
import net.grinder.script.Grinder
import net.grinder.scriptengine.groovy.junit.GrinderRunner
import net.grinder.scriptengine.groovy.junit.annotation.BeforeProcess
import net.grinder.scriptengine.groovy.junit.annotation.BeforeThread
// import static net.grinder.util.GrinderUtils.* // You can use this if you're using nGrinder after 3.2.3
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Test
import org.junit.runner.RunWith

import org.ngrinder.http.HTTPRequest
import org.ngrinder.http.HTTPRequestControl
import org.ngrinder.http.HTTPResponse
import org.ngrinder.http.cookie.Cookie
import org.ngrinder.http.cookie.CookieManager

/**
* 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 Map<String, Object> params = [:]
	public static List<Cookie> cookies = []

	@BeforeProcess
	public static void beforeProcess() {
		HTTPRequestControl.setConnectionTimeout(300000)
		test = new GTest(1, "www.localhost")
		request = new HTTPRequest()
		grinder.logger.info("before process.")
	}

	@BeforeThread
	public void beforeThread() {
		test.record(this, "test")
		grinder.statistics.delayReports = true
		grinder.logger.info("before thread.")
	}

	@Before
	public void before() {
		HTTPResponse response = request.GET("http://localhost:8080/users/test", params)

		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))
		}
		
		request.setHeaders(headers)
		CookieManager.addCookies(cookies)
		grinder.logger.info("before. init headers and cookies")
	}

	
	@Test
	public void test1FindStoresByCurrentLocation() {
		Map<String, Object> coordinateRequest = ["latitudeFrom":"0.00","latitudeTo":"77.77","longitudeFrom":"36.21","longitudeTo":"127.12"]
		HTTPResponse response = request.GET("http://localhost:8080/stores/location", coordinateRequest)

		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))
		}
	}
	

	@Test
	public void test2RentUmbrella() {
		String url = "http://localhost:8080/rent"

		Map<String, Object> postData = [
        "region": "신촌",
        "storeId": 1,
        "umbrellaId": 1,
        "conditionReport": "필요하다면 상태 신고를 해주세요."
		]

		HTTPResponse response = request.POST(url, postData)

		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))
		}
	}
	
	@Test
	public void test3ReturnUmbrella() {
		String url = "http://localhost:8080/rent"
		Map<String, Object> postData = [
			"returnStoreId": 1L, 
			"bank": "SampleBank",
			"accountNumber": "1234567890",
			"improvementReportContent": "This is a sample report content"
		];

		HTTPResponse response = request.PATCH(url, postData);
		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))
		}
	}
}

3 - 3. 테스트 실행

image

이제 Performance Test를 클릭한 후 Script 에서 방금 작성한 스크립트를 선택해줍니다.

Agent는 하나만 실행할 것이기 때문에 1

Vuser는 예상되는 유저의 수 만큼 설정해주면 됩니다.

다음 포스팅에서는 nGrinder를 배포시 자동으로 실행하고 결과를 보고받는 방법에 대해 알아보도록 하겠습니다.

출처

https://github.com/naver/ngrinder/wiki/Installation-Guide