[K8S] 메모리 스와핑 지원 분석

Haruband's CodeBook·2021년 9월 3일
3

k8s

목록 보기
4/9

쿠버네티스는 기본적으로 메모리 스와핑을 지원하지 않는다. 20년 넘는 시간동안 운영체제 관련 연구/개발을 수행하면서 메모리 스와핑이 만들어내는 다양한 문제들에 대한 연구를 많이 봤었고, 이러한 문제들때문에 메모리 스와핑을 강제로 막는 경우도 많이 봤었기 때문에 딱히 이상하진 않았지만, 다양한 워크로드를 지원하는 범용적인 환경에서 아예 지원하지 않는 것은 조금 이상했었다. 그런데, 최근 쿠버네티스 1.22에서 메모리 스와핑을 지원한다는 기사를 보고 호기심이 생겨 관련 내용을 조사해보니 꽤 오래전부터 지원 여부에 대한 많은 논의가 있었고, 지금은 과거와 달리 여러 가지 문제가 많이 개선된 상황이니 사용자가 선택할 수 있게 해줘야 한다는 분위기가 된 것 같다. 관련 기능은 아직 알파 단계이기 때문에 오늘은 해당 기능에 대한 자세한 설명보다는 해당 기능을 이해하기 위해 필요한 기반 지식을 먼저 소개하도록 하겠다.

지금은 아니지만, 예전에는 리눅스 커널을 공부하려는 사람들이 많았었고, 이들이 가장 힘겨워하는 부분이 바로 메모리 관리였다. 그리고 이 메모리 관리에서 가장 핵심적인 부분이 페이징인데, 사실 개념은 간단하지만 리눅스 커널 코드 레벨에서 해당 기능을 정확히 이해하기 위해서는 x86 보호모드 및 MMU 기능부터 상당히 복잡한 많은 지식이 필요하다. 운영체제를 공부하다보면 늘 듣는 사용자 프로세스는 커널 주소 공간에 접근할 수 없다, 메모리를 미리 읽지 않고 사용 시점에 읽어들인다 등등이 모두 보호모드와 페이징에 기반하여 구현된다. 하지만 리눅스 커널 개발에 참여할 계획이 없다면 이렇게 자세한 내용까지 알 필요는 없으니 간단히 개념적인 부분에 대해서만 이해하고 다음 단계로 넘어가보도록 하자.

페이징은 간단히 얘기하면 메모리를 일정한 크기의 페이지(보통 4KB)로 나누어 관리하자는 것이다. 왜 이렇게 해야하지라는 생각이 들면, 페이징없이 수많은 프로세스가 다양한 크기의 메모리를 할당하고 해제하는 과정을 한번 상상해보자. 조만간 메모리는 엄청난 단편화가 발생하여 실제 가용한 메모리가 많더라도 연속적인 메모리를 할당하기 힘든 지경에 이를 것이다. 그렇다면 이러한 문제를 페이징은 어떻게 해결하고 있을까? 바로 아래의 두 가지가 핵심 포인트이다.

  1. 물리 메모리에 직접 접근하지 않고 가상 메모리 주소를 통해 물리 메모리에 접근한다. (물리적으로 연속적이지 않더라도 가상적으로는 연속적으로 보이게 만들겠다는 것이다.)
  2. 가상 메모리와 물리 메모리를 동일한 크기의 페이지로 나누어 관리한다. (페이지 단위로 할당/해제를 반복함으로써 단편화 문제를 해결하겠다는 것이다.)

언제나 연속적인 메모리 공간을 확보하는 것이 메모리 관리의 핵심이고, 페이징은 이를 위해 동일한 크기의 페이지를 사용하여 단편화 문제를 해결하였고, 물리 메모리 주소로 바로 접근하는 것이 아닌 가상 메모리 주소를 사용함으로써 연속성 문제를 해결하였다.

이제 이러한 페이징을 지원하는 리눅스 커널에서 프로세스의 메모리를 어떻게 관리하는지 살펴보자. 위에서 언급했던 것처럼 메모리는 페이지 단위로 관리된다. 그리고 페이지는 크게 두 가지 형태로 사용된다. 첫 번째는 파일의 버퍼로 사용되는 페이지 캐시이고, 두 번째는 힙과 스택 등의 용도로 사용되는 익명 페이지이다. 좀 더 이해하기 쉽게 설명하자면, 어떤 프로그램을 실행하면 해당 프로그램의 코드는 페이지 캐시에 저장되어 실행되고, 해당 프로그램 안에서 malloc() 을 호출하여 메모리를 요청하면 익명 페이지를 할당받아 사용한다는 것이다. 그래서 페이지 캐시와 익명 페이지의 가장 큰 차이는 페이지 캐시는 원본 데이터가 파일에 저장되어있기 때문에 필요시 반환하고 언제든 다시 할당받아 사용할 수 있다는 것이다.

