본문으로 이동
메뉴 여닫기
환경 설정 메뉴 여닫기
개인 메뉴 여닫기
로그인하지 않음
지금 편집한다면 당신의 IP 주소가 공개될 수 있습니다.
Non-Uniform Memory Access, NUMA; 불균일 기억 장치 접근
메모리에 접근하는 시간이 프로세서와 그 메모리의 상대적 위치에 따라 달라지는 다중 프로세서 메모리 구조

불균일 기억 장치 접근(NUMA)은 프로세서마다 가까이 붙은 메모리(로컬 메모리)가 있고, 다른 프로세서에 붙은 메모리(원격 메모리)에는 인터커넥트를 한 번 더 거쳐 접근하는 구조다. 주소 공간은 하나라서 어느 프로세서든 모든 메모리를 읽고 쓸 수 있지만, 로컬 메모리가 원격 메모리보다 빠르다.

프로세서 수가 늘면 버스 하나에 모든 CPU와 메모리를 매다는 방식은 대역폭이 모자란다. NUMA는 메모리 컨트롤러를 프로세서마다 나눠 두어 전체 대역폭을 프로세서 수만큼 늘린다. 대신 운영체제와 프로그램이 데이터를 쓰는 CPU 가까이에 두지 않으면 성능이 크게 떨어진다. 오늘날 2소켓 이상 서버는 사실상 모두 NUMA이고, 코어가 많은 단일 소켓 CPU도 내부적으로 NUMA 영역을 나누기도 한다.

UMA/SMP와 비교

편집 원본 편집

균일 기억 장치 접근(UMA)에서는 모든 프로세서가 같은 버스나 크로스바를 거쳐 메모리에 접근하므로 어느 주소든 지연이 같다. 전통적인 대칭형 다중 처리(SMP) 시스템이 여기에 해당한다. 예전 PC 서버는 CPU가 여러 개여도 메모리 컨트롤러가 노스브리지 칩셋 하나에 있어서 UMA였다. 메모리 컨트롤러가 CPU 안으로 들어오면서 각 소켓이 자기 메모리를 직접 거느리게 되었고, 다른 소켓에 붙은 메모리나 장치는 그 소켓에 요청해야 하는 NUMA가 되었다.

항목 UMA (SMP) NUMA 클러스터
주소 공간 하나 하나 노드마다 따로
메모리 접근 지연 모두 같음 로컬이 빠르고 원격이 느림(시스템에 따라 수십 퍼센트에서 몇 배) 원격 노드는 네트워크를 거쳐 수백 배 이상 느림
확장성 버스 대역폭이 병목 메모리 컨트롤러 수만큼 대역폭 증가 가장 큼
프로그래밍 신경 쓸 것 없음 공유 메모리 그대로, 배치만 신경 메시지 전달(MPI 등) 필요
캐시 일관성 버스 스누핑 디렉터리 또는 스누프 필터 기반(ccNUMA) 없음

NUMA를 아주 강하게 결합된 작은 클러스터로 보기도 하지만, 한 시스템 안의 로컬과 원격 차이는 몇 배 수준이라 공유 메모리 프로그램이 그대로 돈다는 점에서 클러스터와 다르다. 클러스터 위에 가상 메모리 페이징을 얹어 NUMA를 소프트웨어로 흉내 내는 분산 공유 메모리도 연구되었지만 지연은 하드웨어 NUMA보다 몇 자릿수 크다.

  • 노드(node): 같은 로컬 메모리를 공유하는 CPU 코어 묶음과 그 메모리. 보통 소켓 하나가 노드 하나지만, 한 소켓을 여러 노드로 나누기도 한다. 4소켓, 512GB 시스템이라면 물리 주소 0~128GB는 노드 0, 128~256GB는 노드 1 식으로 나뉜다.
  • 로컬 메모리와 원격 메모리: 코어가 자기 노드의 메모리를 읽으면 로컬 접근, 다른 노드의 메모리를 읽으면 원격 접근이다.
  • 인터커넥트: 노드 사이를 잇는 점대점 연결. 원격 접근과 캐시 일관성 메시지가 이 길로 오간다.
  • 노드 거리: 펌웨어가 ACPI SLIT(System Locality Information Table)로 노드 사이 상대 거리를 알려 준다. 로컬을 10으로 두고 원격을 20, 21 같은 값으로 적는다. 노드와 CPU, 메모리의 대응은 ACPI SRAT(System Resource Affinity Table)가 알려 준다.
  • 입출력 장치도 특정 노드의 PCIe 루트에 붙으므로, NIC나 NVMe 장치와 그것을 다루는 스레드, 버퍼를 같은 노드에 두는 것이 좋다.
