brunch

You can make anything
by writing

C.S.Lewis

by 강진우 Oct 10. 2015

메모리 재할당과 커널 파라미터

Linux Kernel Internal

오늘은 리눅스 커널이 어떤 방식으로 메모리를 재할당 하는지 간단하게  살펴보겠습니다. 그리고 더불어 어떤 경우에 스왑을 사용하게 되고 이에 관련해 어떤 식으로 튜닝을 하면 좋을지도  살펴보겠습니다.

저 역시 시스템 엔지니어로 업무 하고 공부하면서 정리한 내용이라 잘못된 내용이 있을 수 있습니다. 잘못된 내용이 있으면 언제든 말씀해 주세요~


메모리 재할당이란?


리눅스 커널은 기본적으로 메모리가 유휴 상태로 있는 것을 선호하지 않습니다. 특정 프로세스에 할당되어 있지 않는 메모리는 자연스럽게 커널이 사용하게 되고요, 커널은 이 메모리를 주로 캐시 용도로  사용합니다. 우리가 흔히 말하는 page cache, dentry cache, inode cache 등이 커널이 사용하는 캐시에 속합니다.

하지만 이런 식으로 메모리를 사용하다 보면 가용한 메모리가 계속 줄게 될 텐데요, 이 때 사용하지 않는 메모리를 확인해서 필요로 하는 다른 곳에 재할당 하는 것을 메모리 재할당 이라고 합니다. 주로 page cache가 반환이 되고요, 특정 조건이 되면 프로세스에 할당된 메모리를 회수하고 다른 프로세스에 할당하는 swapping도 발생하게 됩니다. page cache는 말 그대로 캐시 메모리 이기 때문에 회수하더라도 시스템 성능에 영향을 주진 않지만, swapping의 경우 프로세스에 할당된 메모리이기 때문에 시스템 성능에 영향을 줄 수 있습니다. 

그래서 가능 한한 swapping이 발생하지 않도록 하는 것이 성능 유지의 중요한 포인트가 됩니다.
uptime이 오래되면 이런 그래프를 볼 수 있습니다.

그럼 언제 메모리 재할당이  일어날까요?


프로세스로부터 메모리 할당 요청이 들어오면 커널은 FreeList에서 사용 가능한 메모리 영역을 찾아서  반환합니다. 이 때, 사용 가능한 메모리 영역이 없다면 사용하고 있는 메모리 중에 반환할 수 있는 메모리가 있는지를 찾아서 반환해 줍니다. vm.swappiness 커널 파라미터가 이 과정에 사용이 되며 이 값에 따라 page cache에서 찾아서 반환할 것인지, 다른 프로세스에 할당되어 있는 메모리를 찾아서 반환할 것인지를  결정합니다.

또한 시스템의 메모리가 임계치 이하로 내려갈 경우 kswapd 데몬을 통해서 메모리 반환이  일어납니다. 이 임계치 값은 vm.min_free_kbytes 커널 파라미터로  관리됩니다.


메모리 재할당이 이루어 지는 과정

그럼 각각 파라미터에 대해  살펴보겠습니다.


vm.swappiness


너무나도 유명한 계산식이죠..  swap_tendency입니다. 아마 리눅스 커널에 관심이 있으신 분들은 익숙하실 바로 그  계산식입니다.

swap_tendency 계산식

mapped_ratio는 현재 사용 중인 메모리 사용률,  distress는 현재 scan 중인 메모리 영역의 priority에 따른 가중치,  그리고 제일 뒤에 있는 swappiness 값이 커널  파라미터입니다.

보시면 알 수 있듯이 mapped_ratio, distress 값은 사용자가  수정할 수 없는 값들입니다. 반면에 swappiness 값은  sysctl을 통해서 변경할 수 있는데요, 결과적으로 저 계산식에 따른 값이 100 이상이 되면 page cache 반환을 하지 않고 프로세스에 할당된 메모리를 재할당 합니다. 

