본문 바로가기
Log/Trouble shoot

Troubleshoot_Ngrinder 테스트 중도 멈춤 문제

by RIEM 2023. 3. 8.

Time: 2023-03-07 22:53
doc-id : tsl-management-v1.2- 20230307

Problem

- Ngrinder를 사용하여 어플리케이션의 경매 정보 데이터 생성 API의 성능을 테스트하고 있는 상황이다. Ramp up으로 점진적으로 VUser을 늘려나가다 1240명이 되면 테스트가 종료되는 문제를 발견했다.
목표하고있는 데이터 생성 개수는 30,000개이고 그중 성공 횟수는 평균적으로 12,500개로 약 40-42% 사이 수준이며, 이 이상 올라가지 못하고 있는 상황.

Solution

  • nGrinder 스크립트를 변경하니 테스트가 도중에 중지되지 않고 잘 진행되었다
  • 아마도 일정 수준 이상의 에러가 발생하면 nGrinder가 자체적으로 테스트를 중단시키는 것으로 추정된다.

Discussion

시도 1 : nGrind script 요청 형식 수정(동일값 -> 임의값)

현재 Ngrinder Post 요청 시 동일한 데이터를 중복으로 생성하도록 시도하고 있는 상황이다. 혹시 동일한 데이터가 아니라 랜덤한 값을 줘야하는 것이 아닐까? 왠지 POST 요청되는 body값이 동일한 것 때문에 문제가 발생하는 것 같다는 직감이 든다.

Screen Shot 2023-03-07 at 11 00 35 PM

테스트 진행하다가 도중에 또 멈췄다. 그런데 랜덤값을 적용하기 전 평균 Vuser 1240명대에서 중지되었던 반면 이번에는 VUser 1250명에서 중지되었다. 지금까지 가장 높은 수치다. 그래도 10명 차이 밖에 안난다.

시도2 : 스케일업

그래서 끝까지 미뤄두고 있던 스케일링 카드를 써보기로 했다. 두 개의 카드를 생각했는데, 하나는 스케일아웃을 통한 수평적 확장이고 다른 하나는 스케일업을 통한 수직적 확장이다. 결국 하나의 고품질 성능에 의존할 것인가 아니면 다수의 머신에 의존할 것인가를 선택하는 문제이다.

처음 바로 떠오르는 생각은 스케일업이다. 가용성을 생각한다면 스케일아웃을 통해 다운이 되지 않는 것이 중요하지만 우선 지금은 테스트 환경이기 때문에 가용성 보다는 현재 API 관련 문제의 원인을 찾는 것이 더 중요하다. 이를 위해서는 변수를 통재하는데, 스케일 아웃보다는 스케일업이 성능 하나만 올리기 때문에 즉, 성능에 대한 변수만 바뀌기 때문에 이전 실험과의 비교가 유리할 것이라고 판단했다.

Screen Shot 2023-03-07 at 11 24 05 PM
db.t3.micro -> db.t3.small로 RAM을 2배 수준으로 업그레드해보자. medium이 아니라 small이다.

Screen Shot 2023-03-07 at 11 27 58 PM

  • Vuser : 3000
  • Run count : 10
  • Total request : 30000
  • Ramp-up : True
  • 테스트 시작 전 DB 데이터 개수 : 12915
  • 테스트 완료 후 DB 데이터 개수 : 25643
  • 테스트 중 생성 데이터 개수 : 12728
  • 데이터 생성 성공율 : 42.4%

웹서버 CPU 사용률은 5.61%로 거의 동일한데, DB CPU 사용률은 13.8%에서 9.4%로 감소했다. 이는 t3.micro에서 t3.small로 메모리 1기가가 추가된 영향으로 판단된다. 하지만 게시글 생성 성공률의 개선에는 효과가 없었다.

시도3 : nGrinder(테스트 옵션 thread -> Process 단위)

Screen Shot 2023-03-08 at 12 20 22 AM

nGrinder 옵션 변경 : thread 단위 -> process 단위로 수정해보았다.

  • Vuser : 3000
  • Run count : 10
  • Total request : 30000
  • Ramp-up : True
  • 테스트 시작 전 DB 데이터 개수 : 0
  • 테스트 완료 후 DB 데이터 개수 : 23470
  • 테스트 중 생성 데이터 개수 : 23470
  • 데이터 생성 성공율 : 78.2%

Screen Shot 2023-03-08 at 12 17 38 AM

VUser 1445 지점에서 급 하락하긴 했지만 테스트가 중지되지 않고 끝까지 완료되었다. 게다가 게시글 생성 성공 횟수는 30,000개 중 총 23,470개로 약 78.2%까지 상승했다.

하지만 여전히 게시글 성공률이 78%로 불완전한 수준이고, 그래프 또한 변동 폭이 크다.

시도 4: nGrinder 스크립트 변경

nGrinder 스크립트를 변경하니까 정상적으로 테스트가 진행되었다!

변경 전

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  \"bidSize\": \"275\",\n  \"bidPrice\": \"232500\",\n  \"bidQuantity\": \"1\",\n  \"raffle\": \"10001\",\n  \"user\": \"100\"\n}"
    public static List<Cookie> cookies = []

    @BeforeProcess
    public static void beforeProcess() {
        HTTPRequestControl.setConnectionTimeout(300000)
        test = new GTest(1, "<ip주소>")
        request = new HTTPRequest()

        // Set header data
        headers.put("Content-Type", "application/json")
        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() {

        int bidSize = Math.abs( new Random().nextInt() % (290 - 270) ) + 270
        int bidPrice = Math.abs( new Random().nextInt() % (300000 - 145000) ) + 145000
        int bidQuantity = Math.abs( new Random().nextInt() % (7 - 1) ) + 1
        int raffle = Math.abs( new Random().nextInt() % (9000 - 3) ) + 3
        int user = Math.abs( new Random().nextInt() % (10000 - 1) ) + 10000
        Map<String, Object> request_data = ["bidSize": bidSize, "bidPrice": bidPrice, "bidQuantity": bidQuantity, "raffle": raffle, "user": user]
        HTTPResponse response = request.POST("http://<ip주소>/bids", request_data)

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

변경 이후

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, "<ip주소>")
        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() {
        //int raffleId = new Random().nextInt(10000 - 9001) + 9001
        //int user = new Random().nextInt(200000 - 1) + 1
        //int amount = (new Random().nextInt(901) + 100) * 1000
        Map<String, Object> request_data = ["raffle": 10001, "user": 100, "bidSize": 275, "bidPrice": 232500, "bidQuantity": 1]
        HTTPResponse response = request.POST("http://<ip주소>/bids", request_data)
        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))
        }
  }
}

댓글