Java GC의 또 다른 문제 swap

neptune에 multi-row selection 기능(like, between) 을  구현하고 테스트 하는 중에 계속 GC로 인해 10 ~ 30초 정도 시스템이 멎는 증상이 발생하였습니다. 지난번부터 계속 GC가 문제를 일으키고 있는데 UseConcMarkSweepGC 옵션으로 해결된 것 같은 문제가 다시 재현되었습니다.

일주일 삽질 끝에 발견한 것은 테스트 하는 장비의 환경이 바뀌어서 물리적인 메모리가 작아졌기 때문이었습니다. 메모리가 모자라서 GC가 오래 걸렸다라기 보다는 메모리는 여유 있는데 메모리 swap이 발생하면서 GC 도는 시간이 오래 걸린 증상이었습니다.
GC 로그를 보면

[ParNew: 52353K->6528K(59008K), 7.8146970 secs] 331439K->326208K(2090624K), 7.8233400 secs] [Times: user=0.43 sys=0.00, real=7.82 secs]

GC 시간 7초 정에 user 시간은 0.43인 것을 볼 수 있습니다. 나머지 시간의 swap 메모리를 로딩하거나 디스크로 내리는데 걸린 wait 시간으로 볼 수 있습니다. 프로그램을 잘못 만든것이 아닌가 하고 열심히 튜닝했었는데 결국 원인은 외부에 있었네요...
일주일 삽질의 결과입니다. ㅋ

크리에이티브 커먼즈 라이센스
Creative Commons License

Posted by 김형준


RESTful 웹 서비스란?

RESTful 웹 서비스라는 것이 단순히 HTTP로 request를 받아서 결과를 XML 형태로 전송해주는 것이라고만 생각했는데 관련 문서를 찾아보니 잘못 생각하고 있었네요.

REST와 관련하여 몇가지 개념이 있는데 제가 알고 있는 개념인 HTTP를 이용하고 query string에 처리할 action에 대한 정보 등을 포함하는 방식을 REST-RPC 개념으로 설명하면서 진정한 의미의 REST한 서비스는 아니라고 합니다.

REST는 REpresentational State Transfer의 약자입니다.
이론적 배경은 다음 논문을 참고하세요.
http://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm

일단 개발자에게 피부로 와 닿는 설명은 다음과 같습니다.

1. HTTP 프로토콜의 PUT, GET, POST, DELETE 등과 같은 Method를 의미 그대로 사용한다.
2. Resource에 대한 접근을 URI를 이용한다.

다음 HTTP request가 REST 웹 서비스의 예제입니다.

GET /book HTTP/1.1
Host: www.jaso.co.kr
Accept: application/xml

위의 HTTP request는 책 목록을 가져 오기 위한 요청입니다. 특정 책의 상세 정보를 요청하는 경우에는 다음과 같이 할 수 있습니다.

GET /book/isbn_0001 HTTP/1.1
Host: www.jaso.co.kr
Accept: application/xml

특정 책 정보를 삭제하는 경우는 다음과 같습니다.

DELETE /book/isbn_0001 HTTP/1.1
Host: www.jaso.co.kr
Accept: application/xml


물론 위와 같은 처리를 다음과 같이 URL의 query string에 넣어서 처리할 수도 있습니다.

http://www.jaso.co.kr/book?action=delete&book_id=isbn_0001

이런 방식을 REST-RPC라고 합니다.

굳이 이렇게 나누는 것은 REST한 서비스 구성을 자세히 보면 특정 패턴이 보이고 이 패턴을 이용하여 프로그램을 쉽고 표준적으로 할 수 있기 때문입니다. 표준적으로 할 수 있다는 것은 프레임워크나 경량의 컨테이너가 출현할 가능성이 높고 이런 프레임워크를 통해 아주 쉽게 개발할 수 있게 됩니다.
최근 자바 진영에서는 JSR 311에 JAX-RS: The Java API for RESTful Web Services라는 형태로 표준화 하고 있습니다. 이것을 지원하는 프레임워크도 일부 나오고 있습니다.
단순한 개념인 것 같지만 그 개념의 이론을 만들고 이론에 따르는 시스템들이 개발되어 나오고 다시 이것들이 솔루션으로 만들어져 나오는 이런 선순환 구조를 통해 소프트웨어 산업이 계속 발전하고 성장하는 것 같습니다.

크리에이티브 커먼즈 라이센스
Creative Commons License

Posted by 김형준


JVM 옵션

JVM의 GC 관련 옵션을 찾는 중에 다음 URL을 발견하였는데

http://wiki.ex-em.com/index.php/JVM_Options

http://ukja.tistory.com/51

http://java.sun.com/docs/hotspot/gc5.0/gc_tuning_5.html

