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