이번 시간에는 NUMA 아키텍처가 무엇이며, 해당 아키텍처를 사용함으로써 얻을 수 있는 이점과 반대로 단점은 뭐가 있는지를 살펴보도록 하자.
NUMA는 Non-Uniform Memory Access의 약자로써 불균일 기억 장치 접근을 말한다. NUMA에 대한 자세한 설명을 하기 전에 NUMA가 왜 생겨났는지 확인해보자.
Shared Memory System
출처 : https://www.samsungsds.com/global/ko/news/story/1203229_2919.html MIMD(Multiple Instruction, Multiple Data streams)은 여러 개의 명령어들이 여러 개의 데이터들을 동시에 처리하는 방식으로, 우리가 일반적으로 아는 멀티 프로세스를 포괄한다고 보면 된다. 여기서 우리가 봐야 할 것은 공유 메모리 시스템인 Shared Memory 부분이다. 크게 SMP와 NUMA로 보면 된다.
SMP(Symmetric Multi Processor)
출처 : https://blog.e-zest.com/non-uniform-memory-architecture-numa/ 공유 메모리 시스템의 가장 대표적인 형태이다. 다수의 CPU(프로세서) 들이 메모리와 I/O를 서로 공유하는 구조이다. 보는 것처럼 대칭적인 구조를 지니기 때문에 모든 프로세스가 동일한 종류로 동일한 기능을 수행합니다. UMA(Uniform Memory Access)도 여기에 속한다. 하지만 SMP 구조에서는 한 번에 한 개의 프로세서만이 동일한 메모리에 접근이 가능하기 때문에 다른 프로세서들을 대기하게 만드는 문제점이 있다. 이는 프로세서가 많아질수록 더 성능적인 문제점을 야기할 수 있다. 즉, 병목 현상이 생길 수 있다.
NUMA(Non-Uniform Memory Access)
출처 : http://www.iue.tuwien.ac.at/phd/weinbub/dissertationsu16.html NUMA는 SMP의 단점을 보완하기 위하여 나온 아키텍처이다. 각 CPU(프로세서)가 독립적인 지역 메모리 공간을 갖고 있어 빠른 메모리 접근이 가능하다. 이러한 구조로 인해 모든 프로세서가 로컬 메모리에 동시 접근이 가능하므로 병목현상이 발생하지 않는다. 하지만, 로컬 메모리가 아닌 다른 프로세서의 메모리(원격 메모리)에 접근하게 될 경우에는 링크를 통한 메모리 접근에 시간이 소요되어 성능 저하가 일어나게 된다. 그리고 CPU에 달려있는 작은 크기의 CPU 캐시가 있는데 기본적인 NUMA 형태는 프로그래밍 상으로 공유 메모리에 대해서 동일한 캐시를 제공하는 캐시 일관성을 유지하기 어렵기 때문에 대부분의 NUMA 시스템은 ccNUMA(cache-coherence NUMA) 형태로 하드웨어적으로 구현하여 제공하고 있다.
그림에서 C1,2,3,4 등은 코어를 나타내며 각 node는 CPU(프로세서) 단위로 분할되어 있다.
원격 메모리가 아닌 로컬 메모리를 사용하도록 하는 것이 NUMA의 가장 중요한 포인트이다.
그렇다면 내 서버의 NUMA는 어떻게 되어있을까?
내 리눅스에서 NUMA 상태를 확인하는 방법은 numactl이라는 명령어를 통해서 확인이 가능하다. 해당 툴은 별도로 다운로드하여야 한다. 수행하면 다음과 같다.
위 정보를 통해서 현재 cpu 코어는 총 8개가 존재하며, node는 0번과 1번으로 총 2개가 존재한다는 것을 알 수 있다. 또한, node 0에는 cpu 코어 0, 1, 2, 3번이 속해 있으며, node 1에는 cpu 코어 4,5,6,7번이 속해 있음을 알 수 있다. 그중 policy라는 것이 보이고 값이 default라는 것이 보인다. NUMA의 Policy.. 알아보자.
NUMA의 정책
NUMA에서 Policy는 무엇일까? NUMA는 정책에 따라서 프로세스의 메모리 할당 방법을 달리한다. 그러면 각 정책에 따른 NUMA node 간 메모리 접근 방식을 알아보자.
Default Policy
별도로 numactl을 통해 설정하지 않았다면 기본으로 지정되어 있는 정책이다. 현재 프로세스가 동작하고 있는 CPU(프로세서)가 속한 NUMA node에서 우선적으로 메모리를 할당받는다.
사용 도중 프로세스가 매핑된 cpu를 taskset으로 변경하면?
taskset을 수행한 직후 변경된 cpu가 속한 NUMA node의 메모리를 사용한다.
메모리가 부족하게 된다면?
현재 속한 NUMA node의 메모리가 부족할 경우 다른 원격 메모리에 접근하여 해당 NUMA node의 메모리를 할당받는다.
Bind Policy
특정 NUMA node에 프로세스를 바인딩시키는 정책이다. 어떤 식으로 바인딩시키냐에 따라서 membind, cpunodebind, physcpubind로 나뉜다. 그러면 이 3가지 종류에 대해서 알아보자.
membind (param : node) : 바인딩된 NUMA node에서만 메모리를 할당받도록 한다. 지역성이 높은 장점은 있으나 엄격하게 바인딩된다.
사용 도중 프로세스가 매핑된 cpu를 taskset으로 변경하면?
다른 CPU에 할당하더라도 원격 메모리를 통해서 변경 전에 속했던 NUMA node의 메모리를 할당받는다.
바인딩된 메모리 가 부족하게 된다면?
현재 속한 NUMA node의 메모리가 부족할 경우 내부적으로 cache를 free 하고 swap을 하는 등의 작업을 진행하여 메모리 공간을 늘리며, 그래도 부족하면 OOM Killer에 의해 강제 종료된다.
cpunodebind (param : node) : 이름처럼 특정 NUMA node에 속한 CPU에 프로세스를 바인딩하는 정책이다. 즉, 프로세스를 특정 NUMA node에 있는 CPU 코어들에서만 실행 가능하도록 한다. 이렇게 될 경우 NUMA node 내부에 있는 모든 CPU 코어들에서 해당 프로세스의 실행이 가능하며, 모두 동일한 지역 메모리를 사용하게 되므로 지역성이 좋은 장점이 있다.
사용 도중 프로세스가 매핑된 cpu를 taskset으로 변경하면?
특정 node에 속한 CPU에서만 프로세스가 동작하도록 하는 정책이므로, tasket으로 변경할 경우 프로세스가 동작하지 않을 것으로 예상한다.(현재 갖고 있는 가상 머신이 모두 numa node가 1개다..)
메모리가 부족하게 된다면?
cpunodebind는 membind와 달리 바인딩된 node의 메모리가 없을 경우, 다른 NUMA node를 통해 메모리를 할당받는다. 이러한 장점으로 membind 보다 선호된다.
physcpubind (param : cpu) : numactl에서 볼 수 있던 CPU 단일 코어에 1:1로 프로세스를 매핑시키는 것이다. 즉, 옵션으로 동일한 NUMA node에 속해있는 모든 CPU 코어 번호를 입력하게 되면 cpunodebind와 동일하게 동작한다. 반대로, 서로 다 다른 node에 속해 있는 CPU 코어 번호를 나열하면 해당 CPU 코어에서만 프로세스가 동작하게 된다.
사용 도중 프로세스가 매핑된 cpu를 taskset으로 변경하면?
특정 CPU 코어에서만 프로세스가 동작하도록 하는 정책이므로, tasket으로 변경할 경우 프로세스가 동작하지 않을 것으로 예상한다.
바인딩된 node의 메모리가 부족하게 된다면?
physcpubind는 cpunodebind와 동일하게 바인딩된 CPU 코어가 속한 node의 메모리가 없을 경우, 다른 NUMA node를 통해 메모리를 할당받는다.
Preferred Policy (param : node)
선호하는 NUMA node를 설정한다. BIND보다 좀 더 유연하여 가능한 한 해당 node에서 메모리를 할당받는 방식이다. --cpunobind 옵션과 함께 지정한다면 가급적 해당 프로세스가 동작하는 노드의 CPU로부터 메모리 접근이 이루어지기 때문에 로컬 메모리에 접근할 확률이 높아지게 된다. 특정 프로세스가 모든 코어를 활용할 것이 아니라면 이러한 방법(선호하는 노드 지정)도 좋은 전략이 될 수 있다.
사용 도중 프로세스가 매핑된 cpu를 taskset으로 변경하면?
변경한 프로세스가 어떤 NUMA node에 속해 있는지와 무관하게 preffered로 할당된 node에서 메모리를 할당받는다.
바인딩된 node의 메모리가 부족하게 된다면?
가능한 한 해당 node로부터 메모리를 할당받는 것이므로, membind와 달리 메모리가 부족해지면 다른 node로부터 메모리를 할당받아 OOM이 발생하지 않는다.
Interleave Policy(param : node)
이름에서도 알 수 있듯이 다수의 NUMA node로부터 공평하게 메모리를 할당받는 정책이다. Round-Robin 스케쥴링 방식에 따라 시간에 맞게 다수의 노드로부터 돌아가면서 동일한 비율의 메모리를 할당받는다. 따라서, interleave로 나누어진 메모리에 대한 접근은 로컬 메모리와 원격 메모리의 평균값에 해당하는 응답속도와 대역폭을 기대할 수 있다. 일반적으로 하나의 node 가 보유한 로컬 메모리 크기 이상으로 메모리를 사용하는 프로세스에 유리할 수 있다. 또한, 추가적으로 메모리를 균등하게 분배하게 될 경우 더 많은 캐시를 저장할 수 있다는 장점이 있다.
사용 도중 프로세스가 매핑된 cpu를 taskset으로 변경하면?
모든 node로부터 접근하는 것이기에 taskset을 통해 cpu 매핑을 변경하여도 변화되는 것은 없다.
바인딩된 node의 메모리가 부족하게 된다면?
이 역시, 바인딩된 node와 상관없이 동작하므로 변화가 없다.
그러면 더 나은 서버를 위해서 프로세스마다 cpu를 tasket으로 매핑하고, 정책을 변경하는 작업을 매번 해야 하는 것일까? 시스템의 리소스를 많이 사용하는 애플리케이션이 명확하고 소수라면 numactl을 통해서 지정하는 방법이 효과적 일 수 있지만 일반적으로 다수의 프로세스가 작업을 수행하는 환경에서는 이를 일일이 관리하는 건 매우 어려운 일이다. 이러한 문제점을 해결하고자 numad라는 사용자 레벨의 서비스 데몬이 존재한다.
NUMAD
numad는 백그라운드 데몬과 같은 형태로, 시스템에 상주하면서 프로세스들의 메모리 사용 현황을 주기적으로 모니터링하며 최적화하는 작업을 담당한다.
프로세스를 동일 노드에서 실행되게 하는데 목적을 두며, 이를 통해 메모리 지역성을 높이고 성능을 최적화할 수 있다. 대량의 멀티 프로세스 또는 다수의 싱글 프로세스들이 많은 시스템에 적합한 방식이다.
단, 만일 interleave 정책을 사용 중이라면 numad는 지역성을 높이는 작업을 수행하기 때문에 적합하지 않을 수 있다. 이미 interleave로 분산된 메모리에서 지역성으로 한 곳에서 사용하게 하면 해당 node의 메모리가 거덜 나는 상황이 발생할 수 있다. 따라서 interleave가 중요시되는 환경이라면 numad를 사용하지 않는 것을 추천한다.
NUMA 관련 커널 파라미터는 없나?
앞에서의 free 나 swap에서는 각각에 해당하는 커널 파라미터가 존재했다. 그러면 NUMA 관련 커널 파라미터는 없을까? 바로 vm.zone_reclaim_mode 가 있다. 바로 해석해 보자면 어떤 zone에 대한 재할당 여부를 지정할 수 있는 듯하다. 게다가 zone이라 하면 낯이 익다. 바로 이전 swap때 언급되었던 /proc/buddyinfo 에서 출력 결과로 나왔었다. 다시 한번 보자.
[centos@ip-172-31-27-12 ~]$ cat /proc/buddyinfo
Node 0, zone DMA 1 0 0 1 2 1 1 0 1 1 3
Node 0, zone DMA32 9948 5397 747 343 207 5 2 0 0 0 0
Node 0, zone Normal 27873 17695 5084 2239 274 85 28 1 0 0
Node 1, zone Normal 126 341 5084 2239 274 85 28 1 0 0 0
좌측에 보면 zone이라고 표시되어있다. Node는 0과 1로 두 개가 구분되어 있으며, Node 0은 DMA, DMA32, Normal로 3개의 영역을 갖고 있으며 Node 1은 Normal로 하나의 영역으로 구분되어있다. 커널은 사용 용도에 따라 zone이라는 영역으로 물리 메모리를 관리한다.
좌측부터 각각의 4개의 zone은 모두 메모리 zone을 나타낸다. 그리고 각 zone 마다 buddy system이 존재하여 page들을 관리하고 있다.
앞서 swap 문서에서도 언급한 buddy system을 잠깐 언급하면 buddy system은 물리 메모리를 page 단위로 관리하며 프로세스가 메모리를 요청할 경우 연속된 페이지를 제공함으로써 효율적으로 메모리를 관리하는 시스템이다.
개념적으로 구성해본 전체 구조
여러 page는 하나의 zone을 구성
> 여러 zone은 다시 하나의 NUMA node를 구성
> 여러 NUMA node는 리눅스 물리 메모리를 구성
이를 기반으로 buddyinfo의 값을 대입시켜 보면 이해가 조금은 더 될 것으로 본다.
zone의 종류는 3개 이상으로 보이는데 DMA, DMA32 zone은 Direct Memory Access 주로 오래된 하드웨어 기기들의 동작을 위해 존재하는 영역으로 현재 시점에는 거의 없다. 또한, Normal 영역은 그 이름처럼 일반적으로 사용되는 영역으로 커널 또는 프로세스 등이 메모리를 필요로 할 때 Normal 영역에서 할당받아 사용한다.
커널 파라미터 설명한다는 것이 너무 멀리까지 온 것 같다. 그러면 다시 본론으로 돌아가서 질문해보자.
그래서 vm.zone_reclaim_mode가 뭐야?
vm.zone_reclaim_mode는 분리된 zone 들 간에 특정 영역의 메모리가 부족할 경우 다른 zone의 메모리를 할당할 수 있도록 하는 옵션을 제공하는 파라미터이다. 총 4개의 값을 가지고 있으며 제일 0과 1만 알면 된다(나머지는 안 쓴다고 보면 된다). 그러면 각 값에 해당하는 역할을 확인해보자.
0 : 0 은 False로, zone_reclaim_mode가 False라는 뜻이다. 즉, zone 안에서 재할당을 하지 않겠다는 의미이다. 메모리가 부족할 경우 zone 내부에서 재할당을 진행하지 않고 다른 zone의 메모리(원격 메모리)를 사용한다는 의미가 되겠다. 즉, 캐싱을 보존한다는 점에서 I/O의 이점은 가져가되 원격 메모리 접근에 의한 성능 저하는 감소하겠다는 의미이다.
메모리가 부족하다!
page, dentry, inode 등의 캐시를 반환하거나 swap 하지 않고 다른 zone에 위치한 NUMA node의 메모리를 할당받는다. 다른 zone의 메모리를 사용하게 되면 원격 메모리 접근에 대한 성능 저하가 있지만, 캐시를 반환하지 않는다는 점에서 I/O 발생이 잦은 서버에서는 큰 이점이 될 것이다. 반대로, 로컬 메모리의 성능이 중요시되며 I/O 발생이 없다면 vm.zone_reclaim_mode를 1로 설정하는 것이 좋겠다.
1: 1 은 True로, zone_reclaim_mode가 True. 즉, zone 안에서 재할당을 하겠다는 뜻으로 메모리가 부족할 경우 zone 내부적으로 재할당을 진행하고, 그래도 부족하다면 다른 zone에서 재할당을 받겠다는 뜻이다.
즉, 캐싱 영역을 재할당에 사용함으로써 I/O의 이점은 무시하며 로컬 메모리를 할당 받음으로써 성능 향상을 중점으로 메모리 관리를 하겠다는 의미이다.
메모리가 부족하다!
page, dentry 등의 재할당 캐시들을 모두 반환하여 공간을 확보하거나 inactive 프로세스를 swap 영역으로 이동시켜 공간을 할당하는 방식으로 진행한다. 그럼에도 불구하고 메모리가 부족하다면 다른 zone의 메모리 즉, 원격 메모리를 할당받는 방식으로 진행한다.
자, 그러면 정리해보자!
우리는 앞서 numactl을 사용한 정책 설정을 배웠으며 zone_reclaim_mode라는 커널 파라미터를 통해 각 zone에서의 재할당 정책을 설정하는 것을 배웠다. 그렇다면 이 둘은 어떻게 유기적으로 연결되어있을까?
우선, numactl은 해당 프로세스가 어떤 NUMA node에 바인딩 되고 메모리가 부족할 경우 어떤 방식으로 메모리를 할당받을 수 있는지를 배울 수 있었다. 그렇다면 여기서 '메모리가 부족할 경우'라는 것은 바로 zone_reclaim_mode 로 부터 결정되는 것이다. 자신이 속한 zone 에서 cache 까지 재할당을 했음에도 부족했을 때를 '메모리가 부족하다' 라고 할 것인지, 혹은 cache를 할당하지 않아도 가용 메모리만 없을 경우에 '메모리가 부족하다'라고 할 것인지를 결정하게 되는 것이다.
즉, zone_reclaim_mode를 통해서 메모리 부족의 기준을 정하고 해당 기준에 맞는 메모리 부족현상 발생 시 어떤 식으로 다른 NUMA node를 참조할 것인지를 numactl로 결정하게 된다.
이러한 정책은 사실 크게 와 닿지가 않는다. 그러면 다음과 같은 4가지 케이스를 두고 어떤 numactl 옵션과 zone_reclaim_mode 파라미터의 값을 사용해야 가장 좋은지를 배워보자.
1. 프로세스가 싱글 스레드로 동작하며,
NUMA node의 메모리보다 적게 메모리를 사용한다.
싱글 스레드라는 점에서 단일 CPU 코어에서 동작이 가능하다. 그러므로 CPU 코어의 캐시를 최대한 활용할 수 있도록 하는 것이 가장 좋기 때문에 BIND 정책의 physcpubind 옵션을 사용하는 것이 좋다.
또한, node에 속한 메모리보다 적은 메모리를 사용하기 때문에 굳이 다른 zone의 메모리를 사용하는 옵션을 둘 필요가 없다. 따라서 vm.zone_reclaim_mode을 1로 설정하여 하나의 zone 안에서 로컬 메모리 액세스를 최대화시키는 것이 좋다.
2. 프로세스가 멀티 스레드로 동작하며,
NUMA node의 메모리보다 적게 메모리를 사용한다.
멀티 스레드라는 점에서 단일 CPU 코어를 매핑하는 것이 아닌, NUMA node에 속한 여러 개의 CPU 코어들을 사용할 수 있도록 cpunodebind 옵션을 사용하는 것이 좋다. 노드라는 바운더리로 묶음으로써 로컬 메모리의 이점을 가져갈 수 있다.
또한, node에 속한 메모리보다 적게 사용하므로 위와 동일하게 vm.zone_reclaim_mode을 1로 설정하여 하나의 zone 안에서 로컬 메모리 액세스를 최대화시키는 것이 좋다. 이는 지역성을 강조하는 numad를 가장 잘 활용할 수 있는 환경이다.
3. 프로세스가 싱글 스레드로 동작하며,
NUMA node의 메모리보다 더 많은 메모리를 사용한다.
싱글 스레드라는 점에서 단일 CPU 코어에서 동작이 가능하지만, 프로세스가 속한 node의 메모리보다 더 큰 메모리를 사용하기 때문에 다른 node의 메모리 사용은 필수적이다. 따라서, 최대한 원격 메모리를 사용하는 것을 최소화하며 지역성을 최대화하는 게 포인트다. 이러한 옵션으로 최대한 지역성을 유지하 되, 필요하면 다른 node의 원격 메모리를 사용할 수 있도록 하는 cpunodebind 옵션을 사용한다.
또한, vm.zone_reclaim_mode는 0으로 설정하도록 한다. 왜냐하면 어차피 원격 메모리를 사용해야 하기 때문에 재할당으로 메모리를 확보하기보다는 처음부터 다수의 노드로부터 메모리를 할당받는 것이 더 낫기 때문이다.
4. 프로세스가 멀티 스레드로 동작하며,
NUMA node의 메모리보다 더 많은 메모리를 사용한다.
가장 많은 케이스다. node의 메모리 영역보다 더 크게 소모하므로 무조건 원격 메모리가 사용된다. 또한, 멀티 스레드로 동작하기 때문에 여러 CPU 코어에서 사용되게 된다. 즉, interleave 모드가 최적의 성능을 낼 수 있다. 무조건 원격 메모리로 접근해야 하며, 여러 개의 코어를 사용하는 것은 메모리 할당을 여러 영역에 넓게 펼치는 것이 유리하기 때문이다. 이 경우에도 vm.zone_reclaim_mode를 0으로 설정하는 것이 좋다. 이유는 넓게 펼쳐야 한다는 점에서 동일하다.
그래서 장점과 단점은?
앞서 말한 내용을 인용할 수 있겠다.
장점은?
각 CPU(프로세서)가 독립적인 지역 메모리 공간을 갖고 있어 빠른 메모리 접근이 가능하다. 이러한 구조로 인해 모든 프로세서가 로컬 메모리에 동시 접근이 가능하므로 병목현상이 발생하지 않는다는 점이다.
그렇다면 단점은?
로컬 메모리가 아닌 다른 프로세서의 메모리(원격 메모리)에 접근하게 될 경우에는 링크를 통한 메모리 접근에 시간이 소요되어 성능 저하가 발생한다는 단점이 있다.
추가적으로, CPU에 달려있는 작은 크기의 CPU 캐시가 있는데 기본적인 NUMA 형태는 프로그래밍 상으로 공유 메모리에 대해서 동일한 캐시를 제공하는 캐시 일관성을 유지하기 어렵다는 한계점이 있다. 하지만 이러한 한계점은 NUMA 시스템은 ccNUMA(cache-coherence NUMA) 형태로 하드웨어적으로 구현하여 극복하고 있다.
마치며
NUMA는 swap과 free를 마무리로 연결 짓는 느낌이 강하다. 확실히 앞에서 배운 내용과 함께 버무려지니 맛있는 요리만큼 완성도가 있어지는 느낌이다. 하지만 아직 전체적인 메모리 관리 구조를 알고 싶은 욕심이 있다.