1.3 정도에서 JVM 파라미터로 튜닝하던 때에 옵션에 대해서 공부하다가 지금 보니까 엄청나게 많아지고 다양한 정보를 설정할 수 있게 되어 있네요...

크리에이티브 커먼즈 라이센스
Creative Commons License

Posted by 김형준


Xen 관련 기본 명령

  • vm start: xm create <conf file>
  • vm shutdown: xm shutdown <vm name>
  • vm console: xm console <vm name>
  • vm 목록: vm list
  • vm 디스크 확장
     cd /var/lib/xen/images/vlinux1
    #use dd to create a 1 GB file
    dd if=/dev/zero of=Tempfile bs=1024 count=1000000
    #append this file to virtual image file (in this case is hda)
    cat Tempfile >> hda
    resize2fs -f hda


크리에이티브 커먼즈 라이센스
Creative Commons License

Posted by 김형준


NegativeArraySizeException 관련

분산/병렬 프로그램 만들다 보니 전혀 재현 불가능한 에러도 발생합니다. 오늘 발생한 에러는 거의 재현하기 불가능할 것 같습니다.

코드는 다음과 같습니다.

class Test {
    TreeSet<String> values = new TreeSet<String>();
    
     //이부분이 Multi client로부터 접속되는 코드
     public void exec() {
        values.add(...);
        ...
        //다양한 조건을 이용하여 valus add, remove
        List<String> temp = new ArrayList<String>();
        temp.addAll(values);
     }
}

원래는 Test하는 코드의 exec를 호출하는 것은 앞단에서 하나의 클라이언트만 접속하게 되어 있었는데 이 부분에 약간 잘못이 있어 동시에 접속되는 상황이 발생한 것 같습니다.
예외는 다음과 같이 발생했습니다.

java.lang.NegativeArraySizeException
        at java.util.AbstractCollection.toArray(AbstractCollection.java:119)
        at java.util.ArrayList.addAll(ArrayList.java:472)

AbstractCollection 소스를 보니 TreeSet의 size()에서 반환되는 값이 0 보다 작은 경우에만 이런 예외가 발생하게 되어 있습니다.

크리에이티브 커먼즈 라이센스
Creative Commons License

Posted by 김형준


자바에서 byte[] memory usage

어제 포스팅한 자바 메모리 관련해서 계속 프로파일링 도중에 다음과 같은 아주 심플한 코드들 JProfiler를 이용하여 프로파일링을 해 봤습니다.

길이 10의 byte[]를 10,000개 만들 경우 실제 데이터의 메모리는 100,000 byte가 됩니다. 하지만 프로파일러에는 234KB가 10,0001개 생성되었고 234KB 할당 되어 있다고 나옵니다.
사용자 삽입 이미지
    List<byte[]> values =  new ArrayList<byte[]>();
   
    byte[] aaa = new byte[10];
   
    BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
    String a = "1";
    int size = 0;
    for(int i = 1;  i < 1000000; i++) {
      values.add("1234567890".getBytes());
      size += 10;
     
      if(i % 10000 == 0) {
        System.gc();
        long free = Runtime.getRuntime().freeMemory();
        long total = Runtime.getRuntime().totalMemory();
        System.out.println(i + ">" + size + ">" + free + ">" + total + ">" + (total-free));
        reader.readLine();
      }
    }

