제목: "내가 만든 서비스는 얼마나 많은 사용자가 이용할 수 있을까? - 2편(nGrinder를 활용한 성능테스트)"
작성자: tistory(Hooligans)
작성자 수정일: 2020년12월27일
링크: https://hyuntaeknote.tistory.com/11
작성일: 2022년8월13일
이번에는 서비스의 성능 지표를 확인하기 위해서 부하를 발생시켜보자
nGrinder란 네이버에서 The Grinder 라는 성능 테스트 도구를 기반으로 제작한 오픈소스 성능 테스트 솔루션이다.
스크립트 생성과 테스트 실행, 모니터링 및 결과 보고서 생성을 통합된 Web UI를 통해 사용할 수 있으므로 성능 테스트를 보다 쉽게 할 수 있다.
추가적으로 단순히 부하만을 발생시키는 것이 아니라, Groovy,Jython 스크립트를 통해서 다양한 시나리오를 테스트할 수 있다는 점도 큰 장점이다.
nGrinder를 통해 성능 테스트를 하기 위해서는 위 그림과 같이 Controller,Agent,Target Server가 각각 별도의 서버에 구성되어있어야 한다.
Controller
Agent
Target Server
위의 세가지 요소를 각각 다른 서버에 설치하는 이유는 컨트롤러,Agent 모두 부하를 발생하고 모니터링하는데 있어서 서버의 자원을 사용하기 때문이다.
만약 세 가지 요소가 하나의 서버에 존재한다면 서버는 자원을 나눠서 사용해야 하고, 그만큼 Context Switching이 발생하는 등 테스트에 있어서 불필요한 노이즈가 발생하게 되기 때문에 순수한 Target 서버의 성능을 도출하기 어려워진다.
그러므로 각각 서버를 분리하여 설치해야만 하고, 필요에 따라 vUser 수를 늘리기 위해서 Agent 서버를 추가적으로 설치할 수 있다.
https://github.com/naver/ngrinder/wiki/Installation-Guide
https://hub.docker.com/r/ngrinder/controller/
https://opentutorials.org/module/351/3338
https://velog.io/@rudwnd33/nGrinder%EB%A1%9C-%EB%B6%80%ED%95%98%ED%85%8C%EC%8A%A4%ED%8A%B8%ED%95%B4%EB%B3%B4%EA%B8%B0
$ docker pull ngrinder/controller
docker run -d -v ~/ngrinder-controller:/opt/ngrinder-controller --name controller -p 80:80 -p 16001:16001 -p 12000-12009:12000-12009 ngrinder/controller
$ docker pull ngrinder/agent
docker run -d --name agent --link controller:controller ngrinder/agent
지금부터 서비스의 단위 성능 테스트를 해보자
위와 같이 테스트에 필요한 Controller, Agent, Target Server 가 기본적으로 구성되어 있고, Target 서버 앞에는 이후에 Scale-Out 시에 사용할 로드밸런서를 미리 구성했다.
단일 서버를 사용하는 중에도 로드 밸런서를 두고 테스트하는 이유는 테스트 대상을 고립시키기 위해서입니다. Scale-out을 하려면 로드 밸런서가 필요하게 되고, 로드 밸런서와 서버를 동시에 추가하게 된다면 어떤 대상에 의해서 성능이 증가하고 감소했는지 정확하게 확인하기 어렵기 때문이다.
이처럼 단일 서버 구성에도 로드밸런서를 추가함으로써 Scale-out 시에도 순수하게 서버가 늘어났을 때 증가하는 성능에 대해서 측정을 할 수 있다.
테스트 스크립트는 nGrinder 개발자분이 작성하신 'Groovy 스크립트 구조' 포스팅을 참고하여 작성하자.
import static net.grinder.script.Grinder.grinder
import static org.junit.Assert.*
import static org.hamcrest.Matchers.*
import net.grinder.plugin.http.HTTPRequest
import net.grinder.plugin.http.HTTPPluginControl
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 net.grinder.scriptengine.groovy.junit.annotation.AfterThread
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Test
import org.junit.runner.RunWith
import java.util.Date
import java.util.List
import java.util.ArrayList
import HTTPClient.Cookie
import HTTPClient.CookieModule
import HTTPClient.HTTPResponse
import HTTPClient.NVPair
import groovy.json.JsonOutput
@RunWith(GrinderRunner)
class TestRunner {
public static GTest test
public static HTTPRequest request
public static NVPair[] headers = []
public static Cookie[] cookies = []
@BeforeProcess
public static void beforeProcess() {
HTTPPluginControl.getConnectionDefaults().timeout = 6000
test = new GTest(1, "123.123.123.123")//Target IP
request = new HTTPRequest()
List<NVPair> headerList = new ArrayList<>()
headerList.add(new NVPair("Content-Type", "application/json"))
headers = headerList.toArray()
}
@BeforeThread
public void beforeThread() {
test.record(request)
grinder.statistics.delayReports=true;
def threadContext = HTTPPluginControl.getThreadHTTPClientContext()
cookies = CookieModule.listAllCookies(threadContext)
cookies.each {
CookieModule.removeCookie(it, threadContext)
}
def loginBody = JsonOutput.toJson([userId: 'testuser1', password: 'password'])
HTTPResponse res = request.POST("http://123.123.123.123/users/login", loginBody.getBytes(), headers);
cookies = CookieModule.listAllCookies(threadContext)
}
@Before
public void before() {
request.setHeaders(headers)
cookies.each { CookieModule.addCookie(it, HTTPPluginControl.getThreadHTTPClientContext()) }
}
@Test
public void getSingleFeedTest(){
HTTPResponse result = request.GET("http://123.123.123.123/feeds/test")
}
@AfterThread
public void afterThread() {
HTTPResponse result = request.POST("http://123.123.123.123/users/logout")
}
}
이렇게 테스트 스크립트 작성이 끝나면 테스트 설정을 하자
가장 중요한 것은 vUser 수를 선정하는 것인데, 500명, 1000명의 사용자를 기준으로 각각 테스트해보자
테스트 시간은 Ramp-up 시간을 포함하여 유효한 결과를 도출하고자 10분 동안 테스트하자.
아래는 vUser를 500명 기준으로 테스트한 결과이다.
전체적인 결과도 중요하지만, 가장 필요한 데이터는 throughput,Latency이다.
TPS가 Throughput을 의미하고, 평균 테스트 시간이 Latency를 의미한다.
결과적으로 서비스는 vUser 500명 기준, 약 2000 TPS 처리량을 갖고, 테스트 요청당 230ms의 지연시간이 걸린다는 것을 알 수 있습니다.
다음은 vUser를 1000명으로 테스트한 결과이다.
보시는 바와 같이 사용자가 늘어났는데 Throughput은 약 2000 TPS로 동일하고, Latency만 두 배로 증가했습니다. 이 부분은 아래 그래프를 보자.
현재 상태에서 WAS의 CPU 사용량을 확인해보자
vUser 500명을 기준으로 테스트하는 중간에 htop 명령을 통해 확인한 결과, WAS의 CPU 사용량이 90% 이상 점유하는 것을 보면 현재 WAS에서 최대 처리량에 거의 도달했음을 볼 수 있다.
현재 WAS 상태에 대해 진단을 해보았으니, JVM 튜닝 및 슬로우 쿼리 분석 등 다양한 성능 개선 작업을 시도하면서 서비스의 성능을 최대한 올려보자