스왑메모리가 뭘까? 무조건 설정하는게 좋을까요?

심규민·2024년 8월 27일
post-thumbnail

최근에 서버를 구축하면서 "1GB는 너무 부족하니까, 스왑 메모리 설정해둬야지~"라며, 무의식적으로 스왑 메모리를 설정했습니다. 그러다 문득, "스왑 메모리는 어떻게, 어떤 기준으로 동작하는거지? 스왑 메모리를 설정하는게 항상 좋을까?" 궁금증이 생겼습니다. 그래서 이번 글을 통해 정리해보려 합니다.

스왑 메모리란?

만약에 시스템에서 메모리 누수가 발생하거나 애플리케이션이 큰 메모리 공간을 할당받게 될때. 즉, 시스템에서 가용할 수 있는 메모리가 적어질 때 시스템은 어떻게 동작할까요?

간단히 생각해보면, 시스템은 메모리 공간을 확보하기 위해 메모리 회수하거나, OOM(Out Of Memory) 프로세스 킬을 할 수 있습니다. 더 자세히 설명해보면 다음과 같습니다.

후자의 경우 시스템이 시스템의 중요한 프로세스를 보호하기 위해 메모리 공간을 크게 점유하고 있는 애플리케이션이나 우선순위가 낮은 프로세스를 제거함으로써 메모리 회수를 진행합니다.

전자의 경우 캐시나 버퍼(buff/cache)와 같은 회수 가능한 메모리에 대해 회수하는 과정을 가집니다. 대표적으로 "파일 기반 페이지(File-Backed Page)"가 회수의 대상이 됩니다.

파일 기반 페이지(File-Backed Page)란?

운영 체제는 디스크에 있는 파일 데이터를 읽어올 때, 해당 데이터를 메모리에 페이지 형태로 캐시합니다. 이를 파일 기반 페이지라고 부릅니다.

파일 기반 페이지의 경우 원본이 디스크에 존재하기 때문에 다시 데이터를 불러올 수 있어, 메모리 회수 작업이 가능합니다. 메모리 회수하는 과정에서 데이터가 변경되었다면(dirty page) 해당 데이터를 디스크에 반영한 다음, 메모리 회수를 진행합니다.

그러면 dirty page를 디스크에 어떻게 반영할 수 있을까요? 대략 다음 두 가지 방법이 존재합니다.

  1. 애플리케이션 레벨 동기화: fsync 시스템 콜 호출을 통해 dirty page를 디스크로 반영합니다.
  2. 시스템 레벨 동기화: pdflush 커널 스레드를 통해 dirty page들을 디스크로 반영합니다.

이렇듯, 캐시나 버퍼 그리고 파일 기반 페이지들은 회수될 수 있으며, 필요하다면 다시 불러올 수 있는 특징을 가지고 있습니다. 그러면 이들 외에 회수할 수 있는 메모리 영역이 있을까요? 애플리케이션이 동적으로 할당하는 힙 메모리와 같은 익명 페이지(anonymous page)들은 회수가 가능할까요?

이러한 익명 페이지들은 디스크에 저장되어 있지 않기 때문에 다시 접근할 수 없고, 이로인해 메모리 회수가 어렵습니다. 즉, 익명 페이지가 할당되고 해당 페이지에 대한 접근이 자주 이뤄지지 않는다면 리소스 낭비가 발생하게 됩니다. 그러면 익명 페이지를 임시로 디스크에 저장해두고, 다시 접근할 때 메모리에 할당해준다면 메모리를 좀 더 효율적으로 사용할 수 있지 않을까요?

이 방법이 리눅스의 스왑 메커니즘 입니다.

스왑 메커니즘은 접근 빈도가 낮은 페이지를 디스크에 임시로 저장해둔 뒤, 해당 페이지를 회수합니다. 이 후, 그 페이지에 다시 접근하게 되면, 디스크에 저장된 페이지를 메모리로 다시 불러옵니다. 그러면 스왑이 어떻게 동작하는지 더 자세히 살펴봐볼까요?

스왑 메모리 동작 방식

스왑은 근본적으로 디스크를 램의 확장으로 사용하는 것입니다. 또한 swapping outswapping in 두 주요 기능을 수행합니다.

swapping out은 현재 사용하지 않거나 자주 접근하지 않는 페이지를 임시로 디스크에 저장한 뒤, 메모리 회수를 진행합니다. 반면, swapping in은 디스크에 저장된 페이지를 다시 접근하게 될 때, 디스크로부터 해당 페이지를 조회한 뒤 메모리에 다시 할당해줍니다.

이러한 과정을 통해 스왑은 디스크를 가상 메모리로 사용하여 시스템이 사용할 수 있는 메모리를 확장시켜줍니다.