지금까지 메모리 스와핑을 이해하기 위해 필요한 기반 지식을 소개하였으니 본격적으로 메모리 스와핑에 대해 알아보도록 하자. 메모리는 디스크보다 훨씬 비싼 자원이기 때문에 우리는 항상 메모리 부족 현상을 겪는다. 그렇다면 리눅스 커널은 메모리가 부족할 때 어떻게 이를 해결하려고 할까? 크게 보면 두 가지의 정책이 있다. 첫 번째는 우선 순위가 낮은 프로세스를 죽여서 필요한 메모리를 확보하는 것(OOM)이고, 두 번째는 최근에는 사용되지 않는 페이지를 반환하는 것(LRU)이다. 다들 예상할 수 있듯이, 메모리 스와핑은 바로 두 번째 정책과 관련이 있다.

그렇다면 사용 중인 페이지를 어떻게 반환할까? 그리고 최근에 사용되지 않았을 뿐이지 혹시라도 다시 사용이 된다면 이를 어떻게 처리할까? 위에서 살짝 언급했던 것처럼 페이지 캐시는 파일의 버퍼이기 때문에 이에 대한 처리가 간단하다. 페이지 캐시의 내용이 수정되지 않았다면 바로 반환하고 필요할 때 다시 읽어들이면 되고, 만약 내용이 수정되었다면 수정된 내용을 반영한 다음 반환하면 된다. 하지만 익명 페이지는 어떻게 할까? 이때 사용되는 것이 바로 메모리 스와핑이다. 익명 페이지는 메모리에만 존재하는 데이터이기 때문에 페이지를 반환하기 위해서는 해당 내용을 어딘가에 저장해야하고 이때 사용되는 공간이 바로 스왑이다. 즉, 익명 페이지를 반환하게 되면 스왑에 해당 내용을 저장하고 반환한다. 그리고 다시 필요해지면 새로운 페이지를 할당받아 스왑에서 해당 내용을 읽어들인 후에 사용한다.

이제 메모리 스와핑이 어떤 문제를 가지고 있고, 왜 사용을 막아놓았는지 알아보자. 주로 언급되는 문제들은 아래와 같다.

  • 디스크의 입출력 속도(특히, HDD)와 관련 리눅스 커널의 복잡도로 인해 속도가 느리다.
  • 어떤 페이지가 반환될지 예상할 수 없기에 전반적인 성능에 대한 불확실성이 높아진다.
  • ...

이러한 문제들로 인해 쿠버네티스는 메모리 스와핑을 막아두었고, 메모리가 부족할때는 QoS 정책에 따라 Pod 을 죽이는 정책만을 제공하였다. 하지만 아래와 같이 여러 가지 상황의 변화와 반론으로 인해 메모리 스와핑을 지원하기로 한 것이다.

  • 디스크의 입출력 속도(SSD/NVMe)가 개선되었고, 관련 리눅스 커널의 최적화가 진행되었다.
  • 메모리 스와핑을 지원하더라도 cgroups 를 통해 QoS 관리가 용이해지고 있다.
  • 메모리 스와핑을 지원하지 않더라도 페이지 캐시 반환으로 인해 성능에 대한 불확실성은 높아진다.
  • 메모리 사용량은 많지만 반복적으로 사용되는 메모리 사용량이 적은 경우에는 메모리 스와핑이 필수이다.
  • ...

이와 같은 다양한 개선 사항과 사례 분석을 통해 결국은 쿠버네티스에서도 메모리 스와핑을 지원하게 되었고, 개인적으로도 관련 기술들이 꾸준히 발전하고 있기 때문에 Pod 별로 메모리 스와핑을 설정할 수 있는 기능을 제공하고 사용자가 알아서 사용하는 형태가 바람직하다고 보고 있다.

profile
커널, 컴파일러, 가상화, 컨테이너, 쿠버네티스, ...

0개의 댓글