///
Search

부하 테스트 시나리오

1. 요구사항

인스턴스 한 대 기준으로 핵심기능에 목표 TPS를 정한다. 그리고 안정적으로 서비스될 수 있게 아래 값을 설정하고 이유를 공유해야한다.
톰캣 설정 중 아래 값을 상황에 맞게 설정
threads max
max connections
accept count
hikariCP 커넥션풀 설정
수치를 설정하고 정한 이유를 발표
hikariCP configuration 보고 필요한 값 설정

2. 목표 설정

(1) 목표 TPS 설정

TPS = VirtualUser / AverageResponseTime
응답 시간이 몇 초 이내에 이루어져야 하는가?
피크 시간대, 비피크 시간대 분리해 설정하는 것도 좋음
최대 허용 사용자 수트래픽은 어느 정도인가?
유저 몇 명 받을지 예상해 설정하면 좋음
ex. 동시 사용자 수 1000명, 응답 시간 목표 2초 이내 → TPS = 500TPS
ex. 동시 사용자 수 2000명, 응답 시간 목표 1초 이내 → TPS = 2000TPS
[총대마켓 팀]
동시 사용자 수 1000명, 응답 시간 목표 1초 이내 → TPS = 1000TPS
신규 가입 시 할인 이벤트를 대학교 커뮤니티에 공유
예상 사용자 통계

(2) 톰캣 스레드 설정

최대 스레드 수 (threads.max 개) 초과한 요청 들어오면 대기열에 들어감. 대기열엔 최대 (acceot-count 개)까지 들어갈 수 있음. 서버가 부하를 받을 땐 더 많은 스레드를 준비하도록 하는데, 이때 요청을 처리하기 위해 대기하는 최소 스레드는 (threads.min-spare 개). 서버가 동시에 처리할 수 있는 연결 수는 최대 (max-connections 개)
SQL
복사

maxThreads = 35

동시에 처리할 수 있는 최대 요청 수
ex. 한 스레드가 초당 10개의 요청 처리할 수 있다면, 1000 TPS 목표에선 최소 100개 스레드 필요
[총대마켓 팀]
api latency: 100ms
한 스레드가 초당 10개의 요청 처리할 수 있다면, 70 TPS 목표에선 최소 7개 스레드 필요
api latenct: 400ms (크롬 개발자 도구에서 네트워크 응답시간 확인)
한 스레드가 초당 2개의 요청 처리할 수 있다면, 70 TPS 목표에선 최소 35개 스레드 필요

maxConnections = 1024

동시에 처리할 수 있는 최대 연결 수 (TCP 연결 수)
ex. 1초당 500개의 TCP 연결을 처리할 수 있는 서버
[총대마켓 팀]
`ulimit -n`
file descriptor: 1024 (1 file descriptor - 1 socket)

acceptCount = 242

모든 스레드가 바쁠 때 들어오는 추가요청을 큐에서 대기시킬 수 있는 최대 수
ex. acceptCount=스파이크 TPS×요청 처리 시간 (초)−maxThreads
[총대마켓 팀]
api latency: 400ms, peekTPS: 691
691 * 0.4 - 35 = 242

(3) hikariCP 커넥션풀

maximumPoolSize = 8

커넥션 풀 최대 크기
활성화된 커넥션 개수 = (core_count * 2) + effective_spindle_count = 2 * 2 = 4
[총대마켓 팀]
java.sql.SQLTransientConnectionException
→ 8

3. 테스트 시나리오 작성

(1) 사용자 행동 정의

사용자가 주로 사용하는 기능을 기반으로 행동 패턴 정의
각 행동에 정확한 경로(URL)와 HTTP 메서드 설정
ex. 로그인 → 공구 검색 → 공구 상세 보기 → 공구 참여 → 로그아웃
[총대마켓 팀]
시나리오 0: 공구 목록 조회(최신순)
시나리오 1: 공구 참여
시나리오 2: 댓글 조회
시나리오 3: 댓글 작성
이후

