Files
document-haness/docs/virtualization/tech-log-studio/storage-virtualization/concept/concept-guest-block-io-path-to-virtqueue.md
T
DongHyeonkaandClaude Opus 5 2109f726fe feat(pipeline): keycloak-session-store 25편·virtualization 59편을 S3→S5→S6 으로 돌린다
기록 84편을 계약 에이전트로 다시 썼다. 기존 71편(kss 25 · virt 46)과, 계약에만
있고 안 쓰여 있던 새 글감 13편이다. 원장 84개를 열어 단계마다 스킬 영수증과 관문
종료 코드를 적었고 verify-pipeline-run.py 가 error 0 으로 닫는다.

SSOT 결함 둘을 고쳤다.

- kss 의 `약 58일` 이 반입 중 `약 59일` 로 바뀌어 있었다. 원 증거 파일이
  「남은 일수: 88일 … 실제 갱신까지 약 58일」로 산수를 직접 적는다. D-4a 쪽
  `약 59일` 은 강제 갱신 뒤(`VALID: 89 days`)라 맞는 값이라 그대로 뒀다.
- virt §198 의 `11.6GB` 는 §178 의 원 측정 `Mem: 11648`(MiB)과 어긋나는데
  원 가이드의 표기 그대로라 고치지 않고 쓰이는 자리에 대조를 적었다.

기록의 수치 오류 셋을 고쳤다 — CASE 요약의 「게스트 셋에 8240MB」(5120+3120 은
둘이다), k3s 편이 같은 것을 여섯·일곱·여덟로 세던 것, no-docker 편의 「셋을 더
든다」(§281 의 표는 네 행이고 디스크 행이 빠져 있었다).

계약을 셋 고쳤다.

- kss 의 sourceRepository 리비전이 cdac9b8 이었는데 그 커밋에는 docs/guides/**
  28개가 아예 없다. 9465582b 로 바꾸고, 반입한 바이트가 어느 커밋과도 같지 않다는
  것을 측정값과 함께 적었다 — 반입은 커밋이 아니라 그 시점의 작업 트리에서 떠 온
  것이다(kss 297/306 · virt 12/14 가 작업 트리와 같고, 200 커밋을 거슬러 전수
  대조했을 때 가장 가까운 커밋도 28개가 어긋났다).
- virt 계약이 「2026-09-11 재배분」이라고 적는데 SSOT 는 재배분 날짜를 적지 않고
  재배분 뒤 값은 이미 2026-09-10 측정에 찍혀 있다.
- kss 후보 대장이 지나친 절 아홉에 처분을 적었다(warn 9 → 0). 새 글감은 0건이고
  넷은 앵커가 h3 슬러그의 접두가 아니라 중간 토막이라 검사기가 못 본 것이었다.

style_profile.mjs 의 결함 둘을 고쳤다 — frontmatter 가 문장으로 세어져
(실측 398자짜리 「문장」 하나) 평균 길이를 기준 안으로 밀어 올리고 있었고,
engPerSent 의 분자는 목록을 포함한 글에서, 분모는 목록을 걷어낸 글에서 세고
있었다(Question 기록에서 11.94 → 3.86).

verify-pipeline.py 전 항목 PASS · error 0 · unittest 334건 OK.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 11:01:55 +09:00

164 lines
14 KiB
Markdown

---
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 로 쓰는 경우 · 제4부가 커널과 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, 페이지 캐시, 블록 I/O 계층, virtio-blk 를 지난 뒤에야 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` 에 무엇이 붙어 있는지를 `virsh domblklist` 로 읽은 출력도 아직 없다. 게스트가 보는 것은 `/dev/vda` 에서 파티션으로, 파티션에서 파일시스템으로, 파일시스템에서 마운트 지점으로 이어지는 연결까지다. `cd /var/lib/postgresql` 로 디렉터리를 옮기는 동작은 파일시스템 세계를 지나고, `lsblk` 의 `vda` 는 블록 장치 세계를 보여 준다.
SSOT 의 실험대 배치 절이 2026-09-03 에 그려 둔 이 실험대의 게스트는 위 예시와 다르게 생겼다. `vda` 는 20G 이고 루트를 `ext4` 로 담아 거기서 부팅하는데, 그 옆의 `vdb` 는 370K 짜리 `iso9660` 이고 레이블이 `CIDATA` 라 마운트되지 않는다. `/dev/vdb` 가 붙어 있다고 게스트가 그것을 파일시스템으로 쓰는 것은 아니다.
이름 둘이 가리키는 대상도 다르다. `/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 가 적어 둔 열린 질문 가운데 첫 번째인데, `virsh domblklist` 로 그 대응을 읽은 출력이 아직 없다. 게스트 쪽 파일시스템으로 남아 있는 것은 SSOT 의 실험대 배치 절이 `vda` 의 루트를 `ext4` 로 적어 둔 2026-09-03 스냅샷 하나뿐이고, 이 글은 `ext4` 와 `XFS` 가 같은 계층을 지난다는 데까지만 말한다. 요청 수나 지연을 재려면 게스트의 `fsync()` 지연과 호스트 저장소 지연을 같은 시간축에서 함께 보는 관찰이 먼저 돌아야 한다.
<!-- body:end -->