여기서 잠깐! 프로세스에 할당된 메모리를 재할당 한다고 해서 active 메모리를 재할당 하지는 않습니다. 프로세스에 할당된 메모리 중에서도 inactive 메모리를 재할당 하게 되는데요, 프로세스에 할당된 메모리 이기 때문에 메인 메모리에서 지움과 동시에 swap 영역으로  이동시킵니다.

distress 값을 좀 더  살펴보면, 리눅스에서 사용하는 Buddy System과 관련이 있습니다. 나중에 기회가 되면 다른 글을 통해서  설명하겠지만~ Buddy System 은 내부적으로 11개의 Zone을 가지고 있습니다. 연속된 page의 개수를 가지고 정렬하는데요, 1,2,4,8,16,32,64,128,256,512,1024 개로  관리합니다. 즉 물리적으로 연속된 page가 1개인지, 2개인지, 4개인지 등을 가지고  관리합니다. Buddy System이 이렇게 관리하는 이유는 만약 8KB 의 메모리 할당 요청이 왔을 때 1개짜리 리스트에서 2개를 줄 수도 있지만, 2개짜리 리스트에서 1개를 주는 것이 더 효율적이기  때문입니다. 

그래서 메모리 재할당을 하기 위해 shrink_active_list() 함수가  스캐닝하는 메모리 존이 어디냐에 따라 distress 값이  결정됩니다.

즉, 맨 처음  스캐닝하는 곳은 1개씩 존재하는 메모리 영역을  스캐닝하기 때문에 distress 값이 0이 됩니다. 하지만 이 영역에서 충분한 메모리가 확보되지 않으면 이번엔 연속  2개짜리 메모리 영역에서,  그다음엔 4개까지.. 이렇게 가서 결국 최종적으로는 연속 1024개의 메모리 영역까지  스캐닝합니다. 사실 여기까지 오게 된다면 메모리 재할당이 거의 실패했다고 봐야 하고요, 그래서 distress 값이 상당히 커집니다. priority 에 따른 distress 값은 아래와 같습니다.

distress 값 (Understanding Linux Kernel 참조)

만약 swappiness 값을 0으로 준다면 Distress 값이 100이 되어야 비로소 스왑을 사용하게 됩니다. 

mapped_ratio 값이 50보다 클 순 없으므로, distress 가 50이 되어도 swap_tendency는 100을 넘지 못합니다.

그래서 가능한 page cache를 비우고 나서 스왑을 사용하도록 하게 하려면 swappiness 값을 0으로 해 주는 것이 좋습니다. 다만, 2.6.32 버전에서는 swap_tendency 계산 과정이 빠져 있습니다. 대신 아래와 같이 swappiness 값을 활용하는 코드가  변경되었는데요, 프로세스 메모리를 재할당 하느냐 page cache를 재할당하느냐를 결정한다는 것에서는 같은 역할을 합니다.

2.6.32 버전에서의 swappiness 구현

vm.min_free_kbytes


이 값은 kswapd가 깨어나서 메모리 재할당을 하느냐 마느냐를 결정하는데 사용되는  값입니다. kswapd는 커널에 의해 백그라운드에서 동작하는 데몬인데, 현재 남아 있는 메모리가 어느 정도 이냐에 따라 page cache, dentry cache, inode cache 등을 미리 비워두는 작업을 합니다. 메모리 재할당을 할당 실패 시에만  진행한다면 시스템 성능이 낮아질 수 있기 때문에 특정 임계치 밑으로 내려 가게 된다면 백그라운드에서 미리 재할당을 해 두어서 시스템의 성능을 유지할 수 있도록 도와 줍니다.

kswapd의 동작 과정 (Systems Performance 참조) 

