top bar

글 목록

2016년 4월 2일 토요일

[JAVA] Deal with 'ConcurrentModificationException'

개요



 자바의 Collection을 다루다 보면, 'ConcurrentModificationException'을 마주칠 때가 있다. 이는 동시성 이슈와 관련된 Exception이기때문에 병렬 스레드 기반으로 동작하는 어플리케이션이 아니라면 거의 접할 일이 없겠지만, 과거에 뭣도 모를때(?) 아주 스트레스를 준 녀석이라 조금 근본적인 관점에서 접근 하고자 한다.


기본 개념



https://docs.oracle.com/javase/7/docs/api/java/util/ConcurrentModificationException.html

위의 공식 문서를 요약하자면 이렇다.

해당 Exception은 어떠한 한 오브젝트에 대하여 허가되지 않은 변경이 동시적으로 이루어질때 발생한다. 그렇다면 '허가되지 않은 변경' 이란건 무엇일까?

간단히 말하면 한 스레드가 어떤 Collection을 반복자(iterator)를 이용하여 순회하고 있을때, 다른 한스레드가 해당 Collection에 접근하여 변경을 시도하는 경우이다. 하지만 꼭 멀티스레드 환경에서만 발생 하는것은 아니다. 싱글 스레드 환경에서도 발생할 수 있는데, 위와 같이 어떤 Collection을 순회하고 있는 반복문 안에서, 순회되고 있는 Collection에 대한 변경이 시도 될 때 또한 해당 Exception이 발생 하게 된다.


예제를 통해 알아보자



먼저 2개의 스레드와 'staticList'라는 이름을 가진 shared resource가 존재한다는 것을 전제로, 아래와 같은 시나리오를 구성해 보았다

일단 staticList에는 Integer형의 요소가 1부터 10까지 들어 있다.
public static List<Integer> staticList = new ArrayList<Integer>() {
    private static final long serialVersionUID = 1L;
    {
        add(1);
        add(2);
        add(3);
        add(4);
        add(5);
        add(6);
        add(7);
        add(8);
        add(9);
        add(10);
    }
};

1번 스레드는 staticList를 단순히 순회하며 1초마다 해당 리스트의 요소를 출력한다.
/**
 * @author asuraiv
 */
public class Thread1 implements Runnable {

    public void run() {
        
        for(Integer val : ConcurrentModifyTest.staticList) {
            
            System.out.println(val);
            
            try {
                TimeUnit.MILLISECONDS.sleep(1000);
            } catch (InterruptedException e) {
                System.err.println(e);
            }
        }
    }
}

2번 스레드는 처음 구동되고 나서 3초간 대기하고 있다가, staticList에서 '5'요소를 삭제한다. (index가 아니라 Integer형의 '5' 오브젝트를 삭제)
/**
 * @author asuraiv
 */
public class Thread2 implements Runnable {

    public void run() {     
        
        try {
            TimeUnit.MILLISECONDS.sleep(3000);
        } catch (InterruptedException e) {
            System.err.println(e);
        }
        
        ConcurrentModifyTest.staticList.remove((Integer)5);
    }
}

그림으로 표현하면 아래와 같을 것이다.



이제 시나리오는 세워 졌으니, main메소드를 이용해 돌려보자.
/**
 * @author asuraiv
 */
public class ConcurrentModifyTest {
    
    public static List<Integer> staticList = new ArrayList<Integer>() {
        private static final long serialVersionUID = 1L;
        {
            add(1);
            add(2);
            add(3);
            add(4);
            add(5);
            add(6);
            add(7);
            add(8);
            add(9);
            add(10);
        }
    };

    public static void main(String[] args) {
        
        ExecutorService executor = Executors.newFixedThreadPool(2);
        executor.execute(new Thread1());
        executor.execute(new Thread2());
    }
}

결과는 아래와 같다
1
2
3
4
Exception in thread "pool-1-thread-1" java.util.ConcurrentModificationException
    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:859)
    at java.util.ArrayList$Itr.next(ArrayList.java:831)
    at com.hong.study.concurrent.Thread1.run(Thread1.java:14)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
    at java.lang.Thread.run(Thread.java:745)
우리가 원하는 'ConcurrentModificationException' 이 발생했다.

1번스레드가 Integer 5요소를 출력하기 전에 대기하고 있던 2번스레드가 5를 삭제해 버렸다. 순회하고 있는 List에 '허가되지 않은 변경'이 발생한 것을 탐지한 iterator는 바로 'ConcurrentModificationException'을 발생 시킨다.