하지만 생각해보면, 요즘 메모리의 가격은 예전에 비해 매우 저렴해졌고, t2-micro와 같이 사이드 프로젝트에서 사용하는 서버가 아니라면 메모리를 매우 넉넉하게 사용합니다. 그러면 스왑이 과연 요즘에서 필요한 기능일까요?

네, 아직도 필요합니다. 메모리가 어느 크기를 가지든 요즘에도 스왑을 요긴하게 사용합니다.

앞서 이야기한 메모리 부족상황을 통해 스왑을 설명했습니다. 그러면 스왑 메모리를 부족한 메모리를 확장하기 위해서만 사용할까요?

요즘 컴퓨터의 경우 빠른 시작을 위해 fast boot 기능을 제공합니다. 이 fast boot가 컴퓨터가 종료될 때, 시스템의 상태를 디스크에 저장한 후, 다음 부팅 시 이를 메모리로 다시 불러와 시작 시간을 높이는 방식입니다. 즉, 스왑을 활용한 기능입니다.

그러면 주제로 다시 돌아와서, 스왑은 근본적으로 메모리의 상태를 복구할 목적으로 설계되었음을 알 수 있습니다. 그러면 리눅스에서 메모리 회수를 언제할까요?

메모리 압박(Memory Pressure)

만약에 메모리 여유 공간이 없는데, 큰 메모리 공간 할당 요청이 들어오면 어떻게 처리해야할까요?
우선은 앞서 설명한 캐시 영역의 회수를 진행합니다. 이를 일반적으로 메모리 직접 회수(direct memory reclamation)라고 부릅니다.

이러한 메모리 직접 회수는 kswapd0 프로세스를 통해 처리됩니다. kswapd0watermark로 알려진 메모리 압력 기준을 통해 메모리 회수를 진행합니다. 또한 페이지를 디스크에 임시로 저장하는 스왑을 처리합니다.

  • pages_min: 최소 가용 페이지 갯수
  • pages_low: 가용 페이지의 최소 임계값, 가용 페이지 수가 해당 값보다 낮아지게 되면, ksawpd0는 메모리 회수를 진행합니다.
  • pages_high: 가용 페이지의 최대 입계값, 가용 페이지 수가 해당 값에 도달하면 ksawpd0는 메모리 회수를 중단합니다.

위 값에서 가용 페이지의 상태를 나타내는 값은 pages_free값을 통해 나타냅니다.

kswapd0는 주기적으로 메모리 사용량을 확인하고 가용 메모리의 양을 통해 메모리 회수를 진행합니다. kswapd0의 동작과 앞선 세 기준에 대해 좀 더 자세히 설명해보겠습니다.

  • pages_min 값보다 가용 메모리가 적다면, 가용 메모리가 거의 고갈되었으며 커널 메모리에만 할당가능한 상태입니다.
  • pages_minpages_low 사이일 때, 메모리 압력이 매우 큰 상황입니다. 이때 kswapd0는 가용 메모리가 pages_high에 도달할 때까지 메모리 회수를 진행합니다.
  • pages_lowpages_high 사이일 때, 메모리 압력이 낮은 상황이며 새로운 메모리 할당이 가능한 상태입니다.
  • pages_high보다 큰 상태일 때, 메모리 압력이 거의 없는 상태입니다.

결국 가용 메모리가 pages_low 값보다 낮아지게 되면 메모리 회수함을 알 수 있습니다. 그러면 pages_low값을 어떻게 설정하고 확인할 수 있을까요?

/proc/sys/vm/min_free_kbytes 값을 통해 간접적으로 설정하고 확인할 수 있습니다.

[ec2-user@ip-172-31-3-72 ~]$ cat /proc/sys/vm/min_free_kbytes
45056

min_free_kbytes를 설정하는 것은 pages_min을 설정하는 것과 같습니다. 또한 pages_min을 설정하면 자동으로 pages_lowpages_high가 설정됩니다. 이는 cat /proc/zoneinfo을 통해 간접적으로 확인할 수 있습니다.

[ec2-user@ip-172-31-3-72 ~]$ cat /proc/zoneinfo
...
Node 0, zone    DMA32
  pages free     69611
        min      11076
        low      13845 // pages_low = pages_min * 5 / 4
        high     16614 // pages_high = pages_min * 3 / 2
...

swappiness

앞서 내용을 통해 메모리 회수 매커니즘은 파일 기반 페이지(file-backed page)와 같은 캐시 영역과 익명 페이지(anonymous page)들을 대상으로 처리됨을 알 수 있었습니다. 각 영역에 대한 메모리 회수는 다음과 같은 방식을 통해 진행됩니다.

파일 기반 페이지와 같은 캐시 영역은 직접 메모리 회수를 진행하며, 데이터가 변경된 더티 페이지의 경우 디스크와의 동기화 이후 메모리 회수를 진행합니다.

