docker를 통한 ngrinder 설치 및 사용

|

nGrinder 란?

네이버에서 성능 측정 목적으로 개발된 오픈소스 프로젝트이며, The Grinder라는 오픈소스 기반으로 개발되었다. nGrinder는 서버에 대한 부하 테스트를 하는 것으로 서버의 성능을 측정할 수 있다.

nGrinder Architecture

Controller, Agent, Target 서버로 나누어져 있음.

[ngrinder system 아키텍쳐 이미지]

Controller

  • 퍼포먼스 테스팅(부하테스트)를 위해 웹 인터페이스를 제공
  • 테스트 프로세스를 체계화
  • 테스트 결과를 수집해 통계로 보여줌

Agent: Controller의 명령을 받아 실행.

  • agent 모드가 실행될 때 target이 된 머신에 프로세스와 스레드를 발생시켜 부하를 발생.
  • moniter 모드가 실행되면 대상 시스템의 cpu와 memory를 모니터링.

Target: 부하테스트를 받는 머신.

Controller 및 Agent 설치 및 설명 (도커로 설치)

docker

직접 다운받아서 설치해도 되지만 귀찮은 것들을 만져줘야하는 번거로움이 있어 docker사용을 추천한다.

docker 설치 참고 자료

  • mac: https://docs.docker.com/docker-for-mac/install/
  • windows: https://docs.docker.com/docker-for-windows/install/
  • linux: https://docs.docker.com/install/linux/docker-ce/centos/

nGrinder controller:

$ docker run -d -v ~/ngrinder-controller:/opt/ngrinder-controller -p 8080:80 -p 16001:16001 -p 12000-12009:12000-12009 ngrinder/controller:3.4

grinder controller는 포트옵션으로 웹 포트, 에이전트와의 연결, 부하관리를 위한 포트들로 구성되어 있으며 자세한 내용은 https://hub.docker.com/r/ngrinder/controller/ 에서 확인할 수 있다.

nGrinder agent:

$ docker run -v ~/ngrinder-agent:/opt/ngrinder-agent -d ngrinder/agent:3.4 controller_ip:controller_port
agent는 controller_ip:controller_webport 부분을 옵션 argument로 전달해야 한다.

ex) controller가 떠있는 instance의 public ip가 192.168.100.12이고 80번 포트를 웹포트로 열었다면 “192.168.100.12:80” 을 뒤에 붙여주면 된다.

실행하기

브라우저에서 아래 주소 입력

http://controller_ip:port
id: admin
pw: admin

Agent: Any ==> Controller: 16001 Agent: Any ==> Controller: 12000 ~ 1200x

==> 는 단방향 통신을 뜻함.

16001 : 테스트를 하지 않은 에이전트가 컨트롤러에게 “할 일 없으니 테스트 가능” 이라는 메세지를 알려주는 포트

  • 컨트롤러는 “테스트가 실행하는데 해당 테스트는 1200x에서 발생하니, 해당 포트에 접속해서 테스트 실행 준비” 라는 메세지를 에이전트에게 지시를 한다.

12000 ~ 1200x 포트는 “테스트 실행, 테스트 종료” 와 같은 컨트롤러 명령어와 에이전트별 테스트 실행 통계를 초별로 수집하는 포트.

nGrinder 관련 용어 설명

[ngrinder 설정화면]

  • vuser : virtual user로 동시에 접속하는 유저의 수를 의미. (vuesr = agent * process * thread)
  • TPS : 초당 트랜잭션의 수 - 초당 처리 수
  • 트랜잭션 : HTTP Request가 성공할 때마다, 트랜잭션 수가 1씩 증가.
  • Peak TPS : 초당 처리 수의 최대치.
  • Response Time : 사용자가 request한 시점에서 시스템이 response할 때까지 걸린 시간.
  • Think Time : 사용자에게 전달된 정보는 사용자가 해당 내용을 인지하고 다음 동작을 취할 때까지의 생각하는 시간

스크립트

  • nGrinder 에 로그인을 하면 스크립트 관리 화면에서 SVN URL을 확인할 수 있다. (ngrinder 에 svn 이 내장되어있다.)

[ngrinder 스크립트 관리]

예를 들어 위 화면에서는 http://ngrinder-staging.nhncorp.com:8080/svn/admin/project SVN URL로 Groovy 메이븐 프로젝트를 접근할 수 있다.

인텔리제이에서 엔그라인더의 그루비 스크립트 메이븐 임포트 시 참고자료

  • https://github.com/naver/ngrinder/wiki/Import-Groovy-Maven-Project-in-IntelliJ

스크립트 작성 시 참고

  • https://junoyoon.tistory.com/entry/Groovy-%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EA%B5%AC%EC%A1%B0?category=487801

인텔리제이에서 엔그라인더 그루비 스크립트 실행 시 vm option 에 추가.

-javaagent:/Users/jmlim/.m2/repository/net/sf/grinder/grinder-dcr-agent/3.9.1/grinder-dcr-agent-3.9.1.jar

Get, Post 요청 샘플 스크립트.

  • application/json 요청 (jsonBody 형태의 요청일 경우..)
import HTTPClient.Cookie
import HTTPClient.CookieModule
import HTTPClient.HTTPResponse
import HTTPClient.NVPair
import groovy.json.JsonOutput
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

/**
 * ngrinder get, post 요청 샘플 스크립트.
 */
@RunWith(GrinderRunner)
class SampleGetPostRunner {

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

    static String url = "http://테스트할아이피:포트"
    static String commonPath = "/test"

