nGrinder를 이용해 스프링 어플리케이션 성능 측정기록 여정

sonnng·2024년 2월 11일
0

Spring

목록 보기
39/41
post-thumbnail

성능 테스트 도구의 종류

Gatling

스칼라를 통해 테스트 스크립트를 생성하는 부하테스트 도구이며, 비동기식 아키텍쳐로 가상 사용자를 스레드가 아닌 메세지로 생성해 수천명의 동시 사용자를 재현할 수 있는 장점이 있다. 그러나, 분산 테스트를 지원하지 않아서 다수의 컴퓨터를 통해 테스트할 수 없다. 요청처리를 할땐 비동기를 지원하지 않는다.

JMeter

GUI를 제공하며 한 명의 가상 사용자에 하나의 스레드로 생성하기 때문에 동시성 제한이 있고, 여러 대의 컴퓨터를 통해 테스트하고 테스트 결과를 종합할 수 있다.(분산 테스트) 요청처리를 할땐 비동기를 지원하지 않는다.

nGrinder

Jython, Groovy 스크립트를 활용해 테스트 시나리오 작성이 가능하며 한명의 가상 사용자로 하나의 스레드 생성해 동시성 제한이 있다. 하지만 분산 테스트를 지원한다. 요청처리를 할땐 비동기로 지원해준다.

성능 테스트 중 nGrinder를 사용하게 된 계기

팀 프로젝트를 진행하면서 스프링 어플리케이션 중 토론 API와 메인페이지 API의 성능을 측정해보고자 서치를 진행하던 중, 성능 테스트로 네이버 nGrinder가 오픈소스이면서 무료이고, 한글패치가 되며 Java와 비슷한 환경에서 Groovy 스크립트를 지원, 자료가 많다고 느껴 나도 이번 기회에 사용하게 되었다.

또한 저번 글에서 카페인 캐싱을 이용해 토론 API 게시글 단건 조회의 성능을 개선시켜놓았기 때문에 어느정도의 부하를 견딜 수 있을지 궁금했었다. EC2 인스턴스에 nGrinder를 적용할 수 있었으나, 메모리 부족되는 현상이 발생할까 걱정되서 로컬에서 돌려보기로 했다. 로컬환경에서 테스트해보고 TPS 지점이 낮은 곳은 추후 PR-머지 후 코드 추가로 문제를 해결해나가려고 한다.

단계

크게 툴을 깔고 스크립트를 짜서 실행하는 것이지만, 자세하게는 계획-시나리오 수립-실제 테스트-분석-적용 이 되겠다.

nGrinder의 구조

  • Controller : nGrinder의 GUI를 제공해주는 부분을 말하며 스크립트 생성과 테스트 명령을 Agent에 전달하는 역할을 한다.

  • Agent : 컨트롤러로부터 요청을 받아서 Target에게 요청을 보내는 역할을 한다.

  • Target : Agent로부터 요청을 받아 스트레스 테스트를 해야하는 대상

따라서 nGrinder로 부하테스트를 하려면 요청을 위한 스크립트 생성 및 요청을 에이전트로 전달해주는 컨트롤러, 요청에 따라 스트레스 테스트 타겟 서버에 스트레스 테스트를 해줄 에이전트가 필요하다.

설치

nGrinder war파일 다운로드 깃헙 장소

위 내용 중 war파일을 다운받으면 된다. 바탕화면에 nGrinder내용 뿌려지는게 싫어서 디렉토리 ngrinder를 팠다.

agent 실행시 다음과 같이 된다.

nGrinder agent를 다운로드 받아서 압축을 풀고 실행해주면 준비완료

처음에 Controller포트번호를 8300으로 지정했었기 때문에 localhost:8300으로 접속하면 nGrinder에 접속할 수 있고,admin 탭의 에이전트 관리탭에서 현재 내가 실행한 에이전트가 녹색불 상태의 ip로 보여진다.

1. 계획

부하테스트란, 서버가 얼마만큼의 요청을 견딜 수 있는가를 테스트하는 방법으로, 작성한 API에 병목현상이 어디에서 발생하고 얼만큼의 트래픽을 수용할 수 있는지 여부를 확인하고자 진행한다.

성능이 좋은지 파악하기위한 지표

  • Users : 동시에 사용할 수 있는 유저 수
  • TPS : 초당 몇개의 테스트를 처리할 수 있는지(Test per second)
  • Time : 얼마나 빠른지

