feat: 가상화 문서들 추가
This commit is contained in:
+336
@@ -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) 노드로 이어진다.
|
||||
|
||||

|
||||
|
||||
위쪽 점선 상자 안에서 끝나는 것이 첫 번째 변환이고, 그 상자를 나온 뒤의 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 -->
|
||||
+179
@@ -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 -->
|
||||
+118
@@ -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 -->
|
||||
+123
@@ -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 에 있는 모양이다.
|
||||
|
||||

|
||||
|
||||
이 호스트가 노드 몇 개인지는 근거 문서에 없다. 하나로 나오면 지금 말한 어긋남은 이 환경에 성립하지 않는다.
|
||||
|
||||
## 큰 가상 머신에서는 게스트에게 토폴로지를 보여 준다
|
||||
|
||||
가상 머신이 커지면 그 안에서도 같은 문제가 생긴다. 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 -->
|
||||
+237
@@ -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 관련 가상화 사건을 적어도 셋으로 갈라 놓는다.
|
||||
|
||||

|
||||
|
||||
그림에서 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 -->
|
||||
+106
@@ -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 버전을 한 번도 적지 않았다. 버전과 설정이 갈라 놓는 동작이라 버전을 적기 전에는 이 환경이 어느 쪽인지 말할 수 없다.
|
||||
|
||||

|
||||
|
||||
## 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 -->
|
||||
Reference in New Issue
Block a user