nGrinder 성능테스트

이번에 스터디에서 redis 를 사용한 랭킹 시스템을 개발하기로 했다.

그리고 개발 후 성능 테스트를 nGrinder 를 통해 하기로 결정 

1. nGrinder 다운

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

war 파일을 다운받아줍니다.

mkdir -p ~/tools/ngrinder
cd ~/tools/ngrinder
mv ~/Downloads/ngrinder-controller-3.5.9-p1.war .

따로 파일을 이동해주었음 

그리고 nGrinder 실행시 자바 버전이 높을 경우 Setting of Local DNS provider failed 에러가 발생하여 11로 고정하기 위해서 shell script 작성 

nano run_controller.sh
#!/bin/bash

export JAVA_HOME=$(/usr/libexec/java_home -v 11)
export PATH=$JAVA_HOME/bin:$PATH

mkdir -p ./tmp

java -Djava.io.tmpdir=./tmp -jar ngrinder-controller-3.5.9-p1.war --port=7070
chmod +x run_controller.sh

 

그럼 이제 실행이 잘 된다 

모노스냅 캡쳐가 왜이러지...

에이전트도 다운받아 줍니다 

mkdir -p ~/tools/ngrinder
cd ~/tools/ngrinder
mv ~/Downloads/ngrinder-agent

agent 도 옮겨주고  agent 는 run_agent 라는 sh 파일이 있지만 여기도 java 11 을 적용해줘야되기 때문에 수정을 해줬다 

#!/bin/sh

# Java 11 환경 설정
export JAVA_HOME=$(/usr/libexec/java_home -v 11)
export PATH=$JAVA_HOME/bin:$PATH

해당 내용 추가 !

* 역할요약

Controller 테스트의 두뇌 (명령) 역할. 테스트 실행/모니터링/스크립트 관리
Agent 테스트의 근육 (실행) 역할. 실제 부하를 생성하는 머신 또는 프로세스

 

* 역할 상세 비교

항목 Controller Agent
역할 테스트 실행을 기획하고 통제 실제로 부하(VUser)를 발생시키는 실행기
실행 위치 Web UI 제공하는 중앙 서버 여러 대 분산 실행 가능 (병렬 부하 생성)
인터페이스 브라우저에서 접근 (http://localhost:7070) 없음 (명령 수신만)
주요 기능 - 스크립트 업로드
- VUser 수 설정
- 테스트 모니터링
- 결과 리포트
- VUser 수 만큼 HTTP 요청 수행
- Controller의 명령에 따라 실행
실행 대상 .war (웹 애플리케이션) .jar (CLI 실행)
커뮤니케이션 Agent들에게 테스트 명령 전송 Controller에서 받은 명령대로 테스트 수행

2. 스크립트 작성

상단 탭에 스크립트 클릭 -> 스크립트 만들기를 눌러준다

여기서 좀 헷갈렸던게 저 테스트할 url 저기를 http://localhost:8080/~ 입력했는데 안됨 

그래서 삽질 좀 했는데 그냥 스크립트명만 입력하고 만들기 누르면 됨 

 

 

그리고 이 안엔 테스트 코드를 작성하면 되는데(junit과 비슷) 이건 지피티의 도움을 좀 빌렸다 ^^

주의! ) 여기에 자바 11 까지의 문법만 써야될것  - body 적어줄 때 텍스트 블록 썼다가 에러남 ㅎ

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 List<Cookie> cookies = []

@BeforeProcess
public static void beforeProcess() {
    HTTPRequestControl.setConnectionTimeout(3000)
    test = new GTest(1, "POST /game/scores 테스트")
    request = new HTTPRequest()

    headers.put("Content-Type", "application/json")
    grinder.logger.info("✅ beforeProcess 완료")
}

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

@Before
public void before() {
    request.setHeaders(headers)
    CookieManager.addCookies(cookies)
}

@Test
public void test() {
    def userId = "user" + new Random().nextInt(100)
    def score = new Random().nextInt(100)

    def payload = 
        "{" +
        "\"userId\": \"${userId}\"," +
        "\"nickname\": \"Tester\"," +
        "\"profileImageUrl\": \"https://cdn.example.com/u.png\"," +
        "\"score\": ${score}" +
        "}"

    HTTPResponse response = request.POST("http://localhost:8080/game/scores", payload.getBytes("UTF-8"))

    if (response.statusCode == 200) {
        grinder.logger.info("✅ 요청 성공: ${userId}, score=${score}")
    } else {
        grinder.logger.warn("❌ 응답 실패! statusCode=${response.statusCode}")
    }

    //assertThat(response.statusCode, is(200))
	}
}

 