테스트 환경

실제 운영환경과 유사해야한다. 인프라, 데이터 양 모두 같다면 좋다. 또한 외부api를 호출하는 경우 임의의 mock 로직을 포함한 서버를 이용해야 한다. 외부에 의존성이 있는 로직은 언제나 좋지 못하기 때문이다.

target시스템 범위는 스프링부트와 mysql(redis)로 설정했다. 데이터수는 100개 내외이며 성능 테스트 기간은 10분으로 설정했다.

성능 테스트 종류

부하테스트(Load테스트)

평소 트래픽과 최대 트래픽일때 VUser를 계산 후 시나리오를 검증하는 방법이다. 결과에 따라 개선해보며 테스트를 반복한다. 임계값 한계에 도달할때까지 시스템에 부하를 지속적으로 증가해 시스템을 테스트하는 유형이다.

Stress테스트

최대 사용자 혹은 최대 처리량인 경우의 한계점 확인하는 테스트로, 테스트 이후 시스템의 수동 개입없이 시스템이 과부하된 상태에서 자동 복구되는지(어떤 동작을 보이는지)확인하는 테스트방법이다.

nGrinder에서는 이 두 종류 테스트를 지원하는 것 같았다.

성능판단을 위한 대표적인 지표

1. Throughput 처리량

시간당 처리량으로, 이를 수치로 나타내는 세부항목으로 TPS(Test per seconds), RPS(Request per seconds)가 있다. 보통 1초당 처리량을 의미하는 용도로 쓰이며 처리량이 클수록 긍정적인 값을 나타낸다.

유저 수가 증가하면서 더이상 증가하지않고 특정 임계값으로 유지되는 시점을 포화지점이라고 한다. 스트레스 테스트에서 포화지점이 지난 후 tps가 떨어진다면 튜닝이 필요한 시스템이다.

*포화지점 : 초당 처리할 수 있는 처리량 수가 한계에 도달했고, 이후 사용자가 증가하면 Latency가 증가한다는 것을 의미한다. = 해당서버가 감당할 수 있는 한계지점을 의미

2. Latency 지연시간

클라이언트로부터 요청을 받고 응답까지 전체 시간을 의미하며 낮을수록 긍정적인 값을 나타낸다.

클라이언트를 어떤 대상으로 생각하느냐에 따라 여러 세부항목으로 나뉜다.

  • 실제 유저를 클라이언트라고 생각할때 유저가 웹에 요청 후 응답이 화면에 보이기까지의 시간을 의미한다.
  • 웹 브라우저같은 클라이언트 프로세스를 클라이언트라고 생각할때 백엔드 프로세스에서는 요청받고 응답까지 시간을 의미한다.
  • 백엔드 프로세스를 클라이언트라고 생각할때, DBMS를 서버로 생각하면 DBMS가 요청을 받고 응답까지의 시간을 말한다.

Response time = client time + network time + server processing/sending time

Latency의 지표에는 Mean Test Time(평균 테스트 시간)가 대표적이다.

테스트 종합 목표

처음 테스트를 해보는 것이어서 다른 블로그를 참고하면서 작성했다.

Smoke테스트

  • VUser : 10, 30, 50, 99
  • Throughput : 11~34이상(VUser마다 다르다)
  • Latency : 50~100ms 이하

Load테스트

  • 평소 트래픽 VUser : 10
  • 최대 트래픽 VUser : 99
  • Throughput : 11~34 이상(VUser마다 다르다)
  • Latency : 50~100ms 이하
  • 성능 유지기간 10분

Stress테스트

  • VUser 점진적으로 증가시키기

2. 시나리오 수립

  1. 토론 전체 게시판 {x}페이지 조회
  2. 게시글 1개 조회
  3. 게시글에 댓글 작성
  4. 전체 게시판 {x}페이지 조회
  5. 게시글 작성
  6. 인 특정 프로그램의 전체 게시판 {x}페이지 조회
  7. 게시글 수정

토론 게시판 API에는 CRUD가 있었지만 R의 비율이 통상 웹서비스에는 가장 많다고 한다. 따라서 비슷하게 시나리오를 작성해봤다. 여기서 나는 게시글 단건 조회에 대해 캐싱을 적용했었다.

3. 스크립트 작성


