--- 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 의 지연을 실제로 밀어 올리는가** 스토리지 경쟁은 이 글에서 구조로만 설명했다. 이 환경에서 실제로 겹치는지는 그 질문이 잰다. ## 본문 ## 게스트 디스크 파일이 호스트 블록 요청이 되는 곳 백엔드가 `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 의 스토리지 지연을 올린다는 것은 이 계층 구성에서 나오는 가능성이고 이 서버에서 관측한 값이 아니다. 이 글은 요청이 호스트에서 어떤 순서로 어느 계층을 지나는지까지 설명하고, 이 서버가 그 계층에서 실제로 막히는지는 재지 않았다.