Instrumentation이란?


그라인더는 스크립트가 record되어야 하는 스크립트 코드의 부분에 표시하는 것을 허용합니다. 이를 instrumentation이라 합니다. 코드는 Test 함수를 위해 instrumented됩니다. instrumented code가 호출될 때, 테스트의 통계는 업데이트 됩니다. 표준 통계는 에러의 수, call의 횟수, 호출된 시간을 기록합니다. 향상된 스크립트는 앞에서 기록한 통계를 추가할 수 있습니다. 사용자는 객체의 자바 바이트 코드를 수정하기 위해 Test 함수를 사용해 객체를 instrument할 수 있습니다.

from net.grinder.script import Test
from net.grinder.script.Grinder import grinder
  
test1 = Test(1, "Log method")
  
# Instrument the info() method with our Test.
test1.record(grinder.logger.info)
  
class TestRunner:
    def __call__(self):
        log("Hello World")

매번 "Hello World"는 로그파일에 쓰여지고 쓰여진 시간은 그라인더에 의해 record 됩니다.

Instrumentation은 중첩 될 수 있습니다. 예를 들어, 사용자는 Test 1을 가진 함수를 instrument할 수 있고 함수의 코드는 Test 2와 Test 3을 가진 instrument 된 HTTPRequest 호출 할 수도 있습니다. Test 2와 3에 의해서 instrument된 코드는 Test 1 코드 안에 중첩될 수 있습니다. Test 1에 record된 시간은 Test2와 3에 record된 시간의 합보다 커야 합니다. 예를 들어, grinder.sleep()을 호출한 것과 같이 함수 자체에 소요된 시간을 포함합니다.

Instrumentation 쉬운 예제


def Hello() 
    ... 

Test(1, "Hello Test").record(Hello) 

라고 기재하게 되면 Hello 가 한번 실행될때 마다 트랜잭션이 올라갑니다. 

자동 생성된 스크립트를 보면.. 

test1 = Test(1, "Test1") 
request1 = HTTPRequest() 

# Make any method call on request1 increase TPS 
test1.record(request1) 

와 같은 코드가 있는데.. 

request1 이라는 객체의 어떤 메소드라도 호출되면, 이를 트랜잭션으로 치겠다는 겁니다. 

result = request1.GET("http://www.google.com") 

와 같이 GET 이 호출되어 성공하면 트랜잭션이 올라갑니다. 

이런 기술을 Instrumentation 이라고 합니다. 

Selective instrumentation


그라인더 3.7 버젼은 선택적으로 target 객체가 instrument할 수 있도록 허용하는 record의 오버로드 된 버젼을 추가합니다. 

Selective instrumentation은 일반적으로 테스트 통계에 영향을 주는 것 없이 호출이 필요한 보조 함수를 가지는 HTTPRequest 클래스의 instrument 인스턴스에 유용합니다. 아래는 selective instrumentation을 사용하는 예제입니다.

from net.grinder.script import Test
from net.grinder.plugin.http import HTTPRequest
  
test = Test(1, "my test")
  
class GetAndPostFilter(Test.InstrumentationFilter):
  def matches(self, method):
    return method.name in ["GET", "POST"]
  
request = HTTPRequest(url="http://grinder.sourceforge.net")
test.record(request, GetAndPostFilter())
  
class TestRunner:
    def __call__(self):
        # GET() is instrumented, so call statistics are reported.
        request.GET()
  
        # getUrl() is not instrumented, no call statistics are reported.
        print "Called %s" % request.url

Selective instrumentation 쉬운 예제


public static GTest test 
@BeforeProcess 
   test = new GTest(1, "Test1") 
   request = new HTTPRequest() 
   test.record(request) 

정의후에 
  
@Test 함수에서 
request.setUrl() 
request.setsetHeaders() 
request.setData() 
request.POST(uri) 

request 객체의 어떤 함수라도 실행이 되면 TPS에 영향을 미치는 것 같아 GET이나 POST 호출일 때만 업데이트 되기를 원함 이 것을 Selective instrumentation으로 어떻게 변경할까?


정답


GTest test = new GTest(1,  "hello_test"); 
                test.record(request, new GTest.InstrumentationFilter() { 
                        
                        @Override 
                        public boolean matches(Object item) { 
                                return (item.name == "GET" || item.name == "POST"); 
                        } 
                }); 

위와 같은 방식으로 해결할 수 있지만 request 자체를 instrumentation하는 것 보다는 test 자체를 instrumentation하는 것이 더 편합니다. 방식은 아래와 같습니다.


@BeforeProcess 
        public static void beforeProcess() { 
                test = new GTest(1, "Hello"); 
                request = new HTTPRequest(); 
        } 

        @BeforeThread 
        public void beforeThread() { 
                grinder.statistics.delayReports=true 
                grinder.getLogger().info("before thread in MyTest."); 
                // 여기서 Request 가 아닌 doTest 를 Instrumentation 합니다. 
                test.record(this, "doTest"); 
        } 


        @Test 
        public void testHello(){ 
                // 앞에서 request 가지고 일단 장난 치고 
                // request.setUrl() / request.setsetHeaders() / request.setData()  같은 
                // 실제 테스트로 넘깁니다. 
                doTest(request); 
        } 
        
        /** DoTest 자체를 TPS로 처리합니다. */ 
        public void doTest(HTTPRequest request) { 
                request.POST.... 
        } 


'nGrinder' 카테고리의 다른 글

