시작하며..

0.6.4 버전의 카산드라(Cassandra)를 패치한 0.6.5의 큰 변화는 리눅스 운영체제에 동작하는 java 프로세스가 mmap을 사용하면서 swap 메모리가 증가되는 부분을 막기 위함이다.

아래 내용은 이 글을 이해하기 위한 작은 정보이다.

http://knight76.tistory.com/entry/Swap-메모리
http://knight76.tistory.com/entry/자바-RandomAccessFile-클래스의-map-메서드-내부-구조-분석

 

본론..

<이슈와 결과 내용>

0.6.4버전의 카산드라에서 read 성능이 너무 낮아졌고, swap 메모리가 많아지면서 성능이 떨어졌다고 한다.

(https://issues.apache.org/jira/browse/CASSANDRA-1214)

아래와 같이 성능이 어느 정도 나오지 못하고 들쑥날쑥 하고 있다.

JVM의 full gc 시간이 항상 일어나지 않는다. native heap 의 영역의 메모리가 많아져서 swap memory로 갔다가 gc time 때 swap으로부터 메모리를 읽은 후 gc 데이터를 모두 날리는 작업을 하면서 성능이 떨어지는 것이다.

gc 시점 때 all stop + swap in/out이 일어나면서 계속 성능이 저하되는 현상이 생긴 것이다.
(마치 gc 그래프와 흡사한 성능 그래프가 증명하고 있다.)

0.6.5 버전에서는 0.6.5 버전의 패치를 통해서 13%의 성능 향상이 일어났다.

 

<자세한 내용>

카산드라 0.6.4 버전에서는 SSTable read 성능을 높이기 위해서 NIO를 사용했다. 내부 데이터를 MappedByteBuffer 클래스로 읽는다. 이 때 mmap을 사용한다.

storage-conf.xml 설정 파일의 DisAccessMode 속성의 값은 auto이다.

<DiskAccessMode>auto</DiskAccessMode>

 

DatabaseDescritor 클래스의 static 블럭에서 파일을 읽고 있으며 64비트 머신인 경우에는 mmap이 디폴트값으로 정해진다.

String modeRaw = xmlUtils.getNodeValue("/Storage/DiskAccessMode");
diskAccessMode = DiskAccessMode.valueOf(modeRaw);
if (diskAccessMode == DiskAccessMode.auto) {
          diskAccessMode = System.getProperty("os.arch").contains("64") ?
          DiskAccessMode.mmap
: DiskAccessMode.standard;
          indexAccessMode = diskAccessMode;
}

 

disk access mode 의 값이 standard인 경우는 index buffer 자체가 없기 때문에 큰 성능을 기대할 수는 없지만, mmap 모드인 경우에는 index buffer를 메모리에 사용할 수 있다.

SSTableReader 클래스 생성자 에서는 mmap disk access의 경우 mmap을 사용할 수 있도록 되어 있다.

    long indexLength = new File(indexFilename()).length();
    int bufferCount = 1 + (int) (indexLength / BUFFER_SIZE);
    indexBuffers = new MappedByteBuffer[bufferCount];
    long remaining = indexLength;
    for (int i = 0; i < bufferCount; i++)
    {
        indexBuffers[i] = mmap(indexFilename(), i * BUFFER_SIZE,
                                 (int) Math.min(remaining, BUFFER_SIZE));
        remaining -= BUFFER_SIZE;
    }

 

mmap 메서드는 다음과 같이 정의되어 있다.

private static MappedByteBuffer mmap(String filename, long start, int size) throws IOException
{
    RandomAccessFile raf;
    try
    {
        raf = new RandomAccessFile(filename, "r");
    }
    catch (FileNotFoundException e)
    {
        throw new IOError(e);
    }

    try
    {
        return raf.getChannel().map(FileChannel.MapMode.READ_ONLY, start, size);
    }
    finally
    {
        raf.close();
    }
}


RandromAccessFile 클래스의 map 메서드는 내부적으로 clib의 공유할 수 있는 mmap64 시스템콜을 호출하여 native heap 영역의 메모리를 공유한다. (참고 : http://knight76.tistory.com/entry/자바-RandomAccessFile-클래스의-map-메서드-내부-구조-분석)

 

<버그질라 이슈에 대한 설명>

리눅스 커널의 메모리 관리자는 swap 메모리로 언제든지 데이터를 옮길 수 있다.
리눅스 커널의 vm.swappiness 파라미터가 0 이상이면 메모리가 어느 정도 부족하다고 판단하면 swap 메모리로 옮겨놨다가 필요할 때 램에 적재하게 한다.

mmap을 쓰다보니 native heap 영역에 있는 데이터들의 gc 가 되는 시점이 중요해진다.  java의 heap 영역을 청소하는 full gc 가 호출 될 때 unmap이 호출이 되는 구조로 되어 있다. (참고 : http://knight76.tistory.com/entry/자바-RandomAccessFile-클래스의-map-메서드-내부-구조-분석). unmap이 되는 클래스가 PhantomReference를 상속하였는데, 이 의미는 정말 사용 중이지 않을 때 gc 대상이 된다. (전문 용어로 reachable 하지 않거나 referent 객체로부터 로부터 mark되지 않은 상태). soft reference나 weak reference 보다도 최대한 끝까지 살아남는 놈들이다.  (자바 스펙에는 모호하게 적혀 있고, java.lang.ref api와 소스에서 그 의미를 찾아볼 수 있다.)

full gc가 일어나는 시점에서 본다면, native heap 영역이 가득차거나 java heap 영역이 가득찰 때 일어난다. (사실 정확하게 찬다는 의미보다는 gc가 적당한 비율로 메모리를 찼다고 판단하면 gc한다.) 결국은 많은 메모리를 사용 중에 일어날 수 있다.

리눅스 커널은 mmap으로 할당받은 데이터인 SSTable의 정보를 swap 메모리로 이동한다. 무식한 이 데이터는 full gc에도 차곡차곡 쌓이다가 정말 확실히 gc의 대상이 될 때, gc가 된다.
이 때, gc 시점에 정리를 하는데, swap 영역에 있는 녀석이 gc가 될 수 없다. 즉 swap in(ram으로 불러들임)을 한후, gc가 된다. 만약 swap 영역의 메모리가 ram 메모리의 크기보다 많이 크다면 성능은 전혀 기대할 수 없는 수준으로 내려갈 수 밖에 없다.

따라서, 만약 적당한 크기의 swap 영역을 지정할 필요가 있다.  이 영역을 제대로 잡는 것은 계속적인 테스트를 통해서만 할 수 있다.

이런 부분 때문에 카산드라에서는 swaping 셋팅을 전혀 안하는 것이 최고의 선택(best value)라고 하는 배경이다.

http://wiki.apache.org/cassandra/MemtableThresholds

Virtual Memory and Swap

On a dedicated cassandra machine, the best value for your swap settings is no swap at all -- it's better to have the OS kill the java process (taking the node down but leaving your monitoring, etc. up) than to have the system go into swap death (and become entirely unreachable).

Linux users should understand fully and then consider adjusting the system values for swappiness, overcommit_memory and overcommit_ratio.

 

http://wiki.apache.org/cassandra/FAQ#mmap

Why does top report that Cassandra is using a lot more memory than the Java heap max?

Cassandra uses mmap to do zero-copy reads. That is, we use the operating system's virtual memory system to map the sstable data files into the Cassandra process' address space. This will "use" virtual memory; i.e. address space, and will be reported by tools like top accordingly, but on 64 bit systems virtual address space is effectively unlimited so you should not worry about that.

What matters from the perspective of "memory use" in the sense as it is normally meant, is the amount of data allocated on brk() or mmap'd /dev/zero, which represent real memory used. The key issue is that for a mmap'd file, there is never a need to retain the data resident in physical memory. Thus, whatever you do keep resident in physical memory is essentially just there as a cache, in the same way as normal I/O will cause the kernel page cache to retain data that you read/write.

The difference between normal I/O and mmap() is that in the mmap() case the memory is actually mapped to the process, thus affecting the virtual size as reported by top. The main argument for using mmap() instead of standard I/O is the fact that reading entails just touching memory - in the case of the memory being resident, you just read it - you don't even take a page fault (so no overhead in entering the kernel and doing a semi-context switch). This is covered in more detail here.

 

그럼에도 불구하고, 최근에 릴리즈된 1.0.2소스를 보니 DiskAccessMode의 디폴트값이 auto로 되어 있다. 이는니눅스  64 비트 운영체제를 사용하는 신규 개발자에게는 재앙이 될 수 있다는 얘기도 된다.

1.0.2에서는 약간 구조를 바꾸어서 쓰고 있으며, Table .getDataFileLocation() 호출시 적당한 시점에 System.gc()도 및 unmap를 통해서 메모리가 적당히 있을 수 있도록 수정되었다.  (정확히 말하면, 언제부터 바뀐지는 잘 모르지만, 많이 노력하고 있다는 느낌이다.

 

<mlockall의 사용>

0.6.5에서 바뀐 부분은 mlockall 메서드를 사용할 수 있도록 한 부분이다. 이를 위해서 JNA를 사용하였다. (참조 : http://knight76.tistory.com/entry/jni-vs-jna)

ivy.xml 설정 파일에 jna library가 추가되었다.

<dependency org="net.java.dev.jna" name="jna" rev="3.2.7"/>

ivysettings 설정 파일이 추가되었다.

<ivysettings>
  <settings defaultResolver="ibiblio"/>
  <resolvers>
    <chain name="chain" dual="true">
      <ibiblio name="java.net2" root="http://download.java.net/maven/2/" m2compatible="true"/>
      <ibiblio name="ibiblio" m2compatible="true" />
    </chain>
  </resolvers>
  <modules>
    <module organisation="net.java.dev.jna" name="jna" resolver="chain" />
  </modules>
</ivysettings>

 

CassandraDaemon 클래스의 setup 메서드안에서  FBUtilities.tryMlockall(); 을 호출한다. tryMlockall() 에서는 clib의 mlockAll() 시스템 콜을 호출한다. 즉 처음부터 nio 로 만든 객체들을 swap 메모리로 가지 않게 하고, ram에서만 사용할 수 있도록 바뀐 것이다.


public static void tryMlockall()
{
    int errno = Integer.MIN_VALUE;
    try
    {
       int result = CLibrary.mlockall(CLibrary.MCL_CURRENT);
        if (result != 0)
            errno = Native.getLastError();
    }
    catch (UnsatisfiedLinkError e)
    {
        // this will have already been logged by CLibrary, no need to repeat it
        return;
    }
    catch (Exception e)
    {
        logger_.debug("Unable to mlockall", e);
        // skipping mlockall doesn't seem to be a Big Deal except on Linux.  See CASSANDRA-1214
        if (System.getProperty("os.name").toLowerCase().contains("linux"))
        {
            logger_.warn("Unable to lock JVM memory (" + e.getMessage() + ")."
                         + " This can result in part of the JVM being swapped out, especially with mmapped I/O enabled.");
        }
        else if (!System.getProperty("os.name").toLowerCase().contains("windows"))
        {
            logger_.info("Unable to lock JVM memory: " + e.getMessage());
        }
        return;
    }

    if (errno != Integer.MIN_VALUE)
    {
        if (errno == CLibrary.ENOMEM && System.getProperty("os.name").toLowerCase().contains("linux"))
        {
            logger_.warn("Unable to lock JVM memory (ENOMEM)."
                         + " This can result in part of the JVM being swapped out, especially with mmapped I/O enabled."
                         + " Increase RLIMIT_MEMLOCK or run Cassandra as root.");
        }
        else if (!System.getProperty("os.name").toLowerCase().contains("mac"))
        {
            // OS X allows mlockall to be called, but always returns an error
            logger_.warn("Unknown mlockall error " + errno);
        }
    }
}

 

이런 용도로 CLibrary 클래스가 추가되었다.

public final class CLibrary
{
    private static Logger logger = LoggerFactory.getLogger(CLibrary.class);

    public static final int MCL_CURRENT = 1;
    public static final int MCL_FUTURE = 2;
   
    public static final int ENOMEM = 12;

    static
    {
        try
        {
            Native.register("c");
        }
        catch (NoClassDefFoundError e)
        {
            logger.info("JNA not found. Native methods will be disabled.");
        }
        catch (UnsatisfiedLinkError e)
        {
            logger.info("Unable to link C library. Native methods will be disabled.");
        }
    }

    public static native int mlockall(int flags);

    public static native int munlockall();

    private CLibrary() {}
}

 

참고로 munlockall() 메서드도 native로 바인딩되었지만 1.0.2에서도 실제 사용하지 않고 있다.

 

 

마치며..

2일 동안 카산드라의 파일 시스템쪽이 어떻게 되어 있는지 잘 몰랐는데. 이번 기회에 카산드라의 mmap 이슈와 그 처리방법에 대해서 잘 파악할 수 있었다. 시간이 되면 카산드라 분석해볼까나…

 

 

 

* 참고 내용 : mlock, mlockall (man 페이지를 이용한 정보 처리)


mlock, mlockall, munlock, munlockall 시스템 콜 함수에 대한 정리
"man mlock" 명령어를 사용하면 mlock 에 대한 정보를 확인할 수 있다.

       #include <sys/mman.h>

       int mlock(const void *addr, size_t len);

       int munlock(const void *addr, size_t len);

       int mlockall(int flags);

       int munlockall(void);

man mlock 에 대한 결과값을 확인할 수 있다.

mlock() 또는 mlockall()은 swap 영역으로 메모리 이동이 되지 않도록 RAM 안에 사용된 virtual memory(가상 주소)를 lock 한다.
munlock() 또는 munlockall()은 리눅스 커널 메모리 관리자에 의해서 swap out될 수 있도록 unlock해주는 함수이다.

mlock() 함수는 주소와 길이에 대한 영역을 lock을 개런티한다.
mlockall() 함수는 프로세스에 대한 주소번지 모두를 lock 한다.
코드/데이터/스택/동적라이르리러,사용자 커널데이터/공유메모리/메모리맵 데이터까지모두 lock한다.

mlockall() 의 아규먼트는 int형 타입의 flag 값을 지정하면 된다. 아래 타입 중에 하나만 또는 OR로 사용가능하다.

- MCL_CURRENT (1): 프로세스의 주소 영역으로 매핑된 모든 페이지
- MCL_FUTURE (2) : 앞으로 프로세스의 주소 영역에 매필될 모든 페이지.
               이 옵션을 사용하면 메모리 할당과 같은 시스템 콜(mmap, sbrk, malloc)을 쓰다 RAM 영역을 넘어설때 할당을 해주지 못하게 된다.
               만약 Stack에서 그 일이 발생하면, SIGSEGV signal이 발생하고 stack 영역은 확장을 할 수 없다.

 

Memory locking은 두 개의 주요 어플리케이션에 사용할 수 있다. real time 알고리즘과 보안을 높이 필요로 하는 데이터 처리쪽에 사용할 수 있다.
특히 real time 알고리즘이 필요한 어플리케이션에서는 locking 함으로서, page fault를 원천적으로 봉쇄해서
time-critical section(시간 동기 문제)가 일어나지 않게 할 수 있다.

 

Cassandra 0.6.5~1.0.2 (최신) 까지 mlockall(MCL_CURRENT)을 주어 사용하고 있음. 따라서 언제든지 차곡차곡 쌓이는 일부 메모리는 swap 메모리로 늘어날 수 있음..

Posted by 김용환 '김용환'

Cassandra mmap 이슈로 소스를 살펴보고 있는데, 빌드 체계가  Ant-ivy에서 Ant-maven으로 바뀌었다. 이에 대한 이유를 살펴보도록 한다. 내가 속한 조직에서는 Ant + Ivy를 사용하고 있는데, 이에 대한 보완 또는 빌드 속도 향상에 도움이 되지 않을까 해서 공부해본다.

Ant-Ivy 구조에서 Ant-Maven으로 바뀐 부분에 대한 내용은 Cassadra에서 올라온 이슈(https://issues.apache.org/jira/browse/CASSANDRA-2017)를 살펴본다.

첫번째, cassandra가 플랫폼 형태로 쓰기 때문에 maven central repository로 올려야 사람들이 편하게 쓸 수 있어야 한다. ivy는 pom 파일을 생성하지 못하기 때문에 maven-ant-tasks 가 필요하다.
두번째, gpg 시그내처 (보안) 를 생성하기 위해서 ivy task를 또 다시 실행시켜야 하는 것이 있는데, maven-ant-task는 한번에 gpg 시그내처를 생성할 수 있다.
세번째, dependency 정보를 한곳에서 할 필요가 있다. (아마도 maven central에 올리면서 중복 정보가 생기는 부분에서 얘기가 된 것 같다. 오해를 위해서 ivy.xml 파일 하나에서 충분히 관리될 수 있다. ivy는 maven에 비해 보기는 편하다.)

이외에 빌드할때, dependency 체크가 ivy보다 빨라서 빌드 속도 개선이 되었다고 한다.

그래서 0.7.6부터 배포되었다.  (http://pkgsrc.se/databases/apache-cassandra)

아직 회사에서 사용하는 것이 특별히 이슈는 없지만, ant-maven으로 고민할 필요가 있다는 생각이 들었다..



Posted by 김용환 '김용환'