익명 페이지(애플리케이션 힙 메모리) 의 경우, 애플리케이션의 힙 메모리에 해당하기 때문에 스왑 메커니즘을 사용하여 디스크에 임시로 저장한 다음 메모리 회수를 진행합니다.

메모리 해제 과정에서 두 가지 메커니즘이 어떻게 동작함을 알 수 있습니다. 그러면 가용 메모리 공간이 적을 때 어느 메커니즘을 먼저 사용될까요?

리눅스에서는 /proc/sys/vm/swappiness 설정을 통해 스왑을 얼마나 적극적으로 사용할지 설정할 수 있습니다.

swappiness 값은 0~100 사이의 값으로 설정할 수 있으며, 60이 기본 값입니다. swappiness 값에 따른 특징은 다음과 같습니다.

  • 100에 가까울수록 익명 페이지를 메모리 해제 대상으로 지정합니다. 즉, 스왑을 적극적으로 사용합니다.
  • 0에 가까울수록 캐시 공간을 메모리 해제 대상으로 지정합니다. 또한 0으로 지정되면 스왑을 전혀 사용하지 않습니다.

그러면 이 swappiness 값을 어떻게 지정하면 좋을까요?

JVM에서의 swappiness

우선 JVM 기반으로 동작하며, 대용량 처리와 많은 디스크 I/O가 필요한 카프카를 대상으로 설명하겠습니다.

카프카의 경우 실시간 대용량 처리를 위해서는 GC STW 시간을 최소화해야 합니다. 스왑 사용이 GC STW에 어떤 영향을 줄까요?

만약 스왑을 적극적으로 사용하게 되면, GC STW 시간이 길어지게 됩니다. 그 이유는, 스왑을 사용하게 되면 OS는 old 영역의 데이터들을 자연스럽게 스왑 대상으로 지정하게 됩니다. 이 후 Major GC로 old 영역에 대해 메모리 회수를 진행하면, 디스크에 저장된 old 영역 데이터들을 메모리로 불러오는 과정(디스크 I/O 발생)을 진행한 뒤 메모리 회수를 진행합니다. 이러한 과정으로 인해 전체 STW 시간이 길어지게 됩니다.

또한 디스크 I/O가 많이 발생하는 카프카의 경우 제로 카피를 통해 페이지 캐시를 적극적으로 활용합니다.(이전에 작성한 제로 카피 글을 보시면 더 자세하게 보실 수 있습니다.) 즉, 낮은 swappiness 설정을 통해 페이지 캐시에 대해 최적화하며 GC STW 시간을 줄일 수 있습니다.

그러면 실시간 대용량 처리를 담당하는 애플리케이션의 경우 어떻게 설정해야 할까요?
대용량 처리 서비스의 경우 GC STW 시간이 길어지는 것은 서비스에 치명적인 영향이 있으며, swappiness를 낮게 설정하여 불필요한 스왑을 줄이고 최대한 캐시 영역을 비워, GC STW 시간을 줄여야 합니다.

결론적으로 다음과 같이 정리할 수 있습니다.

  • swappiness를 크게 설정하는 경우: 스왑된 데이터를 불러오는 과정으로 인해 디스크 I/O가 발생하게 되고, GC STW 시간이 길어지게 됩니다.
  • swappiness를 작게 설정하는 경우: 불필요한 스왑을 줄여 GC STW 시간이 줄일 수 있습니다. 또한 캐시 영역을 최대한 회수하기에 해당 영역을 사용하고 있다면 메모리 최적화를 할 수 있습니다.

그러면 어느정도의 값이 최적일까요? swappiness=1로 설정하여 최소한의 스왑이 발생하도록 한다고 합니다.

요약

  • 메모리 회수는 캐시 영역에 대해 직접 회수하는 방법과 익명 페이지를 디스크에 임시로 저장하여 메모리 회수를 진행하는 두 가지 방법이 있습니다.
  • 스왑은 익명 페이지를 대상으로 디스크에 임시 저장하는 메커니즘을 가집니다.
  • 리눅스의 메모리 회수는 현재 가용 메모리와 pages_min, pages_low, pages_high값을 기반으로 진행합니다.
  • swappiness 값을 통해 스왑을 얼마나 적극적으로 사용할지 결정할 수 있습니다.
    • 대용량 처리 JVM 애플리케이션의 경우 낮은 값 설정을 통해 GC STW 시간을 줄일 수 있습니다.

참고
https://medium.com/codex/understanding-the-linux-swap-mechanism-54e14223f419
https://velog.io/@orcasuit/File-backed-Page
https://blog.naver.com/hanajava/223133077977
https://brewagebear.github.io/fundamental-os-page-cache/
https://brunch.co.kr/@alden/14
https://dev.to/ppsrap/troubleshooting-a-jvm-gc-long-pause-10c8

0개의 댓글