[nGrinder]Instrumentation  (0) 2016.05.21
[nGrinder]스크립트(Groovy) 작성법  (0) 2016.05.21
[nGrinder] 사용법 및 테스트  (0) 2016.05.21
[nGrinder]nGrinder란? & docker 설치 방법  (0) 2016.05.21

스크립트(Groovy)를 작성하는 법을 조사하기 전, 먼저 Groovy가 무엇인지를 설명한 다음 스크립트 작성법, 스크립트로 테스트 설정 페이지를 수정할 수 있는지, 없으면 nGrinder 소스 조사를 설명하겠습니다.

Groovy란?


Groovy는 자바에 파이썬, 루비, 스몰토크등의 특징을 더한 동적 객체 지향 프로그래밍 언어입니다.  JVM에서 동작하고 자바의 강점 위에서 파이썬, 루비, 스몰토크 등의 프로그래밍 언어에 영향을 받은 특징 및 장점이 있습니다. 자바 기반이기 때문에 자바 프로그래머들이 많은 학습을 하지 않아도 프로그래밍을 할 수 있다는 점과 단순화된 문법을 지원하여 코드를 읽고 유지보수하기 편하다는 장점이 있습니다. 

자바와의 비교

그루비의 문법체계는 자바를 계승하고 발전시켰습니다. 자바에 없는 간편 표기법을 지원하는 것 외에 LIST, MAP, 정규식을 위한 구문을 제공함으로써 프로그래밍을 쉽고 간결하게 해줍니다. JVM상에서 동작하는 동적 스크립트 언어인 jython, jruby등과 비교해도 손색이 없습니다. 자바는 소스를 컴파일해야만 사용할 수 있지만, 그루비 소스는 스크립트 파일 그대로 실행시킬 수 있고 자바초럼 컴파일하여 사용할 수도 있습니다. 대부분의 자바 소스는 파일 확장자만 변경하면 수정 없이 그루비에서도 사용할 수 있습니다.

다음은 자바 소스입니다.

public class StdJava
{
  public static void main(String argv[])
  {
    for (String it : new String [] {"Rod", "Carlos", "Chris"})
      if (it.length() <= 4)
        System.out.println(it);
  }
}

이러한 소스를 그루비에서는 간단하게 다음과 같이 표현할 수 있습니다.

["Rod", "Carlos", "Chris"].findAll{it.size() <= 4}.each{println it}

스크립트(Groovy)작성법


테스트를 실행할 수 있는 스크립트는 Groovy, Jython, Groovy Maven Project 총 3가지가 있지만 이 중 Groovy를 중심으로 설명하겠습니다.

Groovy 등장 배경

Groovy는 nGrinder 3.2부터 지원했습니다. Groovy 스크립트는 Jython 스크립트와 다르게 JUnit 기반으로 동작하도록 되어 있습니다. nGrinder 개발에 참여한 NHN 개발자들의 대부분이 JUnit을 사용한 적이 있고 대부분의 IDE에서 JUnit을 지원하기 때문에 개발을 했다는 얘기가 있습니다. Groovy Maven Project는 Groovy 스크립트에 Maven 구조를 더한 형태입니다. 만약 Maven 구조 없이 단독으로 Groovy 스크립트형태로 작성했다면, nGrinder에서 제공하는 스크립트 에디터에서 편집 검증을 해야 하지만, Groovy Maven Project 구조를 사용했다면 이클립스같은 개발환경으로 해당 프로젝트를 import 후 로컬에서 테스트를 작성할 수 있습니다.

nGrinder Groovy 스크립트 기본 생성 소스

먼저 Groovy 스크립트를 생성하거나, Groovy Maven Project에 묶어서 사용해도 스크립트 자체의 형태는 아래와 같이 동일합니다.