그렇다. 해당 Exception은 Iterator가 발생시키는 것이다. 그렇다면 향상된 for문이 아닌 Iterator를 사용하지 않는 일반 for문으로 순회할 때는 어떤 일이 발생할까? 해당 포문을 아래와 같이 변경한다.
int size = ConcurrentModifyTest.staticList.size();
for (int i = 0; i < size; i++) {
      .
      .
      .

결과는 아래와 같다.
1
2
3
4
6
7
8
9
10
Exception in thread "pool-1-thread-1" java.lang.IndexOutOfBoundsException: Index: 9, Size: 9
    at java.util.ArrayList.rangeCheck(ArrayList.java:635)
    at java.util.ArrayList.get(ArrayList.java:411)
    at com.hong.study.concurrent.Thread1.run(Thread1.java:17)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
    at java.lang.Thread.run(Thread.java:745)
보는 바와 같이 ConcurrentModificationException이 아닌 IndexOutOfBoundsException이 발생한다. 출력 된 결과도 보면 '5'가 빠져 있다. 최초 size가 10 이므로 for문이 i = 0 부터 9까지 순회하게 되는데, 중간에 요소 하나가 삭제되고 size가 9가 되어 버렸기 때문이다. 때문에 인덱스 바운더리 관련 Exception이 발생한 것이다.


마지막으로 싱글 스레드에서 발생 할 수 있는 경우를 살펴보자.
/**
 * @author Jupyo Hong
 */
public class ConcurrentModifyTest2 {

    public static void main(String[] args) {
        
        List<Integer> list = new ArrayList<Integer>();
        list.add(1);
        list.add(2);
        list.add(3);
        list.add(4);
        list.add(5);
        list.add(6);
        list.add(7);
        list.add(8);
        list.add(9);
        list.add(10);
        
        for(Integer val : list) {
            if(val == 5) {
                list.remove((Integer)5);
            }
        }
    }
}
향상된 for문을 사용해 순회 도중, 순회 되고 있는 리스트의 요소를 삭제하는 코드이다.
역시나 아래와 같이 'ConcurrentModificationException'이 떨어진다
Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:859)
    at java.util.ArrayList$Itr.next(ArrayList.java:831)
    at com.hong.study.concurrent.ConcurrentModifyTest2.main(ConcurrentModifyTest2.java:32)

싱글 스레드라고 할지라도 순회 되고있는 리스트가 중간에 변경이 된건 마찬가지 이기 때문에 해당 Exception이 발생하는 것도 이상하지 않다.

마치며..



 자바 개발을 하면서 평소에 가장 다루기 어렵고 힘든 분야라고 하면 GC관련 성능이슈와 동시성 이슈라고 생각해 왔다. 해당 분야를 전부 파악하기란 매우 어렵겠지만, 차근차근 실무와 이론적인 공부를 병행해 가면서 지식의 깊이를 더해야 겠다는 생각을 하게 되었다.

이제 'java.util.concurrent' 패키지에서 제공하는 라이브러리들에 관해서 정리 해봐야 겠다.
(언제가 될지는 모름...)









2016년 3월 10일 목요일

고통은 결속을 낳는다.

 대륙을 횡단 하면서 가장 기억에 남는 날은 조슈아 나무 국립공원을 지나던 날이었다. 나는 일행에서 50킬로미터쯤 뒤처져 있었는데, 기온이 40도에 육박해서 그늘을 찾지 않을 수 없었다. 16킬로미터쯤 더 가자, 기찻길 옆으로 금속 지붕의 헛간이 하나 나오고, 헛간 옆 콘크리트 위로 15센티미터 가량의 그늘이 떨어져 있었다. 머리를 그늘로 가게 해서 몸을 뉘었다. 훅훅 더운 바람이 끼쳐 왔다. 그저 잠시 해를 가리기만 하면 되었다. 

 80킬로미터나 더 가야 한다는 것과 어쩌면 그 구간이 내 평생 최악의 고통일 수 있음을 알았다. 하지만 그곳에서 나는 '이야기'를 떠올렸다. 갈등이 아무리 힘들지라도 주인공이 용감히 운명에 맞선다면 결국 모든 갈등이 그에게 복으로 돌아온다는 사실을 떠올렸다. 인간이 견뎌내서 복이 되지 않을 갈등이란 없다. 씩 웃음이 났다. 즐거웠다는 말은 아니지만 왠지 웃음이 났다. '지금은 힘들지만 소중한 추억이 될 거야.' 그런 생각이 들었다. 
그리고 정말 그렇게 되었다.

                                                                                                        <천년동안 백만마일 中>

2016년 2월 28일 일요일

[MAVEN] Shade Plugin (2) - artifactSet과 filter 설정 사용 및 minimizeJar

들어가기 앞서



가령 아래와 같은 구조의 project-a, project-b, project-c 라고 하는
3개의 프로젝트가 있다고 하자. (클릭하면 크게 보임)





이때 'MainClass' 가 있는 'project-a'의 pom.xml의 'dependency' 설정은 아래와 같이 구성한다.
<dependencies>

    <dependency>
        <groupId>com.asuraiv.bbb</groupId>
        <artifactId>project-b</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </dependency>

    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>3.8.1</version>
    </dependency>

</dependencies>

간단히 코드도 살펴보자.

'project-a'의 'MainClass'는 아래처럼 'project-b'의
'com.asuraiv.bbb.util_1.B_Utils1' 클래스를 사용한다.
package com.asuraiv.aaa;

import com.asuraiv.bbb.util_1.B_Utils1;

public class MainClass {
    
    public static void main(String[] args) {
        
        System.out.println("Hello Shade Plugin !!");
        
        // project-b 의 Utils 클래스 사용
        B_Utils1.printString("Hello Project B");
    }
}
'B_Utils1.printString' 메소드는 아래처럼 단순히 System out으로 문자열을 찍는다.
package com.asuraiv.bbb.util_1;

public class B_Utils1 {
    
    public static void printString(String text) {
        System.out.println(text);
    }
}

한편, 'com.asuraiv.bbb.util_2.B_Utils2' 클래스는 'project-c' 의 클래스를 사용한다.
package com.asuraiv.bbb.util_2;