업체 인터커넥트 도입
AMD HyperTransport 2003년 옵테론. 메모리 컨트롤러를 CPU에 넣어 외부 칩 없이 ccNUMA를 구성
AMD Infinity Fabric 2017년 Zen(에픽). 소켓 안의 칩렛끼리와 소켓 사이를 모두 연결
인텔 QuickPath Interconnect(QPI) 2008년 네할렘(코어 i7, 제온 5500), 이타늄 투퀼라
인텔 UltraPath Interconnect(UPI) 2017년 스카이레이크-SP(제온 스케일러블)

인텔은 네할렘 이전까지 프런트 사이드 버스와 노스브리지 메모리 컨트롤러를 쓰는 UMA 구조였다.[1]

캐시 일관성을 하드웨어가 보장하는 NUMA를 ccNUMA(cache-coherent NUMA)라 한다. 여러 노드의 캐시가 같은 메모리 줄을 들고 있을 때 인터커넥트로 일관성 메시지를 주고받는다. 캐시 일관성이 없는 NUMA는 만들기는 쉽지만 일반적인 공유 메모리 프로그래밍 모델로는 다루기가 너무 어려워서, 오늘날 NUMA라고 하면 사실상 모두 ccNUMA다. 규모가 커지면 모든 캐시에 방송하는 스누핑 대신, 어느 노드가 어느 줄을 들고 있는지 기록하는 디렉터리 기반 프로토콜을 쓴다. IEEE 표준 SCI(Scalable Coherent Interface)가 디렉터리 기반 일관성 프로토콜을 정의했다. 여러 노드가 같은 캐시 줄을 번갈아 쓰면(거짓 공유나 락 경합) 일관성 메시지가 인터커넥트를 오가며 원격 접근보다 더 큰 손해가 난다.

서브 NUMA 클러스터링과 NPS

편집 원본 편집

코어가 수십 개인 CPU는 칩 안에서도 메모리 컨트롤러까지의 거리가 코어마다 다르다. 그래서 한 소켓을 여러 NUMA 노드로 나눠 보이게 하는 BIOS 설정이 있다.

  • 인텔 서브 NUMA 클러스터링(SNC): 스카이레이크-SP부터 한 소켓을 두 영역(뒤 세대는 네 영역까지)으로 나누고, 각 영역에 코어 절반, 마지막 단계 캐시 절반, 메모리 컨트롤러 하나를 준다. 2소켓 서버는 운영체제에 NUMA 노드 4개로 보인다.[2] 이전 세대에는 비슷한 클러스터 온 다이(COD)가 있었다.
  • AMD NPS(NUMA nodes Per Socket): 에픽 프로세서에서 NPS1(소켓당 노드 1개, 모든 채널 인터리브), NPS2, NPS4(사분면마다 노드 하나)를 고른다. NPS0으로 2소켓 전체를 한 노드처럼 인터리브하는 설정도 있다.[3]

노드를 잘게 나누면 NUMA를 잘 다루는 프로그램은 지연이 줄고, 그렇지 않은 프로그램은 원격 접근이 늘어 오히려 느려질 수 있다. 1세대와 2세대 스레드리퍼처럼 메모리 컨트롤러가 없는 다이가 섞인 제품은 코어마다 메모리 지연이 크게 달라 이 문제가 두드러졌다.

메모리 인터리빙

편집 원본 편집