import static net.grinder.script.Grinder.grinder
import static org.junit.Assert.*
import static org.hamcrest.Matchers.*
import java.util.Random
import java.util.Arrays
import net.grinder.script.GTest
import net.grinder.scriptengine.groovy.junit.GrinderRunner
import net.grinder.scriptengine.groovy.junit.annotation.BeforeProcess
import net.grinder.scriptengine.groovy.junit.annotation.BeforeThread
import org.junit.Before
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


@RunWith(GrinderRunner)
class TestRunner {
	class RandomIssuer{
        private static int DISCUSSION_TOTAL_NUMBER_BOUND = 5
        private static Random random = new Random()
        private static int DISCUSSION_SUBJECTID_BOUND = 4
        private static List<Integer> subjectIdList = Arrays.asList(1, 38, 70, 74)
        private static int PROGRAM_NUMBER_BOUND = 1001
        public static String getRandomNumber(){
            return String.valueOf(random.nextInt(DISCUSSION_TOTAL_NUMBER_BOUND))
        }
        public static String getSubjectNumber(){
            return String.valueOf(subjectIdList.get(random.nextInt(DISCUSSION_SUBJECTID_BOUND)))
        }
        public static String getProgramNumber(){
            return String.valueOf(random.nextInt(PROGRAM_NUMBER_BOUND))
        }
    }
	public static GTest testRecord1
	public static GTest testRecord2
	public static GTest testRecord3
	public static GTest testRecord4
	public static GTest testRecord5
	public static GTest testRecord6
	public static GTest testRecord7
	public static GTest testRecord8
	public static GTest testRecord9
	
	public static HTTPRequest request
	public Map<String, String> headers = [:]
	public Map<String, Object> params = [:]
	public List<Cookie> cookies = []
	
	//request에 담을 토큰 값 설정
	private static String token = "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJBY2Nlc3NUb2tlbiIsImV4cCI6MTcwNzQ3NTMwOSwiZW1haWwiOiJnaGVucmhrd2s4OEBnbWFpbC5jb20ifQ.92lOzzfSJ_Jv_RxgygdNbyOxlyXCoXd9w3IsYMzf64-Ew7kK-_8qBKgev6xDDoNMo1KnvkZUlRk8aJ4G9ewAHg"

	@BeforeProcess
	public static void beforeProcess() {
		HTTPRequestControl.setConnectionTimeout(300000)
		testRecord1 = new GTest(1, "127.0.0.1")
		testRecord2 = new GTest(2, "127.0.0.1")
		testRecord3 = new GTest(3, "127.0.0.1")
		testRecord4 = new GTest(4, "127.0.0.1")
		testRecord5 = new GTest(5, "127.0.0.1")
		testRecord6 = new GTest(6, "127.0.0.1")
		testRecord7 = new GTest(7, "127.0.0.1")
		testRecord8 = new GTest(8, "127.0.0.1")
		testRecord9 = new GTest(9, "127.0.0.1")
		
		request = new HTTPRequest()
		grinder.logger.info("before process.")
	}

	@BeforeThread
	public void beforeThread() {
		testRecord1.record(this, "test01")
		testRecord2.record(this, "test02")
		testRecord3.record(this, "test03")
		testRecord4.record(this, "test04")
		testRecord5.record(this, "test05")
		testRecord6.record(this, "test06")
		testRecord7.record(this, "test07")
		testRecord8.record(this, "test08")
		testRecord9.record(this, "test09")
		
		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")
	}

//1.토론 전체 게시판 {x}페이지 조회 - 2. 게시글 1개 조회 - 3. 댓글 남기기 -
//4.전체 게시판 {x}페이지 조회 - 5. 게시글 작성 - 6. 특정 프로그램{id} 전체 게시판 {x}페이지 조회
//7. 게시글 수정 - 8. 게시글 1개 조회

