--- id: 68811272-5352-4409-aa20-7e22e601ede8 kind: CONCEPT slug: numa-locality-for-vcpu-and-memory title: NUMA 에서는 vCPU 배치와 메모리 배치를 함께 본다 topic: memory-virtualization topicName: 메모리 가상화 project: virtualization status: 초안 basisVersion: multi-socket NUMA x86-64 · Linux lscpu 와 numactl · numastat · libvirt virsh vcpupin 과 vcpuinfo studio: "https://hyeonworks.com/studio/documents/68811272-5352-4409-aa20-7e22e601ede8/edit" sourceRevision: no-commit · 밖에서 반입한 문서 한 편. 고정할 저장소 리비전이 없다 assets: - key: numa-vcpu-and-memory-placement file: ../../../final/assets/diagrams/numa-vcpu-and-memory-placement/numa-vcpu-and-memory-placement.svg source: - final/document.md#73-numa - final/document.md#74-local-memory와-remote-memory - final/document.md#75-vcpu와-numa의-연결 - final/document.md#76-vcpu-pinning만으로는-numa-최적화가-끝나지-않는다 - final/document.md#77-guest-numa - final/document.md#78-numa는-실제-장비-topology부터-확인한다 - final/document.md#81-cpu-network-storage-memory-연결 - final/document.md#82-핵심-claim-registry --- # NUMA 에서는 vCPU 배치와 메모리 배치를 함께 본다 RAM 을 하나의 균일한 자원처럼 다루면 어느 CPU 가 읽든 비용이 같다. CPU 소켓이 둘 이상인 NUMA 시스템에서는 그 가정이 성립하지 않는다. 소켓마다 가까운 RAM 이 따로 있고, 다른 소켓의 RAM 을 읽으면 인터커넥트를 건너므로 추가 비용이 붙을 수 있다. 가상 머신에서 이것이 걸리는 이유는 게스트 vCPU 가 호스트에서는 QEMU 의 vCPU 스레드이기 때문이다. 그 스레드가 도는 노드와 그 가상 머신의 메모리가 놓인 노드가 어긋나면, 게스트 안에서는 단순한 메모리 접근인 동작이 실제 하드웨어에서는 다른 노드까지 건너가는 접근이 된다. 그래서 vCPU 를 어느 CPU 에 묶었는지만 보고 배치를 끝내지 않고 메모리가 어느 노드에 있는지를 함께 본다. CPU 가상화 쪽 문서가 메모리 배치와 NUMA 튜닝을 여기로 넘겼다. ## 관계 - **KVM 에서 vCPU 가 물리 CPU 에서 실행되기까지** 게스트 vCPU 가 QEMU vCPU 스레드로 호스트 스케줄러 위에서 실행된다는 것이 그쪽 설명이다. 이 글은 그 스레드가 도는 노드와 메모리가 있는 노드를 잇는다. - **Guest 메모리 주소가 물리 RAM 에 닿기까지 — GVA · GPA · HPA** 변환이 끝나 닿는 HPA 가 어느 노드의 RAM 인지가 여기서 갈린다. 변환 방식은 그쪽이 설명한다. - **이 호스트의 NUMA 토폴로지는 가상 머신 성능을 고려해야 할 구조인가** 노드가 하나인지 둘 이상인지를 확정하는 질문이다. 그 답이 나오기 전에는 이 글의 어긋남이 이 환경에 있는지 알 수 없다. - **QEMU 메모리는 vCPU 가 도는 NUMA node 와 같은 node 에 있는가** 이 글이 말한 어긋남을 이 호스트에서 실제로 재는 질문이다. - **NUMA remote access 가 이 작업의 지연을 실제로 바꾸는가** 배치가 어긋나 있다는 사실과 그것이 지연을 바꾼다는 사실은 다르다. 뒤쪽을 그 질문이 받는다. - **메모리 증상 하나로 계층을 단정하지 않는다** 그 기준이 가르는 다섯 갈래 가운데 NUMA 갈래를 이 글이 설명한다. ## 본문 ## 어느 CPU 가 어느 RAM 을 읽느냐로 비용이 갈린다 지금까지 RAM 은 하나의 균일한 자원처럼 다룰 수 있었다. CPU 소켓이 둘 이상인 NUMA 시스템에서는 어느 CPU 가 어느 RAM 에 접근하느냐에 따라 비용이 달라질 수 있다. NUMA(Non-Uniform Memory Access)는 이름 그대로 메모리 접근 비용이 균일하지 않다는 뜻이다. 구조는 이렇다. NUMA 노드마다 CPU 소켓이 있고 그 소켓에 붙은 로컬 RAM 이 있으며, 노드들은 인터커넥트(interconnect)로 이어져 있다. CPU 가 자기 노드의 RAM 을 읽으면 로컬 접근(local access)이고, 인터커넥트를 건너 다른 노드의 RAM 을 읽으면 원격 접근(remote access)이다. 일반적으로 원격 접근이 로컬 접근과 동일한 비용이라고 가정할 수 없으며, 추가 지연과 대역폭 비용이 있을 수 있다. ## 게스트의 메모리 접근이 인터커넥트를 건널 때 게스트 vCPU 는 호스트에서 QEMU 의 vCPU 스레드다. 그 스레드는 호스트 Linux 스케줄러를 거쳐 호스트 논리 CPU 에 배치되는데, 그 논리 CPU 는 어느 NUMA 노드엔가 속해 있다. 한 가상 머신의 vCPU 스레드가 노드 0 의 CPU 에서 실행되는데 그 가상 머신을 받치는 호스트 물리 페이지가 노드 1 에 있다고 하자. 그러면 노드 0 의 CPU 가 인터커넥트를 지나 노드 1 의 RAM 을 읽게 된다. 게스트 안에서는 그냥 메모리 접근 한 번이고 게스트 OS 는 그 요청이 소켓을 건넜는지 알지 못하는데, 비용은 그 아래 하드웨어에서 붙는다. ## vCPU pinning 만으로는 끝나지 않는다 vCPU 를 특정 CPU 집합에 묶는 설정을 pinning 이라고 한다. 어떤 가상 머신의 vCPU 를 노드 0 의 CPU 에 pinning 했는데 그 가상 머신의 RAM 이 주로 노드 1 에 배치되어 있으면, pinning 이후에도 원격 접근이 많아질 수 있다. 묶은 것은 실행 위치이고 메모리가 어디에 있는지는 그 설정이 정하지 않기 때문이다. 그래서 vCPU 를 어디에서 실행할지와 메모리를 어느 노드에 두고 묶을지를 함께 봐야 NUMA locality 가 정해진다. 근거 문서가 든 이상적인 구성은 한 가상 머신의 vCPU 넷이 모두 노드 0 의 CPU 에서 실행되고 그 가상 머신의 메모리도 노드 0 의 RAM 에 있는 모양이다. ![QEMU vCPU Thread, VM1 Memory Backing, Node 0 CPU, Node 0 RAM, Node 1 RAM 다섯 참여자 사이의 순서도. 1번에서 vCPU thread 가 Node 0 CPU 에 pinning 되고 2번에서 VM1 의 memory backing 이 Node 1 에 놓이면 3번의 접근이 점선으로 Node 1 RAM 까지 간다. 4번에서 memory 를 Node 0 에 묶은 구성에서는 5번의 같은 접근이 Node 0 RAM 에서 끝난다.](../../../final/assets/diagrams/numa-vcpu-and-memory-placement/numa-vcpu-and-memory-placement.svg) 이 호스트가 노드 몇 개인지는 근거 문서에 없다. §197 이 논리 코어를 8 로 적었지만 NUMA 노드 수는 읽지 않았다. 그 절은 `lscpu | grep -E "^Model name|^CPU\(s\):|^Thread|^Core"` 로 네 줄만 걸러 받았고, 거기 남은 `Core(s) per socket` 4 와 `Thread(s) per core` 2 로 논리 코어 8 이 나온다. `Socket(s)` 줄과 `NUMA node(s)` 줄은 그 네 패턴에 걸리지 않아 실측에 없다. 그 출력을 다시 읽어도 노드 수가 나오지 않으므로 아래 절의 `lscpu` 를 거르지 않고 한 번 더 친다. 노드가 하나로 나오면 지금 말한 어긋남은 이 환경에 성립하지 않는다. ## 큰 가상 머신에서는 게스트에게 토폴로지를 보여 준다 가상 머신이 커지면 그 안에서도 같은 문제가 생긴다. vCPU 열여섯 개와 RAM 64 GiB 를 가진 가상 머신을 하나의 균일한 메모리로 보이게 하면, 게스트 스케줄러는 어느 vCPU 가 어느 메모리에 가까운지 알 방법이 없다. 그래서 큰 가상 머신에서는 게스트에게 NUMA 토폴로지 자체를 노출할 수 있다. 게스트 NUMA 노드 0 에 `vCPU 0~7` 과 RAM 절반, 노드 1 에 `vCPU 8~15` 와 나머지 절반을 두는 식이다. 노출한 뒤에는 게스트가 인식하는 토폴로지와 실제 호스트 배치가 합리적으로 대응되도록 구성한다. 게스트 NUMA 0 이 호스트 NUMA 0 에, 게스트 NUMA 1 이 호스트 NUMA 1 에 대응하지 않으면, 게스트가 로컬이라고 판단해 고른 메모리가 호스트에서는 원격이 된다. 이 실험대의 가상 머신은 그 크기가 아니다. §187 이 적은 게스트 세 대는 vCPU 가 한 개나 두 개이고 배정한 메모리가 1024MB 에서 5120MB 사이다. §197 은 그 vCPU 합을 `2 + 2 + 1 = 5` 로 적었다. ## 이 장비의 토폴로지부터 읽는다 호스트가 NUMA 노드 한 개라면 노드를 건너는 원격 접근 문제가 주요 이슈가 아닐 수 있으므로, 실제 환경에서는 토폴로지를 먼저 측정하고 NUMA 최적화가 필요한지 판단한다. ```bash label="Host 의 NUMA topology" lscpu numactl --hardware ``` `lscpu` 는 노드 수와 노드별 CPU 목록을 알려 준다. 근거 문서가 든 출력 예시는 이런 모양이고, 이 테스트 서버에서 읽은 값이 아니다. ```text label="근거 문서가 든 lscpu 출력 예시" NUMA node(s): 2 NUMA node0 CPU(s): 0-7 NUMA node1 CPU(s): 8-15 ``` `numactl --hardware` 는 여기에 노드별 메모리 크기와 노드 사이 거리를 더해 준다. 노드가 둘 이상으로 나오면 그다음은 이 가상 머신들이 어디에 있는지를 본다. ```bash label="QEMU process 의 node 별 memory 분포와 vCPU 배치" numastat -p virsh vcpupin virsh vcpuinfo ``` `numastat` 은 지정한 프로세스의 메모리가 노드마다 얼마씩 있는지 보여 주므로, QEMU 프로세스를 지정하면 그 가상 머신의 메모리가 어느 노드에 얼마나 놓여 있는지 나온다. `virsh vcpupin` 과 `virsh vcpuinfo` 는 같은 가상 머신의 vCPU 가 어느 CPU 에 묶여 있고 어디서 실행 중인지 알려 준다. 두 출력을 나란히 놓아야 앞 절의 어긋남이 이 장비에 있는지 없는지 말할 수 있다. ## 근거 문서가 번호를 붙여 둔 문장 근거 문서는 이 대목에서 고정한 문장에 번호를 붙여 두었다. 실험 기록에서 어느 문장을 확인했는지 가리킬 때 이 번호를 쓴다. | 번호 | 근거 문서가 고정한 문장 | |---|---| | CLAIM-MEM-18 | NUMA 시스템에서는 vCPU placement와 memory placement를 함께 봐야 한다. | ## 이 문서가 확정하지 않는 것 이 글이 확정하는 것은 배치가 어긋나면 무엇이 원격 접근이 되는가까지다. 이 테스트 서버에서 NUMA 를 잰 값은 하나도 없다. 이 호스트가 노드 하나인지 여럿인지가 정해지지 않았다. 노드 하나로 나오면 앞 절의 어긋남이 이 환경에 성립하지 않으므로 NUMA 를 현재 실험에서 뒤로 미룬다. 여럿으로 나와야 QEMU 프로세스의 노드별 메모리 분포와 vCPU 배치를 나란히 적는 확인이 의미를 갖는다. 토폴로지 확인은 CPU 가상화 쪽 질문이 받고 있다. 배치가 어긋나 있다는 것과 그 어긋남이 이 작업의 지연을 바꾼다는 것도 다른 사실이다. 근거 문서는 토폴로지만 보고 성능 문제라고 단정하지 말라고 적었다. vCPU 와 메모리를 같은 노드로 맞춘 구성과 원격 접근이 많아지도록 바꾼 구성에서 같은 작업을 같은 조건으로 돌려 지연과 처리량을 견줘야 답이 나온다. 이 가상 머신들에 게스트 NUMA 를 노출했는지도 근거 문서에 없다. 세 확인은 각각 열린 질문으로 두었고 관계에 걸어 두었다.