여기서 min_pages 값이 min_free_kbytes 값이며 low pages는 min_free_kbytes 값의 2배, high pages는 3배를  의미합니다. 즉, min_free_kbytes 값의 2배에 해당하는 값 밑으로 내려온  순간부터 kswapd가 메모리 재할당을 시작하며, min_free_kbytes 값까지  내려온다면 백그라운드 데몬이 아닌 포어그라운드 프로세스로 동작하면서 메모리 재할당을  시작합니다. 그리고 min_free_kbytes의 3배에 해당하는 값까지 재할당이 완료되면 더 이상 재할당을 하지 않습니다.

그래서 min_free_kbytes 값이 너무 낮으면 백그라운드 재할당이 제대로  이루어지지 않고, 너무 높으면 오히려 시스템 성능에 악영향을 끼칠 수 있습니다. 

보통 이 값은 설치된 물리 메모리의 값을 기준으로 설정이 되며 특별한 이유가 있지 않는한 기본값을 수정할 필요는 없습니다.


vm.vfs_cache_pressure


그리고 마지막으로 vfs_cache_pressure 값이 있습니다. 이 값은 메모리 재할당 시 page cache 말고 dentry cache, inode cache를 재할당 할 때 영향을 주는데요 소스 코드 상에서 구현된 로직은 아래와 같습니다. (fs/dcache.c, fs/inode.c)

vfs_cache_pressure 가 구현된 소스 코드

보시는 것처럼 dentry cache 중에서 사용하지 않는, 즉 반환 가능한 dentry cache 개수를 100으로 나눈 후에 해당 파라미터 값을 곱하게 되는데요, 그래서 디폴트로 설정된 100으로 계산하면 딱 사용하지 않는 dentry cache만 반환을 합니다. 

이 값을 크게 하면 더 많은 dentry cache, inode cache가 반환되지만, inactive list에 있는  메모리뿐만이 아니라 active list에 있는 메모리까지도 반환이 되기 때문에, 성능에 큰 영향을 끼칠 수 있습니다. 

특히 HBase와 같이 많은 양의 파일을 열어 놓고 I/O 작업을 하는 서버들의 경우 이 값이 커서 active cache까지 반환이 된다면 커널 내부적으로 할당과 반환을  반복하면서 무리한 CPU Usage를 점유하고 커널 행까지도 일으킬 수 있습니다. (실제로 발생했던 이슈이며.. 사실 이 글을  써야겠다는 생각을 만들어 준  이슈입니다.)


결론


지금까지 메모리 재할당에 대해 간단하게  살펴봤는데요, 소스 코드를 하나하나 짚어가며  확인할 수 있다면 좋겠지만.. 제가 능력이 안되기 때문에.. 저도  공부하면서 알게 된 내용들을 바탕으로 전체적인 과정을  살펴봤습니다.

특히 메모리 재할당과 관련된 중요한 커널 파라미터들 3개에 대해서는 반드시 이해한 후에 설정하셔야 합니다. 이 값들은 설정과 동시에 시스템에 영향을 주기 때문에 모니터링을 잘 하셔야 하며, 개인적으로는 vm.swappiness 값을 제외하고는 나머지 값들은 수정하지 않는 것이 좋습니다.


긴 글 읽어 주셔서 감사합니다.


글을 쓴 후 추가할 내용이 발생해서 작성합니다. (2015/10/15)

swappiness=0..?


글을 쓴 후 지인분께서 아래 내용에 대해 얘기해 주셨습니다. 

https://www.percona.com/blog/2014/04/28/oom-relation-vm-swappiness0-new-kernel/

swappiness=0 이 되면 swapping이 disable 된다고 설명되어 있는 글인데요, 관련해서 확인을 해 봤습니다. 수정이 된 코드는 아래와 같습니다.

mm/vmscan.c

- ap = (anon_prio + 1) * (reclaim_stat->recent_scanned[0] + 1);
+ ap = anon_prio * (reclaim_stat->recent_scanned[0] + 1)
ap /= reclaim_stat->recent_rotated[0] + 1;