    static String findUrl = url + commonPath + "/find";
    static String createUrl = url + commonPath + "/create"

    @BeforeProcess
    static void beforeProcess() {
        HTTPPluginControl.getConnectionDefaults().timeout = 6000
        test = new GTest(1, "api.test.com")
        request = new HTTPRequest()
        // Set header datas
        List<NVPair> headerList = new ArrayList<NVPair>()
        headerList.add(new NVPair("Content-Type", "application/json"))
        headerList.add(new NVPair("Authorization", "Bearer 토큰토큰"))

        headers = headerList.toArray()
        grinder.logger.info("before process.");
    }

    @BeforeThread
    void beforeThread() {
        test.record(this, "test")
        grinder.statistics.delayReports = true;
        grinder.logger.info("before thread.");
    }

    @Before
    void before() {
        request.setHeaders(headers)
        cookies.each { CookieModule.addCookie(it, HTTPPluginControl.getThreadHTTPClientContext()) }
        grinder.logger.info("before thread. init headers and cookies");
    }

    @Test
    void test() {
        find();

        create();
    }


    /**
     * get test
     */
    void find() {
        HTTPResponse result = request.GET(findUrl)
        resultCheck(result)
    }

    /**
     * post test
     */
    void create() {
        String goodsId = "1234";
        Integer quantity = 2;

        Map<String, Object> paramData = new HashMap<>();
        List<Map<String, Object>> items = new ArrayList<>();
        Map<String, Object> item = new HashMap<>();
        item.put("goods_id", goodsId);
        item.put("quantity", quantity);
        items.add(item);
        paramData.put("items", items);

        String json = JsonOutput.toJson(paramData);
        HTTPResponse result = request.POST(createUrl, json.getBytes())
        resultCheck(result)
    }

    /**
     * 결과값 체크
     * @param result
     */
    void resultCheck(result) {
        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));
        }
    }
}

  • 일반 form post 요청일 경우
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

/**
 * ngrinder get, post 요청 샘플 스크립트.
 */
@RunWith(GrinderRunner)
class TestRunner {

    public static GTest test
    public static HTTPRequest request
    public static NVPair[] headers = []

    static String url = "https://타겟url"
    static String commonPath = "/api"

    static String appinitUrl = url + commonPath + "/app"

    @BeforeProcess
    static void beforeProcess() {
        HTTPPluginControl.getConnectionDefaults().timeout = 6000
        test = new GTest(1, "타겟명")
        request = new HTTPRequest()
        grinder.logger.info("before process.");
    }

    @BeforeThread
    void beforeThread() {
        test.record(this, "test")
        grinder.statistics.delayReports = true;
        grinder.logger.info("before thread.");
    }

    @Before
    void before() {
        request.setHeaders(headers)
    }

    @Test
    void test() {
        NVPair param1 = new NVPair("appcode", "999");
        NVPair param2 = new NVPair("appversion", "x");
        NVPair param3 = new NVPair("hashcode", "1122334455667788");

        NVPair[] params = [param1, param2, param3]

        HTTPResponse result = request.POST(appinitUrl, params)
        resultCheck(result)
    }

    /**
     * 결과값 체크
     * @param result
     */
    void resultCheck(result) {
        println result
        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));
        }
    }
}

  • 파일 업로드 post 요청 (multipart/form-data)
import HTTPClient.Codecs
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

/**
 * ngrinder get, post 요청 샘플 스크립트.
 */
@RunWith(GrinderRunner)
class MultipartTestRunner {

    public static GTest test
    public static HTTPRequest request
    public static NVPair[] headers = []

    static String url = "http://타겟url"
    static String commonPath = "/api"

    static String moreUploadUrl = url + commonPath + "/upload-app-file"

    @BeforeProcess
    static void beforeProcess() {
        HTTPPluginControl.getConnectionDefaults().timeout = 6000
        test = new GTest(1, "http://타겟명")
        request = new HTTPRequest()
        grinder.logger.info("before process.");
    }

    @BeforeThread
    void beforeThread() {
        test.record(this, "test")
        grinder.statistics.delayReports = true;
        grinder.logger.info("before thread.");
    }

    @Before
    void before() {
        headers = [
                new NVPair("Content-Type", "multipart/form-data")
        ]
        request.setHeaders(headers)
    }

    @Test
    void moreUpload() {
        NVPair param1 = new NVPair("appcode", "999");
        NVPair param2 = new NVPair("appversion", "17");
        NVPair param3 = new NVPair("hashcode", "11223344556677");

        NVPair[] params = [param1, param2, param3]

        NVPair[] files = [new NVPair("userfile", "경로/파일명.확장자")]

        def data = Codecs.mpFormDataEncode(params, files, headers)
        HTTPResponse result = request.POST(moreUploadUrl, data)

        resultCheck(result)
    }

    /**
     * 결과값 체크
     * @param result
     */
    void resultCheck(result) {
        println result
        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));
        }
    }
}

참고자료:

  • https://brownbears.tistory.com/25
  • http://blog.naver.com/PostView.nhn?blogId=simpolor&logNo=221318391959&parentCategoryNo=&categoryNo=27&viewDate=&isShowPopularPosts=true&from=search
  • https://junoyoon.tistory.com/entry/%EC%9D%B4%ED%81%B4%EB%A6%BD%EC%8A%A4%EC%97%90-Groovy-%EB%A9%94%EC%9D%B4%EB%B8%90-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%9E%84%ED%8F%AC%ED%8A%B8
  • https://gist.github.com/ihoneymon/a83b22a42795b349c389a883a7bbf356

Comments