import com.asuraiv.ccc.util_2.C_Utils2;

public class B_Utils2 {
    
    public static void printIntegerUsingCUtils(int value) {
        
        // project-c 의 Utils 클래스 사용
        C_Utils2.printInteger(value);
    }
}
뭐, 이런 상황이다.

이렇게 되면 이들 3개의 프로젝트의 의존관계는 아래처럼 될 것이다.










정리하자면 'project-a' 의 'MainClass' 에서 'project-b'의 'com.asuraiv.bbb.util_1.B_Utils1' 를 사용하고, 'project-b' 의 다른 패키지인 'com.asuraiv.bbb.util_2.B_Utils2'에서는 'project-c'의 'com.asuraiv.ccc.util_2.C_Utils2' 를 사용하는 것이다.

이때 'project-a'를 shade 플러그인을 사용하여 'uber-jar' 만들고,
디렉토리 구조를 보면 아래와 같은 것이다.
├─com
│  └─asuraiv
│      ├─aaa
│      ├─bbb
│      │  ├─util_1
│      │  └─util_2
│      └─ccc
│          ├─util_1
│          └─util_2
├─junit
│  ├─awtui
│  ├─extensions
│  ├─framework
│  ├─runner
│  ├─swingui
│  │  └─icons
│  └─textui
└─META-INF
    └─maven
        ├─com.asuraiv.aaa
        │  └─project-a
        ├─com.asuraiv.bbb
        │  └─project-b
        └─com.asuraiv.ccc
            └─project-c


artifactSet



위의 상황들이 전제로 주어졌을때, 결과적으로 'project-c'는 'project-b'가 의존하고 있지만, 사용되지 않는다. 'project-a'의 'MainClass'가 'project-b'의 'com.asuraiv.bbb.util_1.B_Utils1' 만을 사용하고 있기 때문이다.

하지만 project-c를 사용하지 않음에도 불구하고 shade 플러그인을 사용해 생성된 uber-jar는 위의 tree 구조를 보면 알 수 있듯 물리고 물린 의존관계는 몽땅 패키징이 되어 사용되지 않더라도 uber-jar 안에 포함되 있는것을 볼 수 있다.

이때 사용하지 않는 'proejct-c' 의 의존관계를 uber-jar 생성시에 제외시킬 수 있다.

'configuration' 태그 밑의 'artifactSet' 설정을 사용하여 어떤 라이브러리를 제외하고 포함시킬 것인지 정의가 가능하다. 아래와 같이 설정한다.
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-shade-plugin</artifactId>
    <version>2.4.3</version>
    <executions>
        <execution>
            <phase>package</phase>
            <goals>
                <goal>shade</goal>
            </goals>
            <configuration>
                <transformers>
                    <transformer
                        implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                        <mainClass>com.asuraiv.aaa.MainClass</mainClass>
                    </transformer>
                </transformers>
                <artifactSet>
                    <excludes>
                        <exclude>com.asuraiv.ccc:project-c</exclude>
                    </excludes>                             
                </artifactSet>
                <outputFile>d:/shade/project-a.jar</outputFile>
            </configuration>
        </execution>
    </executions>
</plugin>
'artifactSet'태그와 함께 'excludes' 설정을 하면, 어떤 라이브러리를 제외 시킬 것인지 명시 할 수 있게 된다. 더불어 'outputFile'은 jar 파일이 떨어지는 위치를 말한다.

이러한 설정을 바탕으로 package를 하게 되면 아래와 같은 tree 구조가 된다.
├─com
│  └─asuraiv
│      ├─aaa
│      └─bbb
│          ├─util_1
│          └─util_2
├─junit
│  ├─awtui
│  ├─extensions
│  ├─framework
│  ├─runner
│  ├─swingui
│  │  └─icons
│  └─textui
└─META-INF
    └─maven
        ├─com.asuraiv.aaa
        │  └─project-a
        └─com.asuraiv.bbb
            └─project-b
보는것과 같이 사용하지 않는 'project-c' 가 패키징 되지 않았다.


filters



하지만 아래처럼 'MainClass'에서 'B_Utils1.printString'을 사용하지 않고 'project-c'를 사용하여 문자열을 찍는 'B_Utils2.printIntegerUsingCUtils' 메소드를 사용한다면 어떨까?
package com.asuraiv.aaa;

import com.asuraiv.bbb.util_2.B_Utils2;

public class MainClass {
    
    public static void main(String[] args) {
        
        System.out.println("Hello Shade Plugin !!");
        
        // project-c를 사용하여 문자열을 찍는 project-b의 Utils 클래스를 사용
        B_Utils2.printStringUsingCUtils("Hello Project B");
    }
}
'project-c' 라이브러리를 통째로 날렸다가는 프로그램 실행시에 아래와 같은 에러를 볼 것이다.



'MainClass'에서, 'project-c' 라이브러리를 사용하는 'project-b'의 메소드를 호출했는데, 'project-c'가 통째로 제외 되었으니 런타임에 오류가 나는것은 당연하다.