package org.ngrinder;
import static net.grinder.script.Grinder.grinder
import static org.junit.Assert.*
import static org.hamcrest.Matchers.*
import net.grinder.plugin.http.HTTPRequest
import net.grinder.plugin.http.HTTPPluginControl;
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 HTTPClient.HTTPResponse
import HTTPClient.NVPair
/**
 * 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
	@BeforeProcess
	public static void beforeProcess() {
		HTTPPluginControl.getConnectionDefaults().timeout = 6000
		test = new GTest(1, "Test1")
		request = new HTTPRequest()
		test.record(request);
		grinder.logger.info("before process.");
	}
	@BeforeThread 
	public void beforeThread() {
		grinder.statistics.delayReports=true;
		grinder.logger.info("before thread.");
	}
	@Test
	public void test(){
		HTTPResponse result = request.GET("http://please_modify_this.com")
		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));
		}
	}
}

nGrinder Groovy 스크립트 내의 클래스에는 JUnit을 nGrinder에 맞게 처리할 수 있도록 @RunWith(Grinder Runner)를 사용합니다.

@RunWith(): 스프링의 테스트 컨텍스트 프레임워크의 JUnit 확장기능 지정

  • JUnit은 각각의 테스트가 서로 영향을 주지 않고 독립적으로 실행하는 것을 기본으로 하기에 각 테스트 클래스마다 매번 오브젝트를 생성합니다. 그러므로 각 테스트 클래스를 지정한 ApplicationContext도 매번 새로 생성되는 상황이 발생합니다. 이를 방지하기 위해 @RunWith annotation은 각 테스트 별로 오브젝트가 생성되더라고 싱글톤의 ApplicationContext를 보장하는 역할을 합니다.

@Test로 annotation이 붙은 method는 쓰레드 내에서 반복 실행됩니다. 이때 일반적인 JUnit Assertion을 사용하여 테스트 결과를 검증할 수 있습니다. Assertion에 실패할 경우, 해당 쓰레드에서 실행한 마지막 테스트가 실패 처리됩니다.

@Test: @Test annotation이 선언되면 해당 method는 테스트 대상임을 뜻합니다.

Assertion: 해당 문장이 실행 될 경우, 이 문장은 참이라고 단언할 수 있는 문장입니다. 즉 개발자가 개발한 프로그램에서 가정하고 있는 사실이 올바른 지 검사할 수 있도록 도와주는 기능입니다. 여기서 Assertion이 아니더라도 try/catch문이나 if/else문으로 에러 검사 기능을 처리할 수 있습니다. 여기서 차이점은 예외는 주로 프로그램을 실행하는 도중 예상하지 못한 상태를 처리하는데 사용합니다. 예를 들어, 파일을 열려고 하는데 열리지 않거나, 네트워크의 연결이 끊어지는 현상 등의 상태를 처리할 때 주로 사용됩니다. 반면 Assertion은 개발자가 참이라고 가정하는 상태를 명시하기 위해 사용됩니다. 예를 들어, 어떤 method가 파라미터로 양수만 입력 받아야 한다고 확신한다면, Assertion을 사용해 해당 사실(파라미터가 양수라는 것)을 명시할 수 있습니다.

다시 말해, 예외는 프로그램의 코드가 실행되는 도중 발생하는 예외인 비정상적인 상태를 처리합니다. 하지만 Assertion은 프로그램이 올바르게 수행될 수 있는 조건을 명시해주어 해당 조건을 만족하는 경우에만 코드가 실행될 수 있도록 합니다. 

nGrinder Groovy 테스트에서는 기존에 많이 사용되던 JUnit의 @BeforeClass, @Before, @AfterClass, @After 대신 다음과 같은 annotation을 사용합니다.



설명적용사용 예

@BeforeProcess

프로세스가 생성될때 실행해야 하는 동작 정의

static method 

- 프로세스 내 쓰레드가 공유할 리소스 파일 로드

- GTest를 사용한 테스트 항목Instrumentation

@AfterProcess

프로세스가 종료하기 직전에 실행해야 하는 동작 정의

static method

리소스파일 닫기

@BeforeThread

각 쓰레드가 실행된 전에 실행해야 하는 동작 정의

member method

- 테스트 대상 시스템 로그인.

- 쓰레드 별 쿠키 핸들러 설정

@AfterThread

각 쓰레드가 종료하기 직전에 실행해야 하는 동작 정의

member method

- 테스트 대상 시스템 로그아웃

@Before

모든 @Test 메소드가 실행되기 전에 실행해야 하는 동작 정의

member method

- 여러 @Test 메소드가 공유하는 로직

- 설정 검증

@After

모든 @Test 메소드가 종료된 이후 실행해야 하는 동작 정의

member method

- 거의 사용 안함

@Test

테스트 동작 정의

member method

Test body

Groovy 스크립트 실행 흐름도



어노테이션 별 분석

@BeforeProcess

	public static GTest test;
	public static HTTPRequest request;
	@BeforeProcess
    public static void beforeProcess() {
        // Instead of Test in Jython, GTest is used here
        // It's because the identifier "Test" is alredy used by the @Test 
        // GTest is the replacement to avoid the naming confliction.
        test = new GTest(1, "test1");
        request = new HTTPRequest();
        test.record(request);
        grinder.logger.info("before process.");
    }
}

모든 쓰레드가 공유할 데이터를 정의하기에 좋은 곳입니다. @BeforeProcess가 붙은 static method는 각 테스트 통계를 수집할 때 사용되는 GTest 인스턴스를 정의하고, request 인스턴스를 바이트 코드 조작(record method)를 통해 레코딩하도록 합니다. request 인스턴스에 대해 method 호출을 하게 되면 테스트 별로 TPS를 증가 시킵니다. 만약 다수의 HTTPRequest 객체를 레코딩해야 한다면 해당 객체를 test.record(request2)와 같이 처리하면 됩니다.

@BeforeThread

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

해당 함수는 쓰레드가 시작되기 전 실행되어야 할 부분을 적습니다. 보통 로그인 같은 테스트 사전 처리 코드를 넣습니다. 다음 실제 테스트를 작성합니다. 아래 @Test와 같이 @Test를 추가해 여러 test 함수를 정의할 수 있습니다.

@Test

	private boolean googleResult; 
	@Test
	public void testGoogle(){
    	googleResult = false;
    	HTTPResponse result = request.GET("http://www.google.com");
    	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));
    	}
    	googleResult = true;
	}
	 
	@Test
	public void testYahoo(){
    	if (!googleResult) {
        	grinder.logger.warn("Just return. Because prev google test is failed.");
        	return;
    	}
    	HTTPResponse result = request.GET("http://www.yahoo.com");
    	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));
    	}
	}

첫 번째 testGoogle() 테스트 함수를 실행한 다음, testYahoo()를 실행합니다. 그런데 googleResult라는 멤버 변수를 사용하여 testGoogle() 함수의 실행결과를 참조합니다. 이러한 방식은 JUnit에서는 각 테스트 별로 각각의 테스트 객체를 생성하기 때문에 불가능합니다. 그러나 GrinderRunner는 이와 같은 제약을 수정하여, 쓰레드 당 한개의 테스트 객체만을 사용합니다. 따라서 googleResult와 같은 멤버 변수 참조도 가능합니다.  

'nGrinder' 카테고리의 다른 글

[nGrinder]Instrumentation  (0) 2016.05.21
[nGrinder]스크립트(Groovy) 작성법  (0) 2016.05.21
[nGrinder] 사용법 및 테스트  (0) 2016.05.21
[nGrinder]nGrinder란? & docker 설치 방법  (0) 2016.05.21

앞서 nGrinder가 어떻게 동작하고, 자주 사용되는 용어를 확인했다면, 지금부터는 실제로 테스트를 하면서 테스트 확인 페이지에 나오는 용어 및 그래프들에 대한 설명을 하겠습니다.

로컬환경



VirtualBoxVirtualBox CPUVirtualBox Memorycontainer nameGuest OSdocker CPUdocker MEMORYDocker
coreos-controller2core2gbcontroller




coreos stable(835.11.0) 

2core2gb




1.8.3



coreos-agent



2core



4gb

agent_1c1g1core1gb
agent_1c2g1core2gb
agent_1c4g1core4gb
agent_2c2g2core2gb
agent_2c4g2core4gb

위와 같은 환경을 설정하기 위해, vm을 총 4개가 아닌 2개만 설정하고 docker에서 컨테이너마다 각 다른 cpu와 memory를 할당하여 테스트를 진행했습니다.

사전 설정


vm을 2개만 사용해 docker 컨테이너마다 자원을 다르게 설정해줘야 합니다.

먼저 nGrinder의 controller와 agent를 설치합니다. (nGrinder란#nGrinder설치-docker 참고)

controller는 설명대로 바로 설치합니다. agent는 cpu와 memory를 아래와 같이 설정하여 줍니다.

$  docker run -d -e 'CONTROLLER_ADDR=ip주소:8080' --memory="1g" --memory-swap -1 --cpu-period=50000 --cpu-quota=25000 --name agent_minimum ngrinder/agent:3.3

--memory  : 해당 컨테이너에 물리적으로 메모리를 얼마나 할당할 지 설정하여 줍니다. 할당받은 메모리를 전부 사용했을 시, host의 메모리를 빌려서 사용합니다.

--memory-swap  : 컨테이너에게 얼만큼 메모리를 빌려줄 지 결정합니다. 만약 --memory="300m" --memory-swap="200m"일 경우, 해당 컨테이너에서 사용할 수 있는 메모리의 양은 500m입니다. 여기서는 정확하게 1gb/2gb/4gb만 사용하기 위해 -1로 메모리를 고정합니다.

--cpu-period, --cpu-quota  :  cpu-priod는 cpu-quota와 함께 사용합니다. cpu-priod의 기본 값은 100000(100ms)로, cpu-quota를 이용해서 100ms 동안 어느정도의 cpu 할당할 것 인지를 설정할 수 있습니다. 만약 cpu-quota가 25000이라면, 1/4만큼의 cpu자원을 사용할 수 있습니다. 여기서 agent vm을 2core로 설정하였기 때문에 1/2만큼의 cpu자원을 사용하도록 하기 위해 --cpu-period는 50000, --cpu-quota는 25000을 정했습니다.


이러한 방식으로 agent를 각기 다른 자원을 할당해 생성합니다.

nGrinder Architechure


세팅한 nGrinder는 다음과 같은 구조로 controller와 agent들이 통신하고 agent들은 test 서버에 부하를 가합니다.


포트 별 통신 내용은 nGrinder란#포트에 정리가 되어 있습니다.


테스트 방법 및 화면 설명


 



URL을 입력하고 버튼을 누르면 보이는 페이지 입니다. 테스트명, 태그, 설명은 저장할 때 해당 테스트를 설명하는 항목입니다.

에이전트는 controller와 연결되어 있는 에이전트의 수 만큼 사용할 수 있습니다. + 버튼을 누르면  이와 같이 설정을 할 수 있습니다.

스크립트는 사용자가 해당 주소로 부하를 걸 스크립트를 보여줍니다.

RHEAD는 선택된 스크립트의 내용을 보여주는 페이지로 이동합니다.

테스트 대상 서버는 테스트를할 서버의 리스트를 보여줍니다. 추가버튼을 눌러 테스트를 받을 서버를 늘릴 수 있습니다.

테스트 기간은 해당 테스트를 얼마 만큼 실행할 지 설정하는 항목입니다. 사용자는 테스트 기간과 실행 횟수 둘 중 하나만 골라 실행할 수 있습니다.

실행 횟수는 사용자가 설정한 쓰레드당 몇 번의 테스트를 실행할 것인지 지정합니다.  사용자는 테스트 기간과 실행 횟수 둘 중 하나만 골라 실행할 수 있습니다.

Ramp_Up은 점차 부하를 가할 수 있는 기능입니다. 점차 부하를 가할 때, vuser의 수를 늘리는 것이 아닌, process나 thread를 늘립니다. 

초기 개수는 처음 시작할 때, vuser의 수를 설정합니다.

초기 대기시간은 테스트를 언제부터 실행시킬 지 설정합니다.

증가 단위는 해당 쓰레드/프로세스를 몇 개씩 증가시킬지 설정합니다.

Ramp-Up 주기는 설정한 것들의 상승 시간을 설정합니다.

샘플링 주기는 그래프 x축에 보여질 '초'를 나타냅니다.

샘플링 무시 횟수는 적혀진 숫자 * 샘플링 주기만큼 데이터가 수집되지 않습니다.

파일 안전 전송은 에이전트에 스크립트를 항상 오류없이 전달하고 싶을 때 체크합니다.

파라미터는 테스트 실행 중에 참조할 수 있는 파라미터를 부여할 수 있습니다.



MTT: 평균 테스트 타임 vuser를 늘리면 MTT또한 비례하게 늘어납니다. 

MTFB: 평균 첫 번째 바이트 도달 시간이며 이것을 Response Time으로 볼 수도 있습니다.


상세 보고서 버튼을 클릭 시, 각 항목에 대해 그래프로 확인할 수 있습니다.


상단 성능 테스트 버튼을 누르면 그동안 테스트했던 결과를 확인할 수 있고, 생성할 수도 있습니다.


웹페이지를 테스트했던 스크립트들을 확인할 수 있고, 생성, 업로드할 수도 있습니다.

시스템 설정


시스템 설정에서 제한이 있는 부분들을 해결할 수 있습니다. 예로 vuser는 에이전트당 3000으로 고정되어 있는 것을 50000으로 풀겠습니다.


controller.max_vuser_per_agent의 주석처리를 푼 다음, 3000을 50000으로 변경하여 저장합니다.

테스트모드로 들어가면 아래와 같이 변경된 것을 확인할 수 있습니다.


이처럼 제한이 된 부분들을 사용자가 원하는 대로 풀 수도 있고, 반대로 제한을 줄 수도 있습니다.

사용자 관리


위 사진처럼 사용자 관리를 들어가면 기본으로 설정되어 있는 사용자를 볼 수 있습니다. 사용자추가 및 삭제는 관리자만 가지고 있는 권한입니다. 사용자를 추가할 시, 기본값으로 General User입니다.

아래 표는 역할 별 부여된 권한입니다.

역할설명
Administrator관리자는 다른 테스트나 스크립트들을 볼 수 있고 모든 유저로 사용자 전환이 가능합니다. 또한 에이전트들을 승인하고 시스템 설정을 변경할 수 있습니다.
Super User슈퍼 유저는 다른 테스트나 스크립트들을 볼 수 있고 모든 유저로 사용자 전환이 가능합니다.
System User시스템 내부적 사용을 위해 예약되어 있습니다. (Reserved for system internal use)
General User일반 유저는 본인의 테스트와 스크립트만 볼 수 있습니다.

로그 파일

nGrinder의 로그는 controller쪽에만 존재하며 컨테이너 내부 log 경로는 다음과 같습니다.

$ /root/.ngrinder/perftest

성능 테스트한 결과의 순번대로 쌓이며 0~999까지의 숫자가 0_999 폴더에 쌓입니다.

테스트


테스트는 로컬환경에서 1core/1g 1개, 1core/2g 1개, 1core/4g 1개 ,2core/2g 1개, 2core/4g 1개로 http://www.google.com/에 대한 부하 테스트를 실행하였습니다. 테스트 스크립트는 기본으로 제공해주는 Groovy를 사용했습니다.

부하 테스트시, 데이터 유실이 다음 2가지로 생길 수 있습니다.

  1. 초당 전송되는 에이전트 CPU/ 메모리 수치
  2. 초당 전송되는 샘플링 결과

동시접속이 많을 시, 서버가 뻗어서 다량의 에러로 멈출 수도 있지만, 에이전트의 성능이 부족하여서 에러가 날 수도 있습니다. 또한 하나의 에이전트에서 많은 vuser를 생성한 경우, 쓰레드의 갯수가 한번에 많이 생성되어 cpu사용률이 확 올라가기 때문에 뻗을 위험도 있습니다.

로컬

테스트는 다음과 같이 진행했습니다. (전체 실행 횟수의 30%이상 에러가 나오면 테스트가 중지 됩니다.)

컨테이너프로세스 갯수스레드
501001502003004005006001000
agent_1c1g1OOOO에러발생 후 종료



agent_1c2g1OOOO1c1g보단 성공률 높지만 에러발생 후 종료



agent_1c4g1OOOO에러율 20%에러 발생 후 종료


agent_2c2g1OOOOOO에러 2~ 70개 사이에러 발생 후 종료
agent_2c4g1OOOOOO에러 0~10개 사이1분간 실행 시, 에러 0~100개 사이성공 or 실패

위 표는 테스트한 결과입니다. 로컬에서 실행해서 부정확한 결과가 나왔습니다. 2core에 메모리 4gb로 1개의 프로세스에서 1000개 쓰레드를 실행 시, 에러를 뱉으면서 테스트가 성공하는 반면, 로컬 피씨가 느려졌을 때 테스트를 시도하면 cpu가 버티지 못해 뻗는 경우가 생겼습니다.

agent의 사양을 2core에 메모리 1g로 세팅 후, 실험 결과 1core 메모리 4g보다 더 많은 스레드를 수용하여 에러를 적게 뱉었습니다. 이유는 프로세스의 갯수가 많아지면 메모리 사용량이 늘고 쓰레드의 수가 늘어나면 cpu의 사용량이 많아지기 때문에 프로세스 1개로 고정한 후, 쓰레드의 수를 늘리면 2core 메모리 1g의 컨테이너가 1 core 메모리 4g보다 더 성능이 좋습니다.

2core 메모리 4g의 agent 경우, 1000명의 가상 유저(thread)로 thread마다 각 6번 씩 실행하도록 설정하여 테스트를 했을 때, 에러를 뱉으면서 성공한 것을 확인했습니다.


agent의 성능을 올리기 위해 open file과 max user process의 갯수를 수정하고자 아래와 같이 명령어를 실행해 확인했습니다.

$ ulimit -a
core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 7907
max locked memory       (kbytes, -l) 64
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1048576
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) 1048576
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited

하지만 ngrinder의 컨테이너는 다음과 같이 이미 10000이상의 숫자가 설정되어 있었고 coreos의 경우 openfile이 1024로 설정되어있어 이를 10000이상의 수로 변경했지만 성능엔 차이가 없었습니다.

서버

서버환경에서는 로컬에서 테스트한 것과는 다르게 프로세스의 수도 변경하여 테스트 하였습니다. 


ipport
controller

ip주소 1

80
agent001

ip주소 2


agent002

ip주소 3



컨테이너프로세스스레드
300
agent001101분간 실행하여 에러 1개
agent00210

에이전트 2개를 연결하여 한꺼번에 프로세스 10개, 스레드 300개를 1분 간 실행했습니다. (총 vuser 6000) 로컬과는 다르게 아래와 같이 에러 1개만 뱉어내며 잘 동작하는 것을 확인할 수 있었습니다.



다음 vuser 제한을 50000으로 풀어 다음과 같은 스펙으로 실험했습니다.

컨테이너프로세스스레드
500
agent001101분간 실행하여 에러 9개
agent00210


동작 시간을 1분이 아닌 2분으로 진행했을 때, 1분 13초 경 메모리 부족 문제로 중지가 되었습니다.


다음 아래와 같은 스펙으로 실험을 진행했습니다.

컨테이너프로세스스레드
700
agent00110메모리 부족문제로 중지
agent00210


메모리가 부족하여 중도 중지 되었습니다.

결론

에이전트의 메모리/CPU와 스크립트가 얼마나 무거운지에 따라 수용할 수 있는 vuser가 다르다는 것을 알 수 있었습니다. 아주 간단한 REST Call의 경우 Groovy 스크립트를 사용하면 1개의 에이전트 (2 core/ 4G)당 5~8000 vuser를 견딜 수 있다고 하는데 직접 해본 결과로는 한 개의 에이전트가 1분간 5000유저를 견디는 것은 가능하지만 그 이상의 시간이 되면 메모리 부족 문제로 중지되는 것을 확인했습니다. 이러한 문제를 해결하기 위해선 테스트 시나리오를 작성한 다음, think time을 넣어 더 많은 사용자를 받을 수 있도록 해야 될 것 같습니다.

'nGrinder' 카테고리의 다른 글

[nGrinder]Instrumentation  (0) 2016.05.21
[nGrinder]스크립트(Groovy) 작성법  (0) 2016.05.21
[nGrinder] 사용법 및 테스트  (0) 2016.05.21
[nGrinder]nGrinder란? & docker 설치 방법  (0) 2016.05.21

nGrinder란?


nGrinder는 네이버에서 성능 측정 목적으로 jython(JVM위에서 파이썬이 동작)으로 개발 된 오픈소스 프로젝트이며, 2011년에 공개 하였습니다. 바닥부터 개발을 한 것이 아니라 The Grinder라는 오픈소스 기반에서 개발 하였습니다. nGrinder는서버에 대한 부하를 테스트 하는 것이므로 서버의 성능 측정이라고도 할 수 있습니다. 성능 측정이란 것은 실제 서비스에 투입 되기 전, 실제와 같은 환경을 만들어 놓고 서버가 사용자를 얼마 만큼 수용할 수 있는지를 실험 할 때 사용합니다. 만약 이와 같은 테스트를 하지 않으면, 엔지니어가 동시 접속자를 1000명정도로 예상하고 이에 맞는 설정을 구성하는데 예상에 넘는 동시 접속자가 발생해 버리면 서버가 죽어버려 서비스를 할 수 없는 문제가 있습니다. 이를 방지하기 위해 본 서비스에 앞서 테스트를 해 서버의 성능을 테스트 하는 것입니다. 

nGrinder Architecture


nGrinder는 Controller, Agent, Targer 서버로 나눠져 있습니다.

Controller:

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

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

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


agent들이 실행될 때, agent들은 컨트롤러와의 연결을 시도합니다. 그것들은 AgentControllerServer 구성요소와 연결이 됩니다. AgentControllerServer는 현재 agent pool을 관리합니다. 유저가 performance test를 시작을 할 때마다, agent들을 조절하는 새로운 콘솔이 생성되고, 요구하는 agent의 수는 AgentControllerServer로부터 조절됩니다. 이 콘솔(SingleConsole이라는 이름의 콘솔은 그라인더 내의 콘솔과 차별화 됩니다.) test script와 test resources를 할당받은 다수의 agent들에게 전송하고 test가 끝날 때까지 test flow 통제를 시작합니다.  이 테스트가 끝난 이후에, 테스트에 사용된 agents들은 AgentControllerServer로 돌아갑니다. 이와 마찬가지로 SingleConsole 또한 ConsoleManager에게 돌아갑니다.

nGrinder와 Grinder사이의 가장 큰 차이점은 nGrinder는 컨트롤러에서 다수의 콘솔 인스턴스와 agents들을 유지하는 것입니다. 각각의 콘솔은 다른 콘솔로부터 독립적이고, 모든 콘솔들은 동시에 실행할 수 있습니다. 많은 agent들은 미리 연결이 될 수 있고, agent가 필요하여 요청을 받을 때마다 언제든지 할달 될 수 있습니다. grinder와 같지 않게, nGrinder는 agent 머신의 활용도를 극대화 하기 위해 개발되었습니다.

'Performance Center' 같이 잘 알려진 부하 테스트 툴은 사용자가 테스트를 시작할 때 agent의 가용성을 보장하기 위해 테스트 예약 기능을 갖습니다. 그러나 예약 기능은 agent 활용도 문제의 원인이 됩니다. 사람들은 실제로 테스트를 하지 않는 동안에도 예방 조치로 에이전트를 보유하는 경향이 있음을 나타냈습니다. 이러한 조사 결과로, 평균적으로 agent의 CPU 활용도는 10% 미만인 것으로 결과가 나왔습니다.

이러한 이유로, 예약 대신, nGrinder는 실제 테스트가 실행될 때, 테스트를 위해 동적으로 agents들을 할당하기 위해 다수의 test와 동적인 agent 할당이 가능하도록 하였습니다. 이러한 것은 nGrinder를 모든 부하 테스트 툴 가운데서, unique한 부하 테스트 툴로 만들었습니다.  agent들의 수가 상대적으로 적게 가지고 있으면, 다수의 사용자들은 다수의 테스트를 동시에 실행할 수 있습니다. 동시에 실행할 수 있는 테스트의 수는 free agent(동적으로 할달될 수 있는 agent)의 수에 의존합니다.

nGrinder 성능 향상

nGrinder는 최대한 OS설정과 독립적으로 운영되도록 구현되어 있습니다. 따라서 agent 성능을 최적화 하기 위한 OS 튜닝 포인트가 아래 리스트를 제외하고는 거의 없습니다.

  • ulimit 파일 오픈 가능 카운트를 1만 이상 유지
    • 성능 테스트시, 소켓을 많이 열 수 있기 때문
  • OS영역을 제외하고 3G정도의 Free 메모리를 유지할 것
  • socket linger option 설정을 조정하여, 소켓이 사용 후 바로 반납 되도록 할 것
    • 스크립트의 socket linger를 설정하지 않으면 OS TCP 설정을 따르기 때문에 둘 중 하나를 선택
    • jython socket 패키지를 사용할 경우 스크립트 상에 다음과 같이 설정하면 가능
      •  clientsock.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack('ii', l_onoff, l_linger))

또한 controller의 성능 향상을 위해 db를 변경해도 개선이 되지 않습니다. (e.g, embedded db -> cubrid)


nGrinder 권장 사양


controlleragentmonitor
Core1~2 Core2 Core1
RAM2GB4GB(free memory 3GB)50MB

agent의 경우, Groovy로 sleep time 없이 단순 HTTP CALL 경우, 6000 vuser까지 15분 가냥 1개의 agent 동작 가능

실제 유저 행위를 시뮬레이션하여 10초 씩 thinktime을 준다면, agent당 1만 vuser(thread)를 견딜 수 있음.

agent는 메모리가 가장 중요한 이슈

nGrinder 사용 용어


Controller:

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

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

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

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

vuser: virtual user로 동시에 접속하는 유저의 수를 의미합니다. 

  • virtual user를 구하는 공식은 vuser=agent*process*thread

TPS: 초당 트랜젝션의 수 - 초당 처리 수  (Tranjaction Per Second)

  • 위 공식으로 에이전트 10개, 프로세스 10개, 각 스레드 300이라면 30,000개의 vuser(thread)가 지속적으로 요청하는 상황. 그래서 30,000번의 요청을 서버에서 1초 미만으로 처리한다면 TPS는 30,000이상이 될 것이고, 30,000번의 요청을 서버에서 2초 이상 으로 처리한다면 TPS는 15,000이하가 됩니다.

트랜잭션:  HTTP Request가 성공할 때마다, 트랜잭션의 수가 1씩 증가합니다.

Peak TPS: 초당 처리 수의 최대치

Response Time: 사용자가 request한 시점(클릭한 시점)에서 시스템이 Response를 할 때까지의 시간

Think Time: 사용자에게 전달된 정보는 사용자가 해당 내용을 인지하고 다음 동작을 취할 때까지의 생각하는 시간이 필요한 시간

nGrinder 설치


nGrinder는 자바기반이기 때문에 사전에 설치할 것들이 필요합니다. nGrinder3.3 버젼에서 필요한 스펙은 아래와 같습니다.

Oracle JDK 1.6 이상 또는 오픈 JDK 1.7 이상, 톰캣 6.X 이상이 필요합니다. 이 테스트에서는 우분투를 사용하기 때문에 우분투 설치 시, TOMCAT 서버를 선택, 설치하여 넘어가겠습니다.

Controller 설치

아래의 명령어로 webapps로 이동하여 내부의 파일 및 폴더를 전부 지운 다음, ngrinder를 설치합니다.

$ cd /var/lib/tomcat7/webapps
$ sudo rm -rf ROOT
$ sudo wget http://sourceforge.net/projects/ngrinder/files/ngrinder-3.3/ngrinder-controller-3.3.war


1 직접 실행하기

nGrinder는 편리하게 한 개의 패키지로 구성하다 보니 꽤 많은 Perm Gen 메모리를 필요로 합니다. 왜냐하면 많은 라이브러리(SVNKit, Maven, Groovy, Jython 등)을 하나로 포함하게 되어 메모리 요구량이 많아 졌기 때문입니다.

아래 명령어를 실행시켜 최대 PermSize를 정해 실행시킵니다

java -XX:MaxPermSize=200m -jar  ngrinder-controller-3.3.war --port 8081

만약 포트를 기본포트인 8080이 아닌, 다른 포트를 사용하고 싶을 경우, --port 포트번호 입력을 하면 됩니다.

2016-01-25 14:58:38,257 INFO  Config.java:115 : nGrinder is starting...
2016-01-25 14:58:38,288 INFO  Config.java:310 : nGrinder home directory:/home/controller/.ngrinder.
2016-01-25 14:58:38,316 INFO  Config.java:341 : nGrinder ex home directory:/home/controller/.ngrinder_ex.
2016-01-25 14:58:38,376 INFO  Home.java:116 : /home/controller/.ngrinder/org_conf
INFO 1/25/16 2:58 PM:liquibase: Reading from DATABASECHANGELOG
INFO 1/25/16 2:58 PM:liquibase: Reading from DATABASECHANGELOG
2016-01-25 15:00:32.613:INFO:/:Initializing Spring FrameworkServlet 'appServlet'
2016-01-25 15:00:49.364:INFO::Started SocketConnector@0.0.0.0:8801

위와 같이 로그를 출력했다면, http://localhost:8081로 접근할 수 있습니다.



2 톰캣에 설치하기

다운로드가 성공 되었으면, http://{설치IP}:8080/ngrinder-controller-3.3로 웹에서 접속할 수 있습니다. 이처럼 접근이 불편할 시, ngrinder-controller-3.3.war 파일을 ROOT.war로 변경할 경우, http://{설치IP}:8080으로 접속할 수 있습니다.


nGrinder 컨트롤러는 메모리를 많이 쓰기 때문에 메모리 설정이 필요합니다. 아래 설정을 안할 경우, 몇 가지 관리자 기능이 동작하지 않습니다. 

  • (리눅스 사용 시) /usr/share/tomcat7/bin/catalina.sh 또는 (윈도우즈 사용 시) 톰캣의 /bin/catalina.bat 파일의 맨 처음에 아래를 입력해 줍니다.
# catalina.sh
JAVA_OPTS="-Xms600m -Xmx1024m -XX:MaxPermSize=200m"    # for catalina.sh
 
# catalina.bat
set JAVA_OPTS=-Xms600m -Xmx1024m -XX:MaxPermSize=200m   # for catalina.bat


간혹 어떤 시스템은 다수의 IP를 가지고 있는 경우가 있습니다. nGrinder는 자동으로 이들 중 하나를 agent의 접속을 기다릴 IP로 선정합니다. 만약 특정 IP를 접속 IP로 설정하고 싶으면 다음과 같은 필드를 ${NGRINDER_HOME}/system.conf 에 기재하면 됩니다.

ngrinder.controller.ipaddress=에이전트가 접속할 IP Address

다음 아래 파일을 실행합니다.

$ /usr/share/tomcat7/bin/startup.sh


다음 톰캣을 재시작 합니다.

$ sudo service tomcat7 restart

nGrinder 설치 - docker


앞으로 controller, agent는 docker에 설치하는 것을 설명하겠습니다.

Controller 설치

아래 명령어로 controller의 이미지를 받습니다.

$ docker pull ngrinder/controller:3.3

다음 아래의 명령어로 컨테이너를 생성합니다.

$ docker run -d -v ~/.ngrinder:/root/.ngrinder -p controller_web_port:80 -p 16001:16001 -p 12000-12009:12000-12009 ngrinder/controller:3.3

controller_web_port는 사용자가 원하는 포트 번호를 입력하면 됩니다. (ex. 8080)



default id와 pw는 admin입니다.

포트

Agent : Any ==> Controller : 16001 
Agent : Any ==> Controller : 12000 ~ 1200x(the number of concurrent tests allowed) 

==>은 단방향 통신을 뜻합니다.

16001 포트는 테스트를 하지 않은 에이전트가 컨트롤러에게 "할 일없으니 테스트 가능" 이란 메세지를 알려주는 포트입니다. 또한 컨트롤러는 "테스트가 실행하는데 해당 테스트는 1200x에서 발생하니, 해당 포트에 접속해서 테스트 실행 준비"라는 메세지를 에이전트에게 지시를 합니다.

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

Agent 설치

아래 명령어로 agent의 이미지를 받습니다.

$ docker pull ngrinder/agent:3.3

다음 명령어로 agent 컨테이너를 생성합니다.

$ docker run -d -e 'CONTROLLER_ADDR=controller_ip:controller_web_port' ngrinder/agent:3.3
controller_ip: controller의 아이피
controller_web_port: 앞서 controller의 컨테이너르 생성할 때, 정한 포트



'nGrinder' 카테고리의 다른 글

[nGrinder]Instrumentation  (0) 2016.05.21
[nGrinder]스크립트(Groovy) 작성법  (0) 2016.05.21
[nGrinder] 사용법 및 테스트  (0) 2016.05.21
[nGrinder]nGrinder란? & docker 설치 방법  (0) 2016.05.21

+ Recent posts