- fp = (file_prio + 1) * (reclaim_stat->recent_scanned[1] + 1);
+ fp = file_prio * (reclaim_stat->recent_scanned[1] + 1);
fp /= reclaim_stat->recent_rotated[1] + 1;

- if (priority || noswap) {
+ if (priority || noswap || !vmscan_swappiness(mz, sc)) {

수정 전의 ap 값은 anon_prio +1 에 recent_scanned 값에 1을 더한 후 곱한 값이 됩니다. swappiness가 0이 되어 anon_prio가 0이 되어도 1을 더한 후 곱하기 때문에 recent_scanned + 1 값이 됩니다.

하지만 수정 후의 ap 값은 anon_prio 그대로 이기 때문에 swappiness 가 0이 되면 0을 곱하게 되어 ap 값이 0이 되어 버립니다. 즉, reclaim 영역을 스캔한 결과가 몇이 돼 건 간에 0으로 되어 버립니다. fp 역시  마찬가지입니다. 그리고 그 후에 recent_rotated 값과의 비율을 계산한 후 1을 더하기 때문에 ap와 fp의 최솟값은 1이 됩니다.

이 두 값은 이후에  percent라는 배열의 0과 1 값을 채우는 데  사용됩니다.

percent 배열의 값

percent 값은 shrink_zone 함수에서 프로세스의 메모리를 스캔할 비율과 page cache를 스캔할 비율을  결정합니다. 

shrink_zone 함수를 살펴보면 아주 중요한 로직을 볼 수 있는데요, 바로 아래  부분입니다.

swap이 없을 때의 로직

시스템에 swap 영역이 없다면 percent[0]을 아예 0으로 만들어 버립니다. 즉 프로세스 메모리 스캔을 아예 하지 않습니다. swap 이 있다면 page cache를 비우다가 모자라면 프로세스 메모리를 스캔해서 swap 영역으로 보낼 텐데 아예 비율이 0이기 때문에 스캔 조차 하지 않고  OOM을 발생시키게 되는 겁니다.

이제 마지막으로 구현된 if 문을 보면  noswap과 동일한 로직을 수행하게 되는데요, noswap과 다른 점은 percent[0]이 0이 아니라는  점입니다. 위 percent 계산식을 보시면 아시겠지만 ap의 최솟값은 1이기 때문에 percent[0]은 0이 될 수 없습니다. 즉, if문을 통해 noswap 이 1일 때와 동일한 로직을 수행하긴 하지만 percent[0]이 0이 아니기 때문에 프로세스 메모리 스캔을 skip 하지 않습니다. 

그래서 결과적으로는 page cache를 탈탈 털고 난 후에야 비로소 swap을 사용하게 됩니다. 

percent[0]이 0은 아니지만 percent[1]에 비해 월등히 작기 때문입니다. 그리고 관련된 내용이 커밋된 https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/commit/?id=fe35004fbf9eaf67482b074a2e032abb9c89b1dd 곳의 로그를 살펴봐도, 아래와 같이 free pages와 page cache 남은 양이 아주아주 작아질 때까지  swap을 사용하지 않는다고 되어있지 disable 된다고 기록되어 있지는 않습니다.

the kernel does not swap out completely (for global reclaimuntil the amount of free pages and filebacked pages in a zone has beenreduced to something very very small (nr_free + nr_filebacked < highwatermark)).
따라서 swappiness=0으로 설정해도 swap이 disable 되진 않습니다. 

다만 1로 설정했을 때보다 훨씬 더 많은 page cache를 해제하게 됩니다. 거의 한자리 수까지 털어 버리기 때문에 좀 더 안정적인 성능의 시스템을 원한다면 1로 세팅해서 사용하는 것도 좋을 것이라  생각됩니다. page cache를 지나치게 버리면 I/O가 높아지고 시스템의 load를 상승시킬 수 있기  때문입니다.


브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari