이제, 프로젝트에서 실제 사용하는 시나리오별로 API를 하나 이상 호출하여 어떤 지점에서 병목이 발생할 지 확인해보려 한다.
1. 테스트 종류
Smoke Test : 최소한의 부하를 주어 이 부하를 견딜 수 있는 지 확인해보는 테스트 ( VUser 1~2)
Load Test : 평소 트래픽과 최대 트래픽일 때 부하를 견딜 수 있는 지 확인하는 테스트 ( 점진적으로 VUser수를 늘려가며 이 최대치를 늘리는 것이 목표이다.)
Stress Test : 스트레스 상황(최대 부하치에 해당하는 정도의 접속자가 발생한 상황) 에서 시스템이 안정적으로 운영되는지에 대한 테스트
(서비스가 빠르게 복구되는 지, 높은 스트레스 상황에서 안정적인 응답 속도를 보장하는 지 등등 에 대한 테스트 이다)
단순히 조회하는 API 및 인증이 필요한 API를 대상으로 Smoke -> Load -> Stress 순으로 테스트를 진행해보자.
2.테스트 진행
a. Group관련 API
로그인 한 회원이 특정 그룹의 상세 정보를 조회하는 API (Get) 그리고, 그룹장이 특정 이메일을 통해 그룹 가입 권유를 보내는 API(Post) 이렇게 두 개의 API를 테스트 해보려 한다.
getGroupDetail
스크립트
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, "127.0.0.1")
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() {
String email="edbad94c-f15b-4b91-a73a-c4c73f464a66@uos.ac.kr"
String fcmToken="djDBQUAQTcy-Q7yn5Y_uZG:APA91bHrdu3OC_7EK_XS-UnepZs7H0Z29wcphC2JRqrwG-XHWNOwAIPXJoSOUpGc9RbeCDshPryJJXG8Mry_YA2WyXYh06epNUaJkGBeLQcHXK9wU7pEfhtdTuEnaVAL6cSJ60p5Y29v"
Map<String, Object> loginParams = ["email":email,"password":"123","fcmToken":fcmToken]
HTTPResponse loginResponse = request.POST("http://127.0.0.1:8088/auth/login", loginParams)
String jwtHeader
if (loginResponse.statusCode == 301 || loginResponse.statusCode == 302) {
grinder.logger.warn("Warning. The response may not be correct. The response code was {}.", response.statusCode)
} else {
assertThat(loginResponse.statusCode, is(200))
jwtHeader=loginResponse.getHeader("Authorization")
}
grinder.logger.info("Got JWT Header : "+jwtHeader)
String[] split=jwtHeader.tokenize();
String jwt=split[1]+" "+split[2]
headers.put("Authorization",jwt)
request.setHeaders(headers)
HTTPResponse response = request.GET("http://127.0.0.1:8088/group/getGroupDetail/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))
}
}
}
특정 사용자 ID로 로그인하여 1번 그룹의 상세 정보들을 조회하는 Get요청을 테스트 하려 한다.
Smoke Test (VUser=1)
전반적으로, TPS는 10근처에서 왔다갔다하였고 처음 정보를 조회하는 경우에 시간이 다소 소요되었다.
핀포인트로 요청들의 응답시간을 확인해보니, 로그인하는 과정에서 꽤 많은 시간이 소요되었고 그룹 정보를 가져오는 경우 대체적으로 짧은 응답 속도를 보였다.
Load Test (VUser=10)
프로세스를 2, 쓰레드를 5로한 테스트 (2번째 사진)을 수행하니 초반엔 쓰레드를 10개로 한 테스트와 유사한 스코어를 보이다가, 중간에 서버가 뻗어버렸다.
아무래도, 현재 테스트하는 환경 자체가 노트북 하나로 프로메테우스,그라파나, hBase, 핀포인트 등등 모니터링 관련 서버도 실행하며 nGrinder 에이전트도 실행하는 상태로 테스트할 서버 또한 실행하기에 부하가 꽤 심하게 걸리는 것 같다..
한시간 정도 서버를 껐다가 다시 켜니 정상적으로 테스트가 진행되었고, TPS 55, 평균 응답시간 178ms로 준수한 성능을 보였다고 생각한다.
VUser=50
특정 지점에서 병목이 증가하진 않았지만, 전반적으로 응답 시간이 1초 가까이 까지 늘어난 것을 확인했다.
부하를 걸게 되었을 때, CPU사용량이 100%에 가까웠기에 성능에 저하가 발생한 것으로 생각된다.
전체적으로 봤을 때, DB에 커넥션 풀이 소진되어 그걸 기다리는 시간이 큰 것이 주 원인 으로 보인다.
현재,로그인 과정에서 Refresh Token 및 FCM토큰을 DB에 저장하는 부분이 있다.
즉, 이 과정에서 독점로크가 걸리기에 다른 쓰레드들은 멤버 테이블에 접근할 수 없다. (똑같이 수정을 해야하니 독점 로크가 필요하다)
하지만, 갱신 토큰은 현 프로젝트에서 필요 없고 ( 나중에 필요하다면 레디스를 쓰는게 맞고..), FCM토큰 역시 무작정 저장하는 것이 아닌 회원가입 할 때 같이 저장하고, 그 후 로그인 시 일치 여부를 확인한 후에 일치 하지 않는 경우에만 갱신을 하는 것이 DB의 자원을 덜 쓰는 방안이라 생각 된다.
해당 사항들을 모두 수정하고 다시 동일 테스트를 반복해보자
TPS가 40% 증가했고 , 응답시간이 10% 감소했다.
성능 개선은 있었으나, 핀포인트를 보니 여전히 DB Connection을 얻는 과정에서 부하가 많이 걸렸다.
CPU사용량이 100%를 유지하다보니 DB에 연결하는 시간이 늘어나고 그에 따라 다른 요청들이 기다리는 시간 또한 늘어나게 된다..
매 테스트 마다 각 쓰레드가 로그인을 한 후 Get을 통해 정보를 가져오는데, 현재 측정하고자 하는 API는 그룹 정보를 가져오는 API이기에 로그인시간까지 테스트에 포함시키는 것은 주 관심사가 아니다.
따라서, @BeforeThread에 로그인 로직을 넣어 관심사를 분리하고 다시 테스트를 해보자
TPS도 많이 증가했고, 평균 응답 시간도 많이 짧아졌다.
근데, 테스트를 좀 오래 돌려보려했더니 내 맥북 CPU가 과부화 되고 이걸 인텔리제이가 캐치해서 서버를 강제로 꺼버린다..
아무래도, 강한 스트레스를 주는 테스트는 진행하기 힘들 것이란 생각이 들고 보다 정확한 테스트를 위해선 내 데스크탑에서 DB서버와 백엔드 서버 모두 실행하고 맥북에서 모니터링 및 부하 테스트를 하는 방식이 최선일거 같다.
하지만, 지금 처리량 자체를 높이는 것이 목적이 아니고
현재 상태에서 어느정도 성능 개선을 위해 코드를 리팩토링하고 DB설정을 바꾸는 등의 작업을 우선적으로 수행하는 것이 목적이기에
내 맥북에서 그대로 작업을 진행하려 한다.
Stress Test
1초에 쓰레드를 10개씩 증가시키며 스트레스 테스트를 해보았는데1분이 좀 넘어가니 서버가 죽어버렸다.
이로 미루어 보아, 부하량에 따라 약간의 차이는 있지만, CPU사용량이 100%를 유지하는 시간이 약 1분 정도면 서버가 뻗는 듯 하다.
TPS자체는 괜찮게 나오며, 평균 응답시간도 0.2초로( 물론, 서버가 죽기 이전엔 더 빨랐다) 준수하다고 판단된다.
역시 문제는 DB Connection 풀이 다 소모되고, 계속 Pending되는 상태가 유지는 되는 것이였다.
그라파나로 봤을 때도 시작하자마자 펜딩 상태가 급격하게 늘어나고 그게 계속해서 유지되어 DB에 부하가 강하게 걸려 처리를 하지 못하고 계속 대기 상태가 지속되고, CPU를 다 끌어다가 사용해서 계속해서 서버가 꺼지는 것 같다.
우리가 일반적으로 API를 사용한다면, Read API의 사용량이 압도적으로 많다.
때문에, DB 또한 Read를 담당해주는 부분과 CUD를 담당해주는 부분이 다르다면 좀 더 효율적으로 처리가 가능할 것이다. (즉, Master-slave구조가 되야 한다)
따라서, 다음엔 DB에 Master-Slave구조를 적용하고 이를 스프링에도 같이 연동을 하는 작업 등을 통해 성능을 좀 더 개선해보려 한다.
'Spring' 카테고리의 다른 글
부하 테스트 하기 (7) (0) | 2023.07.30 |
---|---|
부하 테스트 하기 (6) - 성능 개선 with Cache (0) | 2023.07.16 |
부하 테스트 하기 (4) - nGrinder 설치 및 스크립트 만들기 (0) | 2023.07.13 |
부하 테스트 하기(3) -핀포인트 오류 잡기 (1) | 2023.07.13 |
부하 테스트 하기 (2) (0) | 2023.07.12 |