SSARTEL-10th / JPTS_bookstudy

"개발자가 반드시 알아야 할 자바 성능 튜닝 이야기" 완전 정복
7 stars 0 forks source link

부하테스트 툴 NGrinder와 JMeter에 대해서 알아보자. #24

Open kgh2120 opened 9 months ago

kgh2120 commented 9 months ago

👍 문제

성능을 튜닝하기 위해선, 현재 기능들의 성능을 알아야겠죠? 우리가 만든 기능들에 대한 성능을 알아보기 위한, 테스트 툴인 NGrinder와 JMeter에 대해서 간단하게 조사해보고, 사용법을 소개해주세요!

✈️ 선정 배경

p.420을 보면 운영 상황에서의 속도를 알기 위해선 부하 테스트 툴로 부하를 준 이후, 성능을 측정해봐야 한다고 한다. 그러니까 부하를 줘야겠지?

📺 관련 챕터 및 레퍼런스

story23. 튜닝의 절차는 그때그때 달라요.

🐳 비고

다들 2달간 고생했어~

Yg-Hong commented 9 months ago

서론

성능 튜닝은 SW 응용 프로그램이나 웹 서비스의 성능을 최적화화기 위한 중요한 단계이다. NGrinder와 Apache JMeter는 성능 테스트 도구 중 하나로, 소프트웨어의 성능을 테스트하고 분석하는 데 사용된다. 이제부터 이에 대해 자세하게 알아보자.

본론

Ngrinder

오픈 소스의 로드 테스팅 및 성능 테스트 도구로, Java 기반으로 만들어졌으며 대규모 분산 애플리케이션의 성능을 측정하는 데 사용된다.

Java 기반으로 만들어졌으므로 플랫폼에 종속적이지 않아 다양한 애플리케이션 및 웹 서비스를 대상으로 성능 테스트를 수행할 수 있으며 사용자 친화적인 대시보드를 통해 성능 데이터를 시각화하고 분석할 수 있다.

Untitled Ngrinder의 주요 특징은 아래와 같다.

  1. 분산 시스템 테스트 : NGrinder는 여러 대의 에이전트 컴퓨터를 사용하여 대규모 분산 테스트를 지원한다. 이를 통해 실제 환경에서의 성능을 더 정확하게 시뮬레이션할 수 있다.
  2. 시나리오 : NGrinder는 테스트 시나리오를 직접 작성하고 관리할 수 있으며, 사용자가 웹 애플리케이션에서 수행하는 다양한 작업을 정의할 수 있다.
  3. Http 등 통신 프로토콜 지원 : 웹 애플리케이션 대상으로 통신 프로토콜을 지원하여 DB, 메시징, FTP(File Transfer Protocol) 드으이 유형의 서비스도 테스트가 가능하다.

NGrinder 성능 테스트 작업 실습

공식 설치 가이드

https://github.com/naver/ngrinder/wiki/Installation-Guide

nGrinder war 파일 다운로드

https://github.com/naver/ngrinder/releases

일단 다운받자.

nGrinder는 JDK8 또는 11에서 사용해야 한다. 나처럼 JAVA_HOME 시스템 환경 변수 설정을 안했다면 얼른 하고 오자. 바보바보 홍윤기바보 nGrinder는  Controller와 Agent로 이루어져 있다.

✔️ Controller
  ├── 성능 측정을 위한 웹 인터페이스 제공
  ├── 테스트 프로세스 조정
  ├── 테스트 통계를 수집하고 표시
  └── 스크립트 수정 기능 제공

✔️ Agent
  ├── 에이전트 모드에서 실행할 때 대상 시스템에 부하를 주는 프로세스 및 스레드를 실행
  └── 모니터 모드에서 실행 시 대상 시스템 성능(CPU/메모리) 모니터링

Untitled 초기 설정은 친절하게 설명해놓은 블로그가 많으니 넘어가겠다.

Untitled 위 화면이 뜬다면 성공적으로 Controller를 띄운것이다.

본격적인 테스트에 앞서 몇가지 준비단계가 필요한다.

  1. 스크립트 만들기 Untitled

