feat: 가상화 문서들 추가
This commit is contained in:
+161
@@ -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` 의 이 위치에 게스트 메모리의 이 버퍼를 기록하라는 요청이다.
|
||||
|
||||

|
||||
|
||||
## 완료는 같은 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 -->
|
||||
+119
@@ -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 를 읽는 식이다. 이 요청들은 호스트 블록 계층의 큐에서 관리되다가 장치로 나간다.
|
||||
|
||||

|
||||
|
||||
## `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 -->
|
||||
+123
@@ -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 -->
|
||||
+167
@@ -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 와 백엔드로 내려가고, 호스트 스토리지 계층을 지나 물리 저장장치까지 그 의미가 전달되어야 한다.
|
||||
|
||||

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