BIOS에서 노드 인터리빙(node interleaving)을 켜면 메모리를 캐시 줄이나 페이지 단위로 모든 노드에 번갈아 배치해 운영체제에는 UMA처럼 보이게 한다. 평균 지연은 로컬과 원격의 중간이 되고 모든 메모리 컨트롤러를 고르게 쓴다. NUMA를 모르는 운영체제나 프로그램에는 안전한 선택이지만, NUMA를 잘 다루는 프로그램은 로컬 접근의 이점을 잃는다. 비유하자면 NUMA는 디스크를 따로 쓰는 JBOD, 인터리빙은 줄무늬로 나눠 쓰는 RAID 0에 가깝다. 운영체제 수준의 인터리브 정책은 아래 리눅스 절을 보라.

운영체제 지원

편집 원본 편집

하드웨어가 NUMA여도 운영체제가 메모리와 스레드를 아무렇게나 배치하면 이점이 사라진다. NUMA를 지원하는 운영체제는 다음 일을 한다.

  • 스레드를 되도록 같은 노드의 코어에서 돌리고, 다른 노드로 옮기는 것을 꺼린다.
  • 메모리를 요청한 스레드가 도는 노드에서 할당한다.
  • 노드별 메모리 사용량과 원격 접근 통계를 제공한다.
  • 필요하면 페이지를 다른 노드로 옮긴다(migration).

리눅스는 2.5 개발판에서 기본 NUMA 지원이 들어갔다. 기본 할당 정책은 로컬 할당으로, 페이지를 처음 접근해 실제로 할당하는 순간 그 스레드가 도는 노드의 메모리를 준다.[4] malloc은 가상 주소만 예약하고 물리 페이지는 첫 접근 때 붙으므로 이를 첫 접촉(first-touch) 정책이라 부른다. 그래서 메인 스레드가 큰 배열을 한꺼번에 0으로 채운 뒤 작업 스레드들이 나눠 쓰면 배열 전체가 메인 스레드의 노드에 몰린다. 초기화도 작업을 나누는 것과 같은 방식으로 병렬로 해야 한다.

메모리 정책은 set_mempolicy()(스레드 전체), mbind()(주소 범위), get_mempolicy()로 다룬다.[4]

정책 동작
MPOL_DEFAULT 기본값. 로컬 할당
MPOL_BIND 지정한 노드들에서만 할당. 모자라면 할당 실패나 OOM
MPOL_PREFERRED 한 노드를 우선하고 모자라면 다른 노드
MPOL_PREFERRED_MANY 여러 노드를 우선
MPOL_INTERLEAVE 지정한 노드들에 페이지 단위로 번갈아 할당
MPOL_WEIGHTED_INTERLEAVE 노드별 가중치에 따라 번갈아 할당(CXL 메모리처럼 속도가 다른 노드용)
numactl --hardware                 # 노드, 노드별 CPU와 메모리, 노드 거리표
numactl --cpunodebind=0 --membind=0 ./app   # 노드 0의 CPU와 메모리만 사용
numactl --interleave=all ./db      # 모든 노드에 번갈아 할당
numastat -p $(pidof app)           # 프로세스의 노드별 메모리 사용량
cat /proc/$(pidof app)/numa_maps   # 매핑별로 어느 노드에 몇 페이지 있는지
lscpu | grep -i numa

C 프로그램에서는 libnuma(numa_alloc_onnode(), numa_run_on_node() 등, man 3 numa)를 쓴다. move_pages()와 migrate_pages() 시스템 호출은 이미 할당된 페이지를 다른 노드로 옮긴다. cgroups의 cpuset 컨트롤러(cpuset.mems)로 그룹 단위로 쓸 수 있는 노드를 묶을 수도 있다.

자동 NUMA 밸런싱(automatic NUMA balancing)은 리눅스 3.8에서 기반이 들어오고 3.13에서 스케줄러와 연동되며 쓸 만해졌다.[5] 커널이 주기적으로 페이지 매핑을 일부러 해제해 두고, 다시 접근할 때 나는 페이지 폴트(NUMA 힌팅 폴트)로 어느 스레드가 어느 페이지를 쓰는지 표본을 모은다. 그 결과에 따라 페이지를 자주 쓰는 노드로 옮기거나 스레드를 데이터가 있는 노드로 옮긴다. /proc/sys/kernel/numa_balancing으로 켜고 끈다. 매핑 해제와 폴트 처리에 비용이 들고 효과가 항상 보장되지는 않으므로, 이미 노드에 고정해 둔 작업에서는 끄라고 커널 문서가 권한다.[6] 값 2는 CXL 메모리처럼 느린 계층에서 자주 쓰는 페이지를 빠른 메모리로 끌어올리는 메모리 계층화 모드다.

