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,161 @@
---
id: 3b05c1f0-13e9-414a-97a6-2078fb4bab26
kind: CONCEPT
slug: guest-block-io-path-to-virtqueue
title: Guest 의 write() 가 virtqueue 에 실리기까지 — VFS · Filesystem · Page Cache · Block Layer · virtio-blk
topic: storage-virtualization
topicName: 스토리지 가상화
project: virtualization
status: 초안
basisVersion: QEMU/KVM 의 virtio-blk frontend 와 virtqueue · 게스트가 ext4 나 XFS 에 buffered I/O 로 쓰는 경우 · SSOT 가 커널과 QEMU 버전을 적지 않았다
studio: "https://hyeonworks.com/studio/documents/3b05c1f0-13e9-414a-97a6-2078fb4bab26/edit"
assets:
- key: guest-block-io-to-virtqueue
file: ../../../final/assets/diagrams/guest-block-io-to-virtqueue/guest-block-io-to-virtqueue.svg
sourceRevision: no-commit · 밖에서 반입한 문서 한 편. 고정할 저장소 리비전이 없다
source:
- final/document.md#128-전체-구조
- final/document.md#129-guest-application-read-write-에서-시작
- final/document.md#130-vfs-공통-파일-인터페이스-계층
- final/document.md#131-filesystem-ext4-xfs-파일-세계를-block-공간에-배치
- final/document.md#132-inode
- final/document.md#133-page-cache-write-가-바로-ssd-write는-아니다
- final/document.md#134-guest-block-i-o-layer
- final/document.md#135-dev-vda-guest가-보는-가상-block-device
- final/document.md#136-dev-vda-와-filesystem-관계
- final/document.md#137-virtio-blk-guest의-가상-block-device-driver
- final/document.md#138-virtio-blk와-virtqueue
- final/document.md#139-virtqueue의-실제-의미
- final/document.md#166-storage-i-o-completion
- final/document.md#127-문서-목적
- final/document.md#172-storage-virtualization-canonical-flow
- final/document.md#173-network-virtualization과-비교
- final/document.md#177-최종-요약
---
# Guest 의 write() 가 virtqueue 에 실리기까지 — VFS · Filesystem · Page Cache · Block Layer · virtio-blk
가상 머신 안의 PostgreSQL 이 파일에 데이터를 기록하면, 그 요청은 게스트 Linux 안에서만 여러 계층을 지난 뒤에야 가상 머신 경계에 닿는다. VFS 가 열린 파일을 어느 파일시스템 구현으로 보낼지 정하고, ext4 나 XFS 가 파일을 블록 공간에 배치하고, 페이지 캐시가 dirty page 로 받아 두었다가 나중에 writeback 한다. 이어서 블록 I/O 계층이 그 내용을 READ 와 WRITE, FLUSH 요청으로 바꾸고, 마지막에 virtio-blk 드라이버가 Virtio 블록 요청을 만들어 virtqueue 에 게시한다. 계층 이름과 게스트·호스트 구분을 아는 사람을 독자로 둔다. Keycloak 멀티 노드 실험을 가상 머신 두 대 위에서 돌리고 있다 보니, 이 경로를 알아 두면 게스트에서 본 저장소 지연을 애플리케이션 문제와 가상 머신 경계 아래의 문제로 갈라 볼 수 있다.
## 관계
- **/dev/vda 뒤에 있는 것 — qcow2 · RAW · Host block device**
이 글은 virtqueue 까지만 따라간다. 그 요청을 받은 QEMU 가 무엇에 연결돼 있는지는 그 글이 이어받는다.
- **완료라는 말의 네 가지 뜻 — write · writeback · flush · 전원 장애 생존**
여기서는 write() 성공이 SSD 영속화가 아니라는 데까지만 적었다. 그 응답이 어디까지 보장하는지는 그 글이 나눈다.
- **VM 아래에 한 번 더 있는 Host block stack — blk-mq · I/O Scheduler · NVMe**
게스트 블록 계층과 같은 이름의 계층이 호스트에도 한 번 더 있고, 그쪽에서는 다른 가상 머신의 요청과 섞인다.
- **Guest 안에서 본 disk 로 backend 를 단정하지 않는다**
게스트에 /dev/vda 가 보인다는 관측이 그 규칙의 근거가 된다.
- **이 호스트의 /dev/vda 는 어떤 backend 에 붙어 있는가**
이 글은 게스트가 보는 이름까지만 말한다. 이 장비에서 그 이름이 무엇에 붙어 있는지는 아직 확인하지 않았다.
- **Guest 의 fsync() 지연과 Host storage 지연은 같이 오르는가**
경로는 적었지만 각 구간의 지연을 이 호스트에서 재지 않았다.
- **Guest 의 packet 이 Host Physical NIC 에 닿기까지 — virtio-net · virtqueue · vhost-net · TAP · Bridge**
virtqueue 라는 같은 구조를 패킷 쪽에서 설명한 글이다.
## 본문
<!-- body:start -->
## 애플리케이션은 저장장치를 직접 다루지 않는다
가상 머신 안의 PostgreSQL 이나 Keycloak 같은 프로세스는 SSD 나 `virtio-blk` 를 직접 다루지 않는다. 파일에 데이터를 기록할 때 개념적으로 부르는 것은 시스템 콜 하나다.
```c label="게스트 애플리케이션이 파일에 기록할 때 부르는 system call"
write(fd, buffer, size);
```
파일과 관련된 시스템 콜은 이것 말고도 여럿이다.
```text label="대표적인 파일 관련 system call"
open()
read()
write()
close()
fsync()
```
이 시점에는 QEMU 도 `qcow2` 도 호스트의 NVMe 도 등장하지 않는다. 애플리케이션은 저장장치를 조작하는 것이 아니라 게스트 Linux 커널에 파일 연산을 요청하고, 그 뒤의 계층들이 그 요청을 하나씩 옮긴다.
## VFS 가 어느 파일시스템으로 보낼지 정한다
VFS(Virtual File System, 가상 파일 시스템)는 Linux 커널 안에서 여러 파일시스템을 같은 방식으로 쓸 수 있게 묶어 주는 공통 계층이다. 게스트가 `ext4` 를 쓰든 `XFS` 를 쓰든 애플리케이션이 부르는 `write()` 는 같고, 그 뒤를 VFS 가 갈라 준다. VFS 는 이 `fd` 가 어떤 파일인지 확인하고, 그 파일이 어떤 파일시스템에 속하는지 확인한 뒤 해당 파일시스템 구현으로 연산을 넘긴다.
## ext4 와 XFS 가 파일을 블록 공간에 배치한다
SSD 는 `/var/lib/postgresql/data` 같은 디렉터리 구조를 모른다. 저장장치가 보는 것은 번호가 붙은 블록 단위 공간인데 사람과 프로그램이 보는 것은 파일과 디렉터리 계층이다. 그 둘을 잇는 계층이 `ext4` 나 `XFS` 같은 파일시스템이다. 파일시스템이 관리하는 것은 파일 이름과 디렉터리 구조, 파일 크기, 소유자와 권한, 시각 정보, `inode` 와 메타데이터, 파일 데이터가 저장될 블록, 남은 공간, 파일시스템 일관성이다.
`inode` 는 파일 메타데이터와 저장 위치 정보를 관리하는 자료구조다. 디렉터리 항목이 파일 이름을 `inode` 번호로 옮기고, 그 `inode` 가 소유자와 권한, 크기, 시각 정보, 파일 데이터가 저장된 블록 정보를 들고 있다. 그래서 파일 이름과 `inode` 는 같은 것이 아니다. 어디까지 들어갈지는 SSOT 가 직접 그어 두었다.
> Storage 가상화를 이해하기 위해 inode 내부 구현까지 파고들 필요는 없지만, filesystem이 파일과 block을 연결한다는 점은 알아야 한다.
이 글도 거기서 멈춘다.
## write() 가 끝나도 SSD 에는 아직 없다
일반적인 buffered I/O 에서는 `write()` 가 호출될 때마다 물리 SSD 까지 즉시 내려갈 필요가 없다. 애플리케이션이 넘긴 데이터는 메모리의 페이지 캐시에 먼저 닿는다. 저장장치에 `ABC` 가 있는데 애플리케이션이 `DEF` 를 추가하면 페이지 캐시에는 `ABCDEF` 가 들어가고 SSD 에는 아직 `ABC` 만 있다. 이렇게 저장장치보다 최신인 페이지를 dirty page 라고 한다. 이후 커널 writeback 이 그 내용을 파일시스템과 블록 계층을 거쳐 저장장치 쪽으로 내려보낸다.
데이터가 페이지 캐시에 머무는 동안에도 호출이 성공으로 돌아오기 때문에, `write()` 성공과 물리 SSD 영속화 완료는 같은 사건이 아니다. 게스트 애플리케이션이 성공을 돌려받은 시점에 데이터가 어디에 있는지는 게스트 메모리까지만 확정된다.
## 블록 I/O 계층이 파일 세계를 블록 장치 세계로 옮긴다
파일시스템이 파일과 블록 할당을 관리하면, Linux 의 블록 I/O 서브시스템이 그 요청을 아래의 블록 장치 드라이버가 처리할 수 있는 I/O 요청으로 바꿔 전달한다. 파일시스템 세계에서 `/users/data.db` 의 오프셋 8192 에 4KB 를 쓰라고 내려온 것이, 블록 장치 세계에서는 `/dev/vda` 의 특정 위치에 `READ` 나 `WRITE`, `FLUSH` 를 거는 요청이 된다. 대표 요청은 넷이다 — `READ`, `WRITE`, `FLUSH`, `DISCARD`.
이 계층 안에는 `bio` 와 `request`, `queue`, `blk-mq` 가 실제로 있다. SSOT 는 `blk-mq` 의 tag allocator 같은 내부 구현을 별도 문서로 미뤘고 이 글도 이름까지만 적는다.
## /dev/vda 는 게스트가 보는 이름이다
물리 머신에서는 `/dev/sda` 나 `/dev/nvme0n1` 같은 블록 장치가 보인다. `virtio-blk` 를 쓰는 가상 머신에서는 흔히 `/dev/vda` 와 `/dev/vdb` 처럼 보이고, 게스트 Linux 는 그것을 하나의 블록 장치로 인식한다. 게스트 안에서 `lsblk` 를 실행하면 그 장치와 파티션이 나온다.
```bash label="게스트가 보는 block device"
lsblk
```
```text label="SSOT 가 든 출력 예시. 이 호스트에서 실행한 결과가 아니다"
NAME SIZE TYPE MOUNTPOINT
vda 100G disk
├─vda1 1G part /boot
└─vda2 99G part /
```
게스트에 이렇게 보여도 그 장치가 호스트의 실제 SSD 인지는 게스트 안에서 알 수 없다. 이 장비의 `/dev/vda` 가 무엇에 붙어 있는지도 아직 확인하지 않았다. 게스트가 보는 것은 `/dev/vda` 에서 파티션으로, 파티션에서 파일시스템으로, 파일시스템에서 마운트 지점으로 이어지는 연결까지다. `cd /var/lib/postgresql` 로 디렉터리를 옮기는 동작은 파일시스템 세계를 지나고, `lsblk` 의 `vda` 는 블록 장치 세계를 보여 준다.
이름 둘이 가리키는 대상도 다르다. `/dev/vda` 가 게스트 Linux 에 보이는 블록 장치를 가리키고, `virtio-blk` 는 그 가상 블록 장치를 제어하는 게스트 커널 드라이버를 가리킨다. 네트워크 쪽의 `ens3` 와 `virtio-net` 이 같은 짝이다.
## virtio-blk 가 요청을 virtqueue 에 게시한다
게스트 블록 계층에서 `/dev/vda` 의 특정 위치에 데이터를 `WRITE` 하라는 요청이 내려오면, `virtio-blk` 드라이버가 그것을 Virtio 블록 요청으로 구성해 `virtqueue` 에 게시한다. 네트워크에서 TCP/IP 스택이 `virtio-net` 을 거쳐 `virtqueue` 로 내려가던 구조가 저장소 쪽에서도 반복된다.
게스트 메모리 안에 I/O 버퍼가 있고 `virtqueue` 의 디스크립터가 그 버퍼를 가리키기 때문에, `virtqueue` 를 데이터가 흘러가는 관으로 보면 부정확하다. 게시되는 요청에는 `Operation` 이 `WRITE` 인지, `Sector` 가 어디인지, `Data Buffer` 가 게스트 메모리의 어디인지가 들어간다. 뜻을 풀면 `/dev/vda` 의 이 위치에 게스트 메모리의 이 버퍼를 기록하라는 요청이다.
![게스트 사용자 공간의 PostgreSQL / Keycloak 에서 시작한 write() 가 게스트 커널의 VFS, ext4 / XFS, Guest Page Cache, Guest Block Layer, /dev/vda, virtio-blk Frontend 를 차례로 지나 virtqueue 에 실리는 흐름도. Guest Page Cache 에서 Guest Block Layer 로 내려가는 화살표에 writeback 이, Guest Block Layer 에서 /dev/vda 로 가는 화살표에 WRITE / FLUSH 가 붙어 있고, 맨 아래 virtqueue 바로 밑이 VM Boundary 라 이 그림은 거기서 끝난다.](../../../final/assets/diagrams/guest-block-io-to-virtqueue/guest-block-io-to-virtqueue.svg)
## 완료는 같은 virtqueue 로 돌아온다
`WRITE` 요청은 아래로 내려가고 완료는 반대 방향으로 올라온다. NVMe 가 완료를 올리면 NVMe 드라이버와 호스트 블록 계층, QEMU 와 백엔드를 지나 `virtqueue` 완료가 된다. `virtio-blk` 가 그것을 받아 게스트 블록 계층으로 올린다. 그래서 `virtqueue` 는 요청을 내려보내는 통로이면서 완료를 올려보내는 통로이기도 하다.
## 네트워크 가상화와 짝이 되는 이름들
제3부의 네트워크 경로를 먼저 읽었다면 이름이 하나씩 대응된다.
| Network | Storage |
|---|---|
| `virtio-net` | `virtio-blk` |
| packet | block I/O request |
| TX/RX virtqueue | I/O virtqueue |
| TAP / network backend | QEMU block backend |
| Linux Bridge/Route | Host filesystem/block stack |
| Physical NIC | Physical SSD/NVMe |
| Guest TCP/IP Stack | Guest VFS/Filesystem/Block Layer |
| send/recv | read/write/fsync |
SSOT 는 이 표를 학습용 대응으로 두었고, 두 열의 각 요소가 `1:1` 로 같은 종류라는 데까지는 말하지 않는다.
## 확인하지 못한 것과 범위 밖으로 둔 것
SSOT 는 이 부에서 다룰 것과 미룰 것을 먼저 갈라 두었다. `qcow2` 내부의 L1/L2 테이블과 `blk-mq` 의 tag allocator, NVMe 의 submission/completion 큐는 필요할 때 별도 문서에서 다루기로 했다.
이 호스트에서 잰 값은 하나도 없다. 게스트의 `/dev/vda` 가 호스트에서 무엇에 붙어 있는지는 SSOT 가 적어 둔 열린 질문 가운데 첫 번째인데, 아직 확인하지 않았다. 게스트의 파일시스템이 `ext4` 인지 `XFS` 인지도 SSOT 어디에도 없어서, 이 글은 두 파일시스템이 같은 계층을 지난다는 데까지만 말한다. 요청 수나 지연을 재려면 게스트의 `fsync()` 지연과 호스트 저장소 지연을 같은 시간축에서 함께 보는 관찰이 먼저 돌아야 한다.
<!-- body:end -->
@@ -0,0 +1,119 @@
---
id: ecaaa37d-7b33-4cbf-9bc5-4c6caad14ee5
kind: CONCEPT
slug: host-block-stack-under-the-vm
title: VM 아래에 한 번 더 있는 Host block stack — blk-mq · I/O Scheduler · NVMe
topic: storage-virtualization
topicName: 스토리지 가상화
project: virtualization
status: 초안
basisVersion: 현대 Linux 의 blk-mq 기반 block layer 와 NVMe driver · scheduler 는 none · mq-deadline · bfq 기준
studio: "https://hyeonworks.com/studio/documents/ecaaa37d-7b33-4cbf-9bc5-4c6caad14ee5/edit"
assets:
- key: vms-sharing-one-nvme
file: ../../../final/assets/diagrams/vms-sharing-one-nvme/vms-sharing-one-nvme.svg
sourceRevision: no-commit · 밖에서 반입한 문서 한 편. 고정할 저장소 리비전이 없다
source:
- final/document.md#158-host-block-layer
- final/document.md#159-여러-vm이-하나의-nvme를-공유하면
- final/document.md#160-blk-mq-multi-queue-block-layer
- final/document.md#161-i-o-scheduler
- final/document.md#162-none
- final/document.md#163-실제-i-o-scheduler-확인
- final/document.md#164-nvme-driver와-physical-device
- final/document.md#165-nvme와-ssd-구분
- final/document.md#167-storage-contention
- final/document.md#141-qemu가-물리-ssd를-직접-제어하는-것은-아니다
- final/document.md#172-storage-virtualization-canonical-flow
---
# VM 아래에 한 번 더 있는 Host block stack — blk-mq · I/O Scheduler · NVMe
게스트가 디스크라고 부르는 것이 호스트에서는 qcow2 나 RAW 파일일 수 있고, 그러면 QEMU 의 쓰기는 호스트 파일시스템을 거쳐 호스트 블록 I/O 가 된다. 호스트 블록 계층은 그 요청이 가상 머신 안의 PostgreSQL 에서 시작했는지 호스트 프로세스에서 시작했는지를 본질적으로 구분해 처리하는 계층이 아니다. 들어온 것은 모두 호스트 블록 요청이기 때문에 가상 머신 두 대와 호스트의 Nginx 와 나머지 프로세스가 같은 큐로 들어온다. blk-mq 가 CPU 마다 큐를 두어 병렬로 처리하고, I/O 스케줄러가 들어온 순서 그대로 장치에 전달하지 않을 수 있다. 여기서 생기는 경쟁이 스토리지 경쟁(Storage Contention)이고, 호스트 논리 CPU 실행 시간을 두고 벌어지는 CPU 경쟁과는 다투는 자원이 다르다. 블록 계층과 스케줄러 이름을 아는 사람을 독자로 둔다. 가상 머신 위에서 Keycloak 을 돌리며 지연을 볼 때 이 계층을 CPU 계층과 갈라 두면 어느 쪽을 재야 하는지가 먼저 정해진다.
## 관계
- **Guest 의 write() 가 virtqueue 에 실리기까지 — VFS · Filesystem · Page Cache · Block Layer · virtio-blk**
virtqueue 까지가 게스트 쪽 경로이고, 이 글은 그 요청이 가상 머신 경계를 넘어온 뒤를 잇는다.
- **/dev/vda 뒤에 있는 것 — qcow2 · RAW · Host block device**
백엔드가 파일인지 블록 장치인지에 따라 이 글이 설명하는 계층 위에 호스트 파일시스템이 한 겹 더 놓인다.
- **완료라는 말의 네 가지 뜻 — write · writeback · flush · 전원 장애 생존**
같은 계층을 영속성 쪽에서 본 글이다. 이 글은 요청이 어떻게 줄을 서고 누구와 섞이는지를 본다.
- **지연 원인을 CPU 로 읽기 전에 storage 계층을 따로 잰다**
이 글이 가른 두 경쟁을 진단 순서로 편 규칙이다.
- **disk image 는 최종적으로 어느 Host block device 위에 있는가**
이 호스트의 디스크 이미지가 어느 블록 장치 위에 있는지 SSOT 에 적혀 있지 않다.
- **이 호스트의 I/O Scheduler 는 무엇인가**
스케줄러 값을 읽는 방법은 여기 적었고, 이 호스트의 값은 그 질문이 확인한다.
- **VM1 의 storage 부하가 VM2 의 지연을 실제로 밀어 올리는가**
스토리지 경쟁은 이 글에서 구조로만 설명했다. 이 환경에서 실제로 겹치는지는 그 질문이 잰다.
## 본문
<!-- body:start -->
## 게스트 디스크 파일이 호스트 블록 요청이 되는 곳
백엔드가 `qcow2` 파일이면 QEMU 가 물리 SSD 를 직접 제어하지 않는다. QEMU 는 `pread``pwrite` 같은 호출로 호스트 커널에 파일 I/O 를 요청하고, 그 요청이 호스트 파일시스템과 호스트 블록 계층, NVMe 드라이버를 지나 물리 NVMe 에 닿는다. 게스트 스토리지 계층 아래에 호스트 스토리지 계층이 한 번 더 존재할 수 있다.
호스트 블록 계층은 그 I/O 가 가상 머신 안의 PostgreSQL 에서 시작했는지 호스트 프로세스에서 시작했는지를 본질적으로 구분해서 처리하는 계층이 아니다. 내려온 것은 모두 호스트 블록 요청이다.
| 호스트 쪽 어느 계층인가 | 여기서 무엇이 일어나나 |
|---|---|
| 호스트 파일시스템 | `qcow2` 나 RAW 파일을 읽고 쓰는 일이 파일 I/O 로 처리된다 |
| 호스트 블록 계층 | 어디서 왔는지 구분하지 않고 모두 블록 요청으로 다룬다 |
| `blk-mq` | CPU 마다 큐를 두어 여러 CPU 가 병렬로 블록 I/O 를 처리한다 |
| I/O 스케줄러 | 들어온 순서 그대로가 아니라 정책에 따라 장치로 내보낸다 |
| NVMe 드라이버 | 호스트 커널의 장치 드라이버로서 NVMe 컨트롤러에 요청을 보낸다 |
| NVMe 컨트롤러 | PCIe 를 통해 요청을 받아 플래시에 반영한다 |
## 한 NVMe 로 여러 곳의 요청이 들어온다
가상 머신 둘의 QEMU 프로세스와 호스트의 Nginx, 그리고 호스트의 다른 프로세스에서 동시에 I/O 가 들어올 수 있다. VM1 이 X 를 쓰고 Y 를 읽고 Z 를 쓰는 동안 VM2 는 A 를 읽고 B 를 쓰고, 호스트 프로세스는 C 를 읽는 식이다. 이 요청들은 호스트 블록 계층의 큐에서 관리되다가 장치로 나간다.
![VM1 QEMU 와 VM2 QEMU, Nginx, Host 기타 네 곳에서 나온 화살표가 하나의 Host Block Layer 로 모이고 거기서 NVMe 로 dispatch 되는 수렴 흐름도. VM1 QEMU 아래에는 WRITE X 와 READ Y, WRITE Z 가, VM2 QEMU 아래에는 READ A 와 WRITE B 가 적혀 있고, Host Block Layer 는 그것들을 모두 Host block request 로 다룬다.](../../../final/assets/diagrams/vms-sharing-one-nvme/vms-sharing-one-nvme.svg)
## `blk-mq` 는 CPU 마다 큐를 둔다
현대 Linux 의 블록 계층은 `blk-mq` 로 되어 있다. CPU0 부터 CPU3 까지 각 CPU 가 자기 큐를 가지고 그 큐들이 NVMe 로 이어진다. NVMe 가 높은 병렬성과 큐 깊이를 지원하기 때문에 여러 CPU 가 병렬로 블록 I/O 를 처리할 수 있는 구조가 중요해진다. 그래서 스토리지 처리 성능은 CPU 스케줄링과 완전히 분리되지 않는다.
## I/O 스케줄러는 들어온 순서 그대로 내보내지 않는다
여러 I/O 요청이 있어도 항상 들어온 순서 그대로 장치에 전달되지는 않는다. 요청은 I/O 스케줄러를 지나 장치 드라이버로 나간다. 스케줄러마다 목적과 내보내는 정책이 다르다. 대표적으로 볼 수 있는 값은 `none``mq-deadline``bfq` 다.
`none` 을 고르면 복잡한 스케줄링 정책을 최소화해서 비교적 직접 장치 쪽으로 내보낸다. NVMe 처럼 장치 자체가 강한 병렬성과 큐 기능을 가진 경우에는 이런 단순한 정책이 적합할 수 있다. 그렇게 골랐다고 블록 계층이 아무 일도 하지 않지는 않는다.
지금 선택된 스케줄러는 `sysfs` 에서 읽는다.
```bash label="호스트의 현재 I/O Scheduler"
cat /sys/block/nvme0n1/queue/scheduler
```
출력이 `[none] mq-deadline` 이면 대괄호 안의 `none` 이 지금 선택된 스케줄러다. SATA 나 SCSI 장치라면 `cat /sys/block/sda/queue/scheduler` 처럼 장치 이름을 바꿔 읽는다.
여기 든 `[none] mq-deadline` 은 SSOT 가 읽는 법을 보이려고 든 출력이다. 이 호스트에서 그 명령을 아직 돌리지 않아서 이 장비의 스케줄러가 무엇인지는 이 글이 말하지 않는다.
## NVMe 드라이버는 호스트 커널의 장치 드라이버다
호스트 블록 계층에서 I/O 스케줄러를 지난 요청은 NVMe 드라이버로 가고, NVMe 컨트롤러를 거쳐 물리 저장장치에 닿는다. NVMe 드라이버는 호스트 Linux 커널의 장치 드라이버다. 네트워크에서 물리 NIC 드라이버가 하드웨어를 제어하는 것과 같은 계층에 놓인다.
이름이 비슷해 섞이는 두 낱말도 여기서 갈린다. SSD 는 저장장치의 넓은 종류를 가리키고, NVMe 는 PCIe 기반 비휘발성 저장장치를 위한 프로토콜이자 인터페이스다. SSD 아래에서 SATA SSD 는 SATA 와 AHCI 를 쓴다. NVMe SSD 는 PCIe 와 NVMe 를 쓴다. NVMe SSD 에서는 Linux NVMe 드라이버가 PCIe 를 통해 NVMe 컨트롤러에 요청을 보낸다. 컨트롤러는 플래시를 다룬다.
## 스토리지 경쟁과 CPU 경쟁은 다투는 자원이 다르다
여러 가상 머신이 같은 물리 NVMe 를 쓰면 스토리지 자원 경쟁이 생길 수 있다. VM1 의 PostgreSQL 과 VM2 의 Keycloak 이 요청한 I/O 가 같은 호스트 블록 계층과 같은 I/O 큐를 지나 하나의 NVMe 로 가기 때문에, VM1 에서 대량 I/O 가 발생하면 VM2 의 스토리지 지연이 올라갈 수 있다. 이렇게 벌어지는 경쟁을 스토리지 경쟁(Storage Contention)이라고 한다.
```text label="두 경쟁이 다투는 자원"
CPU Contention
→ Host logical CPU 실행 시간 경쟁
Storage Contention
→ IOPS / bandwidth / queue / device 처리시간 경쟁
```
CPU 경쟁은 호스트 논리 CPU 의 실행 시간을 두고 벌어지고, 스토리지 경쟁은 IOPS 와 대역폭, 큐, 장치 처리시간을 두고 벌어진다. 애플리케이션에서는 둘 다 「느리다」로 관찰되지만 다투는 자원이 달라서 따로 잰다.
## 이 호스트에서 확인하지 않은 것
이 글에도 이 테스트 호스트에서 잰 값은 없다. 이 호스트의 I/O 스케줄러가 무엇으로 설정돼 있는지 SSOT 에 없고, 가상 머신들의 디스크 이미지가 최종적으로 어느 호스트 블록 장치 위에 있는지도 없다. 가상 머신 두 대와 호스트 프로세스의 I/O 가 이 환경에서 실제로 같은 시간에 겹치는지도 재지 않았다. VM1 의 대량 I/O 가 VM2 의 스토리지 지연을 올린다는 것은 이 계층 구성에서 나오는 가능성이고 이 서버에서 관측한 값이 아니다. 이 글은 요청이 호스트에서 어떤 순서로 어느 계층을 지나는지까지 설명하고, 이 서버가 그 계층에서 실제로 막히는지는 재지 않았다.
<!-- body:end -->
@@ -0,0 +1,123 @@
---
id: 892b6087-8961-486c-b4d9-d3dd46bc2605
kind: CONCEPT
slug: qemu-block-backend-forms
title: /dev/vda 뒤에 있는 것 — qcow2 · RAW · Host block device
topic: storage-virtualization
topicName: 스토리지 가상화
project: virtualization
status: 초안
basisVersion: QEMU block backend 세 형태 — qcow2 파일 · RAW 파일 · Host block device. libvirt 로 정의한 VM 기준이고 SSOT 가 QEMU 와 libvirt 버전을 적지 않았다
studio: "https://hyeonworks.com/studio/documents/892b6087-8961-486c-b4d9-d3dd46bc2605/edit"
sourceRevision: no-commit · 밖에서 반입한 문서 한 편. 고정할 저장소 리비전이 없다
source:
- final/document.md#140-vm-boundary를-넘으면-qemu가-등장
- final/document.md#141-qemu가-물리-ssd를-직접-제어하는-것은-아니다
- final/document.md#142-qcow2-host에서는-파일-guest에서는-디스크
- final/document.md#143-qcow2-virtual-size와-실제-host-사용량
- final/document.md#144-raw-image
- final/document.md#145-host-block-device를-직접-backend로-사용-가능
- final/document.md#146-실제-연결-확인
- final/document.md#128-전체-구조
- final/document.md#172-storage-virtualization-canonical-flow
- final/document.md#174-핵심-claim
---
# /dev/vda 뒤에 있는 것 — qcow2 · RAW · Host block device
게스트 안에서 /dev/vda 를 보는 것만으로는 호스트에 무엇이 있는지 알 수 없다. 가상 머신 경계를 넘으면 QEMU 의 virtio-blk 장치 모델과 블록 백엔드가 그 요청을 받는데, 그 백엔드는 qcow2 파일일 수도, RAW 파일일 수도, 호스트의 실제 블록 장치일 수도 있다. 갈래마다 바뀌는 것이 다르다. 파일 백엔드면 게스트 스토리지 계층 아래에 호스트 파일시스템과 블록 계층이 한 겹 더 놓이고, qcow2 면 게스트가 보는 크기와 호스트가 실제로 쓰는 크기가 갈린다. 무엇에 붙어 있는지는 게스트가 아니라 호스트에서 virsh domblklist 로 확인한다. 가상 머신에 디스크를 붙여 본 사람을 독자로 둔다.
## 관계
- **Guest 의 write() 가 virtqueue 에 실리기까지 — VFS · Filesystem · Page Cache · Block Layer · virtio-blk**
게스트 안에서 virtqueue 까지 내려온 요청을 이 글이 이어받는다.
- **완료라는 말의 네 가지 뜻 — write · writeback · flush · 전원 장애 생존**
파일 백엔드면 호스트 페이지 캐시가 한 겹 더 생기고, 그 위에서 완료가 무엇을 보장하는지가 갈린다.
- **VM 아래에 한 번 더 있는 Host block stack — blk-mq · I/O Scheduler · NVMe**
이 글은 백엔드 형태까지만 본다. 그 아래에서 요청이 어떻게 줄을 서는지는 그 글이 본다.
- **Guest 안에서 본 disk 로 backend 를 단정하지 않는다**
이 글이 적은 세 갈래가 그 규칙의 근거다.
- **이 호스트의 /dev/vda 는 어떤 backend 에 붙어 있는가**
세 갈래 가운데 어느 것인지 이 장비에서 확인하지 않았다.
- **이 disk image 는 qcow2 인가 RAW 인가, 그리고 Guest 가 보는 크기와 Host 가 실제로 쓰는 크기는 얼마나 다른가**
이 글이 든 크기 차이는 SSOT 의 예시 값이고, 이 장비의 값은 그 질문이 잰다.
- **disk image 는 최종적으로 어느 Host block device 위에 있는가**
파일 백엔드라면 그 파일이 놓인 호스트 장치까지 따라가야 경로가 끝난다.
## 본문
<!-- body:start -->
## 가상 머신 경계를 넘으면 QEMU 가 받는다
게스트의 `virtio-blk``virtqueue` 에 게시한 요청은 가상 머신 경계를 넘어 호스트 사용자 공간의 QEMU 로 간다. QEMU 안에서는 `virtio-blk` 장치 모델과 블록 백엔드가 그 요청을 받는다. QEMU 는 게스트에게 가상 블록 장치를 노출하고, 게스트의 가상 I/O 를 호스트 백엔드에 연결한다.
SSOT 는 제4부의 전체 구조를 그린 뒤 이 갈림을 핵심 문장 하나로 따로 뽑아 두었다.
> Guest는 `/dev/vda`를 실제 block device처럼 보지만, Host에서는 그 disk가 qcow2 파일, RAW 파일, 또는 실제 block device에 연결되어 있을 수 있다.
이 글이 다루는 것이 그 문장이 남긴 세 갈래다.
## QEMU 가 물리 SSD 를 직접 제어하지는 않는다
백엔드가 `qcow2` 파일이면 QEMU 는 결국 호스트 Linux 에 파일 I/O 를 요청한다. `pread``pwrite` 같은 호출이 호스트 커널로 들어가고, 그 뒤로 호스트 파일시스템과 호스트 블록 계층, NVMe 드라이버를 지나 물리 NVMe 에 닿는다. 그 경로가 호스트 커널을 한 번 더 지나기 때문에, 게스트 스토리지 계층 아래에 호스트 스토리지 계층이 한 번 더 존재할 수 있다.
## 같은 대상을 호스트는 파일로, 게스트는 디스크로 본다
호스트에 `/var/lib/libvirt/images/keycloak-node1.qcow2` 라는 파일이 있다고 하자. 호스트 관점에서 그것은 파일 하나인데, 같은 대상을 게스트는 `/dev/vda` 라는 디스크로 보고 그 안을 `/dev/vda1``/dev/vda2` 로 나눠 쓴다. 두 관점 모두 맞다.
## qcow2 는 게스트가 보는 크기와 호스트가 쓰는 크기가 다르다
`qcow2` 는 가상 디스크 크기와 실제 호스트 할당량이 다를 수 있다. SSOT 가 든 예시에서는 게스트가 `/dev/vda` 를 100 GB 로 보는 동안 호스트의 `vm1.qcow2` 가 3GB 를 쓴다. 게스트가 데이터를 기록하면서 가상 크기 100GB 는 그대로인 채 실제 할당량이 1GB 에서 10GB 로, 다시 40GB 로 늘 수 있다. 이 숫자들은 SSOT 가 설명하려고 든 값이고 이 호스트에서 잰 값이 아니다.
```bash label="qcow2 의 virtual size 와 실제 할당량"
qemu-img info vm1.qcow2
```
출력의 `virtual size` 와 실제 할당량은 서로 다른 값이다.
## RAW 와 qcow2 를 성능으로 먼저 가르지 않는다
RAW 는 `qcow2` 보다 구조가 단순하다. 게스트의 블록 요청이 `qcow2` 에서는 QEMU 의 매핑과 메타데이터 처리를 지나 `qcow2` 파일 I/O 가 되고, RAW 에서는 상대적으로 직접적인 오프셋 대응을 지나 RAW 파일 I/O 가 된다. `qcow2` 는 Copy-on-Write 와 sparse allocation, 스냅샷에 유리하지만 그 대신 매핑과 메타데이터 처리가 붙는다.
그렇다고 RAW 가 무조건 빠르고 `qcow2` 가 무조건 느리다고 일반화하면 안 된다. SSOT 는 실제 성능이 캐시 모드와 스토리지 백엔드, 작업 부하 패턴, 큐 깊이, 스냅샷 체인, 그 아래 파일시스템, 물리 장치에 영향을 받는다고 적었다. 형식 이름만으로는 어느 쪽이 빠른지 정해지지 않는다.
## 백엔드가 파일이 아니라 호스트 블록 장치일 수도 있다
백엔드는 파일이 아니어도 된다. 게스트의 `/dev/vda` 가 `virtio-blk` 와 QEMU 를 지나 호스트의 `/dev/nvme0n1p3` 같은 블록 장치에 바로 연결될 수 있고, SSOT 가 그린 이 경로에는 호스트 파일시스템이 없다. 같은 이름 아래에서 경로가 세 갈래로 갈리기 때문에, 게스트에 `/dev/vda` 가 있다는 정보만으로는 백엔드 구조를 알 수 없다.
## 세 갈래가 각각 바꾸는 것
| 호스트에서는 무엇인가 | 그래서 무엇이 달라지나 |
|---|---|
| `qcow2` 파일 | 게스트 파일시스템 아래에 호스트 파일시스템과 블록 계층이 한 겹 더 놓인다. Copy-on-Write 와 sparse allocation, 스냅샷을 쓸 수 있고 매핑과 메타데이터 처리가 붙는다. 게스트가 보는 가상 크기와 호스트 실제 할당량이 갈린다 |
| RAW 파일 | 게스트 파일시스템 아래에 호스트 파일시스템과 블록 계층이 한 겹 더 놓인다. 블록 위치가 파일 오프셋에 상대적으로 직접 대응한다 |
| 호스트 블록 장치 | SSOT 가 그린 경로에 호스트 파일시스템이 없다 |
## 무엇에 붙어 있는지는 호스트에서 확인한다
게스트에서 `lsblk` 로 장치를 보고, 호스트에서 `virsh domblklist` 로 그 장치의 `Source` 를 본다.
```bash label="게스트가 보는 block device"
lsblk
```
```bash label="호스트에서 본 VM 의 disk 연결"
virsh domblklist <VM_NAME>
```
```text label="SSOT 가 든 출력 예시. 이 호스트에서 실행한 결과가 아니다"
Target Source
-----------------------------------------------
vda /var/lib/libvirt/images/vm1.qcow2
```
이 출력이 나오면 게스트의 `/dev/vda` 가 `virtio-blk` 와 QEMU 를 지나 `/var/lib/libvirt/images/vm1.qcow2` 에 닿는 연결이 확인된다.
## 확인하지 못한 것
이 호스트의 백엔드가 셋 중 무엇인지는 SSOT 에 없다. SSOT 가 열린 질문 넷을 적어 두었는데, 그 확인을 아직 돌리지 않았다. `/dev/vda` 가 어떤 백엔드에 붙어 있는지, 백엔드가 `qcow2` 인지 RAW 인지, `qcow2` 의 가상 크기와 실제 호스트 사용량이 얼마나 다른지, 그 이미지가 최종적으로 어느 호스트 블록 장치 위에 있는지 넷이다. 앞에서 든 100 GB 와 3GB, 1GB 에서 40GB 까지의 증가는 SSOT 가 설명하려고 든 예시 값이다.
`qcow2` 와 RAW 의 성능 비교도 이 저장소에 없다. SSOT 는 실제 성능을 가르는 조건만 열거했고 어느 쪽이 이 환경에서 빠른지는 재지 않았다.
<!-- body:end -->
@@ -0,0 +1,167 @@
---
id: 8fafaeea-8746-4537-87a4-679a3a91aa4c
kind: CONCEPT
slug: write-completion-is-not-durability
title: 완료라는 말의 네 가지 뜻 — write · writeback · flush · 전원 장애 생존
topic: storage-virtualization
topicName: 스토리지 가상화
project: virtualization
status: 초안
basisVersion: Linux O_DIRECT 와 QEMU/libvirt disk 의 cache=none · cache=writeback 두 설정 기준
studio: "https://hyeonworks.com/studio/documents/8fafaeea-8746-4537-87a4-679a3a91aa4c/edit"
assets:
- key: write-completion-boundaries
file: ../../../final/assets/diagrams/write-completion-boundaries/write-completion-boundaries.svg
sourceRevision: no-commit · 밖에서 반입한 문서 한 편. 고정할 저장소 리비전이 없다
source:
- final/document.md#147-vm에서는-page-cache가-두-번-나타날-수-있다
- final/document.md#148-write-완료와-영속화는-다르다
- final/document.md#149-direct-i-o
- final/document.md#150-fsync-가-필요한-이유
- final/document.md#151-flush
- final/document.md#152-가장-위험한-상황-거짓-완료
- final/document.md#153-qemu-cache-mode
- final/document.md#154-cache=none
- final/document.md#155-cache=writeback
- final/document.md#156-writeback-=-위험-이라고-단정하면-안-되는-이유
- final/document.md#157-device-side-cache
- final/document.md#170-postgresql-예시-wal과-durability
- final/document.md#133-page-cache-write-가-바로-ssd-write는-아니다
- final/document.md#174-핵심-claim
---
# 완료라는 말의 네 가지 뜻 — write · writeback · flush · 전원 장애 생존
buffered I/O 에서 write() 는 페이지 캐시까지만 내려간다. 가상 머신에서는 그 페이지 캐시가 게스트와 호스트 양쪽에 두 번 나타날 수 있다. 게스트가 성공을 돌려받은 데이터가 호스트 메모리에 머무는 동안 호스트 전원이 나가면 물리 SSD 에는 이전 상태가 남는다. 그래서 완료라는 말이 네 단계로 갈린다 — write() 완료, writeback 완료, fsync/flush 완료, 전원 장애에도 안전한 영속성. Direct I/O 와 QEMU 캐시 모드는 이 가운데 어디까지 갔는지를 바꾼다. 게스트에게 FLUSH 완료라고 응답했는데 데이터가 실제로는 호스트 메모리에만 있으면, 그것은 느린 구성이 아니라 영속성 계약이 깨진 상태다. 페이지 캐시와 fsync 를 아는 사람을 독자로 둔다. Keycloak 과 PostgreSQL 을 가상 머신 위에서 돌리는 실험에서 이 구분을 먼저 세워 두면, 지연을 재는 것과 데이터가 남는지를 재는 것을 한 실험에 섞지 않게 된다.
## 관계
- **Guest 의 write() 가 virtqueue 에 실리기까지 — VFS · Filesystem · Page Cache · Block Layer · virtio-blk**
게스트 쪽 경로를 VFS 부터 virtqueue 까지 이어 그린 글이다. 이 글은 그 경로 위에서 완료 응답이 무엇을 보장하는지만 본다.
- **/dev/vda 뒤에 있는 것 — qcow2 · RAW · Host block device**
백엔드가 파일이면 게스트 파일시스템 아래에 호스트 파일시스템과 호스트 페이지 캐시가 한 겹 더 놓인다. 두 번째 페이지 캐시가 왜 생기는지를 그 글이 설명한다.
- **VM 아래에 한 번 더 있는 Host block stack — blk-mq · I/O Scheduler · NVMe**
fsync 요구가 호스트로 내려간 뒤 어느 계층을 지나는지는 그 글이 맡는다.
- **빨라진 구성이 durability contract 를 지운 것은 아닌지 먼저 가른다**
이 개념이 세운 네 경계를 설정 선택과 성능 개선의 판정에 적용하는 규칙이다.
- **이 VM 들의 QEMU disk cache mode 는 무엇인가**
이 호스트가 cache=none 구성인지 cache=writeback 구성인지 SSOT 에 적혀 있지 않다.
- **Guest 의 fsync() 지연과 Host storage 지연은 같이 오르는가**
fsync() 지연 값은 그 질문이 게스트와 호스트를 같은 시간축으로 잰 뒤에 이 글에 들어온다.
## 본문
<!-- body:start -->
## 게스트와 호스트에 페이지 캐시가 두 번 생긴다
일반적인 buffered I/O 에서는 `write()` 가 호출될 때마다 물리 SSD 까지 즉시 내려갈 필요가 없다. 커널은 그 내용을 메모리의 페이지 캐시에 받아 두고, 나중에 writeback 으로 파일시스템과 블록 계층을 지나 저장장치 쪽에 내려보낸다. 저장장치보다 최신인 페이지를 dirty page 라고 한다. 저장장치에 `ABC` 가 있는 상태에서 애플리케이션이 `DEF` 를 추가하면 페이지 캐시는 `ABCDEF` 를 dirty 상태로 들고 있고 SSD 에는 아직 `ABC` 만 있다.
가상 머신에서는 이 페이지 캐시가 한 번 더 나타날 수 있다. 게스트가 buffered I/O 를 쓰고 디스크 백엔드가 호스트의 파일이며 호스트도 페이지 캐시를 쓰는 구성이라고 하자. 게스트 PostgreSQL 의 쓰기는 게스트 `ext4` 와 게스트 페이지 캐시를 지나 게스트 블록 계층과 `virtio-blk` 를 거쳐 `virtqueue` 로 나간다. 가상 머신 경계를 넘은 다음에는 QEMU 와 디스크 이미지 파일을 지나 호스트 페이지 캐시에 다시 담긴다. 같은 데이터가 게스트 메모리와 호스트 메모리 양쪽에 캐시될 수 있다.
## write() 성공이 어디까지를 뜻하는가
`write()` 가 성공을 돌려준 시점에 데이터는 게스트 페이지 캐시에 있고, virtio 를 지나 호스트 페이지 캐시까지 갔을 수도 있다. 그 상태에서 호스트 전원이 나가면 물리 SSD 에는 그 데이터가 없기 때문에, 완료라는 말이 네 단계로 갈린다.
```text label="완료의 네 단계"
write() 완료
writeback 완료
fsync/flush 완료
전원 장애에도 안전한 durability
```
## fsync() 는 요구를 계층 전체로 내려보낸다
애플리케이션이 부르는 쓰기 호출은 이렇게 생겼다.
```c label="이 호출이 성공해도 정전 이후 생존은 보장되지 않는다"
write(fd, data, size);
```
필요한 시점에 `fsync()` 를 불러 변경 내용을 필요한 영속성 경계까지 반영하도록 요청한다.
```c label="영속성 경계까지 반영을 요청한다"
fsync(fd);
```
가상 머신에서는 이 요청이 한 계층에서 끝나지 않는다. 게스트 PostgreSQL 의 `fsync()` 가 게스트 파일시스템으로 가고, 게스트 블록 계층에서 `FLUSH` 등으로 바뀐다. 그다음 `virtio-blk` 를 통해 QEMU 와 백엔드로 내려가고, 호스트 스토리지 계층을 지나 물리 저장장치까지 그 의미가 전달되어야 한다.
![PostgreSQL 이 부른 fsync() 가 Guest Filesystem 으로 내려가고, Guest Block Layer 에서 FLUSH 등으로 바뀌어 virtio-blk 와 QEMU / Backend, Host Storage Stack 을 지나 맨 아래 Physical Storage 까지 닿는 흐름도. 여섯 번 손이 바뀌는 동안 한 곳이라도 그 의미를 지키지 않으면 게스트가 받은 완료 응답이 거짓이 된다.](../../../final/assets/diagrams/write-completion-boundaries/write-completion-boundaries.svg)
`WRITE` 와 `FLUSH` 가 요구하는 것은 서로 다르다.
```text label="WRITE 와 FLUSH 가 요구하는 것"
WRITE
"이 데이터를 써라"
FLUSH
"앞서 쓴 데이터를 필요한 영속성 경계까지
반영하고 완료 상태를 보장해라"
```
실제 순서 보장과 영속성의 의미는 이 두 줄보다 복잡하다. SSOT 도 그렇게 밝혀 두었고, 이 글은 두 요청의 구분까지만 말한다.
## Direct I/O 가 우회하는 대상은 페이지 캐시다
buffered I/O 에서는 QEMU 의 쓰기가 호스트 페이지 캐시를 거쳐 호스트 파일시스템과 블록 계층을 지나 SSD 로 간다. Direct I/O 에서는 호스트 페이지 캐시를 건너뛰고 호스트 파일시스템과 블록 I/O 경로로 바로 들어간다. Linux 의 `O_DIRECT` 가 여기에 쓰인다. 우회하는 대상은 페이지 캐시이고, Direct I/O 로 썼다고 영속성이 자동으로 보장되지는 않는다.
## cache=none 과 cache=writeback 이 정하는 것
QEMU 와 libvirt 로 정의한 디스크에서 대표적으로 볼 수 있는 설정은 `cache=none` 과 `cache=writeback` 이다. 두 값이 정하는 것은 QEMU 가 호스트 페이지 캐시와 쓰기 완료·플러시의 의미를 어떤 방식으로 쓰는가다. 이름만으로는 캐시가 아예 없는지, 무조건 위험한지가 정해지지 않는다.
| 설정 | 호스트 페이지 캐시를 어떻게 쓰나 |
|---|---|
| `cache=none` | 우회하는 방향으로 구성한다. 이중 캐시를 줄일 수 있다 |
| `cache=writeback` | 쓸 수 있게 구성한다. 일반 쓰기가 호스트 메모리에서 빠르게 완료될 수 있다 |
호스트 페이지 캐시를 우회한다고 해서 쓰기가 곧바로 영속 매체에 반영되지는 않는다. `cache=writeback` 으로 두어도 게스트의 `fsync()` 와 `FLUSH` 가 무시되지 않는다. 정상적인 계층이라면 게스트의 `fsync` 와 `FLUSH` 가 virtio `FLUSH` 로 이어진다. QEMU 와 백엔드에서 호스트 동기화와 플러시로 이어지고, 저장장치에서 필요한 완료를 확인한 뒤 게스트 완료로 돌아온다.
`cache=writeback` 을 위험하다는 한 낱말로 줄이지 않으려고 SSOT 는 정확한 표현을 따로 적어 두었다.
> writeback caching에서는 volatile cache가 존재할 수 있으므로, Guest의 flush/fsync semantics가 전체 backend/storage stack에서 올바르게 보존되는지가 중요하다.
그래서 보는 것은 게스트가 요구한 영속성이 게스트 파일시스템에서 게스트 블록 계층과 virtio, QEMU 와 백엔드, 호스트 스토리지를 지나 장치까지 가는 동안 그 의미가 깨지지 않는지다.
두 설정 가운데 어느 쪽을 쓸지 이 프로젝트는 정하지 않았다. SSOT 는 두 설정의 뜻과 각각의 오해를 갈라 놓기만 했고, 어느 쪽을 쓰기로 했다고도 그렇게 해서 무엇을 감수했다고도 적지 않았다. 이 호스트의 지금 값이 무엇인지부터 확인되지 않아서, 관계로 걸어 둔 질문이 그것을 먼저 답한다.
## 호스트 페이지 캐시를 우회해도 장치 안에 캐시가 있다
Direct I/O 로 호스트 페이지 캐시를 건너뛰어도 요청은 호스트 블록 계층과 NVMe 드라이버, NVMe 컨트롤러를 지나 장치 쪽 캐시를 거쳐 플래시에 닿는다. 스토리지 컨트롤러나 장치가 휘발성 쓰기 캐시를 가질 수 있다.
```text label="RAM 을 떠난 뒤에도 세 상태가 다르다"
RAM에서 나갔다
Device에 command가 전달됐다
전원이 끊겨도 살아남는 상태가 됐다
```
운영에서는 장치의 플러시와 FUA(Force Unit Access) 의미, 전원 손실 보호(power-loss protection)를 갖췄는지도 확인 대상이 된다. SSOT 는 그것이 중요할 수 있다고만 적었고, 이 호스트의 장치가 무엇을 갖췄는지는 적지 않았다.
## 거짓 완료는 성능 문제가 아니다
게스트가 `WRITE` 에 이어 `FLUSH` 를 요청했는데 데이터는 호스트 메모리에만 있고 물리 저장장치에는 이전 데이터가 들어 있다고 하자. 이때 게스트에게 `FLUSH 완료` 라고 응답하면 PostgreSQL 은 영속성이 확보됐다고 판단한다. 직후에 호스트 전원이 나가면 메모리의 데이터가 사라진다. 이것은 영속성 계약이 깨지는 정확성(correctness) 문제다.
PostgreSQL 로 보면 무엇이 어긋나는지 드러난다. 잔액을 바꾸고 커밋하는 트랜잭션을 생각해 본다.
```sql label="durability 가 걸리는 트랜잭션"
BEGIN;
UPDATE users
SET balance = 1000
WHERE id = 1;
COMMIT;
```
PostgreSQL 은 WAL(Write-Ahead Log) 등의 영속성 프로토콜을 쓰며 필요한 시점에 스토리지 동기화를 수행한다. WAL 쓰기는 게스트 페이지 캐시와 `fsync` 를 지나 게스트 파일시스템과 게스트 블록 계층, `virtio-blk`, QEMU, 호스트 스토리지를 거쳐 물리 저장장치까지 간다. 그 완료가 되돌아와야 필요한 영속성 조건이 충족됐다고 보고 `COMMIT` 을 성공으로 처리한다. 가상 머신 스토리지 계층이 플러시와 `fsync` 의 의미를 제대로 보존하지 않으면, PostgreSQL 의 영속성 가정과 실제 저장장치 동작이 어긋날 수 있다.
## 이 호스트에서 확인하지 않은 것
이 글에 이 테스트 호스트에서 잰 값은 하나도 없다. 두 가상 머신의 디스크 캐시 모드가 무엇으로 설정돼 있는지 SSOT 에 없어서, 이 호스트가 `cache=none` 구성인지 `cache=writeback` 구성인지 여기서 말하지 않는다. 거짓 완료가 이 환경에서 실제로 일어나는지도 재현하지 않았다. 장치의 플러시와 FUA 의미가 어떻게 동작하는지, 전원 손실 보호를 갖췄는지도 확인하지 않았다. `fsync()` 지연 값은 관계로 걸어 둔 질문이 게스트 지연과 호스트 스토리지 지연을 같은 시간축으로 잰 뒤에 이 글에 들어온다. 이 글은 이 구조에서 완료 응답이 무엇을 보장하는지까지 설명하고, 이 서버가 그 보장을 지키는지는 재지 않았다.
<!-- body:end -->