120 lines
10 KiB
Markdown
120 lines
10 KiB
Markdown
---
|
|
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 -->
|