vm.zone_reclaim_mode를 켜면 로컬 노드가 모자랄 때 원격 노드에서 빌리기 전에 로컬 페이지 캐시부터 회수한다. 파일 서버나 데이터베이스에서는 캐시가 불필요하게 버려져 오히려 느려지는 경우가 많아 보통 꺼 둔다.

윈도우는 스레드를 되도록 이상적인 노드(ideal node)에서 돌리고 메모리도 그 노드에서 할당하려 하며, 모자라면 다른 노드에서 가져온다. GetNumaHighestNodeNumber, GetNumaNodeProcessorMask, VirtualAllocExNuma, MapViewOfFileExNuma 같은 NUMA API를 제공한다. 논리 프로세서가 64개를 넘는 시스템에서는 노드를 프로세서 그룹에 배정하며, 프로세서 그룹은 윈도우 7과 윈도우 서버 2008 R2부터 지원한다. 윈도우 10 빌드 20348(윈도우 서버 2022 계열)부터는 64개가 넘는 프로세서를 가진 노드를 억지로 쪼개지 않고 실제 토폴로지대로 보고한다.[7]

기타 운영체제

편집 원본 편집
  • FreeBSD는 9.0부터 NUMA를 지원하며 numa(4), cpuset 명령으로 도메인 정책을 정한다. NetBSD 등 다른 BSD도 지원한다.
  • 솔라리스는 지역성 그룹(lgroup)으로 NUMA를 모델링한다.
  • SGI IRIX는 오리진 서버에서 1,000개가 넘는 CPU의 ccNUMA를 지원했다.
  • macOS는 NUMA를 지원하지 않는다. 듀얼 소켓 제온을 쓴 맥 프로(2009, 2010, 2012년 모델) 정도만 해당 하드웨어였고, 애플 실리콘으로 옮긴 뒤로는 M1 Ultra처럼 칩 두 개를 이은 제품도 운영체제에는 하나의 균일한 메모리로 보인다.

응용 프로그램에 미치는 영향

편집 원본 편집

데이터베이스

편집 원본 편집
  • MySQL 스와프 광기(swap insanity): 2010년 Jeremy Cole이 정리한 사례다. 64GB, 2노드 서버에서 InnoDB 버퍼 풀을 48GB로 잡으면, 로컬 할당 정책 때문에 한 노드의 메모리가 먼저 바닥나고, 다른 노드에 여유가 있는데도 커널이 그 노드의 페이지를 스와프로 내보냈다. 처방은 numactl --interleave=all로 mysqld를 띄워 버퍼 풀을 두 노드에 고르게 퍼뜨리는 것이었다.[8] 이후 MySQL에 버퍼 풀을 인터리브로 할당하는 innodb_numa_interleave 옵션(기본 꺼짐)이 생겼다.[9]
  • 몽고DB 운영 문서도 NUMA 서버에서 numactl --interleave=all로 실행하고 zone_reclaim_mode를 끄라고 권한다.[10]
  • 오라클 데이터베이스는 11gR2부터 숨은 초기화 매개변수 _enable_NUMA_support로 NUMA 최적화를 켤 수 있으며 기본값은 꺼져 있다.
  • SQL 서버는 노드마다 스케줄러와 메모리 노드를 따로 두며, 하드웨어 노드를 더 잘게 나누는 소프트 NUMA를 지원한다.

JVM과 메모리 할당기