이것과 관련해서 검색해보니까 Java에서는 32bit JVM의 경우 하나의 byte[]를 저장하는데 12byte의 추가 메모리가 필요하다고 합니다(http://forum.java.sun.com/thread.jspa?threadID=5216017&messageID=9879177). 그러면 100,000byte + 120,000byte = 220,000 byte로 프로파일링된 결과가 많이 비슷해진 것을 알 수 있습니다.
오버헤드가 너무 심하다는 생각입니다. byte[]에 아주 작은 값이 많이 쌓이는 경우라면 실제 생성한 데이터의 크기와 메모리 할당되는 크기가 너무 차이가 나서 개발자가 예상하지 못한 곳에서 OutOfMemory가 발생할 가능성이 많습니다.
제 경우도 이런 노가다 분석을 시작한 이유가 이것 때문입니다. 실제 데이터는 100MB  정도 넣었는데 메모리는 거의 400MB 이상을 사용하고 있었던 거죠...
난감하네요. 해결해야 하는데

http://www.javaworld.com/javaworld/javatips/jw-javatip130.html?page=2
크리에이티브 커먼즈 라이센스
Creative Commons License

Posted by 김형준


자바에서 이상한 메모리 관리

오늘 코드 테스트 중 우연히 발견한 내용입니다. List에 추가된 모든 데이터의 대략 100MB도 안되는데에도 불고하고 OutOfMemory Error가 발생하였습니다. 물론 -Xmx, -Xms 옵션은 모두 256m으로 설정했습니다.
그래서 테스트 코드를 몇개 만들어서 비교해봤습니다.

    String str = "0123456789";
   
    List<String> datas = new ArrayList<String>(100);
    long realSize = 0;
    for (int i = 0; i < 100000000; i++) {
      String data = "0123456789" + str;
      datas.add(data);
       realSize += data.getBytes().length;
      if (i % 10000 == 0) {
        System.out.println("dataCount=" + i + ",dataSize=" + realSize +
            ",free=" + Runtime.getRuntime().freeMemory() +
            ",total=" + Runtime.getRuntime().totalMemory());
      }
    }
dataCount=3130000,dataSize=62600020,free=270816,total=266403840


    String str = "0123456789";
   
    List<String> datas = new ArrayList<String>(100);
    long realSize = 0;
    for (int i = 0; i < 100000000; i++) {
      String data = new String("01234567890123456789");
      datas.add(data);
       realSize += data.getBytes().length;
      if (i % 10000 == 0) {
        System.out.println("dataCount=" + i + ",dataSize=" + realSize +
            ",free=" + Runtime.getRuntime().freeMemory() +
            ",total=" + Runtime.getRuntime().totalMemory());
      }
    }
dataCount=8640000,dataSize=172800020,free=9725240,total=266403840

위의 코드와 아래코드의 구분은 String에 대한 + 처리 부분만 차이가 납니다. 이 코드는 String에 대한 테스트 코드는 아닙니다. 일반적으로 프로그램 내에서 위에 있는 코드와 같이 생성된 객체를 List에 add하는 경우가 많아서 비교하기 쉽게 만들어 본 것입니다.
아래 코드의 경우 약 864만개의 Object를 추가할 수 있는데 이것을 메모리로 계산해보면 데이터가 172M, 860만개를 가리키기 위한 레퍼런스(1객체당 4byte) 30M 정도 소요되어 200M 정도가 데이터 저장에 사용되었습니다. 그리고 free영역으로 10M 정도 사용되었습니다. 나머지 60M는 어디에서 사용되었는지가 첫번째 이슈입니다. 일단 추측해볼 수 있는 것은 ArrayList의 경우 자신의 용량보다 더 많은 레코드에 대해 add 요청이 들어오면 확장을 하게 되는데 이 확장되어 미리 확보한 영역 때문이라고 예측을 해볼 수 있습니다. 그래서 두번째 코드에서 List를 String[] 배열로 변경하여 다시 수행해 보면 다음과 같은 결과가 나타납니다.
"dataCount=9410000,dataSize=188200020,free=56504,total=266403840"

1000만개 레퍼런스에 40M, 데이터 180M -> 220M 이정도면 봐줄만 합니다. 나머지 40M는 다른 용도로 사용하겠죠....

두번째 이슈는 첫번째 코드에서는 300만개 정도밖에 저장할 수 없다는 것입니다. for loop내에서 발생하는 String 연산 처리에서 생성된 객체들은 GC 수행시 모두 해제 되기 때문에 이론적으로는 두번째 코드와 동일해야 할 것 같은데 다르게 나타나고 있습니다.
크리에이티브 커먼즈 라이센스
Creative Commons License

Posted by 김형준


자바로 만든 embedded db 비교

embedded db는 oracle, mysql 처럼 네트워크 기반으로 데이터 서비스를 제공하지 않고 DB를 사용하는 프로그램이 수행되는 장비의 로컬 파일시스템 또는 메모리를 이용하는 DB를 말한다.
자바로 만든 embedded db 오픈소스 중  derby, berkeley db, hsqldb를 가지고 몇가지 테스트를 해 보았다. derby, hsqldb는 네트워크 기반, 임베디드 기반 모두 지원하지만 여기서는 임베디드 기반만 테스트 하였다. berkeley db(이후 bdb)는 db 라기 보다는 hash 형태만 제공하는 아주 간단한 db이다.
1 row = 1000byte, 100,000 rows 데이터를 저장하는 테스트를 수행하였다.

100,000  insert 한 다음 commit(bdb의 경우 sync) 하는 경우

bdb(14초) << hsqldb(42초) < derby(53초)

bdb가 다른 db에 비해 상상 할 수 없을 정도의 속도로 저장되었다.
hsqldb의 경우 옵션에 따라 성능 차이가 많이 나타난다. hsqldb는 server 타입, file 타입, memory 타입 세가지 형태로 사용할 수 있다. 테이블을 생성할 때 create memory table ...형태로 만들면 memory 기반 테이블이 되고 create cached table로 만들면 file 기반 테이블이 된다.
file 기반 테이블은 일정 크기 동안은 메모리에 저장시키고(물론 change log 정보는 파일에 기록) 메모리가 일정 수준 이상 도달하면 디스크로 저장하는 하이브리드 방식을 채택하고 있다. 여기서 문제점은 디스크로 저장할 때 기존 파일에 추가하는 것이 아니라 기존 파일 + 메모리 내용이 되는 새로운 파일을 만든다는 것이다. 데이터 크기가 큰 경우 데이터 파일을 생성하는데 너무 많은 시간이 소요되고 이 시간 동안 insert 작업을 할 수 없게 된다.


1건씩 commit 하는 경우

hsqldb(42초) < derby(158초) << bdb(답이 없음, 수십분 이상)


위 성능으로 보면 hsqldb가 commit, rollback을 지원하지 않는 것 처럼 보이지만 실제 테스트 해보면 rollback도 잘 지원한다. 건당 commit 하는 경우와 전체 commit 하는 경우가 동일한 속도를 보장하고 있는데 다른 side effect는 없는지 확인해봐야 겠다.
어쨋든 여러가지를 고려해보면 안정적인 성능을 발휘하는 derby가 그나마 가장 좋기는 한데 원하는 수준의 속도가 나오지 않는다.
 


 

크리에이티브 커먼즈 라이센스
Creative Commons License

Posted by 김형준


AOP(Aspect Oriented Program)을 위해 Proxy를 이용하여 특정 클래스의 method하는 경우 InvocationHandler를 implements 한다. 이때 invoke() 메소드 내에서 synchronized 처리에 주의해야 한다. 다음 코드의 경우

public class DefaultInvocationHandler implements InvocationHandler {

Object monitor = new Object();

 

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable

    {

      synchronized(monitor) {

        try {

          return method.invoke(targetObject, args);

        } catch (Throwable e) {

          e.printStackTrace(System.out);

          throw e;

        }

      }

    }

}


이 경우 일반적인 경우에는 잘 작동하지만 호출하는 메소드 내에서 다시 Proxy 객체의 메소드를 호출하는 경우 Handler 내부적으로는 recursive하게 call되어 두번째 메소드가 수행되는 시점에서는 monitor에 대한 lock을 가져오지 못해 무한대기 상황이 발생한다.
따라서 method.invoke(targetObject, args); 이부분 수행할 때에는 lock에 대한 처리가 없어야 한다.

크리에이티브 커먼즈 라이센스
Creative Commons License

Posted by 김형준


Java File 클래스 renameTo 메소드 관련

Java의 File 클래스에 있는 기능 중 renameTo 메소드는 잘못 사용할 가능성이 많은 메소드이다. Windows와 Linux에서 다르게 동작할 수 있으며 파일 시스템의 디스크 구성에 따라 다르게 작동할 수 있기 때문이다.
예를 들어 windows에서 C 드라이브에 있는 파일을 D 드라이브로 renameTo 메소드를 이용할 경우 rename 되지 않는다. 이유는 windows의 경우 논리적으로 다른 드라이버에서의 rename은 rename이 아니라 copy & delete old 이기 때문이다. 이런 로직을 renameTo 처리해 주면 좋겠지만 그렇지 않다.
또 다른 문제는 서브 디렉토리에 대한 문제이다. 디렉토리를 renameTo할 경우 해당 디렉토리 아래에 파일 또는 서브 디렉토리가 존재하는 경우에도 정상적으로 작동하지 않을 가능성이 크다. 따라서 rename의 경우 별도로 메소드를 만들어서 사용하는 것이 환경적인 문제로 발생하는 오동작을 피할 수 있다.
어제 개발 도중에 이런 문제로 인해 인터넷을 검색해 봤지만 쓸만한 코드를 찾지 못해 직접 만들었다.

private boolean rename(File fromFile, File toFile) throws IOException {

    if (fromFile.isDirectory()) {

      File[] files = fromFile.listFiles();

      if (files == null) {

        //디렉토리 아무것도 없는 경우

        return fromFile.renameTo(toFile);

      } else {

        //디렉토리내 파일 또는 디렉토리가 존재하는 경우

        if(!toFile.mkdirs()) {

          return false;

        }

        for (File eachFile : files) {

          File toFileChild = new File(toFile, eachFile.getName());

          if (eachFile.isDirectory()) {

            if(!rename(eachFile, toFileChild)) {

              return false;

            }

          } else {

            if(!eachFile.renameTo(toFileChild)) {

              return false;

            }

          }

        }

        return fromFile.delete();

      }

    } else {

      //파일인 경우

      if(fromFile.getParent() != null) {

        if(!toFile.mkdirs()) {

          return false;

        }

      }

      return fromFile.renameTo(toFile);

    }

  }

크리에이티브 커먼즈 라이센스
Creative Commons License

Posted by 김형준


« Previous : 1 : 2 : 3 : 4 : 5 : ... 9 : Next »