13 KiB
id, kind, slug, title, topic, topicName, project, status, basisVersion, studio, assets, sourceRevision, source
| id | kind | slug | title | topic | topicName | project | status | basisVersion | studio | assets | sourceRevision | source | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 3b05c1f0-13e9-414a-97a6-2078fb4bab26 | CONCEPT | guest-block-io-path-to-virtqueue | Guest 의 write() 가 virtqueue 에 실리기까지 — VFS · Filesystem · Page Cache · Block Layer · virtio-blk | storage-virtualization | 스토리지 가상화 | virtualization | 초안 | QEMU/KVM 의 virtio-blk frontend 와 virtqueue · 게스트가 ext4 나 XFS 에 buffered I/O 로 쓰는 경우 · SSOT 가 커널과 QEMU 버전을 적지 않았다 | https://hyeonworks.com/studio/documents/3b05c1f0-13e9-414a-97a6-2078fb4bab26/edit |
|
no-commit · 밖에서 반입한 문서 한 편. 고정할 저장소 리비전이 없다 |
|
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 라는 같은 구조를 패킷 쪽에서 설명한 글이다.
본문
애플리케이션은 저장장치를 직접 다루지 않는다
가상 머신 안의 PostgreSQL 이나 Keycloak 같은 프로세스는 SSD 나 virtio-blk 를 직접 다루지 않는다. 파일에 데이터를 기록할 때 개념적으로 부르는 것은 시스템 콜 하나다.
write(fd, buffer, size);
파일과 관련된 시스템 콜은 이것 말고도 여럿이다.
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 를 실행하면 그 장치와 파티션이 나온다.
lsblk
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() 지연과 호스트 저장소 지연을 같은 시간축에서 함께 보는 관찰이 먼저 돌아야 한다.