시작하며..
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)
아래와 같이 성능이 어느 정도 나오지 못하고 들쑥날쑥 하고 있다.
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 try |
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 SwapOn 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#mmapWhy 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에서만 사용할 수 있도록 바뀐 것이다.
if (errno != Integer.MIN_VALUE) |
이런 용도로 CLibrary 클래스가 추가되었다.
public final class CLibrary public static final int MCL_CURRENT = 1; static 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 메모리로 늘어날 수 있음..
'general java' 카테고리의 다른 글
[이클립스] maven3 - maven-antrun-plugin 컴파일 에러 Failed to execute goal org.apache.maven.plugins:maven-antrun-plugin (1) | 2012.01.12 |
---|---|
리눅스 RAM 디스크에서 컴파일 시간 체크 (0) | 2012.01.06 |
자바 RandomAccessFile 클래스의 map 메서드 내부 구조 분석 (0) | 2012.01.04 |
JAXB 잘 사용하기 (1) | 2011.12.21 |
WindowBuilder Pro 간단한 소개 (0) | 2011.09.20 |