2025-04-14 10:28:20,854 INFO  Setting of nGrinder local DNS successfully
2025-04-14 10:28:20,864 INFO  The Grinder version 3.9.1
2025-04-14 10:28:20,868 INFO  OpenJDK Runtime Environment 11.0.25+9-LTS: OpenJDK 64-Bit Server VM (11.0.25+9-LTS, mixed mode) on Mac OS X x86_64 15.3.2
2025-04-14 10:28:20,914 INFO  time zone is KST (+0900)
2025-04-14 10:28:21,048 INFO  worker process 0 of agent number 0
2025-04-14 10:28:21,066 INFO  Instrumentation agents: byte code transforming instrumenter for Java
2025-04-14 10:28:25,429 INFO  registered plug-in net.grinder.plugin.http.HTTPPlugin
2025-04-14 10:28:25,521 INFO  ✅ beforeProcess 완료
2025-04-14 10:28:25,523 INFO  Running "ranking_test.groovy" using GroovyScriptEngine running with groovy version: 3.0.5
2025-04-14 10:28:25,795 INFO  ✅ beforeThread 완료
2025-04-14 10:28:25,798 INFO  starting, will do 1 run
2025-04-14 10:28:25,799 INFO  Start time is 1744594105799 ms since Epoch
2025-04-14 10:28:26,162 INFO  http://localhost:8080/game/scores -> 200 , 2 bytes
2025-04-14 10:28:26,178 INFO  ✅ 요청 성공: user13, score=91
2025-04-14 10:28:26,180 INFO  finished 1 run
2025-04-14 10:28:26,184 INFO  elapsed time is 386 ms
2025-04-14 10:28:26,185 INFO  Final statistics for this process:
2025-04-14 10:28:26,196 INFO  
             Tests        Errors       Mean Test    Test Time    TPS          Mean         Response     Response     Mean time to Mean time to Mean time to 
                                       Time (ms)    Standard                  response     bytes per    errors       resolve host establish    first byte   
                                                    Deviation                 length       second                                 connection                
                                                    (ms)                                                                                                    

Test 1       1            0            246.00       0.00         2.59         2.00         5.18         0            0.00         37.00        60.00         "POST /game/scores 테스트"

Totals       1            0            246.00       0.00         2.59         2.00         5.18         0            0.00         37.00        60.00        

  Tests resulting in error only contribute to the Errors column.          
  Statistics for individual tests can be found in the data file, including
  (possibly incomplete) statistics for erroneous tests. Composite tests   
  are marked with () and not included in the totals.                      



2025-04-14 10:28:25,530 INFO  validation-0: Starting threads
2025-04-14 10:28:26,196 INFO  validation-0: Finished

이런 식으로 잘 나오면 합격 ! 

 

그럼 이제 성능테스트 실행해보자

3. 성능테스트

테스트를 생성하면 이렇게 작성하는 공간이 나오는데 여기서 수정할 건

에이전트 갯수 (최대 1), 에이전트 별 가상 사용자( 우리는 99, 1000, 3000) 을 테스트 하기로 함 

스크립트와 테스트 기간을 설정하면 된다

설정 후 저장 후 시작을 누르면 성능테스트 start 

VUser 가 99명 일땐 아주 성능이 좋았던 나의 랭킹서비스... 

VUser 수가 3,000이 되자 나락가버리고 말았음 ... 😱

이번 과제는 성능보단 구현에 초점을 맞췄는데 ㅎ 이제 이런 성능테스트 하는 법도 알았으니 성능에도 초점을 맞춰봐야겠다 

 

- 동시 사용자 수(VUser) 기준 서비스 규모 매핑

VUser 수실서비스 규모대표 예시특징
100명 🧪 소규모 트래픽 수준 동시 방문자 수 적은 스타트업, 사내 툴, MVP 서비스 Redis 단일 인스턴스, EC2 1~2대면 충분
1,000명 🟡 중형 서비스 수준 커뮤니티, 쇼핑몰, 중견기업 앱 캐시 전략 + 비동기 큐 필요, 분산 처리 고려
3,000명 이상 🔴 대형 트래픽 or 실시간 경쟁 서비스 게임 랭킹, 인기 투표, 실시간 푸시 알림 Redis 클러스터, Kafka, Cloud 기반 확장 필수