Non-Uniformed Memory Access
불균일 기억장치 접근
NUMA 개략도.
인텔 네할렘 CPU의 NUMA 구조도.
1. 개요
메모리에 접근하는 시간이 CPU와 메모리의 상대적인 위치에 따라 달라지는 컴퓨터 메모리 설계 방법. 각 CPU는 메모리의 일부를 자신의 지역 메모리(Local Memory)로 가지고 있으며 이 지역 메모리에 접근하는 속도는 원격 메모리(Remote Memory)에 접근하는 속도보다 훨씬 빠르다.과거에는 CPU가 메모리에 엑세스 하기 위해서는 노스브릿지를 통해서만 메모리에 접근 할 수 있었다. 그렇기 때문에 두개 이상의 프로세서가 장착되어도 메모리 엑세스는 노스브릿지가 전담하였기 때문에 듀얼 프로세서도 UMA 구조를 가진다. 이 방식은 버스 대역폭으로 인한 한계가 있을지언정 메모리에 접근하는 패스는 모두 공평하기 때문에 접근 속도가 일정한 편이다.
이후 프로세서의 성능이 고도화 되고 집적도가 향상되면서 노스브릿지가 담당하던 기능들이 모두 CPU 프로세서 내에 통합되고 노스브릿지 역할을 하는 I/O기능들을 각자 가지게 되면서 메모리와 주변장치는 자기 자신과 직접 연결되지 않은 장치 또는 메모리에 접근하기 위해서는 그 장치와 연결된 CPU에 접근 요청을 해야 하는데 이 구성을 NUMA라고 부른다.
간단히 예를 들어 시스템에 CPU 소켓이 네 개 있고, 512GB의 메모리가 설치되어 있다면 물리 주소 0~128G-1번지까지의 메모리는 0번 소켓의, 128G~256G-1번지까지의 메모리는 1번 소켓의 지역 메모리가 되는 방식. 각 소켓에서 자기 자신과 연결된 로컬 메모리는 직접 접근하면 되므로 빠르지만 리모트 노드에 연결된 경우 느린 접근 속도 (레이턴시)를 가지게 된다. 하나의 CPU 소켓에 코어 여러개가 들어가 있을 수 있기에 같은 지역 메모리를 사용하는 CPU 코어들을 묶어서 하나의 NUMA 노드로 친다. 8코어 4소켓 CPU라면 (하이퍼스레딩을 가정하지 않을 때에) 0~7번 코어는 NUMA 노드 0번, 8~15번 코어는 NUMA 노드 1번과 같은 방식.
RAM 항목에 설명되어있듯이 본래 '임의 접근 메모리'는 메모리상의 어느 위치에든 제한없이 접근할 수 있다. 그런데 소켓 여러 개에 CPU를 꽂거나, 아예 CPU 하나에 연산 장치를 다수 박아 사용하면서 모든 메모리에 동일하게 접근하게 되면 한 번에 하나의 프로세서만이 메모리 버스를 점유할 수 있게 되어 다른 프로세서들은 그 동안 대기하여야 하는 현상이 발생한다. 만일 알고리즘을 잘 설계해서 각 CPU가 메모리의 서로 다른 위치에 접근하게 하고 CPU간에 간섭이 없이 각 메모리에 접근하여 병렬로 데이터를 처리한다면 그만큼의 성능 향상을 얻을 수 있게 된다.[1] 여기에서 각 CPU 전용 메모리라는 개념이 생기게 되었다.
그리고 CPU의 작동 속도가 빨라지면서 CPU와 메모리 사이의 물리적인 거리가 메모리 접근 속도에 영향을 끼치기 시작하였다. 2GHz로 작동하는 CPU라면 바로 코앞에 있는 메모리에 접근할 때보다 15cm 거리가 있는 메모리에 접근할 때 한 클럭을 더 낭비하게 된다.[2] 메인보드에 CPU 여러 개를 꽂을 수 있다면 CPU와 메모리 뱅크가 번갈아가며 배치되어 있는 것을 볼 수 있는데 이것이 각 CPU 소켓에 메모리를 최대한 가까이 설치하려는 설계이다.
1.1. ccNUMA
Cache-Coherent NUMA. NUMA이면서 캐시 일관성을 가지는 시스템. 프로그래머가 캐시를 직접 제어할 방법이 없다면 하드웨어가 무조건 ccNUMA여야 한다. 현대의 NUMA 하드웨어는 모두 ccNUMA이다.1.2. hUMA
CPU + GPU 처럼 이기종 프로세서의 메모리 공유
해당 문서( hUMA) 참조
1.3. NUMA Interleaving
메모리를 각 페이지 크기마다 Round-Robin 방식으로 골고루 쓰는 방식.NUMA 환경에서 별다른 설정을 하지 않는 경우 운영체제는 메모리를 차례대로 할당하거나 할당을 시도한 스레드가 위치한 노드에 할당하게 되는데 이 방식의 경우 모든 프로세서가 공통적인 작업을 하는 경우 NUMA로 인한 메모리 접근이 느려지는 문제가 생긴다. 이를 완화하기 위해 각 NUMA마다 돌아가면서 메모리에 쓰기 작업을 한다. 어떤 프로세스가 모든 스레드를 동원하는 경우 로컬 노드가 아닌 스레드는 심한 Starvation이 발생하고 메모리 대역과 하나의 컨트롤러에 부하가 집중되지만 Interleaving 구성에서는 모든 스레드가 공평하게 자원에 엑세스 하게 되는 해결책이다. 당연히 싱글 스레드의 프로그램의 경우 성능상 손해를 보겠지만 주로 NUMA가 사용되는 환경은 컴퓨팅 부하가 높은 경우가 대부분이라 이 방식을 사용하기도 한다.
일반적인 NUMA가 JBOD라면 NUMA Interleaving은 RAID-0 을 생각하면 된다.
OS에 따라 이러한 정책을 변경할 수 있지만 (Linux NUMA memory policy) 정책을 변경하는것이 어려운 운영체제의 경우 이를 위해 UEFI와 같은 펌웨어 레벨에서 이를 지정해 OS 차원에서 UMA 시스템처럼 보이도록 하거나 OS차원에서 NUMA를 제대로 지원하지 않는 프로그램들을 위해 기본 동작 정책을 변경할 수 있게 해 준다.
1.4. vs 클러스터
지역 메모리와 원격 메모리의 구분이 있다는 점에서 작은 클러스터로 간주하기도 한다. 그런데 한 시스템 내부에서의 메모리 접근 속도 차이(보통 3~4배)는 어지간한 속도/최적화 덕후가 아니라면 무시하고 프로그래밍하는 경우도 많기 때문에 수백~수천배 이상의 접근 속도 차이를 보이는 클러스터 시스템과는 분명 차이가 있다.2. 소프트웨어 지원
하드웨어에서 NUMA를 지원해도 소프트웨어에서 메모리를 주먹구구식으로 나누어 준다면 NUMA의 이점을 충분히 활용할 수 없게 된다. 서버용 운영체제와 대량의 자원을 소비하는 서버용 애플리케이션들은 대부분 NUMA에 대응해 메모리를 포함한 시스템 자원을 관리한다.2.1. 리눅스
numa(3)/proc/$pid/numa_maps에서 해당 프로세스에 할당해준 메모리 중 어느 논리 주소가 어느 NUMA 노드에 할당되어 있는지 모두 볼 수 있다. 리눅스 시스템 콜 중 일부는 특정 NUMA 노드에서 메모리를 할당하거나, 한 NUMA 노드에서 다른 NUMA 노드로 메모리를 이동시키는 기능을 제공한다.
2.2. Microsoft
NUMA ArchitectureWindows에서 NUMA API를 지원해준다. API 자체는 Windows XP SP2 부터 지원 되기는 했다. 다만 Windows NT 커널의 스케쥴러가 NUMA 환경에서도 한 프로세스의 스레드를 리모드 노드에 스케쥴링 하는 등 UMA와 같은 방식으로 스케쥴링해 성능이 떨어지는 문제가 있었으나 Windows 10 1909부터 조금 나아졌다.
MS의 DBMS 제품인 MS-SQL에서는 하드웨어적인 NUMA 이외에도 소프트웨어적으로 NUMA를 흉내낼 수 있도록 해 주는데 일정 복잡도 이상의 설정은 레지스트리를 편집해야 한다.
2.3. BSD
numa(4)FreeBSD/NetBSD 등 BSD 계열들 또한 NUMA를 지원한다.
2.4. 구글
구글에서는 자체적으로 tcmalloc이라는 고성능 메모리 관리자를 만들어 배포한다. 이것을 개조하여 NUMA에 대응해 메모리를 할당해주는 비공식 업그레이드가 있다.2.5. 오라클
12 버전으로 넘어오면서 NUMA 지원이 대폭 강화되었다. DBA로 로그인해 SQL 명령 ALTER SYSTEM SET을 이용해 NUMA 지원을 켠 후 DBMS를 재시동하면 된다.3. 관련 항목
[1]
세상 일이 그렇게 간단하지는 않다.
암달의 법칙 참고. 실제로 AMD의
쓰레드리퍼 2세대까지는 메모리 컨트롤러가 각 NUMA 병목으로 인한 성능 저하가 있어서 전용 최적화 프로그램이 개발되기도 했다.
[2]
실제로는 메모리가 CPU보다 느리게 작동하기에
캐시 메모리가 아닌 메인 메모리에 접근할 때에는 보통 수십 클럭을 낭비한다.