feat: 가상화 문서들 추가

This commit is contained in:
DongHyeonka
2026-09-10 08:54:05 +09:00
parent e9f6a93327
commit 43e1aadef0
695 changed files with 153404 additions and 12754 deletions
@@ -0,0 +1,336 @@
---
id: 2f2ffafc-91ff-4971-a6a8-7c0e7d844ca2
kind: CONCEPT
slug: guest-memory-address-translation
title: Guest 메모리 주소가 물리 RAM 에 닿기까지 — GVA · GPA · HPA
topic: memory-virtualization
topicName: 메모리 가상화
project: virtualization
status: 초안
basisVersion: x86-64 Intel EPT · AMD 는 NPT 계열로 대응 · Linux KVM 의 KVM_SET_USER_MEMORY_REGION · QEMU/libvirt · 기본 page 4 KiB
studio: "https://hyeonworks.com/studio/documents/2f2ffafc-91ff-4971-a6a8-7c0e7d844ca2/edit"
assets:
- key: guest-memory-address-translation-path
file: ../../../final/assets/diagrams/guest-memory-address-translation-path/guest-memory-address-translation-path.svg
sourceRevision: no-commit · 밖에서 반입한 문서 한 편. 고정할 저장소 리비전이 없다
source:
- final/document.md#29-이-문서에서-먼저-고정할-전체-구조
- final/document.md#30-일반-linux의-virtual-memory부터-시작한다
- final/document.md#31-page와-physical-frame
- final/document.md#32-virtual-address-=-page-+-offset
- final/document.md#33-guest-page-table
- final/document.md#34-mmu-실제-주소-변환을-수행하는-cpu-하드웨어
- final/document.md#35-tlb-주소-변환-결과의-cpu-cache
- final/document.md#36-bare-metal과-vm의-차이
- final/document.md#37-ept-extended-page-tables
- final/document.md#38-왜-ept가-필요한가
- final/document.md#39-shadow-page-table과-ept의-의미
- final/document.md#40-qemu는-guest-ram을-어떻게-준비하는가
- final/document.md#41-kvm_set_user_memory_region
- final/document.md#42-configured-memory와-실제-physical-ram-사용량은-같지-않을-수-있다
- final/document.md#43-guest-page-table-자체도-메모리에-있다
- final/document.md#44-정상-memory-access는-매번-vm-exit하지-않는다
- final/document.md#79-전체-memory-virtualization-실행-경로
- final/document.md#80-전체-memory-virtualization-관리-경로
- final/document.md#82-핵심-claim-registry
- final/document.md#87-최종-기준-그림
- final/document.md#88-결론
---
# Guest 메모리 주소가 물리 RAM 에 닿기까지 — GVA · GPA · HPA
가상 머신 안의 프로세스가 메모리를 한 번 읽으면 그 주소는 이름이 두 번 바뀐다. 게스트 페이지 테이블이 GVA(Guest Virtual Address)를 GPA(Guest Physical Address)로 바꾼다. 그 GPA 를 EPT 가 다시 HPA(Host Physical Address)로 바꾼 뒤에야 접근이 실제 호스트 RAM 에 닿는다. 게스트 Linux 는 GPA 를 자기 물리 주소로 여기지만, 그 값이 서버 RAM 의 주소와 같을 필요는 없다. 메모리를 마련하는 쪽과 주소를 바꾸는 쪽도 갈린다. QEMU 가 게스트 RAM 을 자기 호스트 가상 주소 공간에 마련해 KVM 에 등록하고, 접근마다 일어나는 변환은 CPU 의 MMU 가 맡는다. 가상 머신 위에서 Keycloak 실험을 돌리는 동안 「가상 머신에 RAM 을 몇 GiB 줬다」가 호스트 RAM 에서 무엇을 뜻하는지 가르려면 이 경로가 먼저 필요하다.
## 관계
- **VM 에서 page fault 는 세 계층에서 따로 일어난다**
이 경로의 어느 단계에서 접근이 완료되지 못하는지, 그때 어느 계층이 그 사건을 받는지를 그 글이 이어 받는다.
- **Huge Page 를 VM 에서 볼 때 갈라지는 세 자리**
페이지 크기를 키우는 선택이 이 경로의 두 변환 단계 가운데 어디에 걸리는지를 다룬다.
- **Host 메모리 압박이 Guest 와 스토리지까지 내려가는 경로**
HPA 뒤의 호스트 RAM 이 모자라질 때 메모리 회수(reclaim)와 스왑이 무엇을 하는지는 그 글이 맡는다.
- **NUMA 에서는 vCPU 배치와 메모리 배치를 함께 본다**
이 경로의 마지막 단계인 호스트 물리 페이지가 어느 NUMA 노드에 있는지를 다룬다.
- **이 호스트의 가상 머신들은 설정한 메모리와 지금 잡고 있는 메모리가 얼마나 다른가**
설정한 메모리와 게스트가 쓰는 메모리, 호스트에서 실제로 상주(resident)하는 메모리를 이 글은 구분만 했고 값은 재지 않았다.
- **QEMU 프로세스가 실제로 붙잡고 있는 호스트 메모리는 얼마이고 어떻게 나뉘어 있는가**
QEMU 가 마련한 메모리가 이 서버에서 실제로 물리 메모리를 얼마나 붙잡는지를 그 질문이 받는다.
- **메모리 증상 하나로 계층을 단정하지 않는다**
이 경로가 그 기준이 쓰는 계층 구분을 만든다.
- **KVM 에서 vCPU 가 물리 CPU 에서 실행되기까지**
같은 가상 머신을 CPU 쪽에서 본 글이다. VM Exit 을 그쪽에서 읽고 오면 메모리 접근마다 Exit 이 나는지가 먼저 걸린다.
## 본문
<!-- body:start -->
## GVA · GPA · HPA — 주소 이름이 두 번 바뀐다
KVM/QEMU 가상 머신에서 게스트 애플리케이션이 메모리를 한 번 읽는 길을 가장 단순하게 그리면 이렇다.
```text label="Guest Application 의 memory access 경로"
Guest Application
│ Guest Virtual Address (GVA)
Guest Page Table
│ Guest Physical Address (GPA)
EPT (Intel) / NPT (AMD)
│ Host Physical Address (HPA)
Physical RAM
```
세 주소가 각각 누구의 주소인지부터 갈라 놓는다.
| 주소 | 누구의 주소인가 |
|---|---|
| GVA | 게스트 프로세스가 쓰는 가상 주소 |
| GPA | 게스트 OS 가 물리 메모리라고 여기는 주소 |
| HPA | 실제 호스트 서버 RAM 의 물리 주소 |
게스트 안에서 도는 Keycloak 이 변수 하나를 읽는다고 하면, 그 한 번의 접근이 세 주소를 차례로 지난다.
```text label="Guest 안의 Keycloak 이 변수를 읽을 때 (SSOT 의 예시 주소)"
Keycloak
│ GVA 0x7f001234
Guest Page Table
│ GPA 0x00101234
EPT
│ HPA 0x8a101234
Physical RAM
```
게스트 Linux 는 GPA 를 자기 실제 물리 주소라고 여긴다. 가상 머신이므로 그 GPA 가 실제 서버의 HPA 와 같을 필요는 없고, KVM 과 CPU 가상화 계층이 둘을 갈라 둔다. 위 주소 값은 원문이 설명하려고 든 예시이고 이 서버에서 읽은 값이 아니다.
## 일반 Linux 프로세스도 물리 주소를 직접 쓰지 않는다
첫 단계는 KVM 고유의 기능이 아니다. 일반적인 Linux 프로세스도 실제 RAM 주소를 직접 쓰지 않는다. 게스트 안에서 Keycloak 과 PostgreSQL 이 함께 돌면 두 프로세스에 각각 독립된 가상 주소 공간이 생긴다. 그래서 둘 다 `0x1000` 이라는 주소를 쓸 수 있는데, 같은 가상 주소라도 서로 다른 물리 프레임으로 매핑할 수 있기 때문이다. 그리고 가상 머신 안에서 이 물리 주소는 정확히는 GPA 다.
Linux 는 메모리를 주소 하나씩 매핑하지 않고 일정 크기 단위로 나누어 관리한다. x86-64 Linux 에서 흔히 쓰는 기본 페이지 크기는 4 KiB 이고, 물리 메모리도 같은 크기의 프레임 단위로 볼 수 있다. 그래서 가상 주소는 페이지 번호와 페이지 안의 오프셋으로 갈린다.
```text label="4 KiB page 에서 0x1234 를 나눈 결과"
Virtual Address
0x1234
┌──────────────┬─────────────┐
│ Virtual Page │ Offset │
│ 1 │ 0x234 │
└──────────────┴─────────────┘
```
페이지 테이블에 가상 페이지 1 이 게스트 물리 프레임 7 로 이어지는 매핑이 있으면, 변환 뒤에도 페이지 안의 오프셋 `0x234` 는 그대로다. 페이지 테이블은 어느 물리 프레임으로 갈 것인가를 정하고 오프셋은 건드리지 않는다.
## 페이지 테이블은 게스트 커널이 만들고 MMU 가 읽는다
게스트 Linux 커널은 프로세스마다 가상 페이지와 게스트 물리 프레임의 매핑을 관리한다. 프로세스를 만들 때, `mmap()` 을 부를 때, 페이지를 할당할 때, 권한을 바꿀 때, copy-on-write 가 일어날 때 페이지 테이블을 만들거나 고친다. 다만 CPU 가 메모리에 접근할 때마다 게스트 커널 코드가 직접 테이블을 하나씩 검색하지는 않는다.
주소 변환을 실제로 수행하는 주체는 CPU 의 MMU(Memory Management Unit, 메모리 관리 장치)다. MMU 는 CPU 안에 들어 있는 변환 하드웨어여서, 커널이 구성해 둔 페이지 테이블을 읽어 가상 주소를 물리 주소로 바꾼다. 만드는 쪽과 쓰는 쪽이 이렇게 갈린다.
```text label="Page Table 을 구성하는 쪽과 사용하는 쪽"
Guest Linux Kernel
│ Page Table 구성/관리
Page Table
│ 사용
MMU
│ 주소 변환
Memory Access
```
## TLB 가 페이지 테이블 탐색을 건너뛴다
메모리에 접근할 때마다 페이지 테이블 전체를 훑으면 비용이 크다. 그래서 CPU 는 최근 변환 결과를 TLB(Translation Lookaside Buffer, 주소 변환 캐시)에 담아 둔다. 가상 페이지 1 이 물리 프레임 7 로 이어진다는 변환이 TLB 에 있으면 같은 페이지의 다음 접근에서는 그 탐색을 건너뛴다.
```text label="TLB HIT 과 MISS"
Virtual Address
TLB
┌──┴──┐
│ │
HIT MISS
│ │
│ ▼
│ Page Table Walk
│ │
└──┬──┘
Physical Address
```
TLB 에 없으면 TLB Miss 가 나고, 페이지 테이블을 조회해 정상 매핑을 찾은 뒤 실행을 이어 간다. 접근 자체를 완료할 수 없는 Page Fault 와는 다른 사건이며, 그 구분과 계층별 처리는 fault 를 다루는 글이 이어 받는다.
## 베어메탈은 변환이 한 번, 가상 머신은 두 번
베어메탈 Linux 에서는 프로세스의 가상 주소가 페이지 테이블을 지나 호스트 물리 주소가 되고 거기서 끝난다. 가상 머신에서는 게스트가 얻은 물리 주소가 실제 호스트 물리 주소가 아니어서 한 단계가 더 붙는다. 이 `GPA → HPA` 두 번째 변환을 위해 Intel 에서는 EPT(Extended Page Tables)를 쓴다. EPT 는 Intel 의 second-level address translation 기술이고, AMD 에는 대응되는 NPT(Nested Page Tables) 계열 기능이 있다.
| 무엇이 | 무엇을 무엇으로 바꾸나 | 주요 관리 주체 |
|---|---|---|
| 게스트 페이지 테이블 | GVA → GPA | 게스트 OS |
| EPT | GPA → HPA | KVM/호스트 가상화 계층 |
| 실행 시점의 실제 변환 | 두 변환 계층 활용 | CPU MMU |
게스트 페이지 테이블과 EPT 는 같은 테이블이 아니다. 하나는 게스트 OS 가 자기 프로세스를 위해 관리하고, 다른 하나는 가상화 계층이 게스트 전체의 물리 주소 공간을 호스트 RAM 에 이으려고 관리한다.
두 번째 변환이 왜 필요한지는 가상 머신 두 대를 놓고 보면 드러난다. 원문이 든 예에서는 각각 8 GiB RAM 을 가진 VM1 과 VM2 가 둘 다 GPA `0x1000` 을 쓸 수 있고, EPT 가 그 둘을 서로 다른 HPA 로 잇는다. 그래서 게스트가 보는 물리 메모리 주소 공간을 실제 호스트 RAM 에서 격리해 구현할 수 있다.
하드웨어가 두 번째 변환을 맡아 주지 않던 시절에는 방식이 달랐다. 하이퍼바이저가 게스트 페이지 테이블 변경을 추적하면서, GVA 에서 실제 호스트 메모리까지 이어지는 shadow mapping 을 관리하는 방식이 쓰일 수 있었다. 게스트 페이지 테이블이 바뀔 때마다 하이퍼바이저가 관련 매핑을 유지해야 하므로 관리 비용과 복잡성이 커질 수 있다. EPT 와 NPT 는 그 두 단계 변환을 CPU 가 하드웨어로 지원하게 한다.
## QEMU 가 게스트 RAM 을 마련하고 KVM 이 그 영역을 등록한다
가상 머신에 8 GiB RAM 을 설정했다고 하자. QEMU 는 호스트의 사용자 공간 프로세스이므로 QEMU 자신도 호스트 가상 주소 공간을 갖고, 게스트 RAM 을 담을 메모리도 그 안에 마련한다. QEMU 가 직접 "물리 주소 X부터 8 GiB를 달라"고 RAM 하드웨어를 제어하는 것이 아니다. QEMU 의 메모리도 일반 호스트 프로세스의 메모리와 같은 길을 지난다.
```text label="QEMU 가 마련한 Guest RAM backing 이 Host 물리 메모리에 닿는 길"
QEMU Host Virtual Address
Host Page Table
Host Physical Address
```
그다음 QEMU 는 자신이 마련한 호스트 사용자 공간 메모리 영역이 게스트의 어느 GPA 범위에 대응하는지를 KVM 에 등록한다. 대표 ioctl 이 `KVM_SET_USER_MEMORY_REGION` 이고, 개념적으로 게스트 GPA 범위와 QEMU 호스트 가상 주소 범위가 어떻게 대응하는지를 전달한다. 등록이 끝나면 세 계층이 하는 일이 갈린다.
```text label="Guest 메모리를 두고 QEMU · KVM · CPU 가 하는 일"
QEMU
→ Guest RAM을 위한 Host userspace backing 제공
KVM
→ Guest memory region 및 virtualization mapping 관리
CPU
→ 실제 runtime address translation 수행
```
QEMU 가 메모리 접근마다 EPT 를 소프트웨어로 검색하지는 않는다. 등록은 관리 동작이고 변환은 하드웨어 동작이다.
## 설정한 메모리와 호스트가 지금 붙잡은 메모리는 다르다
가상 머신에 16 GiB 를 설정했다고 해서, 어떤 일반 구성에서든 시작하는 순간 실제 호스트 RAM 16 GiB 가 반드시 전부 곧바로 물리적으로 점유되는 것은 아니다. 값 셋을 따로 세어야 한다.
```text label="같다고 볼 수 없는 세 값"
Configured Memory
Guest가 현재 실제 사용하는 Memory
Host에서 현재 resident한 Physical Memory
```
물리 메모리가 실제로 붙는 시점과 방식은 호스트 쪽 구성에 따라 달라질 수 있다. 호스트의 demand paging, 어떤 메모리로 뒷받침하는지, HugeTLB, memory locking, preallocation, overcommit 정책 등이 거기에 걸린다. 그래서 "VM RAM 16GiB = Host RAM에서 고정된 연속 16GiB"라고 단순화하면 안 된다. 여기 쓴 16 GiB 도 원문의 예시 값이고 이 서버의 설정이 아니다. 이 호스트에서 그 셋을 견주지도 않았다. 세 값은 같은 시점에 나란히 읽어야 갈리는데, 그 관찰을 아직 돌리지 않았다.
## 게스트 페이지 테이블 자체도 게스트 메모리에 있다
두 단계 변환에 비용이 붙는 까닭이 여기 있다. 게스트 페이지 테이블은 게스트 물리 메모리에 저장된 자료구조이므로, CPU 가 게스트 페이지 테이블 엔트리를 읽는 과정에서도 그 엔트리가 저장된 GPA 를 실제 HPA 로 바꿔야 한다.
```text label="entry 를 읽는 데에도 EPT 변환이 든다"
GVA
Guest Page Table Walk
│ Page Table 자체가 Guest Memory에 존재
└────→ EPT를 이용해 실제 RAM에서 entry를 읽음
GPA 획득
EPT
HPA
```
그래서 두 단계에 걸친 페이지 테이블 탐색에는 비용이 들고 TLB 가 그만큼 중요해진다. Huge Page 가 TLB 로 덮는 범위와 페이지 테이블 탐색 부담을 함께 건드리는 까닭도 여기에서 온다.
## 정상 메모리 접근은 매번 VM Exit 하지 않는다
원문은 이 대목을 「잘못된 이해」라고 이름 붙인 그림으로 먼저 연다.
```text label="원문이 먼저 놓은 「잘못된 이해」"
잘못된 이해
Guest Memory Access
VM Exit
KVM
RAM
```
게스트의 메모리 접근이 곧장 VM Exit 을 거쳐 KVM 으로 가고 거기서 RAM 에 닿는 경로다. 제1부에서 VM Exit 을 읽고 온 사람이 메모리 접근에 그대로 옮겨 붙일 그림이라, 원문은 맞는 경로를 그리기 전에 이것부터 아니라고 적었다. 정상 매핑이 있으면 CPU 하드웨어가 직접 변환을 수행한다.
```text label="정상 mapping 이 있을 때의 실행 경로"
Guest instruction
CPU MMU / TLB
Guest Page Table + EPT
HPA
Physical RAM
```
따라서 정상적인 게스트 RAM 접근마다 QEMU/KVM 의 사용자 공간·커널 경로를 왕복하지는 않는다. 하이퍼바이저가 끼어드는 때는 지금 조건으로 접근을 완료할 수 없을 때이고, 두 번째 변환 단계에서 그렇게 되면 EPT Violation 으로 VM Exit 이 일어난다.
## 실행 경로와 관리 경로를 나누어 본다
실행 경로는 CPU 하드웨어가 접근할 때마다 지나는 길이고, 관리 경로는 가상 머신을 만들고 바꿀 때 지나는 길이다. 둘을 한 그림에 두면 `virsh` 명령 하나가 메모리 접근 경로에 끼어 있는 것처럼 읽힌다.
```text label="Memory Virtualization 관리 경로"
User
virsh
libvirt
QEMU
├─ Guest RAM backing
├─ QEMU HVA
├─ virtio-balloon device
└─ ioctl(KVM_SET_USER_MEMORY_REGION)
KVM
├─ Guest memory slots/regions 관리
└─ EPT 관련 virtualization mapping 관리
CPU
```
`virsh` 와 libvirt 는 가상 머신 구성과 관리를 맡고, QEMU 는 게스트 RAM 을 담을 호스트 사용자 공간 메모리와 장치 구성을 마련하며, KVM 은 게스트 메모리 영역을 하드웨어 가상화 기능에 잇는다. 실행 시점의 변환은 CPU 의 MMU 와 EPT 하드웨어가 한다. 실행 경로 쪽은 게스트 애플리케이션에서 시작해 TLB 와 게스트 페이지 테이블을 지나 GPA 가 된다. 그다음 VM Boundary 를 넘어 EPT 를 지나 HPA 가 되고, 호스트 물리 페이지와 그 페이지가 속한 NUMA(Non-Uniform Memory Access) 노드로 이어진다.
![Guest 구역 안의 Keycloak 과 PostgreSQL 에서 나온 GVA 가 Guest TLB 를 지나고, TLB Miss 이면 Guest Page Table 을 조회해 GPA 가 되며, 그 GPA 가 VM Boundary 를 넘어 EPT 에서 HPA 로 바뀐 뒤 Host RAM 구역의 Host Physical Memory 에 닿고 마지막에 NUMA Node 0 과 NUMA Node 1 로 갈라지는 주소 변환 경로도. 상자 안의 작은 글씨는 그 단계가 어느 계층인지와 무엇을 무엇으로 바꾸는지를 적은 것이다.](../../../final/assets/diagrams/guest-memory-address-translation-path/guest-memory-address-translation-path.svg)
위쪽 점선 상자 안에서 끝나는 것이 첫 번째 변환이고, 그 상자를 나온 뒤의 EPT 가 두 번째 변환을 맡는다. 관리 경로는 이 그림에 없다 — 위의 `virsh` 목록이 그쪽을 그린 것이다.
## 이 글이 확정하지 않는 것
이 호스트에서 잰 값은 하나도 없다. 원문 제2부는 개념 서술이고, 실제 서버에 딸린 설정과 동작은 OQ(Open Question) 번호가 붙은 확인 목록 열넷으로 미뤄져 있다. 그 목록을 아직 돌리지 않았으므로 이 글은 구조가 어떻게 동작하는지까지만 말한다.
설정한 메모리와 실제로 붙잡힌 메모리의 차이는 OQ-2 와 OQ-3 이 받는다. 가상 머신에 설정된 메모리와 지금 쓰는 메모리를 읽고, 같은 시점에 QEMU 프로세스가 호스트에서 상주하는 메모리를 읽어 견주는 확인이다.
```bash label="OQ-2 · OQ-3 이 이 호스트에서 읽을 값"
virsh dominfo <VM_NAME>
virsh dommemstat <VM_NAME>
ps -o pid,rss,vsz,cmd -p <QEMU_PID>
```
원문이 커널·QEMU·libvirt 버전을 한 번도 적지 않아 이 글도 특정 버전에 고정하지 않았다. 여기 적은 것은 그 문서가 서술한 구성이고, 버전이 달라져 서술이 낡았는지는 위 확인을 실제 호스트에서 돌릴 때 드러난다.
범위 밖으로 미룬 것도 있다. 호스트 메모리 압박과 회수, 스왑, ballooning, OOM(Out Of Memory), NUMA 배치는 이 경로 위에서 벌어지는 자원 압박 쪽이라 각각 다른 글이 다룬다. Huge Page 도 마찬가지로 이 경로의 두 변환 단계에 걸리는 선택이어서 따로 뺐다.
<!-- body:end -->
@@ -0,0 +1,179 @@
---
id: efce9a6e-5f5d-45ec-86e5-056d8cb1b8d2
kind: CONCEPT
slug: huge-pages-in-a-vm
title: Huge Page 를 VM 에서 볼 때 갈라지는 세 자리
topic: memory-virtualization
topicName: 메모리 가상화
project: virtualization
status: 초안
basisVersion: Linux THP(/sys/kernel/mm/transparent_hugepage/enabled) · HugeTLB pool · x86-64 의 4 KiB / 2 MiB / 1 GiB page
studio: "https://hyeonworks.com/studio/documents/efce9a6e-5f5d-45ec-86e5-056d8cb1b8d2/edit"
sourceRevision: no-commit · 밖에서 반입한 문서 한 편. 고정할 저장소 리비전이 없다
source:
- final/document.md#50-huge-page가-필요한-이유
- final/document.md#51-huge-page와-tlb-coverage
- final/document.md#52-vm에서-huge-page를-볼-때-주의할-점
- final/document.md#53-thp-transparent-huge-pages
- final/document.md#54-thp의-trade-off
- final/document.md#55-hugetlb
- final/document.md#56-thp와-hugetlb-비교
- final/document.md#82-핵심-claim-registry
---
# Huge Page 를 VM 에서 볼 때 갈라지는 세 자리
페이지 크기를 키우면 같은 메모리 범위를 더 적은 매핑으로 표현할 수 있고, TLB(Translation Lookaside Buffer, 주소 변환 캐시) 엔트리 하나가 덮는 범위도 넓어진다. 그런데 가상 머신에는 주소 변환 단계가 둘이라 「Huge Page 를 쓴다」는 말만으로는 어디를 말하는지 정해지지 않는다. 게스트 페이지 테이블 단계인지, QEMU 가 마련한 호스트 쪽 메모리인지, EPT(Extended Page Tables) 매핑인지를 나누어 물어야 그 말이 뜻을 갖는다. Linux 가 큰 페이지를 마련하는 방법도 THP(Transparent Huge Pages)와 HugeTLB 두 가지인데, 둘은 관리 주체도 사전 예약 여부도 다르다.
## 관계
- **Guest 메모리 주소가 물리 RAM 에 닿기까지 — GVA · GPA · HPA**
페이지 크기 선택이 걸리는 두 변환 단계를 그 글이 세운다.
- **이 호스트의 THP 정책과 huge page 상태는 무엇인가**
이 서버의 THP 정책과 huge page 관련 값을 읽는 질문이다. 이 글은 그 값이 무엇을 뜻하는지까지만 적었다.
- **가상 머신 RAM 이 HugeTLB 로 명시적으로 backing 되어 있는가**
세 곳 가운데 호스트 쪽을 이 서버에서 확인하는 질문이다.
- **메모리 실험 결과에는 Host · VM · Workload 조건을 함께 남긴다**
THP 정책과 huge page 설정은 그 기준이 요구하는 기록 항목에 들어간다.
## 본문
<!-- body:start -->
## 페이지가 커지면 매핑 수가 준다
같은 메모리 범위라도 페이지 크기에 따라 필요한 매핑 수가 달라진다. 원문은 8 GiB 를 예로 들어 셋을 나란히 계산했다.
| 페이지 크기 | 8 GiB 를 표현하는 데 필요한 페이지 수 |
|---|---|
| 4 KiB | 2,097,152 pages |
| 2 MiB | 4,096 pages |
| 1 GiB | 8 pages |
큰 페이지는 더 적은 매핑으로 넓은 메모리 범위를 표현할 수 있다. 위 8 GiB 는 계산을 보이려고 원문이 든 예시 값이고 이 서버 가상 머신의 설정이 아니다.
## TLB 엔트리 하나가 덮는 범위
TLB 엔트리 하나가 가리키는 페이지가 커지면 캐시된 변환 하나로 더 넓은 주소 범위를 덮을 수 있다.
```text label="mapping 수가 같을 때 덮이는 범위 (원문의 단순 예)"
4 KiB page × 512 mappings
= 2 MiB coverage
2 MiB page × 512 mappings
= 1 GiB coverage
```
실제 CPU 는 페이지 크기마다 TLB 구조와 엔트리 수가 다르기 때문에, 이 숫자를 특정 CPU 의 실제 TLB 용량으로 해석하면 안 된다. 여기서 남는 것은 방향 하나다. 페이지 크기가 커지면 변환 하나가 덮는 범위가 넓어지고 TLB 압박이 줄어들 가능성이 생긴다. 페이지 테이블 엔트리 수와 페이지 테이블 탐색 부담도 함께 줄어들 가능성이 있다.
## 가상 머신에서는 세 곳을 따로 물어야 한다
가상 머신에는 GVA(Guest Virtual Address)를 GPA(Guest Physical Address)로 바꾸는 단계와, 그 GPA 를 HPA(Host Physical Address)로 바꾸는 단계가 있다.
```text label="page 크기 선택이 걸릴 수 있는 두 변환 단계"
GVA
│ Guest Page Table
GPA
│ EPT
HPA
```
그래서 "Huge Page를 사용한다"는 말만으로는 부족하다. 세 가지를 갈라 물어야 한다.
- 게스트 페이지 테이블 단계에서 큰 페이지를 쓰는가?
- 호스트가 그 메모리를 Huge Page 로 받치고 있는가?
- EPT 매핑에서 큰 매핑을 활용하는가?
게스트와 호스트의 페이지 크기 선택을 같은 설정 하나로 묶어 다루면 안 된다. 게스트 안에서 THP 가 켜져 있어도 그것은 첫 질문에 대한 답이고, 그 가상 머신의 RAM 을 호스트가 무엇으로 받치는지는 두 번째 질문이 따로 받는다.
## THP — 커널이 조건이 맞을 때 알아서 쓰는 방식
THP 는 Linux 가 가능한 메모리 영역에 대해 Huge Page 를 투명하게 활용하려는 기능이다. 애플리케이션이 평소처럼 `malloc()` 이나 `mmap()` 을 부르면 커널이 조건이 맞는 영역에 Huge Page 활용을 시도한다. 애플리케이션이 큰 페이지를 달라고 따로 요청하지 않는다는 뜻에서 투명하다.
지금 어떤 정책이 걸려 있는지는 `/sys/kernel/mm/transparent_hugepage/enabled` 에서 읽는다.
```bash label="THP 정책 확인"
cat /sys/kernel/mm/transparent_hugepage/enabled
```
```text label="정책 값의 예 — 대괄호가 현재 선택된 값이다"
always [madvise] never
```
이 값은 커널과 배포판, 호스트 설정에 따라 다르므로 실제 시스템에서 확인한다. 그래서 원문은 메모리 실험마다 반드시 같이 남길 조건 목록에 THP policy 를 Host 항목으로, Memory backing 설정을 VM 항목으로 넣어 두었다. 조건을 남기지 않으면 "Memory pressure에서 느려졌다"는 결과를 다른 환경에 재사용하기 어렵다는 것이 그 목록에 붙은 이유다.
## THP 가 치르는 비용
Huge Page 에는 이어진 큰 물리 메모리 영역이 필요하다. 2 MiB 하나가 4 KiB 페이지 512 개에 해당해서, 그만큼 이어진 영역을 확보하지 못하면 커널이 compaction 같은 작업을 수행할 수 있다.
```text label="fragmentation 이 심할 때 끼어드는 작업"
Huge Page 필요
큰 contiguous memory 필요
Fragmentation
Compaction 가능
Latency 영향 가능
```
그래서 THP 는 항상 성능을 높인다고 단정할 수 없다. 특히 지연에 민감한 워크로드에서는 측정이 필요하다고 원문이 직접 적었고, 이 서버에서 그 측정을 하지 않았다.
## HugeTLB — 미리 확보해 둔 풀
HugeTLB 는 Huge Page 풀을 명시적으로 두고 쓸 수 있는 Linux 메커니즘이다. 커널이 알아서 활용하는 THP 와 달리, 관리자가 풀을 준비하고 애플리케이션이나 가상 머신이 그 풀을 명시적으로 쓴다.
```text label="Host physical RAM 안에 잡아 둔 pool"
Physical RAM
┌──────────────────────────┐
│ Normal Memory │
├──────────────────────────┤
│ HugeTLB Pool │
│ 2 MiB │
│ 2 MiB │
│ 2 MiB │
│ ... │
└──────────────────────────┘
```
미리 확보해 두면 예측 가능성을 높일 수 있지만, 풀로 떼어 둔 만큼은 일반 할당이 쓸 수 없기 때문에 일반적인 메모리 할당의 유연성은 그만큼 줄어든다.
## 둘을 가르는 여섯 항목
| 무엇을 견주나 | THP | HugeTLB |
|---|---|---|
| 관리 | 커널이 투명하게 활용 | 명시적인 풀 |
| 애플리케이션 개입 | 상대적으로 적음 | 명시적 구성 가능 |
| 유연성 | 상대적으로 높음 | 상대적으로 낮음 |
| 사전 예약 | 핵심 방식 아님 | 가능 |
| compaction 영향 | 발생 가능 | 미리 확보해 일부 상황 회피 가능 |
| 가상 머신 RAM backing | 사용 가능 | 명시적으로 사용 가능 |
호스트에서 두 값을 함께 읽으면 어느 쪽이 쓰이고 있는지 갈린다.
```bash label="Host 의 huge page 상태와 THP 정책"
grep -i huge /proc/meminfo
cat /sys/kernel/mm/transparent_hugepage/enabled
```
`AnonHugePages` 와 `HugePages_Total` 은 같은 뜻이 아니다. 앞의 값은 익명 메모리에서 THP 로 쓰이고 있는 양이고, 뒤의 값은 HugeTLB 풀로 확보해 둔 페이지 수다. 한쪽이 0 이어도 다른 쪽이 커질 수 있어서, 두 값을 한 이름으로 묶어 읽으면 어떤 방식이 돌고 있는지 알 수 없다.
## 이 글이 확정하지 않는 것
이 호스트에서 잰 값은 하나도 없다. THP 정책도 `HugePages_Total` 도 읽지 않았고 가상 머신의 메모리 backing 설정도 확인하지 않았다. 원문 제2부는 개념 서술이고 실제 서버에 딸린 값은 OQ(Open Question) 번호가 붙은 확인 목록 열넷으로 미뤄져 있다.
호스트 쪽 값은 OQ-4 가 받는다. THP 정책과 함께 `AnonHugePages`, `HugePages_Total`, `HugePages_Free`, `Hugepagesize` 를 읽어 이 서버가 어느 방식을 쓰고 있는지 확정하는 확인이다.
가상 머신 쪽 backing 은 OQ-5 가 받는다. libvirt 의 memory backing 설정을 읽고 호스트의 `/proc/meminfo`, QEMU 의 `smaps` 계열 값과 교차 검증한다.
```bash label="OQ-5 가 읽을 가상 머신 정의"
virsh dumpxml <VM_NAME>
```
두 확인이 끝나기 전에는 이 글의 세 질문 가운데 어느 것도 이 서버에 대해 답이 없다. 그리고 두 확인이 끝나도 셋째 질문은 남는다. EPT 매핑에서 큰 매핑을 쓰는가를 이 호스트에서 재라고 적은 항목은 그 열넷에 없다. 원문이 커널·QEMU·libvirt 버전을 적지 않아 이 글도 특정 버전에 고정하지 않았고, THP 정책의 기본값이 배포판마다 다르다는 점도 원문이 확인하라고만 적었다.
<!-- body:end -->
@@ -0,0 +1,118 @@
---
id: 647d6d11-5bb2-4028-a530-b10c8925aa11
kind: CONCEPT
slug: memory-pressure-reclaim-swap-oom
title: Host 메모리 압박이 Guest 와 스토리지까지 내려가는 경로
topic: memory-virtualization
topicName: 메모리 가상화
project: virtualization
status: 초안
basisVersion: Linux memory reclaim · swap · OOM Killer · cgroup memory limit · QEMU 의 Guest RAM backing
studio: "https://hyeonworks.com/studio/documents/647d6d11-5bb2-4028-a530-b10c8925aa11/edit"
sourceRevision: no-commit · 밖에서 반입한 문서 한 편. 고정할 저장소 리비전이 없다
source:
- final/document.md#57-memory-overcommit
- final/document.md#58-cpu-overcommit과-memory-overcommit의-차이
- final/document.md#59-host-memory-pressure와-reclaim
- final/document.md#60-host-swap이-vm에-미치는-영향
- final/document.md#61-guest-swap과-host-swap
- final/document.md#62-memory-pressure와-storage-contention의-연결
- final/document.md#71-oom
- final/document.md#72-guest-oom과-host-oom
- final/document.md#81-cpu-network-storage-memory-연결
- final/document.md#82-핵심-claim-registry
---
# Host 메모리 압박이 Guest 와 스토리지까지 내려가는 경로
가상 머신들에 설정한 메모리 총량이 호스트의 물리 RAM 보다 클 수 있다. 설정한 용량과 지금 실제로 쓰는 양이 같지 않다 보니 그런 구성이 성립하는데, 가상 머신들의 수요가 동시에 오르면 Linux 는 회수와 스왑으로 대응한다. CPU 가 모자랄 때는 스케줄러가 실행 시간을 나눠 주지만, RAM 이 모자랄 때는 나눌 시간이 없고 지금 존재해야 하는 페이지를 어디에 둘 것인가만 남는다. 그래서 호스트의 메모리 압박은 게스트 안에서 단순한 메모리 접근으로 보이던 동작을 스토리지 I/O 대기로 바꾸고, 여러 갈래의 I/O 가 한 물리 장치로 몰리면 DB 와 애플리케이션 지연까지 올린다. 회수로도 모자라면 OOM(Out Of Memory)으로 가는데, 그것을 게스트 커널이 처리했는지 호스트 커널이 처리했는지에 따라 멈추는 범위가 다르다.
## 관계
- **Guest 메모리 주소가 물리 RAM 에 닿기까지 — GVA · GPA · HPA**
여기서 눌리는 것은 그 경로의 마지막 단계인 호스트 물리 페이지다. 그 경로를 먼저 읽으면 무엇이 스왑으로 나가는지가 정해진다.
- **VM 에서 page fault 는 세 계층에서 따로 일어난다**
호스트 쪽 페이지가 없을 때 발생하는 Host Page Fault 가 이 압박 경로를 관측하는 지점이다. 세 계층의 fault 를 가르는 기준은 그쪽에 있다.
- **virtio-balloon 이 Guest 메모리를 회수하고 돌려주는 방식**
무작정 스왑으로 내보내는 대신 게스트 커널과 협력해 회수하는 다른 방법이다. 압박에 대응하는 수단이라 이 경로와 짝을 이룬다.
- **이 호스트의 가상 머신들은 설정한 메모리와 지금 잡고 있는 메모리가 얼마나 다른가**
이 환경이 초과 할당 상태인지를 이 질문이 확정한다. 여기서는 그것을 사실로 두지 않았다.
- **지금 게스트와 호스트에서 swap 이 실제로 오가고 있는가**
게스트 쪽과 호스트 쪽 스왑을 같은 시각에 재는 질문이다. 이 글은 두 스왑이 다른 계층이라는 것까지만 적었다.
- **호스트 메모리 압박이 게스트 애플리케이션 지연을 실제로 밀어 올리는가**
압박에서 지연까지 이어지는 경로를 이 환경에서 재현하는 질문이다. 이 글에서는 그 경로를 개념으로만 그렸다.
- **메모리 증상 하나로 계층을 단정하지 않는다**
스왑 관측이나 OOM 을 보고 원인을 정하려 할 때 쓰는 기준이다. 이 글이 그 기준의 Host Memory 갈래를 설명한다.
## 본문
<!-- body:start -->
## CPU 가 모자랄 때와 RAM 이 모자랄 때
가상 머신들에 설정한 메모리 총량이 호스트의 물리 RAM 보다 큰 구성을 메모리 초과 할당(memory overcommit)이라고 한다. 근거 문서는 이 구성을 예시 숫자로 설명한다.
```text label="설정 총량이 Host RAM 을 넘는 구성 — 근거 문서의 예시"
Host Physical RAM = 32 GiB
VM1 configured = 16 GiB
VM2 configured = 16 GiB
VM3 configured = 16 GiB
Total configured = 48 GiB
```
이 숫자는 개념을 보이려고 든 예시이고 이 테스트 서버에서 잰 값이 아니다. 설정한 용량과, 그 가상 머신이 실제로 쓰고 있는 메모리(working set)나 호스트에서 그 몫으로 붙잡고 있는 메모리(resident memory)가 같지 않을 수 있기 때문에 이런 구성이 가능할 수 있다. 같은 예시에서 세 가상 머신이 실제로 쓰는 양을 각각 약 5G, 4G, 3G 로 잡으면 합이 약 12G 라 32 GiB 안에 들어간다. 세 대의 수요가 동시에 증가하면 그때부터 문제가 발생한다. 이 호스트의 값은 근거 문서 어디에도 없다. 물리 RAM 도 스왑 설정도 가상 머신들에 설정한 메모리의 합도 적혀 있지 않아서, 아래에 적는 경로가 이 환경에서 지금 돌고 있는지는 이 글이 정하지 못한다.
CPU 가 모자랄 때와 RAM 이 모자랄 때 Linux 가 하는 일이 다르다. CPU 가 모자라면 스케줄러가 실행 시간을 나누고 실행 가능한 작업은 자기 차례를 기다리는데, 시간은 잘라서 뒤로 미룰 수 있기 때문이다. RAM 이 모자랄 때는 미룰 것이 없어서 지금 존재해야 하는 페이지를 어디에 둘 것인가만 남는다. 그래서 메모리 압박에서는 회수와 스왑, ballooning, OOM 같은 메커니즘이 추가로 개입한다. 메모리 초과 할당은 CPU 초과 할당과 동일한 성격의 자원 공유가 아니다.
## 호스트가 메모리를 회수하는 순서
호스트 RAM 수요가 실제로 쓸 수 있는 물리 메모리에 접근하면 Linux 는 메모리 회수(reclaim)를 시도한다. 회수할 수 있는 캐시와 페이지를 먼저 처리하고, 그것으로 모자라면 힙이나 스택처럼 원본 파일이 없는 익명 메모리(anonymous memory)를 스왑(swap)으로 내보내고, 그래도 부족하면 심각한 압박이나 OOM 으로 간다.
회수 비용은 페이지 종류에 따라 갈린다. 원본이 스토리지에 있는 깨끗한 파일 기반 페이지(file-backed clean page)는 RAM 에서 버렸다가 필요할 때 스토리지에서 다시 읽으면 된다. 같은 파일 기반 페이지라도 내용이 바뀐 dirty 상태라면 버리기 전에 디스크로 써 내리는 writeback 이 먼저 필요할 수 있다. 익명 메모리는 원본 파일을 그대로 다시 읽어 올 수 없으므로 스왑처럼 내용을 받아 두는 backing 이 있어야 내보낼 수 있다.
## 게스트의 RAM 접근이 스토리지 대기로 바뀔 때
QEMU 가 게스트 RAM 으로 잡아 둔 호스트 물리 페이지가 스왑으로 나갈 수 있는 구성이라고 하자. 가상 머신 안의 Keycloak 은 그냥 메모리에 접근한다고 생각하고 실행된다. 그런데 그 접근에 필요한 호스트 쪽 페이지가 RAM 에 없으면 Host Page Fault 가 발생하고, 스왑인 I/O 가 끝나 물리 RAM 으로 복원된 뒤에야 게스트 실행이 이어진다. 게스트 관점에서 RAM 접근인 동작이 호스트에서는 스토리지 I/O 를 기다리는 동작으로 바뀌게 된다.
## 게스트 스왑과 호스트 스왑은 다른 계층에서 일어난다
게스트 스왑은 게스트 애플리케이션의 메모리 압박을 게스트 커널이 받아 `/dev/vda` 로 내려보내는 경로다. 그 요청은 `virtio-blk` 와 virtqueue 를 거쳐 QEMU 로 가고 호스트 스토리지에 닿는다. 호스트 스왑은 시작하는 곳이 다르다. QEMU 가 게스트 RAM 으로 잡아 둔 호스트 메모리가 호스트 메모리 압박 아래에서 호스트 커널에 눌려 호스트 스왑으로 나간다. 시작하는 곳도 지나는 계층도 달라서 한쪽 값으로 다른 쪽을 짐작할 수 없다. 게스트가 메모리에 여유가 있어 보이는 동안에도 호스트에서는 스왑과 회수가 심할 수 있다.
## 네 갈래 I/O 가 한 장치로 몰릴 때
게스트와 호스트가 동시에 메모리 압박을 겪으면 게스트 스왑 I/O 와 호스트 스왑 I/O, DB I/O, 파일 시스템 writeback 이 하나의 물리 NVMe 로 몰릴 수 있다. 그러면 호스트 메모리 압박이 회수와 스왑을 부르고, 늘어난 스토리지 I/O 가 스토리지 경쟁이 되고, DB 지연이 오르고, 그 위의 애플리케이션 지연까지 오른다.
메모리는 CPU 와 네트워크, 스토리지 실행을 모두 받치는 계층이라 압박이 메모리 안에서 끝나지 않는다. CPU 사용률이 낮은 구간에도 이 경로가 돌고 있을 수 있다.
이 절에는 그림을 넣지 않았다. 근거 절이 「Guest와 Host가 동시에 memory pressure를 겪으면」이라고 적어 네 갈래에 순서가 없는데, 그림 도구가 이 문맥에서 허용한 구성 문법은 순서를 요구하는 `sequence` 하나뿐이었다. 그리려면 없는 순서를 지어내야 한다.
## 회수로도 모자라면 OOM
Linux 가 메모리 할당 요구를 만족시키지 못하고 회수를 비롯한 방법으로도 필요한 메모리를 확보하지 못하면 OOM 상황이 발생할 수 있다. 그러면 OOM Killer 가 프로세스를 골라 종료하고 그만큼의 메모리를 확보한다.
같은 OOM 이라도 어느 커널이 처리했는지에 따라 멈추는 범위가 달라진다. 게스트 RAM 이 부족해 게스트 커널이 OOM 을 처리하면 게스트 안의 프로세스가 종료된다. 근거 문서가 든 예가 Keycloak 프로세스 종료다. 호스트 물리 RAM 이 부족해 호스트 커널이 OOM 을 처리하면 호스트 프로세스가 종료될 수 있는데, 그때 QEMU 가 골라지면 해당 가상 머신 전체가 중단되고 그 안에서 돌던 프로세스가 모두 함께 멈춘다.
cgroup 메모리 상한이 걸린 환경에서는 호스트 전체 RAM 에 여유가 있어도 그 cgroup 경계에서 OOM 이 발생할 수 있다. 그래서 OOM 하나를 보고 호스트 RAM 이 모자랐다고 정하지 않고 OOM 이 어느 계층에서 났는지를 확인한다.
## 근거 문서가 번호를 붙여 둔 문장
근거 문서는 이 경로에서 고정한 문장에 번호를 붙여 두었다. 실험 기록에서 어느 문장을 확인했는지 가리킬 때 이 번호를 쓴다.
| 번호 | 근거 문서가 고정한 문장 |
|---|---|
| CLAIM-MEM-12 | Memory Overcommit은 CPU Overcommit과 성격이 다르다. RAM pressure에서는 reclaim/swap/ballooning/OOM이 개입할 수 있다. |
| CLAIM-MEM-13 | Guest Swap과 Host Swap은 서로 다른 계층에서 발생한다. |
| CLAIM-MEM-14 | Host memory pressure는 swap/writeback을 통해 storage contention과 application latency를 악화시킬 수 있다. |
| CLAIM-MEM-17 | Guest OOM과 Host OOM은 영향 범위가 다르다. Host OOM에서 QEMU가 종료되면 VM 전체가 중단될 수 있다. |
## 이 문서가 확정하지 않는 것
이 글이 확정하는 것은 구조가 어떻게 동작하는가까지다. 이 테스트 서버에서 잰 값은 하나도 없다.
이 호스트의 물리 RAM 도, 스왑 설정도, 가상 머신들에 설정한 메모리의 합도 근거 문서에 적혀 있지 않다. 그래서 이 환경이 초과 할당 상태인지조차 아직 사실이 아니다. 설정한 메모리와 게스트가 실제로 쓰는 메모리, 호스트에서 그 몫으로 붙잡고 있는 메모리를 가상 머신마다 적으면 그 답이 나온다.
스왑이 지금 오가고 있는지도 재지 않았다. `Swap Used` 값 하나로는 진행 중인 활동인지 과거에 내려간 뒤 남아 있는 cold page 인지 갈리지 않으니 게스트와 호스트를 같은 시각에 관측해야 한다. 호스트 압박이 게스트 애플리케이션 지연을 실제로 밀어 올리는지는 압박이 없는 구간과 있는 구간을 나눠 재야 알 수 있고, 그 구간에서 스토리지 지연과 CPU 도 같이 기록해야 원인을 메모리 쪽으로 좁힐 수 있다. 세 확인은 각각 열린 질문으로 두었고 관계에 걸어 두었다.
<!-- body:end -->
@@ -0,0 +1,123 @@
---
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 갈래를 이 글이 설명한다.
## 본문
<!-- body:start -->
## 어느 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)
이 호스트가 노드 몇 개인지는 근거 문서에 없다. 하나로 나오면 지금 말한 어긋남은 이 환경에 성립하지 않는다.
## 큰 가상 머신에서는 게스트에게 토폴로지를 보여 준다
가상 머신이 커지면 그 안에서도 같은 문제가 생긴다. vCPU 열여섯 개와 RAM 64 GiB 를 가진 가상 머신을 하나의 균일한 메모리로 보이게 하면, 게스트 스케줄러는 어느 vCPU 가 어느 메모리에 가까운지 알 방법이 없다. 그래서 큰 가상 머신에서는 게스트에게 NUMA 토폴로지 자체를 노출할 수 있다. 게스트 NUMA 노드 0 에 `vCPU 0~7` 과 RAM 절반, 노드 1 에 `vCPU 8~15` 와 나머지 절반을 두는 식이다.
노출한 뒤에는 게스트가 인식하는 토폴로지와 실제 호스트 배치가 합리적으로 대응되도록 구성한다. 게스트 NUMA 0 이 호스트 NUMA 0 에, 게스트 NUMA 1 이 호스트 NUMA 1 에 대응하지 않으면, 게스트가 로컬이라고 판단해 고른 메모리가 호스트에서는 원격이 된다.
이 프로젝트의 가상 머신이 그런 크기인지는 근거 문서에 적혀 있지 않다. vCPU 수도 설정한 RAM 도 나오지 않아서, 이 절이 이 환경에 걸리는 이야기인지는 그 값을 적은 뒤에 정해진다.
## 이 장비의 토폴로지부터 읽는다
호스트가 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 <QEMU_PID>
virsh vcpupin <VM_NAME>
virsh vcpuinfo <VM_NAME>
```
`numastat` 은 지정한 프로세스의 메모리가 노드마다 얼마씩 있는지 보여 주므로, QEMU 프로세스를 지정하면 그 가상 머신의 메모리가 어느 노드에 얼마나 놓여 있는지 나온다. `virsh vcpupin` 과 `virsh vcpuinfo` 는 같은 가상 머신의 vCPU 가 어느 CPU 에 묶여 있고 어디서 실행 중인지 알려 준다. 두 출력을 나란히 놓아야 앞 절의 어긋남이 이 장비에 있는지 없는지 말할 수 있다.
## 근거 문서가 번호를 붙여 둔 문장
근거 문서는 이 대목에서 고정한 문장에 번호를 붙여 두었다. 실험 기록에서 어느 문장을 확인했는지 가리킬 때 이 번호를 쓴다.
| 번호 | 근거 문서가 고정한 문장 |
|---|---|
| CLAIM-MEM-18 | NUMA 시스템에서는 vCPU placement와 memory placement를 함께 봐야 한다. |
## 이 문서가 확정하지 않는 것
이 글이 확정하는 것은 배치가 어긋나면 무엇이 원격 접근이 되는가까지다. 이 테스트 서버에서 잰 값은 하나도 없다.
이 호스트가 노드 하나인지 여럿인지가 정해지지 않았다. 노드 하나로 나오면 앞 절의 어긋남이 이 환경에 성립하지 않으므로 NUMA 를 현재 실험에서 뒤로 미룬다. 여럿으로 나와야 QEMU 프로세스의 노드별 메모리 분포와 vCPU 배치를 나란히 적는 확인이 의미를 갖는다. 토폴로지 확인은 CPU 가상화 쪽 질문이 받고 있다.
배치가 어긋나 있다는 것과 그 어긋남이 이 작업의 지연을 바꾼다는 것도 다른 사실이다. 근거 문서는 토폴로지만 보고 성능 문제라고 단정하지 말라고 적었다. vCPU 와 메모리를 같은 노드로 맞춘 구성과 원격 접근이 많아지도록 바꾼 구성에서 같은 작업을 같은 조건으로 돌려 지연과 처리량을 견줘야 답이 나온다. 이 가상 머신들에 게스트 NUMA 를 노출했는지도 근거 문서에 없다. 세 확인은 각각 열린 질문으로 두었고 관계에 걸어 두었다.
<!-- body:end -->
@@ -0,0 +1,237 @@
---
id: e80853fa-98b8-4cc3-a2df-427c7147794b
kind: CONCEPT
slug: page-fault-layers-in-a-vm
title: VM 에서 page fault 는 세 계층에서 따로 일어난다
topic: memory-virtualization
topicName: 메모리 가상화
project: virtualization
status: 초안
basisVersion: x86-64 Intel EPT 기준의 second-stage translation · Linux Guest/Host kernel 의 page fault 처리 · AMD 는 NPT 계열로 대응
studio: "https://hyeonworks.com/studio/documents/e80853fa-98b8-4cc3-a2df-427c7147794b/edit"
assets:
- key: page-fault-layers-in-a-vm
file: ../../../final/assets/diagrams/page-fault-layers-in-a-vm/page-fault-layers-in-a-vm.svg
sourceRevision: no-commit · 밖에서 반입한 문서 한 편. 고정할 저장소 리비전이 없다
source:
- final/document.md#35-tlb-주소-변환-결과의-cpu-cache
- final/document.md#45-guest-page-fault
- final/document.md#46-page-fault의-대표적인-원인
- final/document.md#47-ept-violation
- final/document.md#48-guest-page-fault와-ept-violation-비교
- final/document.md#49-host-page-fault도-별도로-존재한다
- final/document.md#82-핵심-claim-registry
- final/document.md#86-문제를-진단할-때의-분류
---
# VM 에서 page fault 는 세 계층에서 따로 일어난다
가상 머신 안에서 메모리 접근이 완료되지 못하면 그 사건을 받는 곳이 셋이다. GVA(Guest Virtual Address)를 GPA(Guest Physical Address)로 바꾸는 첫 단계에서 막히면 Guest Page Fault 가 나서 게스트 커널이 처리한다. 그 GPA 를 HPA(Host Physical Address)로 바꾸는 두 번째 단계에서 막히면 EPT Violation 이 나고, VM Exit 뒤 KVM 이 받는다. QEMU 도 호스트의 일반 프로세스여서 자기가 마련한 메모리에 대해 Host Page Fault 를 따로 겪는다. 이름이 모두 fault 라 한 덩어리로 읽히지만 일어나는 단계도 처리하는 쪽도 다르고, 셋 중 어느 것이 늘었는지에 따라 볼 지표가 갈린다.
## 관계
- **Guest 메모리 주소가 물리 RAM 에 닿기까지 — GVA · GPA · HPA**
정상일 때 접근이 어떤 경로를 지나는지를 그 글이 세운다. 이 글은 그 경로가 완료되지 않을 때를 맡는다.
- **Host 메모리 압박이 Guest 와 스토리지까지 내려가는 경로**
호스트 쪽 fault 가 늘어나는 배경인 reclaim 과 swap 을 그 글이 다룬다.
- **게스트 page fault 증가는 workload 변화를 따라가는가**
게스트 쪽 fault 지표를 이 호스트에서 재는 질문이다. 이 글은 원인 분류까지만 적었다.
- **호스트 major fault 와 스토리지 지연은 같은 시간축에서 함께 움직이는가**
호스트 쪽 major fault 와 스토리지 지연을 같은 시간축에 놓는 질문이다.
- **메모리 증상 하나로 계층을 단정하지 않는다**
이 세 갈래가 그 기준이 요구하는 계층 구분을 만든다.
## 본문
<!-- body:start -->
## TLB Miss 와 Page Fault 는 다른 사건이다
주소 변환 결과는 CPU 의 TLB(Translation Lookaside Buffer, 주소 변환 캐시)에 담긴다. 거기에 찾는 변환이 없으면 TLB Miss 가 나지만, 그것으로 접근이 실패하지는 않는다.
```text label="TLB Miss 가 났을 때"
TLB에 translation cache가 없음
Page Table을 조회
정상 mapping 존재
계속 실행
```
Page Fault 는 조건이 다르다. 페이지 테이블 상태로는 지금 접근을 정상으로 끝낼 수 없어서, 커널이 끼어들어 페이지를 마련하거나 매핑을 고쳐야 명령을 다시 시도할 수 있다. TLB Miss 는 테이블을 한 번 더 읽으면 풀리는데, 이쪽은 커널 처리를 부른다.
## 첫 번째 단계 — Guest Page Fault
이 fault 는 GVA 를 GPA 로 바꾸는 첫 번째 변환 단계에서 일어난다.
```text label="첫 번째 단계에서 접근이 완료되지 못할 때"
GVA
Guest Page Table
현재 접근을 완료할 수 없음
Guest #PF
Guest Kernel Page Fault Handler
```
게스트 프로세스가 아직 물리 페이지가 붙지 않은 가상 메모리 영역에 처음 접근하면 이 사건이 난다. 게스트 안의 Keycloak 을 예로 들면, 새 영역의 첫 접근에서 fault 가 나고 게스트 커널이 페이지를 확보해 매핑을 갱신한 뒤 명령을 다시 시도한다.
```text label="Guest 프로세스가 새 영역에 처음 접근할 때"
Keycloak
새 Virtual Memory 영역에 첫 접근
Guest Page Table
현재 usable physical mapping 없음
Page Fault
Guest Kernel
Page 확보 / mapping 갱신
Instruction 재시도
```
이 사건 자체가 프로그램 오류를 뜻하지는 않는다. 정상적인 메모리 관리에서도 늘 일어난다.
## Guest Page Fault 를 부르는 다섯 가지
원문은 대표적인 원인을 다섯으로 나눈다. 앞의 넷은 커널이 처리해 실행이 이어지고, 마지막 하나만 프로세스로 신호가 간다.
| 원인 | 게스트 커널이 무엇을 하나 |
|---|---|
| Demand Paging | 아직 물리 페이지가 필요하지 않았던 영역에 첫 접근이 오면 페이지를 준비한다 |
| Swap-in | 필요한 페이지가 게스트 RAM 에 없으면 게스트 swap 에서 읽어 RAM 을 복원하고 페이지 테이블을 갱신한다 |
| Permission Fault | 읽기 전용 페이지에 쓰기가 들어오는 접근을 걸러 낸다 |
| Copy-on-Write | 쓰기에서 난 fault 를 의도적으로 이용해 페이지를 복제하고 쓰기 가능한 매핑을 새로 만든다 |
| Invalid Access | 정상 매핑으로 해결할 수 없는 접근이면 `SIGSEGV` 등으로 이어질 수 있다 |
권한이 fault 를 부르는 까닭은 페이지 테이블 엔트리가 매핑만 담고 있지 않기 때문이다. 같은 엔트리에 읽기·쓰기·실행 권한이 함께 들어 있다.
```text label="Page Table Entry 에 함께 들어 있는 값"
Physical Frame: 1234
Present: 1
Writable: 0
Executable: 0
```
이 엔트리로 매핑된 페이지에 쓰기가 들어가면 fault 가 날 수 있고, Copy-on-Write 는 그 성질을 거꾸로 이용해 쓰기가 들어온 순간에만 페이지를 복제한다. 다섯 가운데 Invalid Access 만 결과가 다르다. 게스트 커널이 정상적인 매핑으로 해결할 수 없는 접근이면 `SIGSEGV` 로 이어질 수 있고, 그래서 Page Fault 와 Segmentation Fault 는 같은 것이 아니다.
## 두 번째 단계 — EPT Violation
이번에는 게스트 페이지 테이블 변환이 성공해 GPA 까지 얻었다고 하자. 그런데 그 GPA 에 대한 두 번째 단계 접근을 지금 EPT(Extended Page Tables) 조건으로 끝낼 수 없으면 EPT Violation 이 된다.
```text label="Guest 변환은 성공했는데 second-stage 에서 막힐 때"
GVA
Guest Page Table
GPA ← Guest translation 성공
EPT
EPT Violation
VM Exit
KVM
```
게스트 안에서는 첫 단계가 정상으로 끝났으므로 게스트 커널의 fault 처리기가 이 사건을 보지 않는다. 제어권이 VM Exit 으로 VMX(Virtual Machine Extensions) Root 쪽으로 넘어가 KVM 이 처리한다.
## 두 사건이 갈리는 다섯 항목
| 무엇을 견주나 | Guest Page Fault | EPT Violation |
|---|---|---|
| 문제 위치 | GVA → GPA | GPA → HPA |
| 관련 테이블 | 게스트 페이지 테이블 | EPT |
| 기본 관점 | 게스트 가상 메모리 | 가상화 메모리 매핑 |
| 주요 처리 계층 | 게스트 커널 | VM Exit 후 KVM 쪽 |
| 앱 오류를 뜻하는가 | 반드시 아님 | 반드시 아님 |
Guest Page Fault 는 게스트가 자기 가상 메모리를 처리하는 사건이고, EPT Violation 은 두 번째 단계 가상화 변환에서 하이퍼바이저 처리가 필요한 사건이다. 마지막 줄이 둘 다 「반드시 아님」인 까닭은 앞에서 본 대로다. demand paging 이나 copy-on-write 처럼 정상 동작이 두 계층 모두에서 fault 를 만든다.
## 세 번째 — Host Page Fault
QEMU 도 호스트의 일반 사용자 공간 프로세스이므로 QEMU 가 마련한 메모리에는 호스트의 가상 메모리 관리가 그대로 적용된다. 게스트 RAM 을 담고 있는 그 영역도 호스트 페이지 테이블을 지나 호스트 물리 주소로 이어지고, 그 경로에서 demand allocation 이나 reclaim, swap 때문에 fault 가 날 수 있다.
```text label="QEMU backing 쪽에서 나는 fault"
QEMU / Guest RAM Backing
Host Virtual Memory
Host Page Fault
Host Kernel
필요한 Host page 처리
```
게스트 안에서는 이 사건이 fault 로 보이지 않는다. 게스트는 자기 메모리 접근이 오래 걸렸다고만 관측하고, 그 지연의 원인은 호스트 쪽 지표에 남는다. 그래서 가상 머신 메모리를 분석할 때는 Guest Page Fault 와 Host Page Fault, 그리고 EPT 관련 가상화 사건을 적어도 셋으로 갈라 놓는다.
![Guest Page Table 에서 갈래가 둘로 나뉘어 하나는 Guest Page Fault 로 Guest Kernel 에 가고 다른 하나는 GPA 를 들고 EPT 로 내려가며, EPT 에서는 EPT Violation 이 VM Exit 을 거쳐 KVM 으로 가고, 따로 놓인 QEMU 의 Guest RAM Backing 에서는 Host Page Fault 가 Host Kernel 로 가는 갈래도. 받는 곳의 이름이 그대로 처리 계층의 이름이다.](../../../final/assets/diagrams/page-fault-layers-in-a-vm/page-fault-layers-in-a-vm.svg)
그림에서 QEMU 쪽 갈래는 위의 두 갈래와 선으로 이어져 있지 않다. 근거 문서가 EPT 로 얻은 HPA 와 QEMU 가 마련한 메모리의 호스트 물리 주소를 같은 것이라고 적지 않아서 그 선을 긋지 않았다.
## 메모리 지연이나 OOM(Out Of Memory) 이 보일 때 셋을 어디에 놓나
증상 하나로 "메모리 부족"을 결론내리지 않고 어느 계층의 사건인지부터 가른다. 세 사건은 이 분류에서 서로 다른 가지에 들어간다.
```text label="메모리 문제를 계층으로 가르는 분류 (fault 가 걸리는 가지)"
문제
├─ Guest Virtual Memory?
│ ├─ Page Fault
│ ├─ Guest reclaim
│ ├─ Guest swap
│ └─ Guest OOM
├─ Virtualization Translation?
│ ├─ EPT-related event
│ ├─ TLB pressure
│ └─ Huge-page/mapping 특성
└─ Host Memory?
├─ Host reclaim
├─ Host swap
├─ Host major fault
└─ Host OOM
```
원문의 분류에는 Dynamic Memory 와 NUMA(Non-Uniform Memory Access) 가지가 더 있고, 위에 옮긴 셋이 fault 가 걸리는 가지다. 게스트 쪽 fault 가 늘었으면 게스트의 reclaim 과 swap 을 같이 보고, 호스트 쪽 major fault 가 늘었으면 호스트의 reclaim 과 swap 을 같이 본다. 어느 쪽 지표를 먼저 여느냐가 이 구분에서 정해진다. 다만 이 호스트에서는 어느 쪽도 아직 열지 않았다. 게스트 쪽 fault 지표도 호스트 쪽 major fault 도 읽은 적이 없어서, 이 분류는 아직 어느 지표부터 열지 정하는 데만 쓰인다.
## 이 글이 확정하지 않는 것
이 호스트에서 잰 값은 하나도 없다. 게스트 쪽 fault 지표도 호스트 쪽 major fault 도 읽지 않았고, 앞의 다섯 원인 가운데 무엇이 이 환경에서 실제로 일어나는지도 세지 않았다. 원문 제2부는 개념 서술이고 실제 서버에 딸린 값은 OQ(Open Question) 번호가 붙은 확인 목록 열넷으로 미뤄져 있다.
게스트 쪽은 OQ-13 이 받는다. page-fault 관련 지표를 관측한 뒤 그 증가가 무엇에서 왔는지를 네 갈래로 갈라 보는 확인이고, 증가 자체를 오류로 읽지 않는 것이 조건이다.
```text label="Guest page fault 가 늘었을 때 갈라 보는 것 (OQ-13)"
정상 demand paging?
COW?
Guest swap-in?
application working-set 증가?
```
호스트 쪽은 OQ-14 가 받는다. 호스트 메모리 압박 실험을 하면서 아래 넷을 같은 시간축에 놓고 견준다.
```text label="같은 시간축에 놓고 견줄 값 (OQ-14)"
Host Fault
+
Swap activity
+
Storage latency
+
Guest application latency
```
가운데 계층은 그 목록이 받지 않는다. 위 분류에는 EPT 관련 사건과 TLB 압박이 한 가지로 들어 있는데, 그것을 이 호스트에서 재라고 적은 항목은 열넷 가운데 없다. 이 글이 갈라 놓은 세 계층에서 확인 계획이 붙은 것은 게스트 쪽과 호스트 쪽 둘이다.
원문이 커널 버전을 적지 않아 이 글도 특정 버전에 고정하지 않았다. 여기 적은 것은 그 문서가 서술한 처리 구조이고, 이 서버에서 어떤 값이 나오는지는 위 두 확인을 돌려야 안다.
<!-- body:end -->
@@ -0,0 +1,106 @@
---
id: ebdf93d7-2dcb-4fc8-91b9-2364ac8196e6
kind: CONCEPT
slug: virtio-balloon-memory-reclaim
title: virtio-balloon 이 Guest 메모리를 회수하고 돌려주는 방식
topic: memory-virtualization
topicName: 메모리 가상화
project: virtualization
status: 초안
basisVersion: virtio-balloon — Guest kernel 쪽 driver 와 QEMU 쪽 device 가 virtqueue 로 맞물린 구성 · libvirt 의 balloon target
studio: "https://hyeonworks.com/studio/documents/ebdf93d7-2dcb-4fc8-91b9-2364ac8196e6/edit"
sourceRevision: no-commit · 밖에서 반입한 문서 한 편. 고정할 저장소 리비전이 없다
assets:
- key: virtio-balloon-inflate-deflate
file: ../../../final/assets/diagrams/virtio-balloon-inflate-deflate/virtio-balloon-inflate-deflate.svg
source:
- final/document.md#64-ballooning이-필요한-이유
- final/document.md#65-virtio-balloon-구조
- final/document.md#66-balloon-inflate
- final/document.md#67-balloon-page-반환의-의미
- final/document.md#68-balloon-deflate
- final/document.md#69-ballooning을-과도하게-하면-guest가-압박을-받는다
- final/document.md#70-ballooning과-memory-hotplug
- final/document.md#82-핵심-claim-registry
---
# virtio-balloon 이 Guest 메모리를 회수하고 돌려주는 방식
호스트는 QEMU 가 마련한 게스트 RAM 을 볼 수 있지만 그 안에서 어떤 메모리가 중요한지는 알지 못한다. 그 구분을 아는 쪽은 게스트 커널이라, 호스트가 그 메모리를 그냥 스왑으로 밀어내는 대신 게스트와 협력해 필요 없는 몫을 돌려받는 방법이 따로 있다. `virtio-balloon` 이 그 협력에 쓰는 가상 장치다. 게스트 안의 드라이버와 QEMU 쪽 장치가 virtqueue 로 맞물려 있어서, 호스트가 balloon target 을 올리면 게스트 안의 balloon 이 커지고 게스트가 쓸 수 있는 메모리가 줄며 호스트가 회수할 수 있는 몫이 는다. 값을 낮추면 반대 방향으로 움직인다. 이 장치는 게스트에 RAM 을 더 주는 장치가 아니라 이미 준 용량 안에서 쓸 수 있는 양을 옮기는 장치이고, 과도하게 회수하면 호스트 RAM 을 확보하려던 조치가 게스트를 압박한다.
## 관계
- **Host 메모리 압박이 Guest 와 스토리지까지 내려가는 경로**
ballooning 은 그 압박에 대응하는 수단 가운데 하나다. 호스트가 무작정 스왑으로 밀어낼 때 무엇이 일어나는지는 그쪽에 적혀 있다.
- **이 가상 머신들에 virtio-balloon 이 붙어 있는가**
이 프로젝트의 가상 머신에 이 장치가 구성돼 있는지는 근거 문서에 없다. 그것을 확인하는 질문이다.
- **balloon target 을 바꾸면 게스트가 쓸 수 있는 메모리는 어떻게 따라 움직이는가**
부풀리고 줄이는 방향은 이 글이 적었고, 이 환경에서 얼마나 어떤 속도로 반영되는지는 그 질문이 잰다.
- **메모리 증상 하나로 계층을 단정하지 않는다**
balloon target 은 그 기준이 가르는 다섯 갈래 중 Dynamic Memory 갈래다. 게스트 안의 압박을 보고 호스트 RAM 부족으로 넘기기 전에 이 값을 본다.
## 본문
<!-- body:start -->
## 호스트는 게스트 안에서 무엇이 중요한지 알지 못한다
호스트는 QEMU 가 마련한 게스트 RAM 을 볼 수 있다. 그러나 그 안에서 어떤 메모리가 중요한지는 게스트 밖에서 완전히 알 수 없다. 게스트는 자기 메모리를 애플리케이션이 실제로 쓰는 몫(working set)과 JVM heap, 페이지 캐시, 비어 있는 몫처럼 의미가 다른 묶음으로 구분해 알고 있다. 호스트가 보는 것은 같은 크기의 익명 메모리라서 그 안에서 페이지 캐시를 눌렀는지 실제로 쓰는 몫을 눌렀는지 가려낼 수 없다.
그래서 호스트가 그 메모리를 무작정 스왑으로 밀어내기보다 게스트 커널과 협력해 필요 없는 몫을 돌려받는 편이 유리할 수 있다. 그 협력에 쓰는 대표적인 메커니즘이 `virtio-balloon` 이다.
## 게스트 드라이버와 QEMU 장치가 virtqueue 로 맞물린다
`virtio-balloon` 은 게스트 커널 쪽의 balloon 드라이버와 QEMU 쪽의 balloon 장치가 virtqueue 로 이어진 구성이다. virtqueue 는 게스트와 QEMU 가 요청과 응답을 주고받는 공유 큐이고, balloon 관련 요청도 이 큐를 지나 VM 경계를 넘는다. QEMU 쪽 장치는 받은 내용을 호스트의 메모리 관리로 넘긴다.
이 장치는 게스트 RAM 자체를 제공하지 않는다. 이미 존재하는 게스트 RAM 을 호스트와 게스트가 협력해 회수하고 반환하는 데 쓰는 가상 장치다.
이 프로젝트의 가상 머신에 이 장치가 붙어 있는지는 근거 문서에 없다. 아래 두 절이 적는 것은 장치가 있을 때 어느 방향으로 움직이는가다.
## Inflate — 게스트 안의 balloon 이 커진다
호스트가 게스트 메모리를 회수하려고 하면 balloon target 을 조정해 balloon 을 부풀린다. 이 동작을 inflate 라고 한다. 그 요청이 `virtio-balloon` 을 지나 게스트 안의 balloon 드라이버에 닿으면 드라이버가 게스트 페이지를 확보한다. 게스트 안의 balloon 이 커지기 때문에 게스트가 쓸 수 있는 RAM 이 그만큼 줄고, 호스트 쪽에서는 회수할 수 있는 메모리가 는다.
드라이버가 하는 일은 확보한 페이지를 일반적인 게스트 작업이 쓰지 못하도록 붙잡아 두고 그 사실을 호스트 쪽에 알리는 것까지다. 그 뒤 호스트가 그 메모리를 실제로 언제 어떻게 놓아주는지는 QEMU/KVM 버전과 backing 종류 및 설정에 따라 달라질 수 있다. 근거 문서는 그 동작을 하나로 단정하지 않았고 이 환경에서도 확인하지 않았다. 그 문서는 이 호스트의 QEMU/KVM 버전을 한 번도 적지 않았다. 버전과 설정이 갈라 놓는 동작이라 버전을 적기 전에는 이 환경이 어느 쪽인지 말할 수 없다.
![virtio-balloon Driver 와 virtqueue, QEMU virtio-balloon Device, Host Memory Management 네 참여자 사이를 여섯 개의 메시지가 오가는 순서도. 1번부터 3번까지는 Host 쪽에서 정한 balloon target 조정이 QEMU device 와 virtqueue 를 지나 VM Boundary 를 넘어 Guest kernel 의 driver 에 닿는 방향이고, 4번부터 6번까지는 driver 가 확보한 Guest page 가 같은 경계를 반대로 건너 Host 의 회수 가능 backing 이 되는 방향이다.](../../../final/assets/diagrams/virtio-balloon-inflate-deflate/virtio-balloon-inflate-deflate.svg)
## Deflate — target 을 줄이면 되돌아온다
호스트가 게스트에게 메모리를 다시 내줄 수 있으면 balloon target 을 줄인다. 그러면 게스트 balloon 드라이버가 붙잡고 있던 페이지를 반환하고 게스트가 쓸 수 있는 메모리가 늘어난다.
| 호스트가 balloon target 을 | 게스트가 쓸 수 있는 메모리는 |
|---|---|
| 올린다 (Inflate) | 감소 |
| 내린다 (Deflate) | 증가 |
## 과도한 inflate 가 게스트를 압박한다
게스트 애플리케이션이 실제로 쓰는 메모리가 큰데 balloon 을 과도하게 부풀리면, 게스트가 쓸 수 있는 메모리가 줄면서 게스트 안에 메모리 압박이 생긴다. 그러면 게스트 안에서 회수가 돌고 페이지 캐시가 회수되고 게스트 스왑이 시작되며, 심하면 게스트 OOM(Out Of Memory)까지 간다.
호스트 RAM 을 확보하려는 조치가 게스트의 스토리지 I/O 와 애플리케이션 지연을 늘릴 수 있다는 뜻이다. 회수한 만큼 호스트가 편해지는 대신 게스트가 그 압박을 받으므로, balloon target 을 어디까지 올릴지는 게스트가 실제로 쓰는 메모리를 보고 정한다.
## ballooning 과 memory hotplug 는 같은 것이 아니다
ballooning 은 게스트에 이미 설정된 메모리 용량 안에서 호스트와 게스트 사이의 쓸 수 있는 양을 회수하고 반환한다. 용량 자체는 그대로다. memory hotplug 는 기존 게스트 RAM 에 메모리 장치나 영역을 더해 게스트가 늘어난 용량을 인식하게 한다. 하나는 이미 준 용량 안에서 쓸 수 있는 양을 옮기고 다른 하나는 용량을 더한다.
현대 가상화에는 `virtio-mem` 같은 다른 동적 메모리 관리 방식도 있어서, 가상 머신의 동적 메모리 관리를 ballooning 하나로 일반화하지 않는다. 근거 문서에 있는 것은 그런 방식이 존재한다는 사실까지다.
## 근거 문서가 번호를 붙여 둔 문장
근거 문서는 이 장치에서 고정한 문장에 번호를 붙여 두었다. 실험 기록에서 어느 문장을 확인했는지 가리킬 때 이 번호를 쓴다.
| 번호 | 근거 문서가 고정한 문장 |
|---|---|
| CLAIM-MEM-15 | virtio-balloon은 Guest와 Host가 memory 회수/반환에 협력하기 위한 가상 장치이며 RAM 자체를 제공하는 장치는 아니다. |
| CLAIM-MEM-16 | Balloon inflate가 과도하면 Guest reclaim/swap/OOM을 유발할 수 있다. |
## 이 문서가 확정하지 않는 것
이 글이 확정하는 것은 balloon 이 어떤 구조로 어느 방향으로 동작하는가까지다. 이 테스트 서버에서 잰 값은 하나도 없다.
이 프로젝트의 가상 머신에 `virtio-balloon` 이 붙어 있는지조차 근거 문서에 적혀 있지 않다. libvirt 설정에 balloon 관련 요소가 있는지, 있다면 게스트 안에서 그 드라이버가 어떤 이름으로 올라와 있는지를 먼저 확인해야 한다. 장치가 없으면 balloon target 실험은 성립하지 않는다.
값을 한 단계 바꿨을 때 게스트가 쓸 수 있는 메모리가 얼마나 움직이는지, 그 반영이 즉시인지 늦는지, 어느 값부터 게스트 안에서 회수와 스왑이 시작되는지도 재지 않았다. 호스트가 그 메모리를 언제 놓아주는지가 이 QEMU 버전과 이 backing 설정에서 어떻게 되는지도 마찬가지다. 두 확인은 각각 열린 질문으로 두었고 관계에 걸어 두었다.
<!-- body:end -->