	//1.토론 전체 게시판 {x}페이지 조회
	@Test
	public void discussion_total_x() {
		grinder.logger.info("test : 토론 전체게시판 x페이지 조회")
		
		String frontURL =  "http://127.0.0.1:8080/api/v1/discussion/total?page="
		String backURL = "&size=10"
		
		String url = frontURL + RandomIssuer.getRandomNumber() + backURL;
		
		HTTPResponse response = request.GET(url, params)
		
		headers.put("Authorization", token)

		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))
		}
	}
	
	//2. 게시글 1개 조회
	@Test
	public void discussion_subjectId() {
		grinder.logger.info("test : 토론 게시글 단건 조회")
		
		String frontURL=  "http://127.0.0.1:8080/api/v1/discussion/"
		String randomNumber = RandomIssuer.getSubjectNumber()
		
		String url = frontURL + randomNumber
		
		HTTPResponse response = request.GET(url, params)
		
		headers.put("Authorization", token)

		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))
		}
	}
	
	//3. 댓글 1개 남기기
	@Test
	public void discussion_comment() {
		grinder.logger.info("test : 토론 댓글 1개 남기기")

		String url = "http://127.0.0.1:8080/api/v1/discussion/comment"
		String subjectId = RandomIssuer.getSubjectNumber()
		String comment = "very good discussion"

		// JSON 형식의 문자열을 생성할 때는 comment 값을 큰따옴표로 둘러싸주어야 합니다.
		String body = "{\"subjectId\":" + subjectId + ", \"comment\":\"" + comment + "\"}"

		grinder.logger.info(body)

		HTTPResponse response = request.POST(url, body.getBytes())

		headers.put("Authorization", token)

		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))
		}
	}

	
	//4.전체 게시판 {x+1}페이지 조회
	@Test
	public void discussion_total_x1() {
		grinder.logger.info("test : 토론 전체게시판 x페이지 조회")
		
		String frontURL =  "http://127.0.0.1:8080/api/v1/discussion/total?page="
		String backURL = "&size=10"
		
		String url = frontURL + RandomIssuer.getRandomNumber() + backURL;
		
		HTTPResponse response = request.GET(url, params)
		
		headers.put("Authorization", token)

		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))
		}
	}
	
	// 5. 게시글 작성
	@Test
	public void discussion_subject() {
		grinder.logger.info("test : 토론 게시글 작성")

		String url = "http://127.0.0.1:8080/api/v1/discussion/subject"
		String programId = RandomIssuer.getProgramNumber()
		String subjectName = "얼티밋 어벤져스의 다양성과 혁신성"
		String content = "안녕하세요, 어벤져스 팬 여러분! 🦸‍♂️🌟 오늘은 얼티밋 어벤져스에 대한 흥미로운 토론 주제를 가져왔어요. 함께 토론해봐요! 다양성의 힘: 얼티밋 어벤져스에서 등장하는 다양한 캐릭터들은 어떤 면에서 독특하게 표현되었나요? 특정 캐릭터의 다양성이 영화에 어떤 의미를 부여했는지 나눠보세요."

		// JSON 형식의 문자열을 생성할 때는 subjectName과 content 값을 큰따옴표로 감싸주어야 합니다.
		String body = "{\"programId\":" + programId + ", \"subjectName\":\"" + subjectName + "\", \"content\":\"" + content + "\"}"

		grinder.logger.info(body);

		HTTPResponse response = request.POST(url, body.getBytes())

		headers.put("Authorization", token)

		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))
		}

	}
	
	//6. 특정 프로그램{id} 전체 게시판 {x}페이지 조회
	@Test
	public void discussion_program() {
		grinder.logger.info("test : 프로그램별 전체 토론 게시판 x페이지 조회")
		
		String frontURL =  "http://127.0.0.1:8080/api/v1/discussion/program?page="
		String middleURL = "&programId="
		String backURL = "&size=10"
		
		String url = frontURL + RandomIssuer.getRandomNumber() + middleURL + RandomIssuer.getProgramNumber() + backURL;
		
		HTTPResponse response = request.GET(url, params)
		
		headers.put("Authorization", token)

		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))
		}
	}
	
	//7. 게시글 수정
	@Test
	public void discussion_subject_put() {
		grinder.logger.info("test : 토론 게시글 수정")

		String url = "http://127.0.0.1:8080/api/v1/discussion/subject"
		String subjectId = RandomIssuer.getSubjectNumber()
		String subjectName = "어벤져스 캐릭터들, 우리에게 미치는 영향에 대한 생각들"
		String content = "어벤져스는 정말로 특별한 영화에요. 각 캐릭터들은 자신만의 독특한 특징과 능력을 가지고 있어, 우리 각자에게 다양한 감정과 영감을 전하고 있습니다. 아이언맨의 창의력과 용기, 캡틴 아메리카의 헌신과 리더십, 블랙 위도우의 강인함 등 각 캐릭터가 보여주는 가치에 대해 어떻게 생각하시나요? 어벤져스는 우리의 일상에 미치는 영향을 고민해보는 것도 재미있을 것 같아요! 🎥✨"
		String imageUrl = null

		// JSON 형식의 문자열을 생성할 때는 subjectName과 content 값을 큰따옴표로 감싸주어야 합니다.
		String body = "{\"subjectId\":" + subjectId + ", \"subjectName\":\"" + subjectName + "\", \"content\":\"" + content + "\", \"imageUrl\":\"" + imageUrl + "\"}"

		grinder.logger.info(body)

		HTTPResponse response = request.POST(url, body.getBytes())

		headers.put("Authorization", token)

		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))
		}
	}

	//8. 게시글 1개 조회
	@Test
	public void discussion_subjectId_2() {
		grinder.logger.info("test : 토론 게시글 단건 조회")
		
		String frontURL=  "http://127.0.0.1:8080/api/v1/discussion/"
		String randomNumber = RandomIssuer.getRandomNumber()
		
		String url = frontURL + randomNumber
		
		HTTPResponse response = request.GET(url, params)
		
		headers.put("Authorization", token)

		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))
		}
	}
}

