개요
서비스의 성능을 측정하기 위해서는 대게 Throughput과 Latency 성능지표가 필요하다. 보통 Throughput은 얼마나 많은 요청을 처리할 수 있는지에 대한 처리량을 의미하고, Latency는 요청을 처리하는 속도를 의미한다.
처음 nGrinder를 썼을 때 왜 안되지? 이것들은 뭐지? (가령, TPS는 뭔지, 스크립트는 어떻게 만드는지, Agent는 뭔지) 하며 많이 당황했었고 이 글을 보는 개발자 분들이 단순 구현보다는 성능과 모니터링에 관심을 가졌으면 해서 nGrinder를 활용한 성능 테스트 측정 방법을 정리해보고자 한다.
nGrinder 알아보기
nGrinder는 네이버에서 The Grinder라는 성능 테스트 도구를 기반으로 제작한 오픈소스 성능 테스트 솔루션이다. (오픈소스 솔루션이므로 무료이다!) 스크립트 생성과 테스트 실행, 모니터링 및 결과 보고서 생성을 통합된 Web UI를 통해 사용할 수 있으므로 성능 테스트를 보다 쉽게 할 수 있다.
또한, 스크립트를 통해 테스트를 작성하여 다양한 시나리오 테스트를 할 수도 있다.
가장 큰 장점은 한국어가 지원된다.
nGrinder는 많은 성능 테스트 도구 중 하나일 뿐이지 무조건 이것을 쓰라는 법은 없다.
중요한 것은 정확한 측정치를 내느냐! 라고 생각한다.
가령, JMeter라던가 Gatling 등 다양한 선택지들 중에서 선택해서 사용하도록 하자.
이제 nGrinder의 대략적인 구조를 알아보도록 하자.
nGrinder 구조 살펴보기
Controller의 역할은 테스트 스크립트를 작성하고, Agent에 부하 발생 명령을 해서 테스트를 시작할 수 있도록 하는 웹 애플리케이션 서버이다. 추가적으로 성능 테스트를 위해서 UI를 제공하고 테스트 실행 절차를 설정할 수 있도록 하며, 실행 중인 테스트를 모니터링하거나, 테스트 결과를 수집해서 시각화해 주는 역할을 한다. 간단하게는 성능 테스트를 위한 UI 웹이라고 생각해도 좋다.
Agent는 실질적으로 부하를 발생시키는 서버이다. 프로세스와 스레드 수를 조정하여 vUser(가상 사용자)를 생성하고, vUser는 Controller에서 실행한 테스트 스크립트에 따라 동작하여 Target Server에 부하를 발생시킨다. 즉, 가상의 사용자가 동시에 서버에 요청을 보내도록 하여 부하를 발생하고 성능을 측정한다.
Target 서버는 우리가 테스트하고자 하는 대상 서버를 의미하며, 테스트 중 Target 서버에서 발생한 오류들 혹은 실시간 CPU, Memory 상태 등 조금 더 자세한 정보를 확인하고 싶다면 Target 서버에 nGrinder Monitor를 설치하면 확인할 수도 있다.
controller와 Agent의 서버는 모두 부하를 발생하고 모니터링 하는 데 있어 각각 CPU와 메모리 등의 서버 자원을 소모한다. 만약 이들이 하나의 서버에 존재한다면 서버는 자원을 나눠서 사용해야 하고, 그만큼 Context Switching 이 발생하는 등 테스트에 있어서 불필요한 노이즈가 발생하게 되기 때문에 순수한 Target 서버의 성능을 도출하기 어려워지기 때문에 다른 서버에 두는 것을 권장한다.
보통 Controller 서버를 하나 두고, Agent 서버는 부하를 많이 생성하기 위해 (vUser를 늘리기 위해) 추가적으로 여러 대로 설치할 수 있다.
nGrinder 설치 및 수행
이번 포스팅에서는 서버 비용 등을 감안하여 우선 local 환경인 m2 mac에서 Controller와 Agent 서버 모두 두고 진행하도록 하겠습니다 😓
우선, 나는 위에 Docker를 통해서 nGrinder를 설치하는 것을 매우매우 권장한다.
이유는 이 포스팅에 제목에서도 알 수 있는데, nGrinder 를 직접 다운하고 설치하며 agent 서버 등을 가동하기 위해서는 controller가 실행될 서버의 환경 설정(jdk, tmpDir 등등)에 주의하여야 하기 때문이다.
이어서 Docker를 통한 설치와 war 파일을 다운 받아 수동 설치하는 절차를 동시에 보도록 하겠다.
war 파일 수동 다운 및 controller 실행
위에서 원하는 버전의 war 파일을 다운 받는다.
war 파일 실행 전 위 releases 들을 자세히 보면, 현재 nGrinder는 jdk 버전 11까지만 지원한다고 쓰여있다. (2023.09.01 기준, 정확히는 그 보다 높은 버전을 지원하는 release가 나오지 않았다.)
즉, Controller나 Agent가 실행될 서버의 jdk 버전이 11 보다 높다면 스크립트 혹은 테스트 실행 시에
General error during conversion: Unsupported class file major version 61 에러
를 만날 수 있으니 조심하자.
웹 서버를 실행하기 전에 본인의 jdk 버전을 미리 확인하고 실행하자. (java -version)
java -Djava.io.tmpdir=/Users/${username}/${생성할 임시 폴더명} -jar ngrinder-controller-3.5.8.war --port=8080
위에서 tmpdir을 지정하지 않으면 tmpdir을 셋팅하라는 에러 메세지가 출력될 수 있다.
port는 지정하지 않으면 웹 서버 기본으로 8080 포트에서 실행된다.
Docker Controller 설치 및 실행
controller image pull
docker pull ngrinder/controller
controller 실행하기
기본적으로 80 port로 외부에 접속할 수 있다
docker run -d -v ~/ngrinder-controller:/opt/ngrinder-controller --name controller -p 80:80 -p 16001:16001 -p 12000-12009:12000-12009 ngrinder/controller
만약 다른 port 로 접속하고 싶다면 아래와 같이 80을 원하는 port 로 바꾼다 ex) 8081
docker run -d -v ~/ngrinder-controller:/opt/ngrinder-controller --name controller -p 8081:80 -p 16001:16001 -p 12000-12009:12000-12009 ngrinder/controller
무엇이 편한가? 나는 jdk 등은 일절 신경 쓸 필요 없는 Docker가 훨씬 편하다고 생각한다.
Controller 웹 접근 및 Agent 실행
웹서버 실행 후 localhost:{$설정한 port} 에 접속한다.
- 이때 첫 아이디와 패스워드는 모두 admin으로 동일하며, 사용자 정보에서 password는 변경하는 것을 권장한다.
에이전트를 다운로드하고, 다운로드받은 폴더에 접속하여 압축을 풀고 ./run-agent.sh 명령어로 에이전트를 실행하면 실제 원하는 부하를 직접 줄 Agent가 실행된다.
// 압축 해제
tar -xvf ngrinder-agent-{version}-localhost.tar
// 폴더로 이동
cd ngrinder-agent
// 쉘파일 실행
./run_agent.sh
- 이때 첫 아이디와 패스워드는 모두 admin으로 동일하며, 사용자 정보에서 password는 변경하는 것을 권장한다.
에이전트를 다운받고, 다운로드받은 폴더에 접속하여 압축을 풀고 ./run-agent.sh 명령어로 에이전트를 실행하면 실제 원하는 부하를 직접 줄 Agent가 실행된다.
// 압축 해제
tar -xvf ngrinder-agent-{version}-localhost.tar
// 폴더로 이동
cd ngrinder-agent
// 쉘파일 실행
./run_agent.sh
Docker를 통한 Agent 실행
agent 이미지 pull
docker pull ngrinder/agent
agent 실행
docker run -d -v ~/ngrinder-agent:/opt/ngrinder-agent --name agent ngrinder/agent controller_ip:controller_web_port
가령 controller 서버 주소가 192.168.0.17:80 라면 아래와 같이 설치 가능하다. (한 컴퓨터에서 하고 있다면 localhost로 지정해 줘도 된다.)
docker run -d -v ~/ngrinder-agent:/opt/ngrinder-agent --name agent ngrinder/agent 192.168.0.17:80
아래와 같이 계정 -> 에이전트 관리에 들어가면 실행 중인 에이전트도 확인할 수 있다.
스크립트 작성
이제 아래 스크립트 화면에서 테스트 스크립트를 작성하고 성능 테스트를 수행하면 된다.
스크립트를 만들 때 테스트할 URL(API)을 입력해 주면 알아서 해당 요청에 대한 기본 템플릿 테스트 스크립트를 만들어준다.
물론, 더 상세한 테스트 시나리오를 작성하기 위해서 혹은 유저의 인증을 위해 jwt가 필요한 요청을 위해서는 스크립트를 수정해야 한다.
jwt 가 필요할 때 스크립트
로그인이 필요한 테스트
JWT 토큰을 이용해서 인증을 하고 있을 때의 스크립트이다.
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
import groovy.json.JsonSlurper
/**
* 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 {
def toJSON = { new JsonSlurper().parseText(it) }
public static GTest test
public static HTTPRequest request
public static Map<string, string=""> headers = [:]
public static Map<string, object=""> params = [:]
public static List cookies = []
// login
public static HTTPRequest loginRequest
public static Map<string, string=""> loginHeaders = [:]
public static String fcmTokenBody = "{\n\"fcmToken\" : \"test\"\n}"
public static String accessToken
@BeforeProcess
public static void beforeProcess() {
HTTPRequestControl.setConnectionTimeout(300000)
test = new GTest(1, "gjgs-test.com")
request = new HTTPRequest()
// login
loginRequest = new HTTPRequest()
// Set param data
params.put("type", "ALL")
grinder.logger.info("before process.")
}
@BeforeThread
public void beforeThread() {
// Set Login
loginHeaders.put("KakaoAccessToken", "Bearer Example")
loginHeaders.put("Content-Type", "application/json")
loginRequest.setHeaders(loginHeaders)
HTTPResponse loginResponse = loginRequest.POST("https://gjgs-test.com/api/v1/ngrinder/login", fcmTokenBody.getBytes());
grinder.logger.info(loginResponse.getBodyText())
accessToken = loginResponse.getBody(toJSON).tokenDto.grantType + " " + loginResponse.getBody(toJSON).tokenDto.accessToken
test.record(this, "test")
grinder.statistics.delayReports = true
grinder.logger.info("before thread.")
}
@Before
public void before() {
headers.put("Authorization", accessToken)
headers.put("Content-Type", "application/json")
request.setHeaders(headers)
CookieManager.addCookies(cookies)
grinder.logger.info("before. init headers and cookies")
}
@Test
public void test() {
HTTPResponse response = request.GET("https://gjgs-test.com/api/v1/notices", 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))
}
}
}</string,></string,></string,>
import groovy.json.JsonSlurper를 통해서 Json을 파싱 하도록 toJSON을 선언한다.
BeforeThread에서 로그인 요청을 보내고 응답 파싱해서 JWT 토큰을 static 변수에 저장한다.
각각의 Test 전에 수행되는 Before에서 토큰을 헤더에 세팅한다.
테스트 수행
이제 방금 만든 스크립트를 사용하여 성능 테스트를 진행해 보자.
아래는 성능 테스트 페이지로 가서 새로운 성능 테스트를 생성하는 모습이다.
에이전트는 현재 하나만 가동 중이므로 1개 이하만 사용할 수 있다. 즉, 여기서는 1로 설정해 주었다.
프로세스 * 스레드만큼 에이전트별 가상 사용자 수가 자동 설정된다.
위 사진에서는 테스트 기간을 1분으로 했으니 성능 테스트 타깃 API에 대하여 (프로세스 * 스레드)의 동시 접속자가 요청을 1분 동안 보내는 케이스의 성능을 테스트하게 된다.
Ramp-Up 기능을 사용하여 n초마다 가상 사용자 수가 m씩 증가할 수 있게끔도 설정할 수도 있다.
테스트 결과 및 결과 분석
모카콩 이라는 사이드 프로젝트의 회원 전체 조회에 대한 get 요청 시 에이전트 1대, 가상유저 100명일 때 성능 테스트 결과이다.
전체적인 결과도 중요하지만 우리에게 가장 필요한 데이터는 Throughput과 Latency이다. 위의 결과를 보면 TPS가 Throughput을 의미하고, 평균 테스트 시간이 Latency를 의미한다.
TPS (transcations per second)는 초당 수용 가능한 트랜잭션으로 217 정도면 굉장히 좋은 수준이라고 볼 수 있다.
평균 테스트 시간은 MTT(Mean Test Time)라고 하는데 테스트를 수행했을 때 걸리는 평균 시간이라고 볼 수 있다.
즉, 테스트 요청 한번 당 걸린 시간이다.
대략 0.45초로 빠르진 않지만 그래도 수용할 수 있는 구치이다.
다음은 모카콩 이라는 사이드 프로젝트의 회원 전체 조회에 대한 get 요청 시 에이전트 1대, 가상유저 1000명일 때 성능 테스트 결과이다.
TPS는 유지되지만 평균 테스트 시간은 약 4초에 육박하는 것을 볼 수 있다.
즉, 4초 후에 응답을 줄 수 있는 것인데 이는 사용자에게 엄청난 불편감을 줄 수 있다.
(지금 테스트하는 서버는 scale out 등 처리가 안되어있고 서버가 단 한대로 수행되고 있어서 당연하다고 볼 수도 있겠다. 아직 개선할 점이 많이 남았다!!)
위 표를 보면 알 수 있듯이 현재 vUser가 증가해도 TPS는 200 대로 유지가 되는 것을 보면 High Load Zone에 도달하여 현재 테스트한 API에 대한 시스템 성능은 약 250 정도의 TPS라고 예상할 수 있다.
하지만 이는 단위 성능 테스트이므로 전체 시스템의 성능이라고 단정할 수 없고, 여러 단위 테스트를 종합하고 성능 개선을 한 이후, 통합 시나리오 테스트 및 성능 개선 과정까지 끝내야 전체 시스템의 성능을 예상할 수 있다.
심지어 현재 로컬 Mac에서 Controller서버와 Agent서버 모두 돌아가고 있고, vUser도 Agent서버가 돌아가는 서버의 Memory, CPU 등의 자원에 따라 적절하게 결정해주어야 한다고 생각하는데 더 조사 및 공부가 필요할 듯하다.
그러나 위 간단한 성능 테스트 만으로도 100명일 때는 아무 문제없던 요청도 사용자가 1000명일 때는 사용자 입장에서 엄청난 불편함을 느끼게 됨을 알 수 있다.
따라서 동시 접속자 수라든지, 평소 사용자 수를 모니터링하여 적절한 scale up, scale out이나 쿼리튜닝, JVM 튜닝을 하는 것이 굉장히 중요하다는 것을 알 수 있다!
결론
- nGrinder를 활용하면 스크립트 생성과 테스트 실행, 모니터링 및 결과 보고서 생성을 통합된 Web UI를 통해 사용할 수 있으므로 성능 테스트를 보다 쉽게 할 수 있다.
- nGrinder를 활용한 성능 테스트를 하기 위해서는 Controller, Agent, Target 서버가 필요하고, 이는 각각 다른 서버로 구성되어 있어야 한다. (그러나 간단한 테스트는 위처럼 하나의 컴퓨터에서도 가능하다.)
- vUser가 증가함에 따라 Throughput이 한계에 도달하면 현 상태의 최대 처리량으로 추정할 수 있다. 단, 실제 서비스의 성능은 이보다 더 적을 수도 있다.
- 단위 성능 테스트 결과는 전체 서비스의 성능을 나타내지 않는다. 하지만 이를 개선함으로써 전체적인 성능을 증가시킬 수 있다.
- 평소 모니터링을 통해 사용자 수를 예측하고 적절한 성능 개선 작업이 필요성을 알 수 있다.
'서버' 카테고리의 다른 글
Refactoring : if-else문을 사용하는 팩토리 클래스 제거하기 (3) | 2023.12.26 |
---|---|
Redis에서 사용하는 분산락 알고리즘인 RedLock에 대해 알아보자 (0) | 2023.09.16 |
서버의 구동 원리 - APM (0) | 2022.07.11 |
서버란? 서버 프로그램 NGINX (0) | 2022.07.11 |