e-commerce 대용량 서버 프로젝트가 실제로 트래픽을 받았을 때 성능이 어느정도까지 도달할 수 있는지 측정해보고자 합니다. 때문에 실제와 같은 환경을 만들어 놓고 서버가 사용자를 얼마 만큼 수용할 수 있는지 nGrinder을 사용하여 테스트를 진행하기로 하였습니다.
: Naver에서 개발한 부하 테스트 툴
# 서버 접속 명령어
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 1월 30 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
[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
@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));
}
}
}
@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)
}
}
VUser을 500으로 설정한 후 10분동안 테스트를 진행한 결과는 다음과 같습니다.
평균 TPS : 662
피크 TPS : 921
CPU Usage : 100%
Errors : 2,674
VUser을 500으로 설정한 후 10분동안 테스트를 진행해보니, 아래처럼 결과가 확인되었습니다.
평균 TPS : 426
피크 TPS : 538
CPU Usage : 100%
Errors : 0
성능측정을 처음 해보았는데, nGrinder의 사용방법이 공식문서나 기술 블로그에 자료가 많았기 때문에 비교적 편하게 할 수 있었다. 다만, script에 오류가 있는 경우 무엇이 문제인지 찾기가 조금 어려웠지만 nGrinder에서 로그파일을 제공해주어서 참고하여 해결하는데 도움을 받았습니다. nGrinder말고도 JMeter과 같은 다른 성능테스트 툴이 있는데, 다음번엔 다른 툴을 사용하고 비교해볼까 합니다.
공식문서 - Installation-Guide
공식문서 - User-Guide
Groovy Script 작성법