이때 'filter' 설정을 사용해 'project-c' 라이브러리에서 사용하는 패키지를 남기고 나머진 제외 시켜서 uber-jar를 생성할 수 있다!
<configuration>
    <transformers>
        <transformer
            implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
            <mainClass>com.asuraiv.aaa.MainClass</mainClass>
        </transformer>
    </transformers>
    <artifactSet>
        <excludes>
            <exclude>junit:junit</exclude>
        </excludes>
    </artifactSet>
    <filters>
        <filter>
            <artifact>com.asuraiv.ccc:project-c</artifact>
            <excludes>
                <exclude>com/asuraiv/ccc/util_2/**</exclude>
            </excludes>
        </filter>
    </filters>
    <outputFile>d:/project-a.jar</outputFile>
</configuration>
앞서 배운 'artifactSet' 설정을 사용하여 'junit'을 통째로 제외 시켰다.

여기서 'filters' 설정에 주목하자. 현재 'project-b'의 'printStringUsingCUtils' 메소드는, 'project-c'의 'com.asuraiv.ccc.util_1' 패키지만을 사용하고 'com.asuraiv.ccc.util_2' 패키지는 사용하지 않는다. 따라서 'filters' 설정을 사용하여, 특정 라이브러리의 특정 패키지만을 제외 시킨 것이다.

tree로 디렉토리 구조를 보면 아래와 같이 필요한 패키지만 uber-jar에 포함되어 있다.
├─com
│  └─asuraiv
│      ├─aaa
│      ├─bbb
│      │  ├─util_1
│      │  └─util_2
│      └─ccc
│          └─util_1
└─META-INF
    └─maven
        ├─com.asuraiv.aaa
        │  └─project-a
        ├─com.asuraiv.bbb
        │  └─project-b
        └─com.asuraiv.ccc
            └─project-c
ccc.utils_2 패키지가 제외 되었다. 근데 사실 bbb.utils_1 패키지도 사용하지 않는다.
filter 설정을 추가해보자
<filters>
    <filter>
        <artifact>com.asuraiv.bbb:project-b</artifact>
        <excludes>
            <exclude>com/asuraiv/bbb/util_1/**</exclude>
        </excludes>
    </filter>
    <filter>
        <artifact>com.asuraiv.ccc:project-c</artifact>
        <excludes>
            <exclude>com/asuraiv/ccc/util_2/**</exclude>
        </excludes>
    </filter>                               
</filters>
위와같이 'project-b' 에 관련한 필터설정도 추가했다.

빌드후에 디렉토리 구조를 보면 아래와 같이 필요없는 패키지들이 모두
제거된 것을 볼 수 있다.
├─com
│  └─asuraiv
│      ├─aaa
│      ├─bbb
│      │  └─util_2
│      └─ccc
│          └─util_1
└─META-INF
    └─maven
        ├─com.asuraiv.aaa
        │  └─project-a
        ├─com.asuraiv.bbb
        │  └─project-b
        └─com.asuraiv.ccc
            └─project-c


minimizeJar



사실 'artifactSet' 이나 'filter' 설정처럼 세밀하게 포함될/제외될 라이브러리나 패키지를 설정하기 힘들다면, 'minimizeJar' 설정으로 간편하게 필요없는 소스코드들을 제외 시킬 수 있다
<configuration>
    <transformers>
        <transformer
            implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
            <mainClass>com.asuraiv.aaa.MainClass</mainClass>
        </transformer>
    </transformers>
    <minimizeJar>true</minimizeJar> <!-- 필요없는 소스코드 제외! -->
    <outputFile>d:/shade/project-a.jar</outputFile>
</configuration>
위와 같이 'minimizeJar' 태그 설정만 하면 우리 위에서 살펴봤던 'artifactSet'과 'filter'설정을 한 것과 동일한 결과를 가져온다.

여태까지 'exclude'의 경우만 설명하고 'include'의 경우를 설명하지 않았는데, 사용법은 'exclude'와 동일하다.

만약 minimizeJar를 사용했는데 런타임에 필요한 패키지까지 제외되어 오류가 난다면 'include' 설정을 사용하여 필요한 패키지를 포함 시킬 수 있을 것이다.

'artifactSet'을 사용하여 어떤 라이브러리를 포함/제외 시킬 것인지, 'filter'를 사용하여 패키지 레벨로 세세하게 명시할 것인지, minimizeJar를 사용하여 간편하게 jar파일을 만들것인지.. 이 모든 방법들을 적당하게 조합하여 사용하면 효율적인 용량의 jar 파일을 만들 수 있을 것이다.


2016년 2월 12일 금요일

[Performance] The Grinder 3 (3) - 테스트 수행 및 리포트 산출

샘플 스크립트 작성



아주 간단한 HTTP Get 요청을 수행하는 스크립트를 작성해 본다.
그라인더 설치 디렉토리의 'example' 밑에 있는 스크립트를 참고하여 작성해보았다
import string
import random

from java.lang import String
from java.net import URLEncoder
from net.grinder.script import Test
from net.grinder.plugin.http import HTTPRequest
from net.grinder.common import GrinderException
from net.grinder.script.Grinder import grinder

# Server Properties
SERVER = "http://10.113.182.195:9001"
URI = "/test.jsp"

test = Test(1, "http_test")

class TestRunner:
    def __call__(self):
        request = HTTPRequest()
        requestString = "%s%s" % (SERVER, URI)
        test.record(request)
        result = request.GET(requestString)
        
        print "# Response text :", result.text
이전 포스트에서 언급했지만 그라인더는 'Jython' 스크립트를 사용해 부하 테스트를 수행 한다. 위의 코드는 HTTP Get 요청을 수행하는 간단한 스크립트이다.

위 파일을 'simplehttp.py' 라는 이름으로 저장하고, 'D:\grinder\python_source\grinder' 경로에 두었다. 따라서 'grinder.properties' 파일의 'grinder.script' 값을 아래와 같이 수정한다.
grinder.script = d:/grinder/python_source/grinder/simplehttp.py


테스트 수행



테스트하기 전, 프로세스와 스레드, 실행 횟수도 알맞게 설정해보자.
grinder.processes = 1
grinder.threads = 10
grinder.runs = 100
Agent 프로세스는 1개, 그 안에서 돌아가는 스레드를 50개로 설정 하였고, 각 스레드의 실행횟수는 100번으로 설정 하였다.

그리고 아래의 'start collecting' 버튼을 클릭한다.


















그러면 'Collection stopped' 라는 메시지가 'Wating for samples' 라고 바뀔 것이다. 이제 Worker Agent로부터 샘플 데이터를 기다리는 상태가 된것이다.

이제 아래의 'Start the Worker processes' 버튼을 클릭하여 Worker Agent를 구동시킨다.


















해당 버튼을 클릭하면 Agent 에 아래와 같은 로그가 찍힌다.












grinder.processes 값이 2이기때문에 2개의 Worker 프로세스가 시작된다. 특별히 정해진 네이밍 규칙이 있는데 '[host이름]-[seq번호]' 와 같은 식이다.

Agent 가 실행되면 'grinder.properties'파일의 grinder.consoleHost 와, grinder.consolePort 에 설정한(default는 localhost, 6372 이다) 호스트와 포트를 통해 수집된 데이터를 콘솔로 보낸다.

이때 Console창의 'Results' 탭을 보면 아래와 같이 수집되고 있는 데이터가 실시간으로 변화하는 모습을 볼 수 있다.





















TPS, Mean Time 등의 용어들은 알아서 찾아보자! (이것도 시간나면 정리해야겠다)


GrinderAnalyzer를 이용한 리포트 산출



테스트를 완료하면 그라인더 'log' 디렉토리에 아래와 같은 로그가 쌓인다.












위 사진을 보면, 'host-n.log' 의 형식으로 생성되는 각 프로세스별 정보 로그 파일과, 'host-n-data.log' 형식으로 생성되는 각 테스트 건별 데이터 로그 파일이 보인다.

파일을 열어보면 알겠지만 건당 수행 결과와 측정 요소들의 값들이 로그로 잔뜩 찍혀 있는 것을 볼 수 있다. 이것은 두말 할 것 없이 한 눈에 보기가 불가능하다.

하지만 'GrinderAnalyzer' 라는 툴을 사용하면 위의 정보들을 종합하여
보기좋게 결과 리포트를 생성해준다.

아래에서 다운 받는다

http://track.sourceforge.net/analyzer.html

다운 받은 파일을 압축 해제 하면 아래와 같은 구조가 보일 것이다.
















일단 윈도우 커맨드 창을 이용해 위의 'analyzer.py' 파일이 있는
디렉토리까지 이동한다.

그리고 아래와 같은 명령어를 입력 해보자.
(물론 jython home 경로가 윈도우 환경변수의 'path' 에 추가되어 있어야 한다.)
> jython analyzer.py "D:\grinder\grinder-3.11\log\hong-0-data.log D:
\grinder\grinder-3.11\log\hong-1-data.log" D:\grinder\grinder-3.11\log\hong-1.log 1
'jython analyzer.py' 다음에 쌍따옴표안에는 각 data log 파일의 '절대경로' 목록을 스페이스로 구분하여 입력한다. 그 다음에는 마지막 프로세스의 로그 파일인 'hong-1.log' 파일의 절대 경로를 입력한다.

마지막 파라미터로 주어지는 값은 몇개의 머신에서 Agent를 실행 시켰는지에 대한 값인데, 하나의 로컬 PC에서 Agent를 구동 하였으므로 '1'을 입력한다

위 명령어를 실행하면 아래와 같이 콘솔창에 로그가 찍힐 것이다.



































그러고 나면 'grinderReport' 라는 폴더가 생성되어 있을 것이다.


















해당 폴더안에 'report.html' 파일을 브라우저로 열어보자.
아래와 같이 테스트 수행 결과가 보기좋게 그래프와 표로 정리되어 나타난다.



















마치며...



이것으로 그라인더 설치, 설정, 테스트 수행, 결과 리포트 산출까지 모든 정리를 마쳤다.

이외에도 그라인더를 다양한 방법과 스크립트를 이용해 사용할 수 있지만, 차차 기회가 되면 정리해보도록 하겠다.

2016년 2월 2일 화요일

[Performance] The Grinder 3 (2) - 기본 원리 및 실행 환경 설정

기본 원리



그라인더는 크게 3가지 Process로 나뉜다





  • Worker Processes :
    Jython 으로 작성된 테스트 스크립트를 Interpret하고 테스트를 수행한다
    다수의 Worker 스레드가 병렬적으로 동시에 테스트를 수행하게 된다
  • Agent Process :
    Worker 스레드의 실행/중지를 수행한다
    Console로부터 전달 받은 테스트 스크립트를 캐싱한다
  • The Console :
    위 2가지의 프로세스들을 제어 한다
    스크립트 작성과 배포를 담당한다
    테스트 수행시, 그래프 및 현황을 보여준다


실행 환경 설정 (윈도우 기반)



1) setGrinderEnv.cmd

set GRINDERPATH=D:\grinder\grinder-3.11
set GRINDERPROPERTIES=D:\grinder\grinder-3.11\conf\grinder.properties
set CLASSPATH=%GRINDERPATH%\lib\grinder.jar
set JAVA_HOME=C:\dev_tools\Java\jdk1.7.0_65
PATH=%JAVA_HOME%\bin;%PATH%
그라인더 home 경로와 properties 파일경로, jdk 경로 등을 정의하는 스크립트이다.

2) startConsole.cmd

call D:\grinder\grinder-3.11\bin\setGrinderEnv.cmd
java -Dgrinder.console.consolePort=6372 -Dgrinder.console.consoleHost=localhost -Xms1024m -Xmx1024m -cp %CLASSPATH% net.grinder.Console 
Console을 구동하는 스크립트이다. call명령어를 이용해 setGrinderEnv에서 정의한 변수들을 활용한다. Agent와의 통신을 위해 console port와 host를 자바 옵션으로 준다. (아마 옵션으로 주지 않아도 default 설정으로 되어 있을 것이다)

3) startAgent.cmd

set GRINDERPATH=D:\grinder\grinder-3.11\bin
call D:\grinder\grinder-3.11\bin\setGrinderEnv.cmd
echo %CLASSPATH%
java -cp %CLASSPATH% net.grinder.Grinder -daemon 5 %GRINDERPROPERTIES%
Agent를 구동하는 스크립트이다. 구동시에 '-daemon 5' 라는 옵션을 주는데, Console으로부터 응답이 없다면(Console이 죽었다면) 5초 간격으로 재접속을 시도하라는 의미이다.

4) 디렉토리 추가 및 설정


위 3가지 스크립트를 생성하면, grinder 설치 폴더에 'bin' 폴더를 생성한뒤 3개 스크립트를 그쪽으로 몰아 넣는다. Agent 설정파일인 'grinder.properties' 는 최초 'examples' 폴더에 존재하는데, 이 파일 또한 grinder 설치 폴더에 'conf' 폴더를 만들어서 그쪽에 넣도록 하자.

그러면 아래와 같은 구조가 될 것이다.


grinder.properties



Agent의 설정은 grinder.properties 파일에 작성된다. 해당 properties 파일의 경로는 setGrinderEnv.cmd 스크립트에서 변수로 정의되고, startAgent.cmd 스크립트에서 그 경로를 사용한다.

grinder.properties 의 주요 설정은 아래와 같다
#
# Commonly used properties
#
# The file name of the script to run.
#
# Relative paths are evaluated from the directory containing the
# properties file. The default is "grinder.py".
grinder.script = d:/grinder/python_source/grinder/stress_test.py
 
# The number of worker processes each agent should start. The default
# is 1.
grinder.processes = 1
# The number of worker threads each worker process should start. The
# default is 1.
grinder.threads = 50
# The number of runs each worker process will perform. When using the
# console this is usually set to 0, meaning "run until the console
# sneds a stop or reset signal". The default is 1.
grinder.runs = 1000
# The IP address or host name that the agent and worker processes use
# to contact the console. The default is all the network interfaces
# of the local machine.
; grinder.consoleHost = consolehost
# The IP port that the agent and worker processes use to contact the
# console. Defaults to 6372.
; grinder.consolePort
grinder.script : 테스트 스크립트의 경로를 입력
grinder.processes : worker 프로세스의 갯수
grinder.threads : 하나의 worker 프로세스 안에서 실행되는 스레드 갯수
grinder.runs : 테스트 수행 횟수
grinder.consoleHost : 콘솔과의 TCP 통신을 위한 host
grinder.consolePort : 콘솔과의 TCP 통신을 위한 port


실행



1) Console 구동


먼저 윈도우 cmd 창에서 'startConsole.cmd'를 실행한다.



조금 기다리면 grinder 콘솔 창이 노출된다.



빨간색 박스 부분이 Agent를 제어하는 부분인데, 비활성화 되어있다.
아직 Agent를 구동하지 않았기 때문이다.

2) Agent 구동


startAgent.cmd 를 실행하여 Agent를 구동해 보자.



위 처럼 startAgent.cmd 를 실행하면 콘솔의 host:port 로 connect를 성공했다는 로그가 찍힌다. 그리고 다시 Console 창을 보자.



방금전 비활성화 되어있던 Agent 제어 버튼이 활성화 되었다.

첫번째 버튼은 Agent가 테스트를 시작/중지 할 수 있게 명령을 내리는 버튼이고,
두번째 버튼은 Agent를 reset 하는 버튼인데, 이때 Agent는 grinder.properties 의 속성들을 다시 읽어 들인다. 따라서 grinder.properties의 값을 바꿨을때 Agent를 굳이 재시작 하지 않아도 reset 버튼으로 변경된 grinder.properties의 값을 적용 시킬 수 있다.

3) script root directory 설정




빨간색 박스안에 있는 버튼을 이용해 배포될 테스트 스크립트의 저장 경로를 설정 할 수 있다. 해당 경로 밑의 모든 파일들이 worker 프로세스에 배포되므로, 리눅스 환경에서의 '/home' 이나 윈도우 환경에서의 'C:\' 와 같은 최상단 디렉토리를 설정하면 절대 안된다.

보통 grinder 설치 폴더의 'examples' 폴더로 설정하는 것이 일반적이다.


이제 기본적으로 테스트를 수행하기 위한 모든 설정은 끝이 났다.

다음 포스팅에서는 기본적인 http 테스트를 위한 Jython 스크립트 작성과,
테스트 수행, 그리고 'GrinderAnalyzer' 를 이용한 결과 리포트 산출 등을 정리하겠다.


2016년 1월 31일 일요일

[Performance] The Grinder 3 (1) - 개요 및 환경 세팅

개요



그라인더는 'Load Test Tool' 이다. Http 기반의 일반적인 웹서비스 뿐만 아니라, SOAP, REST 웹서비스 등 에 대한 로드 테스트를 할 수 있다. 주로 'Stress Test' 에 사용된다.

그라인더의 강력함은 Jython 스크립트 작성을 통해서 다양한 시나리오를 테스트 할 수 있다는 것이다. 아래 그라인더 공식 페이지의 'Script Gallery'에서 종류별로 스크립트 예제를 참고 할 수 있다.

http://grinder.sourceforge.net/g3/script-gallery.html

Jython은 말 그대로 JAVA + Python 의 합성어로, 좀 혼란스러울 수도있지만 파이썬에서 자바 라이브러리를 사용 할 수 있게 해준다. 자바와 파이썬에 대한 기본적인 지식만 있으면 위 Script Gallery의 예제들은 대략적으로 이해 할 수 있을 것이다.

솔직히 Grinder 기반의 툴은 네이버에서 개발한 'nGrinder'가 대표적이고, 은근히 많이 사용되는 것 같다. 하지만 nGrider는 초딩도 할 수 있다고(...) 하여, 괜한 자존심이 발동 했다.

앞으로 몇번의 포스팅을 통해서 Grinder의 설치 및 세팅과, 기본적인 http request 스크립트 작성, GrinderAnalyzer를 이용한 리포트 산출 등을 정리 하고자 한다.

환경 세팅



그라인더를 사용하기 위해서는 몇가지 환경 설정이 필요하다.
앞으로 모든 실습은 '윈도우' 기반으로 진행 하도록 하겠다.

아참.. 당연히 JAVA 는 설치되어 있어야 한다!

(1) Jython 설치


http://www.jython.org/downloads.html

























자이썬 공식 사이트에 접속하여 Installer를 다운로드 한다. 이때 jar 파일이 다운로드 되는데 아래와 같은 명령어로 설치한다.
> java -jar jython_installer-2.7.0.jar
jar 파일을 구동하면 아래와 같은 install 창이 뜬다.

















이후의 과정은 알아서...(?!)

(2) Grinder 다운로드 및 설치


http://grinder.sourceforge.net/



























SourceForge 링크로 따라가서 'zip' 파일을 다운로드한다.
다운로드한 파일을 압축 해제하면 아래와 같은 파일들이 보일 것이다.














그라인더는 기본적으로 자바기반이다. 앞으로 살펴볼 Agent, Worker, Console 등 그라인더의 주요 컴포넌트들도 자바로 구성되어 있다. 따라서 'lib' 디렉토리에는 이와 관련된 자바 라이브러리들이 한가득 들어차 있다.

'example' 디렉토리에는 공식 사이트에도 나와있는 예제 script들이 존재 한다. 이걸 참고해도 되지만 부가적인 설명이 있는 공식 사이트의 'Script Gallery' 를 참고 하도록 하자.

기본적인 실행환경 설정 등은 다음 포스팅으로 미뤄야 겠다.. To be continue...

2016년 1월 26일 화요일

[MAVEN] Shade Plugin (1) - 기본 사용법과 Resource Transformer

개요



메이븐에서 자바로 구성된 프로젝트를 자바 아카이브 파일 (이하 jar 파일)로 말아(?) 주는 플러그인이 몇가지가 있는데, 오늘 정리할 플러그인은 'maven-shade-plugin' 이다.

일단 정리하기에 앞서 'uber-jar' 라는 개념에 대해서 살펴볼 필요가 있다.

아래 페이지에 전형적인 질/답 이 나와 있다.

http://stackoverflow.com/questions/11947037/what-is-an-uber-jar

'uber' 라는 말은 독일어로서, 'above' 또는 'over' 라는 뜻이 있다고 한다.
그러니까 영어로 따지면 'over-jar' 인 것이다.

각설하고, 'uber-jar'라는 것은 자바 어플리케이션의 모든 패키지와, 그에 의존관계에 있는 패키지 라이브러리까지 모두 하나의 'jar' 에 담겨져 있는 것 을 말한다.

'uber-jar'를 사용하면 어플리케이션을 배포할때 의존관계를 생각할 필요가 없다. 왜냐면 이미 필요한 의존관계 라이브러리들을 가지고 있기 때문이다.


Maven shade plugin



이러한 'uber-jar'를 생성할때, 해당 어플리케이션에 모든 의존관계까지 포함하다 보니, 어플리케이션 배포시에 필요없는 라이브러리까지(예를들어서 개발 프로세스에만 필요한 라이브러리 - JUnit같은..? - 라던가..) 몽땅 패키징되는 경우가 있다.

shade 플러그인의 강력함은, 배포시에 필요한 라이브러리들을 'exclude/include' 시킬 수있고, 라이브러리 수준뿐만 아니라 class 파일 수준으로 jar 파일을 minimize 함으로서 보다 가벼운 jar 파일을 생성할 수 있다는 것이다. (그러니까 미사용 class 파일도 걸러낸다는 말이다.)


기본 사용법



아래는 기본적인 사용법이다.
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-shade-plugin</artifactId>
            <version>2.4.3</version>
            <configuration>
                <!-- put your configurations here -->                         
            </configuration>
            <executions>
                <execution>
                    <phase>package</phase>
                    <goals>
                        <goal>shade</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>
메이븐 골(goal)로 'shade:shade' 를 입력하여 직접 구동 시킬 수 있지만, <executions> 설정으로 package phase에 shade 골을 바인딩 하는 설정을 하면, 'mvn package' 으로 구동 시킬 수 있다.

Resource Transformer



shade 플러그인을 적용 할때  'Resource Transformer' 라는 개념을 이해할 필요가 있다

Resource Transformer 설정을 하면 서로 다른 artifacts 들로부터 uber-jar 를 생성할때, classes 및 resources 파일들을 '중복없이' 패키징 할 수 있게 해준다.

각 Resources Transformer 설정의 종류 및 특징은 아래와 같다

Transformers in org.apache.maven.plugins.shade.resource
ApacheLicenseResourceTransformerPrevents license duplication
ApacheNoticeResourceTransformerPrepares merged NOTICE
AppendingTransformerAdds content to a resource
ComponentsXmlResourceTransformerAggregates Plexus components.xml
DontIncludeResourceTransformerPrevents inclusion of matching resources
IncludeResourceTransformerAdds files from the project
ManifestResourceTransformerSets entries in the MANIFEST
PluginXmlResourceTransformerAggregates Mavens plugin.xml
ServicesResourceTransformerRelocated class names in META-INF/services resources and merges them.
XmlAppendingTransformerAdds XML content to an XML resource
이들 설정 가운데 흔히 쓰이는 몇가지 설정만 살펴보자.

(1) ManifestResourcesTransformer


여기서 주로 쓰이는 것은 ManifestResourcesTransformer 인데, 설명에 나와있는대로 자바 'MANIFEST' 파일의 entries 를 세팅해 준다.

아래와 같이 <configuration> 설정에 추가한다
<configuration>
    <transformers>
        <transformer
            implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
            <mainClass>com.asuraiv.project.aaa.MainClass</mainClass>
        </transformer>
    </transformers>
</configuration>
실행 가능한 jar 파일을 생성할시에 자바 어플리케이션을 구동할 MainClass를 지정해야하는데, 이것은 'MANIFEST' 파일의 entry 중 하나이다. 'MANIFEST' 파일 관련은 이전 포스트를 참고하길 바란다.

위 예제처럼 <mainClass> 설정으로 해당 어플리케이션의 메인클래스를 입력한다.

(2) AppendingTransformer


만약 스프링 batch 프로젝트를 shade 플러그인을 통해 'Executable JAR' 파일로 패키징 한다고 하자. 그럴 경우 Main 클래스는 스프링 batch job을 커맨드 라인에서 실행 할 수 있게 해주는 'org.springframework.batch.core.launch.support.CommandLineJobRunner' 가 된다.

그러면 ManifestResourceTransformer의 <mainClass> 설정만 해당 클래스로 설정해주면 될까?
그것만 해서는 안된다.

스프링으로 구성된 어플리케이션은 스프링 컨텍스트 xml의 namespace를 핸들링 해주는 Handler 클래스들이 정의되어 있는 spring.handlers 파일과, 스프링 컨텍스트 xml 설정 파일의 스키마(xsd 파일 등)가 정의되어 있는 spring.schemas 파일이 필요하다.

바로 이때, AppendingTransformer 설정을 사용하여 uber-jar에 포함 시킬 수 있다.
<configuration>
    <transformers>
        <transformer
            implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
            <mainClass>org.springframework.batch.core.launch.support.CommandLineJobRunner</mainClass>                                   
        </transformer>
        <transformer
            implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
            <resource>META-INF/spring.handlers</resource>
        </transformer>
        <transformer
            implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
            <resource>META-INF/spring.schemas</resource>
        </transformer>
</configuration>
기본적으로 스프링 라이브러리 jar 파일(spring-context, spring-aop, spring-beans 등등...)을 까보면 각각 META-INF 밑에 spring.handlers, spring.schemas 파일이 존재한다.

앞서 설명했듯이 shade 플러그인이 이 모든 의존관계 라이브러리들을 한데 묶어서 uber-jar 를 생성할때, 위 2개의 파일들이 각각 스프링 라이브러리에 동일한 이름으로 존재(하지만 그 내용은 또 각각 다르다ㅠㅠ)하기 때문에 중복의 문제가 존재한다.

따라서 AppendingTransformer 설정으로 해당 파일들을 포함시키면, 마치 'merge' 를 하는것과 같이 각 라이브러리의 핸들러, 스키마 정보들이 각각 하나의 spring.handlers, spring.shcemas 파일로 생성되는 것이다. 신통하다!

ResourcesTransformer에 관해 여기서 다 정리할 수는 없으니, 나머지는 공식 도큐먼트를 참고하자

다음 포스트에는 앞서 설명한 artifacts 단위로 include/exclude 하는 방법, minimize 설정을 통해 가벼운 jar 파일을 생성하는 방법 등을 정리해야 것다!