(2) 부하 모델 설정

피크 부하 테스트 : 짧은 시간에 갑작스레 부하 주어 서버가 급격한 트래픽 증가에 어떻게 반응하는지 확인
지속 부하 테스트 : 일정한 부하를 오랜 시간 유지해 메모리 누수 같은 장기적 문제 확인
[총대마켓 팀]
피크 부하 테스트

(3) nGrinder 스크립트 작성

Groovy 기반 스크립트
ex
[총대마켓 팀]
GET /read-only/offerings : 공모 첫페이지 조회
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 Map<String, Object> params = [:] public static List<Cookie> cookies = [] @BeforeProcess public static void beforeProcess() { HTTPRequestControl.setConnectionTimeout(300000) test = new GTest(1, "43.203.234.171") 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() { request.setHeaders(headers) CookieManager.addCookies(cookies) grinder.logger.info("before. init headers and cookies") } @Test public void test() { HTTPResponse response = request.GET("http://43.203.234.171/read-only/offerings", params) if (response.statusCode == 301 || response.statusCode == 302) { grinder.logger.warn("Warning. The response may not be correct. The response code was {}.", response.statusCode) } else { assertThat(response.statusCode, is(200)) } } }
Groovy
복사
POST /read-only/offerings : 공모 작성
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 String body = "{\n \"title\": \"공모 제목\",\n \"productUrl\": \"www.naver.com\",\n \"thumbnailUrl\": \"www.naver.com/favicon.ico\",\n \"totalCount\": 5,\n \"totalPrice\": 10000,\n \"originPrice\": null,\n \"meetingAddress\": \"서울특별시 광진구 구의강변로 3길 11\",\n \"meetingAddressDetail\": \"상세주소아파트\",\n \"meetingAddressDong\": \"구의동\",\n \"meetingDate\": \"2025-04-08T02:00:00\",\n \"description\": \"내용입니다.\"\n}" public static List<Cookie> cookies = [] @BeforeProcess public static void beforeProcess() { HTTPRequestControl.setConnectionTimeout(300000) test = new GTest(1, "43.203.234.171") request = new HTTPRequest() // Set header data headers.put("Content-Type", "application/json") headers.put("Cookie","access_token=accesstokenHere") 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() { request.setHeaders(headers) grinder.logger.info("before. init headers and cookies") } @Test public void test() { HTTPResponse response = request.POST("http://43.203.234.171/offerings", body.getBytes()) if (response.statusCode == 301 || response.statusCode == 302) { grinder.logger.warn("Warning. The response may not be correct. The response code was {}.", response.statusCode) } else { assertThat(response.statusCode, is(201)) } } }
Groovy
복사

4. 성능 메트릭 정의

부하 테스트 결과 평가 시 사용할 성능 메트릭 정의
응답 시간(Response Time): 각 요청이 완료되기까지 걸리는 시간.
TPS(Transaction per Second): 초당 처리되는 트랜잭션 수.
에러율(Error Rate): 요청 중 에러가 발생하는 비율.
CPU 및 메모리 사용률: 서버 자원의 사용량.
DB 커넥션 풀 사용률: HikariCP 등의 커넥션 풀 사용 상태.
[총대마켓 팀]

5. 부하 시나리오 실행 및 결과 분석

ex. 응답 시간 길어지거나 에러율 높아짐 → CPU 과부하, 메모리 부족, 커넥션 풀 부족 의심 가능
[총대마켓 팀]

6. 최적화 및 재실행

부하 테스트에서 발견된 문제에 대한 최적화 작업 수행 후, 다시 부하 테스트 실행해 개선된 성능 확인
ex. 로그인 후 제품 검색 테스트
1000명의 사용자가 동시에 로그인해 제품 검색하고 로그아웃 하는 시나리오
목표: 초당 500 TPS 처리하며, 응답 시간 2초 이하로 유지
[총대마켓 팀]

참고자료