해당 글에서는 nGrinder를 이용한 성능테스트 방법에 대해 다룹니다.
사이드 프로젝트 `모카콩`의 Wiki에 작성한 글에 해당된다.
해당 프로젝트 github: https://github.com/mocacong/Mocacong-Backend
들어가며
대부분의 웹 또는 앱 애플리케이션에서는 성능을 위해 페이지네이션을 적용하고 있습니다. 실제로 데이터가 매우 적지 않은 이상, 페이지네이션은 성능 상 유의미한 지표를 가져옵니다.
그렇다면 과연 페이지네이션을 적용하지 않은 경우에는 얼마나 느린걸까요? 마침, 모카콩에서는 개발서버와 운영서버 분리 작업을 최근에 수행했습니다. 그렇기 때문에 운영 서버에 영향을 주지 않으면서 더미 데이터를 넣어줌으로써 성능 테스트가 가능해졌다는 장점이 존재합니다.
성능 테스트 툴을 이용하면 vUser(가상 접속자) 수를 우리가 조정하여 부하를 일으키고 요청에 대한 응답을 기다리는 동시 접속자 수를 조정할 수 있습니다. 또, 이러한 응답까지 걸리는 시간(MTT) 및 tps(transaction per second)를 확인할 수 있습니다. 보통 사용자가 많고 규모가 큰 애플리케이션일수록 높은 tps를 요구합니다.
이번 위키에서는 성능 테스트 도구 중 하나인 nGrinder로 페이지네이션이 적용돼있지 않은 회원 전체조회 API(운영 서버 전용 API라기보단, 개발 및 테스트 용도의 API)를 테스트해보고, 페이지네이션이 왜 필요한지 얘기해보려 합니다.
성능테스트 툴로 nGrinder를 선택한 이유
이전에 저는 JMeter를 사용해본 경험이 있습니다. JMeter와 비교했을 때 nGrinder의 장점은 아래와 같습니다.
- Naver에서 제작한 성능 테스트 툴이어서 한국어 지원이 된다.
- UI가 비교적 예쁜 편이다.
- 테스트 스크립트 문법으로 JUnit과 비슷한 groovy를 지원한다.
구글링했을 때의 레퍼런스나 github stars 수는 JMeter가 nGrinder보다는 많은 듯합니다. 또, JMeter도 사용법이 어렵지 않아 편리하게 사용할 수 있습니다. 그럼에도 불구하고, nGrinder의 한국어 지원이랑 UI가 마음에 들어서 nGrinder를 선택했습니다. 또, JMeter 사용 경험이 있으니 nGrinder도 한 번 새롭게 사용해보고 싶은 마음도 있었습니다.
nGrinder 구성
nGrinder에는 크게 nGrinder-Controller, nGrinder-Agent로 구성돼있습니다.
먼저 Controller는 성능 테스트를 위한 웹 인터페이스를 제공하는 담당입니다. 위 사진처럼 성능 테스트 스크립트를 작성하고, 테스트를 실행하는 웹 인터페이스 서버가 nGrinder-Controller 입니다.
Agent는 Controller에서 작성된 성능테스트 스크립트에 맞춰 실제 target 서버에 부하를 주는 담당입니다. Agent 서버를 여러 대 두어 분산환경에서의 성능 테스트 환경 또한 수행할 수 있습니다.
Agent는 보통 부하를 직접 주는 역할을 하기 때문에, Controller와 Agent를 같은 서버에 두고 수행하면 해당 서버에 부담이 커질 수 있습니다. 그렇기 때문에 Controller와 Agent는 서로 다른 서버에 두어 성능테스트를 수행하는 것을 권장합니다. 모카콩은 서버비가 충분한 상황이 아니었기 때문에, Controller와 Agent를 둘 다 제 Mac M1 로컬에 두어 수행했습니다. (Mac M1이 성능이 좋은 편이라, 어느 정도 부하까진 충분히 버텨주는 모습을 확인할 수 있었습니다. 다만, 동접자 수가 1500을 넘어가는 테스트는 아슬아슬하게 버티거나, 다운되는 모습을 확인할 수 있었습니다.)
만약 분산 환경에서의 부하 및 성능 테스트를 수행하고 싶다면 Controller 전용 서버를 하나 두는 것을 추천합니다. Agent를 수행할 때 Controller와 통신이 돼야 하기 때문입니다.
nGrinder 설치 및 수행
2. 아래 명령어로 war 파일 실행
java -Djava.io.tmpdir=/Users/${username}/${생성할 임시파일명} -jar ngrinder-controller-3.5.8.war --port=8300
- 여기서 tmpdir을 지정해준 이유는, 지정해주지 않을 경우 Please set `java.io.tmpdir` property like following. tmpdir should be different from the OS default tmpdir. 에러를 만날 수 있기 때문입니다.
- --port 옵션으로 따로 포트를 설정해주지 않으면 8080 포트로 수행됩니다.
3. localhost:8300 (별도 서버에 설정했다면 해당 서버 IP 입력) 에 접속 후, 한국어를 선택하고 로그인
- 처음 계정은 아이디, 패스워드 모두 admin
- 이후 사용자 정보에서 password 는 변경하는 것을 권장합니다.
4. 우측 상단의 에이전트 다운로드 클릭
5. 에이전트 다운로드받은 폴더에 접속하여 ./run-agent.sh 명령어로 에이전트 실행
6. 좌측 상단에 스크립트 클릭하여 테스트 스크립트 작성
- 테스트할 URL을 입력하면 해당 테스트의 기본적인 스크립트는 자동으로 생성해줍니다.
+) 24.03.13. 추가
스크립트는 아래를 참고하자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
|
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 HTTPClient.NVPair
import org.apache.commons.io.IOUtils;
/**
* 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 example
*/
@RunWith(GrinderRunner)
class TestRunner {
public static GTest test
public static HTTPRequest request
public static Map<String, String> headers = [:]
public static Map<String, Object> body = [:]
public static List<Cookie> cookies = []
@BeforeProcess
public static void beforeProcess() {
HTTPRequestControl.setConnectionTimeout(300000)
test = new GTest(1, "test-internal-api.net")
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() {
headers.put("Authorization", "BASIC testBearerToken")
request.setHeaders(headers)
CookieManager.addCookies(cookies)
grinder.logger.info("before. init headers and cookies")
}
@Test
public void test() {
body.put("expireDate", "2022-09-15")
body.put("memberIds", ["1","2"])
body.put("teamIds",[1, 2, 3])
HTTPResponse response = request.POST("https://test-internal-api.net/v1/test", body)
if (response.statusCode == 301 || response.statusCode == 302) {
grinder.logger.warn("Warning. The response may not be correct. The response code was {}.", response.statusCode)
} else {
if (response.statusCode == 500) {
grinder.logger.warn("Error. The response was {}.", IOUtils.toString(response.message.body, "UTF-8"))
}
assertThat(response.statusCode, is(200))
}
}
}
|
cs |
- header, body 모두 Map 형태로 넣어주면 되며, 별도의 getBytes 코드나 변환 함수는 필요하지 않다. nvPair 꼴로도 바꾸어주지 않아도 된다.
- body로 넣을 때, List의 경우 역시 [] 꼴로 넣어주면 된다. Map 형태로 넣어주는 걸 생각하면 된다.
7. 성능 테스트를 클릭하여 테스트 수행
- 에이전트는 현재 수행중(./run-agent.sh 명령어를 수행한) 에이전트 수만큼 지정 가능합니다.
- 프로세스 * 스레드 수만큼 Active Users가 설정됩니다.
- 위 사진은 회원전체조회요청을 100명의 동시접속자가 요청을 1분동안 보내는 케이스의 성능을 테스트하는 경우입니다.
테스트 결과
회원 전체 조회 API는 별도의 페이지네이션이 적용돼있지 않습니다. 운영 서버 전용 API라기보단, 개발 및 테스트 용도의 API라 볼 수 있습니다. 실행 환경은 아래와 같습니다.
- AWS EC2 t2.micro (vCPU 1, RAM 1GB)
- AWS RDS t2.micro (vCPU 1, RAM 1GB)
- Agent: 1
- Active Users (vUser): 100
- 테스트 수행 시간: 1분
먼저 전체 회원이 5명 정도일 때의 API 테스트 결과부터 보죠.
평균 tps 약 240, 최고 tps가 262까지 나오는 모습을 확인할 수 있습니다. MTT(평균 테스트 시간)는 약 420ms 정도가 나왔네요. MTT만 보면 좋은 성능은 아닌 듯하지만, tps는 꽤나 만족스러운 성능이 나왔습니다.
그런데 만약 회원이 10,000명이 있고 페이지네이션이 적용이 안돼있다면 어떻게 될까요? 이 경우, 같은 코드임에도 불구하고 성능은 확 낮아지게 됩니다. 동일한 환경에서의 테스트를 수행해보겠습니다.
평균 tps가 약 4, 최고 tps 조차 두 자리 수를 넘지 못한 7 임을 확인할 수 있습니다. 보통 tps는 아무리 사용자가 적은 애플리케이션이어도 최소 20~30은 요구됩니다. 초당 처리되는 트랜잭션이 10 이하인 것은 매우 심각한 성능 이슈가 존재한다는 것을 의미하게 되겠죠.
MTT를 보면 더욱 와닿을 수 있을 듯합니다. MTT는 무려 약 18000ms, 다시 말해 응답까지 걸리는 시간이 약 18초나 된다는 점입니다. 동시접속자가 100명일 때, 사용자 입장에서는 굉장한 불편함을 느낄 수 있습니다.
따라서 한 번에 수많은 데이터를 받아오는 것은 서버 입장에서 큰 부담이 될 수 있습니다. Http 통신 비용이 굉장히 크기 때문이죠. 그렇기 때문에 한 페이지에 적절한 개수(5~30개 정도)로 받아오는 것이 부담을 훨씬 줄일 수 있어보입니다.
참고로 동시접속자가 1명, 다시 말해 성능테스트 툴로 테스트하지 않고 swagger나 postman으로 요청을 보낼 때는 MTT가 약 500ms 정도인 것을 확인할 수 있었습니다. (tps는 아까보다 더 낮은 2~3인데요, 이는 요청이 적어 트랜잭션 처리를 덜한 것으로 보입니다.) 성능테스트 툴로 테스트하지 않는다면 해당 API에서 사용자가 불편함을 느낄 것이라 생각하지 못할 수도 있겠죠. 실제로 애플리케이션을 배포하면 동시 접속자가 1명만 존재하는 경우는 사실상 거의 없을테니 성능 테스트 툴로 반드시 테스트를 진행해보는 것이 좋아보입니다.
따라서 동시 접속자 수라든지, 평소 사용자 수를 모니터링하여 적절한 scale up, scale out이나 쿼리튜닝을 하는 것이 굉장히 중요합니다.
추가로, 위와 같이 점진적으로 요청이 증가하는 Ramp-up 기능도 사용가능합니다. 위 화면은 1초마다 사용자 수가 3000씩(프로세스 50증가 → 하나의 프로세스 당 60 스레드 증가 -> 사용자 수 50 * 60 = 3000만큼씩 증가)하게 하는 테스트입니다. 당연히 위처럼 설정하면 제 컴퓨터에선 부담이 너무 커서 돌아가지 않습니다. 에이전트를 여러 개로 분산해서 부하를 주는 것이 좋아보입니다.
보통 가상사용자를 500 정도로 설정하고, 1~2초마다 50 정도씩 증가하게 하는 것이 좋아보입니다. vUser 수가 1000 정도만 돼도 제 로컬 환경에서 CPU를 90% 이상 사용하는 것을 확인했기 때문입니다.
마치며
nGrinder로 성능테스트를 진행해보았는데요. 서버비용이 부담이 되지 않는다면 Naver pinpoint와 같은 APM을 추가로 두어 모니터링도 해보고 싶다는 생각이 들었습니다.
성능 테스트 툴과 달리 APM은 특정 지표나 요청에서 어떠한 트랜잭션에 의해 느린 response time을 가지는지 기록이 됩니다. 따라서 두 개를 같이 사용할 때에 큰 시너지 효과를 얻을 수 있는 것이죠. 기회가 된다면 다음에 APM을 곁들여 성능 개선을 하는 경험을 공유할 수 있으면 좋겠습니다 🙂
+) 24.03.13 추가
ngrinder 성능테스트 또는 부하테스트 시, 베타환경을 운영환경에 맞게 Scale Up 할 때 주의점 및 삽질일기
https://kth990303.tistory.com/478
'Infra' 카테고리의 다른 글
[Jenkins] Pipeline 내 Job 실패 시 재시도 처리 및 OpsGenie 알림 전송 (1) | 2024.02.13 |
---|