Spring

부하 테스트 하기 (7)

시롱시롱 2023. 7. 30. 15:44

이제 nGrinder로 다시 부하를 걸어보자

 

VUser=50

 

캐시를 사용하기 전보다 아주 훌륭한 성적을 보였다.

전에는 1~2분만에 서버가 다운되었는데, 이번엔 서버가 다운되지는 않았다. (물론, CPU과열로 쓰로틀링이 되는 것 같긴 했다)

 

이제 Read하는 경우에 대한 테스트 및 성능 개선은 수행해보았으니 CUD가 섞인 요청도 해보려 한다.

 

시나리오 : 회원 가입 -> 로그인 -> (그룹장이) 가입 권유-> 받은 가입 권유 승낙 -> 그룹 게시물에 댓글 남기기 -> 해당 게시물 조회

5개의 POST요청과 1건의 GET요청으로 이루어진 시나리오이다.

 

대충, 생각해보면 POST요청 각각에 시간이 소요될 것이고 댓글 목록에 페이징을 적용하지 않았으니 아마 시간이 지나면 게시물을 조회하는 부분에서 큰 병목이 발생할 것이다.

 

우선 스크립트를 작성해보자

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 test1
	public static GTest test2
	public static GTest test3
	public static GTest test4
	public static GTest test5
	
	public static HTTPRequest request
	public static Map<String, String> headers = [:]
	public static Map<String, Object> params = [:]
	public static List<Cookie> cookies = []
	public static String userJwt
	public static String adminJwt
	public static String email
	
	@BeforeProcess
	public static void beforeProcess() {
		HTTPRequestControl.setConnectionTimeout(300000)
		test1 = new GTest(1, "127.0.0.1")
		test2 = new GTest(2, "127.0.0.1")
		test3 = new GTest(3, "127.0.0.1")
		test4 = new GTest(4, "127.0.0.1")
		test5 = new GTest(5, "127.0.0.1")
							
		request = new HTTPRequest()
		grinder.logger.info("before process.")
	}

	@BeforeThread
	public void beforeThread() {
		test1.record(this, "test1")
		test2.record(this, "test2")
		test3.record(this, "test3")
		test4.record(this, "test4")
		test5.record(this, "test5")
		grinder.statistics.delayReports = true
		//get admin JWT
		
		Map<String, Object> adminLoginParams = ["email":"sok5188@uos.ac.kr","password":"123","fcmToken":"djDBQUAQTcy-Q7yn5Y_uZG:APA91bHrdu3OC_7EK_XS-UnepZs7H0Z29wcphC2JRqrwG-XHWNOwAIPXJoSOUpGc9RbeCDshPryJJXG8Mry_YA2WyXYh06epNUaJkGBeLQcHXK9wU7pEfhtdTuEnaVAL6cSJ60p5Y29v"]
		
		HTTPResponse adminLoginResponse = request.POST("http://127.0.0.1:8088/auth/login", adminLoginParams)
		
		if (adminLoginResponse.statusCode == 301 || adminLoginResponse.statusCode == 302) {
			grinder.logger.warn("Warning. The response may not be correct. The response code was {}.", adminLoginResponse.statusCode)
		} else {
			assertThat(adminLoginResponse.statusCode, is(200))
			def adminJwtHeader=adminLoginResponse.getHeader("Authorization")
			grinder.logger.info("adminJwt Header: "+adminJwtHeader)
			String[] splits=adminJwtHeader.toString().tokenize();
			adminJwt=splits[1]+" "+splits[2]
		}
		
		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 test1(){
		//Sign Up
		def id = UUID.randomUUID().toString()
		email=id+"@uos.ac.kr"
		params.put("email",email)
		params.put("password","123")
		params.put("classOf","111")
		params.put("name","nGrinderTest")
		params.put("nickname","nGrinderUser")
		
		HTTPResponse response = request.POST("http://127.0.0.1:8088/auth/signup", 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))
		}
		
		//Login
		Map<String, Object> loginParams = ["email":email,"password":"123","fcmToken":"djDBQUAQTcy-Q7yn5Y_uZG:APA91bHrdu3OC_7EK_XS-UnepZs7H0Z29wcphC2JRqrwG-XHWNOwAIPXJoSOUpGc9RbeCDshPryJJXG8Mry_YA2WyXYh06epNUaJkGBeLQcHXK9wU7pEfhtdTuEnaVAL6cSJ60p5Y29v"]
		
		HTTPResponse loginResponse = request.POST("http://127.0.0.1:8088/auth/login", loginParams)
		
		if (loginResponse.statusCode == 301 || loginResponse.statusCode == 302) {
			grinder.logger.warn("Warning. The response may not be correct. The response code was {}.", loginResponse.statusCode)
		} else {
			assertThat(loginResponse.statusCode, is(200))
			def userJwtHeader=loginResponse.getHeader("Authorization")
			grinder.logger.info("userJwt Header: "+userJwtHeader)
			String[] split=userJwtHeader.toString().tokenize();
			userJwt=split[1]+" "+split[2]
		}
	}
	@Test
	public void test2() {
		//send request to join Group 1
		headers.put("Authorization",adminJwt)
		grinder.logger.info("adminJwt:"+adminJwt)
		request.setHeaders(headers)
		Map<String, Object> inviteParams = [:]
		inviteParams.put("email",email)
		inviteParams.put("groupId",1)
		HTTPResponse response = request.POST("http://127.0.0.1:8088/group/sendInviteByEmail",inviteParams)

		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))
		}
	}
	@Test
	public void test3() {
		//accept Member
		headers.put("Authorization",userJwt)
		grinder.logger.info("userJwt:"+userJwt)
		request.setHeaders(headers)
		HTTPResponse response = request.PUT("http://127.0.0.1:8088/group/acceptInvite/1")

		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))
		}
	}
	@Test
	public void test4() {
		Map<String, Object> commentParams = [:]
		commentParams.put("gruopId",1)
		commentParams.put("parentCommentId",-1)
		commentParams.put("text","hello ! from nGrinder")
		commentParams.put("postId",1)
		HTTPResponse response = request.POST("http://127.0.0.1:8088/board/addPostComment", commentParams)

		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))
		}
	}
	@Test
	public void test5() {
		HTTPResponse response = request.GET("http://127.0.0.1:8088/board/getPost/1")

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

 

간단히 SmokeTest부터 해보자

VUser=1

평균 응답 시간 0.1초로 아주 무난한 결과가 나왔다.

 

이제 부하량을 좀 올려 Load Test를 해보자

 

VUser=10

 

멀티 쓰레드로 돌리니 에러가 엄청 터졌다.

 

로그인, 회원가입 부분에서도 문제가 있는 것 같아 따로 다시 확인해보니

작지만 에러가 발생하고 있었다.

 

서버 로그를 보면

이런 오류 로그가 남았고

이렇게 DB에 fcmToken이 NULL인 상태로 남아있는 경우가 발견되었고, 또 처음 실행하는 부분에서 같은 UUID로 중복해서 가입을 진행하는 현상이 발견되었다..

 

이유를 천천히 생각해보다 동시성 문제가 발생한 것 같다는 생각이 들었다..

 


면접,장염, 알고리즘 아카데미 등등 으로 인해 한동안 해당 이슈를 해결하지 못했는데 다음 포스팅에서 동시성 문제에 대해 다뤄보고 이에 대한 해결책을 프로젝트에 적용해보며 문제를 해보려 한다.