이런 내용으로 진행했다. 다만 테스트 db를 활용하였기 때문에 그 과정에서 1~100의 id가 있다면 중간중간 delete요청으로 삭제된 쿼리가 많아서 validate실행시 404에러가 발생했었고 게시글 작성 및 수정 API은 multipart/form-data를 포함하고 있었기에 415에러가 발생했다.

위 스크립트 내용 중 게시글 단건 조회에 대해 캐싱을 적용했을때와 안했을때의 성능차이만을 비교하고 싶었다.

스크립트 작성에서 큰 특징은 여러 테스트 스크립트를 작성할 경우 headers, params, cookies가 static변수로 생성되어있기에 에이전트가 여러 프로세스에서 변수 사용시 동시성 문제로 서버에서 문제가 발생할 수 있다는 점이다. 따라서 단건조회 API url만으로 테스트를 간단하게 실행해볼 수 있었다.

4. 테스트 실행

성능 테스트 설정

Agent : 에이전트 수
VUser per agent : 테스트할 사용자 수
Duration : 테스트할 기간

테스트 결과

VUser가 1000 이상으로 테스트를 시도해볼때 서버가 먹통이 되었다는 글을 본 기억이 있어서 나도 VUser를 10, 30, 50, 99로 나누어 테스트하기로 했다. 결과를 캐싱을 적용했을때와 적용하지 않았을때를 나누어서 비교해봤다. TPS가 높을수록 Mean Test Time(평균 응답시간)이 적을수록 긍정적인 값을 나타낸다고 한다. 따라서 TPS/Mean Test Time으로 판단한다.



case1)캐싱x + VUser : 10

TPS : 54.6
Peak TPS : 73
Mean Test Time : 182.41ms
Executed Tests : 32,454
Successful Tests : 32,454

결과 : TPS/Mean Test Time = 54.6/182.41 = 0.2993



case1)캐싱O + VUser : 10

TPS : 693.9
Peak TPS : 965
Mean Test Time : 13.84ms
Executed Tests : 412,441
Successful Tests : 412,441

📍📍결과 : TPS/Mean Test Time = 693.9/13.84 = 50.1373📍📍

➡️ 캐싱을 적용했더니 TPS가 54.6 -> 약 700까지 올랐다.
➡️ 캐싱 적용안했을때는 Mean Test Time이 200ms~피크 400ms까지를 그리는 그래프가 보였으나 캐싱 적용후 맨 처음 40ms가 가장 피크였고 이후엔 13ms 평균 그래프의 안정적인 그래프를 보여줌
➡️ 평균 응답속도 13.18ms배가 빨라졌다.



case2)캐싱x + VUser : 30

TPS : 57.4
Peak TPS : 75
Mean Test Time : 521.84ms
Executed Tests : 32,017
Successful Tests : 32,017

결과 : TPS/Mean Test Time = 57.4/521.84 = 0.1099



case2)캐싱O + VUser : 30

TPS : 1,167.4
Peak TPS : 1,642
Mean Test Time : 24.65ms
Executed Tests : 697,087
Successful Tests : 697,087

📍📍 결과 : TPS/Mean Test Time = 1,167.4/24.65 = 47.3590📍📍

➡️ 캐싱을 적용했더니 TPS가 57.4 -> 약 1,167.4까지 올랐다. 그래프를 보면 알겠지만 캐싱이 만능은 아니라는 사실이다. 물론 매우 높은 TPS를 보여주며 조회시 빠르게 응답해줄 순 있지만 처리량이 불안정하다.
➡️ 캐싱 적용안했을때는 Mean Test Time이 500ms~피크 1100ms까지를 그리는 그래프가 보였으나 캐싱 적용후 53.123이 피크, 이후에는 안정적이다.
➡️ 평균 응답속도 21.17ms배가 빨라졌다.



