Linux Kernel Internal
이번 글에서는 vm.overcommit에 대해서 다뤄볼까 합니다. 막연하게 메모리를 많이 쓸 수 있게 해주는 거 아니야?라고 생각 해왔었는데요, 살펴보니 조금 다른 의미였습니다. 그럼, 함께 살펴보시죠~
overcommit이란 말 그대로 commit을 더 할 수 있게 해준다는 의미입니다. 그렇다면 commit이란 뭘까요? 프로세스가 커널에게 메모리 요청을 할 때 malloc()과 같은 시스템 콜을 사용하는데요, 커널은 시스템 콜 요청을 받고 해당하는 메모리 영역의 주소를 전달자로 넘겨 줍니다. 이 때, 프로세스가 할당받고 사용하지 않을 수 있기 때문에 할당해준 메모리 영역을 물리 메모리에 바인딩하지는 않습니다. 즉, 프로세스는 받았다고 생각하지만 실제로는 물리 메모리 어느 곳에도 할당되어지지 않은 상태입니다. 그리고 이것을 memory commit이라고 합니다.
실제 sar 명령을 통해서 살펴보면 아래와 같이 commit(%)를 볼 수 있습니다.
프로세스가 메모리 공간을 요청하고 사용은 하지 않은 공간이 3% 정도 된다는 이야기입니다.
그렇다면 커널에는 왜 overcommit 기능이 있을까요? 순간적으로 많은 양의 메모리를 필요로 하는 작업이 필요하기 때문입니다. 특히 fork()시스템 콜과 같은 작업을 처리할 때 필요합니다. fork()는 새로운 프로세스를 만드는 시스템 콜인데요, 이 시스템 콜을 호출하면 자식 프로세스는 부모 프로세스의 모든 주소 공간을 그대로 복사해 옵니다. 그래서 부모 프로세스가 사용하던 메모리가 많으면 많을수록 많은 주소 공간을 필요로 하게 되죠. 하지만 대부분의 경우 fork() -> exec() 콜을 통해서 새로운 작업으로 넘어가는 경우가 많기 때문에 복사해 온 공간을 그대로 사용하는 경우는 매우 드물게 일어납니다. 이 때 메모리 복사를 효율적으로 하기 위해 CoW (Copy on Write)라는 기술도 사용이 됩니다. (이 글의 주제는 아니기 때문에 자세한 설명은 생략합니다.) 그리고 부모 프로세스의 메모리 영역을 복사하는 과정에서 memory commit이 일어나게 되고, 경우에 따라서는 overcommit이 필요한 경우가 생깁니다.
예를 들어 보겠습니다. 4GB 의 물리 메모리를 가지고 있는 서버에서 3GB 의 프로세스가 돌고 있다고 가정합니다. 이 프로세스가 어떤 작업을 하기 위해 fork()를 통해 자식 프로세스를 만들고, exec()으로 해당 작업을 진행하려고 하는데요, 3GB 프로세스가 fork()를 하게 되면 똑같은 3GB 의 메모리 공간이 더 필요합니다. 하지만 물리 메모리는 4GB 이기 때문에 남은 영역 1GB로는 fork()를 하지 못하죠. 하지만 이 메모리 공간은 fork() 할 때만 잠깐 필요하기 때문에 실제로는 memory commit이 됩니다. (usage로 계산되지 않습니다.) 즉, 3GB를 요청은 하지만 사용하지 않을 수 있기 때문에 잠시 동안 실제로 존재하는 것처럼 할당받을 수만 있으면 됩니다. 그리고 이 때 overcommit이 필요하게 됩니다.
그림에서는 1GB Free 메모리를 사용한 것처럼 보이지만, 실제로 자식 프로세스를 위해 할당되지는 않았습니다.
만약 overcommit이 되지 않는다면 커널은 메모리 할당 과정에서 가용 메모리 부족을 이유로 fork()에 대해 error를 주고 결국 자식 프로세스를 만들지 못하게 됩니다. 실제로 사용하지 않을 메모리 때문에 fork()가 실패하면 안 되겠죠.
그렇다면 overcommit은 어떻게 설정할 수 있을까요?
첫 번째 옵션은 vm.overcommit_memory입니다. overcommit에 대한 enable/disable을 결정하는 중요한 파라미터입니다.
0 - 커널에서 설정하는 기본값이며, Heuristic 하게 overcommit을 허용하는 옵션입니다. 계산식은 아래와 같습니다. mm/mmap.c 안에 __vm_enough_memory() 함수에서 계산을 합니다.
보이는 것처럼 Page Cache + Swap Memory + Slab Reclaimable을 합친 수가 요청한 메모리 수보다 클 경우 commit이 일어납니다.
실제 시스템의 Free 메모리를 가지고 계산하지 않는 다는 것에 주의 하셔야 합니다.
1 - 항상 overcommit을 합니다. 이 옵션으로 설정하게 되면 메모리 할당이 항상 성공합니다.
2 - 제한적 overcommit을 허용합니다. 현재 시스템에 설정되어 있는 Swap 영역의 크기 + vm.overcommit_ratio 값으로 결정된 메모리 영역만큼 overcommit 할 수 있습니다. 지정된 영역을 넘어가게 되면 commit 실패합니다.
두 번째 옵션은 vm.overcommit_ratio입니다. 물리 메모리에 대한 %를 지지하며, 기본값은 50입니다. 이 값은 위 스크린샷에 sysctl_overcommit_ratio 변수에 저장됩니다.
실제 시스템에서 commit 할 수 있는 최댓값은 /proc/meminfo를 통해서 확인할 수 있습니다. cat /proc/meminfo에서 CommitLimit 값을 통해 확인할 수 있습니다.
이 값은 vm.overcommit_memory = 2 일 때만 의미가 있습니다.
아래는 예제입니다. Swap 영역 10GB + 물리 메모리 8GB * 0.5 = 14GB 의 크기가 최대 commit 값임을 확인할 수 있습니다.
MemTotal: 8061404 kB
SwapTotal: 10485756 kB
CommitLimit: 14516456 kB
vm.overcommit_ratio를 10으로 변경하고 커널 파라미터를 reload 하면 아래와 같이 11GB 정도 되는 것을 확인할 수 있습니다.
CommitLimit: 11291896 kB
그럼, 간단한 테스트를 통해서 overcommit이 어떻게 동작하는지 살펴보겠습니다.
기본적으로 테스트할 프로그램은 아래와 같이 malloc() 만 호출하고 실제 메모리 영역에 쓰기 작업은 하지 않습니다.
while (1) {
myblock = (void *) malloc((double)GIGABYTE);
// memset(myblock, 1, GIGABYTE);
printf("Doing...\n");
sleep(1);
}
overcommit_memory 값이 0일 때를 확인해 보기 위해 세 개의 프로그램을 만듭니다. 하나는 시작하자마자 4GB 의 메모리를 요청하고 memset을 통해서 실제 쓰기 작업도 합니다. 두 번째 프로그램은 8GB 의 메모리 영역을 요청합니다. 그리고 세 번째 프로그램은 Swap까지 포함한 가용 메모리를 넘길 수 있도록 20GB 의 메모리를 요청합니다.
스크린샷에서 볼 수 있는 것처럼 8GB 메모리 요청은 Swap까지 포함한 가용 메모리 영역보다 작기 때문에 malloc()이 계속해서 성공하고 commit 메모리 양도 늘어나는 것을 볼 수 있습니다. 하지만, 20GB를 한 번에 요청할 경우 실패하는 것을 볼 수 있습니다.
같은 작업을 overcommit_memory가 2일 때 해보겠습니다. 결과는 아래와 같이 CommitLimit 값을 넘어간 순간 malloc() 호출이 실패합니다.
overcommit_memory는 프로세스가 메모리를 할당하는데 큰 영향을 주는 파라미터입니다. 2로 세팅해서 사용하게 되면 가용한 메모리 안에서만 할당이 가능하기 때문에 OOM killer가 발생하진 않지만, 메모리 할당 문제로 갑작스럽게 프로세스가 죽을 수 있습니다.
또한 1은 제한 없는 commit으로 인해 시스템의 갑작스러운 패닉이나 행 상태를 일으킬 수 있습니다. 해당 값을 튜닝해서 사용하실 때에 끼치는 영향에 대해 반드시 이해하고 튜닝하시기 바랍니다~
참고 사이트 : http://www.linuxdevcenter.com/pub/a/linux/2006/11/30/linux-out-of-memory.html