NGrinder 는 스크립트 언어로 Groovy를 지원한다. 친숙한 자바 기반 문법을 보면 마음이 편안해지고 스무스하게 코드를 볼 수 있을 것이다. +Create를 누르면 아래와 같은 스크립트가 자동 생성된다.

  1. 스크립트 작성하기
    
    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

/**

애너테이션 설명 적용 메서드 형태 사용 예
@BeforeProcess 프로세스가 호출되기 전 처리할 동작 정의 static method 쓰레드가 공유할 자원을 로드한다.GTest 와 타겟에 대하여 테스트할 레코드를 설정한다.
@AfterProcess 프로세스가 종료되기 전 처리할 동작 정의 static method 리소스를 반납한다.
@BeforeThread 각 쓰레드가 실행되기 전 처리할 동작 정의 member method 타겟 시스템에 로그인한다.쿠키 핸들러와 같은 쓰레드 단위로 가질 데이터를 정의한다.
@AfterThread 각 쓰레드가 실행된 후 처리할 동작 정의 member method 타겟 시스템에서 로그아웃한다
@Before 각각의 @Test 메서드가 수행되기 전 처리할 동작 정의 member method 테스트에 사용할 변수 세팅
@After 각각의 @Test 메서드가 수행된 후 처리할 동작 정의 member method 잘 사용되지 않는다...
@Test 테스트할 동작 정의. 다중호출 됨 member method 테스트

각 메서드 위에 붙어 있는 애노테이션의 설명이다. nGrinder Groovy 테스트를 위해서는 클래스 위에 @RunWith(GrinderRunner) 애너테이션을 꼭 붙여주어야 한다.

@Test 애노테이션으로 각각 수행되어야 할 테스트 행위를 정의한다. 내부에 JUnit assertion을 이용할 수 있다.

스크립트를 모두 작성했다면 Validate 버튼을 클릭하여 groovy 파일을 검사하고 저장한다.

환경이 계속 바뀌는 바람에 … 부동산 실거래가 프로그램 회원가입이 정상적으로 동작하지 않는 이슈가 있어서 무려 21년 싸피 선배님의 테스트 기록을 가져왔다.

import HTTPClient.Cookie
import HTTPClient.CookieModule
import HTTPClient.HTTPResponse
import HTTPClient.NVPair
import net.grinder.plugin.http.HTTPPluginControl
import net.grinder.plugin.http.HTTPRequest
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 static net.grinder.script.Grinder.grinder  
import static org.hamcrest.Matchers.is  
import static org.junit.Assert.assertThat

@RunWith(GrinderRunner)  
class AuthTestRunner {

public static GTest test
public static HTTPRequest request
public static NVPair[] headers = []
public static NVPair[] params = []
public static Cookie[] cookies = []

@BeforeProcess
static void beforeProcess() {
    HTTPPluginControl.getConnectionDefaults().timeout = 6000
    test = new GTest(1, "타겟서버url")
    request = new HTTPRequest()
    headers=[new NVPair("Content-Type","application/json")]
    grinder.logger.info("before process.")
}

@BeforeThread
void beforeThread() {
    test.record(this, "test")
    grinder.statistics.delayReports = true
    request.setHeaders(headers)
    String id = RandomStrIssuer.getRandomNumbers();
    String password = RandomStrIssuer.getRandomNumbers();
    def userParam ='{"id":"'+id+'","password":"'+password+'"}'

    // reset to the all cookies
    def threadContext = HTTPPluginControl.getThreadHTTPClientContext()
    cookies = CookieModule.listAllCookies(threadContext)
    cookies.each {
        CookieModule.removeCookie(it, threadContext)
    }

    request.POST("타겟서버url", userParam.getBytes(),headers)
    HTTPResponse result = request.POST("타겟서버url", userParam.getBytes(),headers)
    cookies = CookieModule.listAllCookies(threadContext)
    grinder.logger.info("before thread.")
}

@Before
void before() {
    def threadContext = HTTPPluginControl.getThreadHTTPClientContext()
    cookies.each {
        CookieModule.addCookie(it ,threadContext)
        grinder.logger.info("{}", it)
    }
}

@Test
void test() {
    HTTPResponse result = request.GET("타겟서버url")
    if (result.statusCode == 301 || result.statusCode == 302) {
        grinder.logger.warn("Warning. The response may not be correct. The response code was {}.", result.statusCode)
    } else {
        assertThat(result.statusCode, is(200))
    }
}

  class RandomStrIssuer {

      private static int NUMBER_LENGTH=5;
      private static int NUMBER_BOUND=100;

      public static String getRandomNumbers(){
          Random random = new Random();
          StringBuilder sb= new StringBuilder();
          for(int i=0; i<NUMBER_LENGTH; i++){
              sb.append(String.valueOf(random.nextInt(NUMBER_BOUND)));
          }
          return sb.toString();
      }
  }
}

beforeThread()에서 로그인 URL로 id와 password 파라미터를 전송하고 로그인을 수행하여 성공한다면 Session을 생성하는 간단한 로그인 동작 확인 스크립트이다. 단 이때, RandomStrIssuer를 이용하여 랜덤 계정으로 로그인을 시도했다.

  1. 테스트 실행

테스트 실행에는 몇가지 파라미터를 설정해야 한다. Untitled

Vuser per agent : 예상되는 가상 사용자의 수 Script : 방금 작성한 스크립트 파일 Duration : 실행되는 테스트 기간 설정 Run Count : 실행 횟수

1개의 서버에서 동시 사용자 100명을 잡고 각 스레드당 5000건을 실행시키는 로그인 부하 테스트를 진행한 결과는 아래와 같다. Untitled

TPS는 평균 933.4로 모든 처리가 error없이 안정적으로 처리되어진다.

자 이제 내 관통프로젝트이다.

아파트 목록을 연월일지역으로 호출하여 가져오는 URL로 호출을 보내자. 파라미터는 1개의 서버에 동시 사용자 148(현재 환경에서 MAX)명을 잡고 각 스레드당 5000건씩 10분간 수행하였다. Untitled

테스트가 진행되는 내내 cpu 사용량이 심상치 않았다. ㅁㅊ;; Untitled 와..와이라노….

테스트결과는 아래와 같다. Untitled 평균 TPS는 101.5로 지옥에 떨어졌다.

조회에 페이징을 구현하지 않은 상황이라, 데이트를 조회하는데 너무 오랜시간이 걸린다는 것이다. 페이징과 캐싱이 반드시 필요해 보인다.

이런식으로 서비스에서 병목이 발생할 만한 곳이나 시스템 성능 상에 이슈가 터진 부분에 대해 구체적인 수치와 함께 부하에 대해 진단하고 이를 해결하기 위한 부분을 예측할 수 있다!

+ nGrinder 테스트 구조 Untitled

Jmeter

Apache 재단에서 서버가 제공하는 성능 및 부하를 측정할 수 있도록 개발한 테스트 툴이다. JMeter는 순수 Java 애플리케이션 오픈소스이며 서버나 네트워크 또는 개체에 대해 과부하를 시뮬레이션하여 강도를 테스트하거나 다양한 부하 유형에서 전체 성능을 분석하는 데 사용할 수 있다.

다양한 프로토콜과 서버에 대한 테스트를 지원한다.

CLI(Command line interface)를 지원한다.

따라서 CI/CD툴과 연동하거나 UI를 사용하는 것보다 메모리 등 시스템 리소스를 적게 사용한다.

다양한 외부 플러그인을 사용하여 기능 확장이 가능하다.

nGrinder와 마찬가지로 시나리오 기반으로 테스트가 가능하다.

핵심 개념_ nGrinder와 많이 유사하다.

마치면서

부하테스트 생각보다 재밌고 신기하다. 내가 만든 서비스에서 어떤 부분이 취약한지도 분석해보는 경험은 새로웠다. 관통 pjt가 완성되면 서비스에 대해 한번씩 확인해보는 것도 괜찮을 것 같다.

Yg-Hong commented 9 months ago

출처는 아래와 같다.

https://jaehoney.tistory.com/224 https://blog.naver.com/wideeyed/222173944239 https://jerry92k.tistory.com/48