case3)캐싱x + VUser : 50

TPS : 57.4
Peak TPS : 72
Mean Test Time : 869.99ms
Executed Tests : 34,128
Successful Tests : 34,128

📍📍 결과 : TPS/Mean Test Time = 57.4/869.99 = 0.0659📍📍



case3)캐싱O + VUser : 50

TPS : 1,405.5
Peak TPS : 1,835
Mean Test Time : 35.32ms
Executed Tests : 836,412
Successful Tests : 836,412

📍📍 결과 : TPS/Mean Test Time = 1,405.5/35.32 = 39.7933📍📍


➡️ 캐싱을 적용했더니 TPS가 57.4 -> 1,405.5까지 올랐다.
➡️ 캐싱 적용안했을때는 Mean Test Time이 869.99ms~피크 2275.216ms까지를 그리는 그래프가 보였으나 캐싱 적용후 사용자가 매우 많이 몰리는 경우를 제외하면 매우 일정했다.
➡️ 평균 응답속도 24.63ms배가 빨라졌다.



case4)캐싱x + VUser : 99

TPS : 63.2
Peak TPS : 75
Mean Test Time : 1,565.53ms
Executed Tests : 37,680
Successful Tests : 37,680

📍📍 결과 : TPS/Mean Test Time = 63.2/1,565.53 = 0.0404📍📍



case4)캐싱O + VUser : 99

TPS : 1,365.9
Peak TPS : 1,828
Mean Test Time : 72.15ms
Executed Tests : 812,114
Successful Tests : 812,114

📍📍 결과 : TPS/Mean Test Time = 1,365.9/72.15 = 18.9313📍📍


➡️ 캐싱을 적용했더니 TPS가 63.2 -> 1,365.9까지 올랐다.
➡️ 캐싱 적용안했을때는 Mean Test Time이 1,565.53ms~피크 2,536.783ms까지를 그리는 그래프가 보였으나 캐싱 적용후 60~120ms의 빠른 응답값을 보였다.
➡️ 평균 응답속도 21.69ms배가 빨라졌다.



병목지점

VUser 99일때가 가장 많은 이용자수라고 보면 TPS 975.5~1.828을 목표 처리량이라고 가정했을때 575.5로 가장 낮은 결과를 보였다. 이 결과를 개선하기 위해서 원인을 분석하고 개선해보자

1. 병목지점 예상하는 곳

현재 테스트한 내용은 spring boot + mysql + redis + 로컬환경 내용이며 테스트 데이터는 100개 내외로 많진 않았다. 병목지점이 될만한 케이스를 분류해보자


DB access의 커넥션 풀이 문제인가?

하카리CP 성능테스트로 최적의 maxPoolSize를 찾아서 TPS를 높일 수 있다. 하지만 데이터 수가 적은채로 테스트했는데 커넥션 풀 사이즈를 늘려야할 이유가 없다고 생각한다. 만약 데이터 수가 천개를 넘어간다면 WAS의 CPU 점유율 모니터링, DB 모니터링(프로메테우스 + 그라파나)하면서 하카리cp 최적화를 하면 된다.

트랜잭션 설정 문제인가?

읽기전용이면 영속성 컨텍스트에서는 스냅샷을 보관하지 않는다. 따라서 메모리 사용량을 최적화하며 엔티티 등록/수정/삭제 제어 동작이 발생하지 않으며 강제로 플러시를 호출하지 않는다면 플러시가 일어나지 않게 되기 때문이다. 나는 CRUD 중 R의 비중이 토론게시판에서는 많지는 않았어서 클래스에 Transaction=false로 적용이 되어있다. 이것이 문제일수도 있다.

이렇게 봤을때는 어떤 문제인지 정확히 가늠하긴 힘들지만 로컬환경에서 테스트를 하다보니 스프링부트만이 아닌 다른 어플리케이션과 함께 사용하면서 생기는 부분일 수 있겠다는 생각이다. PR머지 후 ubuntu환경에서 cpu사용량을 비교하면서 병목지점이 생기는 부분을 파헤쳐보고 다음시간에 개선, 분석해보는걸로 하자

0개의 댓글