편집 원본 편집
  • HotSpot JVM은 -XX:+UseNUMA로 NUMA 인지 할당을 켠다. 병렬(Parallel) GC는 오래전부터 지원했고, G1은 JDK 14(JEP 345)부터 힙 영역을 노드에 고르게 나누고 스레드가 속한 노드의 영역에서 새 객체를 할당한다.[11] 자바 7에서 병렬 GC의 NUMA 할당기가 공식 성능 개선 항목으로 소개되었다.
  • 구글 TCMalloc은 NUMA 노드별로 크기 클래스와 페이지 할당기를 나누는 NUMA 인지 모드를 소스에 갖추고 있다.[12]
  • 가상 머신의 vCPU와 메모리가 한 물리 노드에 들어가면 하이퍼바이저가 그 노드에 몰아 배치한다.
  • 한 노드보다 큰 가상 머신(wide VM)은 VMware ESXi가 가상 NUMA(vNUMA) 토폴로지를 게스트에 보여 주어, 게스트 운영체제가 스스로 NUMA 최적화를 하게 한다.[13] Hyper-V, KVM(libvirt의 numatune, 게스트 NUMA 셀 설정)도 비슷한 기능이 있다.
  • 가상 머신의 소켓당 코어 수를 물리 구성과 어긋나게 잡으면 게스트가 잘못된 토폴로지를 보고 성능이 떨어질 수 있다. vSphere 7까지는 CPU 핫애드를 켜면 vNUMA가 게스트에 보이지 않았고, vSphere 8에서 이 제약이 풀렸다.[14]
  • 가상 머신 크기를 물리 노드 하나 안에 들어가게 잡는 것이 가장 단순한 최적화다.

일반 프로그래밍

편집 원본 편집
  • 스레드와 그 스레드가 주로 쓰는 데이터를 같은 노드에 둔다. 노드마다 작업 큐와 메모리 풀을 따로 두는 설계가 흔하다.
  • 여러 노드의 스레드가 한 락이나 카운터를 두드리면 캐시 줄이 노드 사이를 오가며 확장성이 무너진다. 노드별 카운터를 두고 나중에 합친다.
  • 스레드가 노드 사이를 자주 옮겨 다니면 캐시와 로컬 메모리 이점을 잃는다. 지연에 민감한 작업은 CPU 친화도(affinity)를 고정한다.
  • 암달의 법칙대로 모든 작업이 노드별로 깔끔하게 나뉘지는 않으므로, NUMA가 주는 이득은 작업 성격에 크게 좌우된다.
  • 1980년대 말~1990년대: 스탠퍼드 DASH 같은 연구용 디렉터리 기반 ccNUMA 기계가 나왔다.
  • 1990년대: 상용 NUMA 서버가 여럿 나왔다. 시퀀트 컴퓨터 시스템즈의 NUMA-Q(뒤에 IBM이 인수), 실리콘 그래픽스(SGI)의 오리진 2000, 데이터 제너럴의 NUMALiiNE, 컨벡스(뒤에 HP)의 Exemplar, 디지털(DEC), 유니시스, ICL, 하니웰 정보 시스템 이탈리아 등이 NUMA 시스템을 만들었다. 이 기술들은 뒤에 여러 유닉스와 윈도우 NT에 반영되었다.
  • 2003년: AMD 옵테론이 메모리 컨트롤러를 CPU에 넣고 하이퍼트랜스포트로 소켓을 이어, 추가 칩 없이 x86 ccNUMA를 대중화했다.
  • 2008년: 인텔 네할렘이 QPI와 내장 메모리 컨트롤러로 NUMA로 전환했다.
  • 2013년: 리눅스 3.8~3.13에 자동 NUMA 밸런싱이 들어갔다.
  • 2017년: 인텔 UPI, 서브 NUMA 클러스터링과 AMD 에픽의 칩렛 구조가 나오면서 소켓 안에서도 NUMA를 다루게 되었다.
  • 2020년대: CXL로 붙인 메모리가 CPU 없는 NUMA 노드로 나타나면서, 속도가 다른 메모리 계층을 NUMA 정책으로 다루는 기능(가중 인터리브, 메모리 계층화)이 커널에 들어갔다.
  • hUMA(heterogeneous UMA): CPU와 GPU 같은 이기종 프로세서가 같은 가상 주소 공간과 메모리를 캐시 일관성 있게 공유하는 AMD의 설계. NUMA와 반대로 균일한 접근을 목표로 한다.
  • COMA(Cache-Only Memory Architecture): 각 노드의 메모리를 거대한 캐시처럼 써서 데이터가 쓰는 쪽으로 옮겨 다니게 하는 연구용 구조.
  • PGAS(분할 전역 주소 공간): 원격 메모리가 따로 있다는 사실을 언어 수준에서 드러내는 병렬 프로그래밍 모델.