NVMe (Non-Volatile Memory Express)
NVMe는 PCIe에 직접 연결된 고속 저장 장치를 위해 설계된 저지연·고병렬 프로토콜이며, Linux에서는 blk-mq와 결합해 높은 IOPS와 낮은 tail latency를 동시에 추구합니다. 이 문서는 NVMe 스펙 변천과 커맨드 세트, 컨트롤러/네임스페이스(Namespace)/큐 구조, PRP/SGL 데이터 경로, 인터럽트(Interrupt)·폴링(Polling)·NUMA 배치 전략, nvme/nvme-core 드라이버 내부 동작, NVMe-oF 전송 계층(TCP/RDMA/FC), ZNS와 고급 기능, 보안·가상화(Virtualization)·멀티패스·장애 복구, sysfs/nvme-cli 관측 지표, 워크로드 기반 성능 튜닝과 실전 트러블슈팅까지 운영에 필요한 내용을 end-to-end로 종합적으로 다룹니다.
핵심 요약
- SQ/CQ 큐 모델 — 호스트와 컨트롤러가 공유 메모리의 원형 버퍼(Buffer)를 통해 커맨드를 교환합니다. 최대 65,535개 I/O 큐 쌍을 지원합니다.
- Admin Queue + I/O Queue — Admin Queue(SQ0/CQ0)는 컨트롤러 관리 전용이고, I/O Queue는 데이터 읽기/쓰기를 처리합니다.
- Linux NVMe 드라이버 —
drivers/nvme/host/에 공통(core.c), PCIe(pci.c), RDMA, TCP, FC 전송 계층이 분리되어 있습니다. - blk-mq 매핑(Mapping) — NVMe I/O 큐 쌍이 blk-mq HW 큐에 1:1 매핑되며, blk-mq tag이 NVMe command_id에 직접 대응합니다.
- NVMe-oF — NVMe 프로토콜을 네트워크(RDMA/TCP/FC)로 확장하여 원격 스토리지에 로컬과 동일한 의미론으로 접근합니다.
- ZNS — 순차 쓰기 전용 존으로 네임스페이스를 분할하여 SSD 내부 GC를 최소화하고 쓰기 증폭을 줄입니다.
단계별 이해
- NVMe 개념 파악 — NVMe가 AHCI/SCSI 대비 어떤 구조적 이점을 가지는지 이해합니다.
핵심: 다수의 병렬 큐, 간결한 커맨드 셋, PCIe 직결로 인한 저지연.
- 큐 모델 이해 — SQ/CQ 원형 버퍼, Doorbell 레지스터(Register), Phase Tag의 동작을 파악합니다.
nvme list로 시스템의 NVMe 디바이스를 확인하고,nvme id-ctrl로 컨트롤러 정보를 조회합니다. - 드라이버 구조 파악 — Linux NVMe 드라이버의 계층 구조(core/pci/multipath/fabrics)를 이해합니다.
lsmod | grep nvme로 로드된 NVMe 관련 모듈을 확인합니다. - 관리와 모니터링 — nvme-cli로 SMART 정보를 확인하고, fio로 성능을 벤치마크합니다.
nvme smart-log /dev/nvme0으로 건강 상태를,iostat -x 1으로 I/O 통계를 모니터링합니다.
NVMe 개요와 스펙 변천
NVMe 등장 배경
NAND 플래시 기반 SSD가 HDD를 대체하면서, 기존 스토리지 인터페이스의 한계가 드러났습니다. AHCI(Advanced Host Controller Interface)는 HDD의 단일 회전 미디어를 위해 설계되어 단 하나의 커맨드 큐(깊이 32)만 지원합니다. SCSI는 다중 큐를 지원하지만, 복잡한 명령 변환 계층(SAM/SPC/SBC)과 프로토콜 오버헤드(Overhead)가 μs 단위 지연(Latency)이 가능한 플래시 미디어에 병목(Bottleneck)이 됩니다.
NVMe는 이 문제를 근본적으로 해결합니다:
- 최대 65,535개 I/O 큐, 큐당 65,536개 엔트리 — 멀티코어 CPU에서 락 없는 병렬 I/O
- 간결한 커맨드 셋 — 13개 Admin + 8개 I/O 커맨드 (SCSI의 수백 개 대비)
- PCIe 직결 — HBA 변환 계층 없이 MMIO로 직접 통신, μs 단위 지연
- MSI-X 인터럽트 — 큐별 독립 인터럽트 벡터로 CPU 간 경합(Contention) 제거
- 4바이트 Doorbell — 단일 MMIO 쓰기로 커맨드 제출/완료 통지
NVMe 스펙 버전 변천사
| 버전 | 연도 | 주요 변경사항 |
|---|---|---|
| 1.0 | 2011 | 최초 릴리즈. SQ/CQ 큐 모델, PRP, Admin/IO 커맨드 셋 정의 |
| 1.1 | 2012 | SGL(Scatter-Gather List) 지원, 다중 네임스페이스, 예약(Reservation) |
| 1.2 | 2014 | 네임스페이스 관리(Create/Delete/Attach), End-to-End 데이터 보호 |
| 1.3 | 2017 | Sanitize, Boot Partition, Virtualization Enhancements, Device Self-Test |
| 1.4 | 2019 | Multipath 강화(ANA), Persistent Event Log, NVM Sets, Endurance Groups |
| 2.0 | 2021 | 커맨드 셋 분리(NVM/ZNS/KV), Rotational Media 지원, I/O Command Set 독립 |
| 2.1 | 2024 | Copy Offload 개선, Endurance Group 관리 향상, 다양한 TP(Technical Proposal) 통합 |
NVMe vs SCSI vs AHCI 비교
| 항목 | NVMe | SCSI (SAS) | AHCI (SATA) |
|---|---|---|---|
| 인터페이스 | PCIe (x4 Gen4 = 8 GB/s) | SAS (12 Gb/s = 1.2 GB/s) | SATA (6 Gb/s = 600 MB/s) |
| 최대 큐 수 | 65,535 | 1 (태그 큐잉으로 확장) | 1 |
| 큐 깊이 | 65,536 | 254 (TCQ) | 32 (NCQ) |
| 커맨드 크기 | 64B (고정) | 16-32B (가변) | 32B (FIS) |
| 일반 지연 | 10-20 μs | 50-100 μs | 50-100 μs |
| CPU 효율 | 높음 (MMIO 직접) | 중간 (HBA 변환) | 낮음 (레거시 PIO/DMA) |
| 멀티코어 확장성 | 우수 (per-CPU 큐) | 제한적 | 불가 |
NVMe 하드웨어 아키텍처
M.2 NVMe 폼 팩터
NVMe SSD는 주로 M.2 M-key 폼 팩터로 제공됩니다. M-key 커넥터는 PCIe x4 레인을 지원하여 NVMe 프로토콜의 전체 대역폭(Bandwidth)을 활용할 수 있습니다.
- 일반적인 M.2 NVMe 크기: 2230 (22×30mm), 2242 (22×42mm), 2280 (22×80mm, 가장 일반적)
- M-key 전용: M-key만 있는 M.2 슬롯은 NVMe(PCIe x4)만 지원하며, SATA M.2(B+M-key)와 물리적으로 호환되지 않음
- CPU 직결 vs 칩셋 경유: CPU에 직접 연결된 PCIe 레인은 칩셋(PCH)을 경유하는 것보다 레이턴시가 낮음. 메인보드 매뉴얼에서 M.2 슬롯이 CPU 직결인지 확인 필요
- PCIe 세대 차이: Gen3 x4 = 약 3.5 GB/s, Gen4 x4 = 약 7 GB/s, Gen5 x4 = 약 14 GB/s
SQ/CQ 큐 모델
NVMe 컨트롤러는 PCIe 기반 메모리 매핑 I/O(MMIO)를 사용합니다. 호스트와 컨트롤러는 공유 메모리에 위치한 Submission Queue(SQ)와 Completion Queue(CQ)를 통해 커맨드를 교환합니다.
- Submission Queue (SQ): 호스트가 NVMe 커맨드(64바이트)를 기록하는 원형 버퍼. 호스트가 SQ Tail Doorbell을 쓰면 컨트롤러가 커맨드를 페치
- Completion Queue (CQ): 컨트롤러가 완료 엔트리(16바이트)를 기록하는 원형 버퍼. 호스트가 CQ Head Doorbell을 쓰면 엔트리 소비 완료를 알림
- Admin Queue: 컨트롤러 관리 커맨드 전용 (SQ0/CQ0). Identify, Create I/O Queue, Set Features 등
- I/O Queue Pair: 데이터 I/O 전용. 최대 65,535개 큐 쌍, 큐당 최대 65,536개 엔트리 지원
- Doorbell Register: PCIe BAR0에 매핑된 레지스터. SQ Tail / CQ Head 포인터를 업데이트하여 컨트롤러와 동기화
컨트롤러 레지스터와 BAR 레이아웃
NVMe 컨트롤러의 레지스터는 PCIe BAR0에 메모리 매핑됩니다. 핵심 레지스터:
| 오프셋(Offset) | 레지스터 | 크기 | 설명 |
|---|---|---|---|
0x00 | CAP | 8B | Controller Capabilities — 최대 큐 엔트리 수(MQES), Doorbell 간격(DSTRD), 타임아웃(TO), 지원 커맨드 셋 |
0x08 | VS | 4B | Version — NVMe 스펙 버전 (예: 0x00010400 = 1.4) |
0x0C | INTMS | 4B | Interrupt Mask Set — 특정 인터럽트 벡터 비활성화 |
0x10 | INTMC | 4B | Interrupt Mask Clear — 인터럽트 벡터 활성화 |
0x14 | CC | 4B | Controller Configuration — EN(활성화), I/O SQE/CQE 크기, 메모리 페이지(Page) 크기, 커맨드 셋 |
0x1C | CSTS | 4B | Controller Status — RDY(준비), CFS(치명적 오류), SHST(셧다운 상태) |
0x24 | AQA | 4B | Admin Queue Attributes — Admin SQ/CQ 크기 |
0x28 | ASQ | 8B | Admin Submission Queue Base Address |
0x30 | ACQ | 8B | Admin Completion Queue Base Address |
0x1000+ | SQ/CQ Doorbells | 4B each | 각 큐의 Tail(SQ)/Head(CQ) Doorbell. 간격 = 4 << DSTRD |
인터럽트 모델 (MSI-X / Polling)
NVMe는 세 가지 인터럽트 전달 방식을 지원합니다:
| 방식 | 설명 | 지연 | CPU 사용 |
|---|---|---|---|
| MSI-X | 큐별 독립 인터럽트 벡터. IRQ affinity로 CPU에 분산. 기본 모드 | ~2-5 μs | 낮음 |
| Polling (io_poll) | 인터럽트 없이 CPU가 CQ를 직접 확인. nvme.poll_queues로 활성화 | <1 μs | 높음 (busy-wait) |
| Interrupt Coalescing | Set Features 0x08로 다수 완료를 모아 단일 인터럽트. 대역폭 최적화 | 설정 의존 | 매우 낮음 |
/* Interrupt Coalescing 설정 (Set Features) */
/* Aggregation Time: 100μs 간격으로 모아서 통지 */
/* Aggregation Threshold: 최대 8개 CQE 모아서 통지 */
$ nvme set-feature /dev/nvme0 -f 0x08 -v 0x00080064
/* bits 15:8 = threshold(8), bits 7:0 = time(100 = 100*100μs) */
NVMe 커맨드 구조
SQE/CQE 구조체(Struct)
모든 NVMe 커맨드는 64바이트 고정 크기의 Submission Queue Entry(SQE)입니다. 완료 엔트리(CQE)는 16바이트입니다.
/* NVMe Submission Queue Entry (64 bytes) — 간략화 */
struct nvme_command {
__u8 opcode; /* 커맨드 opcode */
__u8 flags; /* FUSE, PSDT 등 */
__u16 command_id; /* blk-mq tag과 1:1 매핑 */
__le32 nsid; /* 네임스페이스 ID */
__le64 metadata; /* 메타데이터 포인터 */
union nvme_data_ptr dptr; /* PRP 또는 SGL */
union {
struct nvme_rw_command rw; /* Read/Write */
struct nvme_identify identify; /* Identify */
struct nvme_features features; /* Set/Get Features */
struct nvme_create_cq create_cq;
struct nvme_create_sq create_sq;
struct nvme_dsm_cmd dsm; /* Dataset Management (TRIM) */
struct nvme_write_zeroes_cmd write_zeroes;
struct nvme_zone_mgmt_send zms; /* ZNS 존 관리 */
/* ... */
};
};
/* NVMe Completion Queue Entry (16 bytes) */
struct nvme_completion {
__le32 result; /* 커맨드별 결과값 */
__le32 rsvd;
__le16 sq_head; /* SQ Head 포인터 (flow control) */
__le16 sq_id; /* 이 CQE가 속한 SQ ID */
__u16 command_id; /* 완료된 커맨드의 ID */
__le16 status; /* 상태 코드 + Phase Tag */
};
Admin SQE 와이어 포맷(Wire Format)
모든 Admin 커맨드는 동일한 64바이트 SQE 구조를 사용합니다. CDW0~CDW9는 공통 필드이고, CDW10~CDW15는 커맨드별로 의미가 달라집니다.
Admin 커맨드 세트
| Admin 커맨드 | Opcode | 설명 |
|---|---|---|
Identify | 0x06 | 컨트롤러/네임스페이스 정보 조회 |
Create I/O CQ | 0x05 | I/O Completion Queue 생성 |
Create I/O SQ | 0x01 | I/O Submission Queue 생성 |
Delete I/O SQ | 0x00 | I/O Submission Queue 삭제 |
Delete I/O CQ | 0x04 | I/O Completion Queue 삭제 |
Set Features | 0x09 | 컨트롤러 기능 설정 (큐 수, 인터럽트 합산 등) |
Get Features | 0x0A | 현재 기능 설정 값 조회 |
Get Log Page | 0x02 | SMART/Health, Error, FW Slot 정보 조회 |
Async Event Request | 0x0C | 비동기 이벤트 통지 등록 |
Format NVM | 0x80 | 네임스페이스 포맷 (LBA 크기 변경 등) |
Namespace Management | 0x0D | 네임스페이스 생성/삭제 |
Sanitize | 0x84 | 데이터 완전 삭제 (Block Erase/Crypto Erase/Overwrite) |
Abort | 0x08 | 미처리 커맨드 취소 (SQ ID + Command ID 지정) |
Firmware Image Download | 0x11 | 펌웨어 이미지를 컨트롤러로 전송 (오프셋 기반 분할 전송) |
Firmware Commit | 0x10 | 다운로드된 펌웨어를 슬롯에 커밋하고 활성화 |
Admin Queue(SQ0/CQ0)를 통한 컨트롤러 부트스트랩 시퀀스입니다. 호스트는 레지스터 설정으로 Admin Queue를 생성한 뒤, Admin 커맨드만으로 I/O Queue를 포함한 전체 데이터 경로를 구성합니다.
I/O 커맨드 세트
| I/O 커맨드 | Opcode | 설명 |
|---|---|---|
Read | 0x02 | LBA 범위 읽기 |
Write | 0x01 | LBA 범위 쓰기 |
Flush | 0x00 | 휘발성 캐시(Cache) → 비휘발성 미디어 플러시 |
Write Zeroes | 0x08 | LBA 범위를 0으로 초기화 (데이터 전송 없이) |
Dataset Management | 0x09 | TRIM/Deallocate — 사용하지 않는 LBA 알림 |
Compare | 0x05 | LBA 데이터와 호스트 데이터 비교 |
Copy | 0x19 | 컨트롤러 내부 데이터 복사 (Simple Copy, NVMe 1.4+) |
Zone Append | 0x7D | ZNS: 존의 WP(Write Pointer)에 데이터 추가 |
커맨드 실행 흐름
NVMe 커맨드의 전체 실행 사이클:
Linux NVMe 드라이버 아키텍처
드라이버 소스 구조
Linux NVMe 드라이버는 drivers/nvme/ 하위에 세 가지 전송 계층으로 나뉩니다.
| 디렉토리 | 전송 | 설명 |
|---|---|---|
drivers/nvme/host/core.c | 공통 | NVMe 프로토콜 로직, 네임스페이스 관리, 에러 처리 |
drivers/nvme/host/pci.c | PCIe | 로컬 PCIe NVMe 디바이스 드라이버 |
drivers/nvme/host/rdma.c | RDMA | NVMe-oF RDMA 전송 (InfiniBand, RoCE) |
drivers/nvme/host/tcp.c | TCP | NVMe-oF TCP 전송 |
drivers/nvme/host/fc.c | FC | NVMe-oF Fibre Channel 전송 |
drivers/nvme/host/multipath.c | 공통 | 다중 경로(multipath) 지원 |
drivers/nvme/host/hwmon.c | 공통 | 온도 센서 hwmon 인터페이스 |
drivers/nvme/host/auth.c | 공통 | DH-HMAC-CHAP 인증 (NVMe-oF) |
drivers/nvme/target/ | 타겟 | NVMe-oF 타겟 서브시스템 (스토리지 서버 측) |
핵심 구조체
/* drivers/nvme/host/pci.c — PCIe NVMe 드라이버 핵심 구조체 */
struct nvme_dev {
struct nvme_ctrl ctrl; /* 공통 컨트롤러 추상화 */
struct pci_dev *pci_dev; /* PCI 디바이스 */
void __iomem *bar; /* BAR0 MMIO 매핑 (Doorbell 포함) */
struct nvme_queue *queues; /* 큐 배열 [0]=admin, [1..n]=I/O */
unsigned int num_vecs; /* MSI-X 인터럽트 벡터 수 */
u32 db_stride; /* Doorbell 레지스터 간격 */
struct dma_pool *prp_page_pool; /* PRP 리스트 DMA 풀 */
struct dma_pool *prp_small_pool; /* 소규모 PRP DMA 풀 */
};
/* 개별 큐 (Admin 또는 I/O) */
struct nvme_queue {
struct nvme_dev *dev;
struct nvme_command *sq_cmds; /* SQ 커맨드 링 버퍼 (DMA) */
struct nvme_completion *cqes; /* CQ 엔트리 링 버퍼 (DMA) */
dma_addr_t sq_dma_addr; /* SQ의 DMA 주소 */
dma_addr_t cq_dma_addr; /* CQ의 DMA 주소 */
u32 __iomem *q_db; /* Doorbell 레지스터 포인터 */
u32 q_depth; /* 큐 깊이 */
u16 sq_tail; /* SQ Tail (호스트 관리) */
u16 cq_head; /* CQ Head (호스트 관리) */
u16 qid; /* 큐 ID (0=admin) */
u8 cq_phase; /* Phase Tag (새 CQE 감지) */
};
PCIe NVMe 초기화 흐름
nvme_probe()에서 시작하는 PCIe NVMe 디바이스 초기화 과정:
- PCI 디바이스 활성화 —
pci_enable_device_mem(), 버스(Bus) 마스터 설정, BAR0 MMIO 매핑 - CAP 레지스터 읽기 — 최대 큐 엔트리(MQES), Doorbell 간격(DSTRD), 메모리 페이지 크기 범위
- Admin Queue 생성 — DMA로 Admin SQ/CQ 할당, AQA/ASQ/ACQ 레지스터에 주소 기록
- CC.EN=1 설정 — 컨트롤러 활성화, CSTS.RDY=1 대기 (타임아웃 = CAP.TO × 500ms)
- Identify Controller — Admin Queue로 Identify 커맨드 발행, 컨트롤러 정보(MQES, MDTS, NN 등) 수집
- Set Features — Number of Queues(0x07)로 I/O 큐 수 요청, 인터럽트 합산 설정
- MSI-X 벡터 할당 —
pci_alloc_irq_vectors()로 per-queue 인터럽트 설정 - I/O Queue 생성 — Create I/O CQ → Create I/O SQ (각 큐에 MSI-X 벡터 할당)
- 네임스페이스 스캔 — Identify Namespace List로 네임스페이스 검색,
gendisk등록
/* drivers/nvme/host/pci.c — 초기화 핵심 경로 */
static int nvme_probe(struct pci_dev *pdev,
const struct pci_device_id *id)
{
nvme_dev_map(dev); /* BAR0 MMIO 매핑 */
nvme_configure_admin_queue(dev); /* Admin Queue 생성 + CC.EN=1 */
nvme_init_ctrl_finish(&dev->ctrl); /* Identify Controller */
nvme_setup_io_queues(dev); /* I/O Queue 생성 */
nvme_start_ctrl(&dev->ctrl); /* 네임스페이스 스캔 시작 */
}
컨트롤러 상태 머신
NVMe 컨트롤러는 커널 내부에서 상태 머신으로 관리됩니다:
/* include/linux/nvme.h */
enum nvme_ctrl_state {
NVME_CTRL_NEW, /* 초기 상태, 프로브 중 */
NVME_CTRL_LIVE, /* 정상 동작 중, I/O 가능 */
NVME_CTRL_RESETTING, /* 컨트롤러 리셋 진행 중 */
NVME_CTRL_CONNECTING, /* Fabrics: 재연결 진행 중 */
NVME_CTRL_DELETING, /* 디바이스 제거 진행 중 */
NVME_CTRL_DEAD, /* 복구 불가능, 모든 I/O 실패 */
};
NVMe와 blk-mq 매핑
Linux NVMe 드라이버는 blk-mq(Multi-Queue Block Layer)의 가장 직접적인 사용자입니다. NVMe 하드웨어 큐(SQ/CQ)가 blk-mq의 하드웨어 디스패치(Dispatch) 큐에 1:1로 매핑되어, CPU별 독립 I/O 경로를 구성합니다.
blk_mq_ops 콜백(Callback)
NVMe 드라이버는 blk_mq_ops 구조체를 통해 블록 계층과 인터페이스합니다:
static const struct blk_mq_ops nvme_mq_ops = {
.queue_rq = nvme_queue_rq, /* I/O 제출 */
.complete = nvme_pci_complete_rq, /* 완료 처리 */
.commit_rqs = nvme_commit_rqs, /* 배치 커밋 */
.init_hctx = nvme_init_hctx, /* HW 큐 초기화 */
.init_request = nvme_pci_init_request,
.map_queues = nvme_pci_map_queues, /* 큐 매핑 */
.timeout = nvme_timeout, /* 타임아웃 처리 */
.poll = nvme_poll, /* 폴링 I/O */
};
핵심 함수 nvme_queue_rq()의 처리 흐름:
blk_mq_rq_to_pdu()로 request에서 NVMe 커맨드 구조체 추출nvme_setup_cmd()로 블록 요청을 NVMe SQE로 변환- PRP/SGL 매핑:
nvme_map_data()로 scatter-gather 리스트 구성 - Doorbell 레지스터에 SQ Tail 기록하여 커맨드 제출
큐 타입 분리 (default / read / poll)
커널 5.12+에서 NVMe 드라이버는 I/O 특성에 따라 큐를 분리합니다:
| 큐 타입 | blk-mq 매핑 | 용도 | 인터럽트 |
|---|---|---|---|
| default | HCTX_TYPE_DEFAULT | 일반 쓰기 + 혼합 I/O | MSI-X |
| read | HCTX_TYPE_READ | 읽기 전용(Read-Only) I/O (선택적) | MSI-X |
| poll | HCTX_TYPE_POLL | io_uring 폴링 I/O | 인터럽트 없음 |
/* drivers/nvme/host/pci.c — 큐 매핑 */
static void nvme_pci_map_queues(struct blk_mq_tag_set *set)
{
struct nvme_dev *dev = set->driver_data;
/* default 큐: 모든 CPU에 매핑 */
blk_mq_pci_map_queues(&set->map[HCTX_TYPE_DEFAULT],
to_pci_dev(dev->dev), 0);
/* read 큐: 별도 큐 세트 (옵션) */
if (dev->io_queues[HCTX_TYPE_READ])
blk_mq_pci_map_queues(&set->map[HCTX_TYPE_READ],
to_pci_dev(dev->dev),
dev->io_queues[HCTX_TYPE_DEFAULT]);
/* poll 큐: 인터럽트 없이 직접 폴링 */
if (dev->io_queues[HCTX_TYPE_POLL])
blk_mq_map_queues(&set->map[HCTX_TYPE_POLL]);
}
poll 큐: io_uring에서 IORING_SETUP_IOPOLL 플래그로 서브미션하면, 인터럽트 없이 CQ를 직접 폴링하여 μs 단위 지연을 달성합니다. 고성능 워크로드에서 인터럽트 오버헤드를 제거합니다.
배치 제출과 Doorbell 최적화
NVMe 드라이버는 여러 커맨드를 모아서 한 번의 Doorbell 기록으로 제출하는 배치 최적화를 수행합니다:
/* commit_rqs: 배치된 커맨드를 한 번에 제출 */
static void nvme_commit_rqs(struct blk_mq_hw_ctx *hctx)
{
struct nvme_queue *nvmeq = hctx->driver_data;
spin_lock(&nvmeq->sq_lock);
if (nvmeq->sq_tail != nvmeq->last_sq_tail) {
nvme_write_sq_db(nvmeq, true); /* Doorbell 1회 기록 */
}
spin_unlock(&nvmeq->sq_lock);
}
Shadow Doorbell: NVMe 1.3+의 Shadow Doorbell Buffer 기능을 사용하면, 호스트가 MMIO 대신 메모리에 Tail 포인터를 기록하고 컨트롤러가 이를 읽어갑니다. 이는 MMIO 기록 비용(수백 ns)을 절약합니다:
/* Shadow Doorbell: MMIO 대신 메모리 기록 */
static void nvme_write_sq_db(struct nvme_queue *nvmeq, bool write_sq)
{
if (!nvmeq->sq_doorbell_addr) { /* Shadow Doorbell 미지원 */
writel(nvmeq->sq_tail, nvmeq->q_db);
} else {
/* Shadow Doorbell: 메모리 기록 후 필요시에만 MMIO */
WRITE_ONCE(*nvmeq->sq_doorbell_addr, nvmeq->sq_tail);
mb();
if (nvme_need_event(*nvmeq->sq_eventidx_addr, nvmeq->sq_tail,
nvmeq->last_sq_tail))
writel(nvmeq->sq_tail, nvmeq->q_db);
}
nvmeq->last_sq_tail = nvmeq->sq_tail;
}
NVMe 네임스페이스
NVMe 네임스페이스(Namespace)는 논리적 블록 주소(LBA) 공간을 분할하여 독립적인 저장 단위를 구성합니다. 하나의 NVMe 컨트롤러가 여러 네임스페이스를 관리할 수 있으며, 각 네임스페이스는 별도의 블록 디바이스(/dev/nvmeXnY)로 노출됩니다.
네임스페이스 개념과 디바이스 노드
/* include/linux/nvme.h — 네임스페이스 식별 */
struct nvme_ns {
struct list_head list; /* 컨트롤러의 ns 리스트 */
struct nvme_ctrl *ctrl;
struct request_queue *queue;
struct gendisk *disk;
struct nvme_ns_head *head; /* multipath head */
unsigned ns_id; /* NSID: 1-based */
u8 lba_shift; /* log2(LBA 크기) */
u16 ms; /* 메타데이터 크기 */
bool ext; /* 확장 LBA 모드 */
};
| 디바이스 노드 | 설명 | 예시 |
|---|---|---|
/dev/nvme0 | 컨트롤러 캐릭터 디바이스 | Admin 커맨드 passthrough |
/dev/nvme0n1 | 네임스페이스 1 블록 디바이스 | 파일시스템(Filesystem) 마운트(Mount) |
/dev/nvme0n1p1 | 네임스페이스 1의 파티션 1 | GPT/MBR 파티션 |
/dev/ng0n1 | 네임스페이스 1 캐릭터 디바이스 | I/O passthrough (커널 5.13+) |
네임스페이스 관리 (Create / Delete / Attach)
NVMe 1.2+에서 네임스페이스를 동적으로 생성·삭제·연결할 수 있습니다:
# 네임스페이스 생성 (10GB, 4K 블록)
$ nvme create-ns /dev/nvme0 --nsze=2621440 --ncap=2621440 --block-size=4096
# 컨트롤러에 네임스페이스 연결 (attach)
$ nvme attach-ns /dev/nvme0 --namespace-id=2 --controllers=0x41
# 네임스페이스 분리 (detach)
$ nvme detach-ns /dev/nvme0 --namespace-id=2 --controllers=0x41
# 네임스페이스 삭제
$ nvme delete-ns /dev/nvme0 --namespace-id=2
# 현재 네임스페이스 목록 확인
$ nvme list-ns /dev/nvme0
$ nvme id-ns /dev/nvme0n1
주의: 네임스페이스 관리 기능은 모든 NVMe 드라이브에서 지원되지 않습니다. nvme id-ctrl /dev/nvme0 | grep oacs로 OACS(Optional Admin Command Support) 비트를 확인하세요. 비트 3이 설정되어야 NS 관리를 지원합니다.
PRP과 SGL
NVMe는 호스트 메모리와 컨트롤러 간의 데이터 전송 주소를 지정하는 두 가지 메커니즘을 제공합니다:
| 특성 | PRP (Physical Region Page) | SGL (Scatter Gather List) |
|---|---|---|
| 주소 지정 | 페이지 단위 (4KB 정렬) | 임의 오프셋 + 길이 |
| 최소 단위 | 메모리 페이지 | 1바이트 |
| 연쇄 | PRP List (물리 페이지 배열) | SGL Segment (다음 SGL 포인터) |
| 지원 | NVMe 1.0+ (필수) | NVMe 1.1+ (선택, 점차 필수화) |
| NVMe-oF | 미사용 | SGL 필수 |
| 커널 사용 | PCIe 기본 | NVMe-oF, CMB 전송 |
/* PRP 구조: SQE의 dptr 필드 */
struct nvme_common_command {
...
union nvme_data_ptr {
struct {
__le64 prp1; /* 첫 번째 PRP 엔트리 */
__le64 prp2; /* 두 번째 또는 PRP List 주소 */
};
struct nvme_sgl_desc sgl; /* SGL 디스크립터 */
} dptr;
};
/* PRP 매핑 로직 */
/* 데이터 ≤ 1 페이지: prp1만 사용
* 데이터 ≤ 2 페이지: prp1 + prp2
* 데이터 > 2 페이지: prp1 + prp2(→PRP List)
*/
/* SGL 디스크립터 (16바이트) */
struct nvme_sgl_desc {
__le64 addr; /* 데이터/세그먼트 주소 */
__le32 length; /* 바이트 길이 */
__u8 rsvd[3];
__u8 type; /* SGL 타입 + 서브타입 */
};
/* SGL 타입:
* 0x00 — Data Block (데이터가 여기에)
* 0x01 — Bit Bucket (데이터 버림)
* 0x02 — Segment (다음 SGL 디스크립터 체인)
* 0x03 — Last Segment (마지막 세그먼트)
* 0x04 — Keyed Data Block (NVMe-oF RDMA용)
*/
CMB, PMR, HMB
NVMe 스펙은 호스트와 컨트롤러 간 메모리 공유 메커니즘을 정의하여 데이터 경로를 최적화합니다.
CMB (Controller Memory Buffer)
CMB는 컨트롤러의 PCIe BAR 공간에 위치한 메모리로, 호스트가 직접 접근할 수 있습니다. SQ를 CMB에 배치하면 컨트롤러가 DMA로 SQE를 가져올 필요 없이 직접 읽을 수 있어 지연이 감소합니다.
/* drivers/nvme/host/pci.c — CMB 매핑 */
static void nvme_map_cmb(struct nvme_dev *dev)
{
u64 szu, size, offset;
resource_size_t bar_size;
struct pci_dev *pdev = to_pci_dev(dev->dev);
/* CMBSZ 레지스터에서 CMB 크기 확인 */
dev->cmbsz = readl(dev->bar + NVME_REG_CMBSZ);
if (!dev->cmbsz)
return;
/* CMB가 SQ 배치를 지원하는지 확인 (SQS 비트) */
if (!(dev->cmbsz & NVME_CMBSZ_SQS))
return;
/* PCIe BAR에 CMB 매핑 */
dev->cmb = pci_iomap_wc(pdev, bar, size);
}
| CMB 용도 | 지원 비트 (CMBSZ) | 설명 |
|---|---|---|
| SQ 배치 | SQS | Submission Queue를 CMB에 할당 |
| CQ 배치 | CQS | Completion Queue를 CMB에 할당 |
| PRP List | LISTS | PRP/SGL 리스트를 CMB에 배치 |
| 읽기 데이터 | RDS | 읽기 데이터 버퍼 |
| 쓰기 데이터 | WDS | 쓰기 데이터 버퍼 |
PMR (Persistent Memory Region)
PMR은 NVMe 1.4에서 도입된 비휘발성 메모리 영역입니다. CMB와 달리 전원이 꺼져도 데이터가 유지되어, 저널링(Journaling)이나 메타데이터 캐싱에 활용할 수 있습니다:
/* PMR 초기화 (drivers/nvme/host/pci.c) */
static void nvme_map_pmr(struct nvme_dev *dev)
{
u32 pmrcap = readl(dev->bar + NVME_REG_PMRCAP);
/* PMR 크기와 속성 확인 */
dev->pmr_size = nvme_pmr_size(dev);
dev->pmr = pci_iomap_wc(pdev, pmr_bar, dev->pmr_size);
/* DAX (Direct Access) 지원: 파일시스템이 직접 접근 */
dev->pmr_dax = dax_alloc(dev->pmr, dev->pmr_size);
}
HMB (Host Memory Buffer)
HMB는 CMB가 없는 저가형 NVMe 장치에서 호스트 DRAM의 일부를 컨트롤러가 캐시처럼 사용하는 메커니즘입니다. 주로 M.2 NVMe SSD에서 DRAM 없이 성능을 유지하기 위해 사용됩니다:
/* HMB 활성화 (drivers/nvme/host/core.c) */
static int nvme_setup_host_mem(struct nvme_dev *dev)
{
u64 preferred = le32_to_cpu(id->hmpre) * 4096ULL;
u64 min = le32_to_cpu(id->hmmin) * 4096ULL;
/* 호스트 메모리 할당 (Scatter 방식) */
dev->host_mem_descs = nvme_alloc_host_mem(dev, preferred);
/* Set Features로 HMB 활성화 */
nvme_set_host_mem(dev, 1); /* enable=1 */
}
HMB 크기: 일반적으로 64MB~256MB의 호스트 메모리를 사용합니다. nvme id-ctrl /dev/nvme0 | grep -E "hmpre|hmmin"으로 선호/최소 크기를 확인할 수 있습니다. dmesg | grep hmb로 실제 할당 크기를 확인합니다.
NVMe 전원 관리(Power Management)
NVMe 장치는 다중 전원 상태(Power State)를 지원하며, 호스트와 컨트롤러가 협력하여 성능과 전력 소비 사이의 균형을 조절합니다.
전원 상태 (PS0 ~ PS5)
NVMe 컨트롤러는 최대 32개의 전원 상태를 정의할 수 있습니다. 각 상태는 최대 전력 소비, 진입/탈출 지연 시간을 명시합니다:
| 상태 | 분류 | 전력 (일반적) | 진입 지연 | 탈출 지연 | 설명 |
|---|---|---|---|---|---|
| PS0 | Operational | ~10W | — | — | 최대 성능 |
| PS1 | Operational | ~5W | ~5μs | ~10μs | 약간 낮은 성능 |
| PS2 | Operational | ~3W | ~50μs | ~50μs | 중간 성능 |
| PS3 | Non-Operational | ~50mW | ~5ms | ~10ms | 유휴 절전 |
| PS4 | Non-Operational | ~5mW | ~50ms | ~200ms | 깊은 절전 |
| PS5 | Non-Operational | ~2mW | ~500ms | ~1s | 최저 전력 |
# 전원 상태 확인
$ nvme id-ctrl /dev/nvme0 -H | grep -A 5 "ps "
$ nvme get-feature /dev/nvme0 -f 0x02 -H # Power Management 기능
# 수동 전원 상태 전환
$ nvme set-feature /dev/nvme0 -f 0x02 -v 3 # PS3으로 전환
APST (Autonomous Power State Transition)
APST는 컨트롤러가 유휴 시간을 감지하여 자동으로 낮은 전원 상태로 전환하는 메커니즘입니다. 리눅스 커널은 기본적으로 APST를 활성화합니다:
/* drivers/nvme/host/core.c — APST 설정 */
static void nvme_configure_apst(struct nvme_ctrl *ctrl)
{
struct nvme_feat_auto_pst *table;
u64 target = ctrl->ps_max_latency_us;
/* 각 전원 상태에 대해 유휴 전환 시간 설정 */
for (int state = ctrl->npss; state >= 0; state--) {
if (total_latency_us > target)
continue;
table->entries[state] = cpu_to_le64(
(idle_time_ms << 3) | 1 /* ITPT | ITPS */
);
}
/* Set Features (0x0C) 커맨드로 APST 테이블 전송 */
nvme_set_features(ctrl, NVME_FEAT_AUTO_PST, 1, table, ...);
}
# APST 설정 확인
$ nvme get-feature /dev/nvme0 -f 0x0c -H
# APST 비활성화 (디버깅/벤치마킹용)
$ echo 0 | tee /sys/class/nvme/nvme0/power/ps_max_latency_us
# 또는 커널 파라미터: nvme_core.default_ps_max_latency_us=0
Suspend / Resume 통합
시스템 Suspend 시 NVMe 컨트롤러는 올바른 종료 절차를 수행합니다:
/* 시스템 Suspend 시퀀스 */
nvme_dev_disable() /* 1. I/O 큐 중지 */
→ nvme_quiesce_io_queues() /* 2. 진행 중인 I/O 대기 */
→ nvme_wait_freeze() /* 3. 큐 동결 */
→ nvme_disable_ctrl() /* 4. CC.EN=0 → 컨트롤러 비활성화 */
/* 시스템 Resume 시퀀스 */
nvme_reset_ctrl() /* 1. 컨트롤러 리셋 */
→ nvme_pci_enable() /* 2. PCIe 재활성화 */
→ nvme_pci_configure_admin_queue() /* 3. Admin 큐 복원 */
→ nvme_init_ctrl_finish() /* 4. Identify 재실행 */
→ nvme_create_io_queues() /* 5. I/O 큐 재생성 */
Simple Suspend: 커널 5.14+에서 nvme_core.noacpi=1 대신 NVMe 자체의 Simple Suspend를 사용합니다. 이는 ACPI StorageD3Enable 속성을 확인하여, 지원되는 경우 완전한 전원 차단 없이 빠른 Suspend/Resume을 수행합니다.
NVMe 열 관리(Thermal Management)
NVMe 컨트롤러는 내장 온도 센서와 열 관리 메커니즘을 통해 과열로 인한 데이터 손실이나 하드웨어 손상을 방지합니다.
온도 임계값 (WCTEMP, CCTEMP, TMT1/TMT2)
| 임계값 | 설명 | 동작 |
|---|---|---|
| WCTEMP | Warning Composite Temperature | 비동기 이벤트(AER) 알림 발생 |
| CCTEMP | Critical Composite Temperature | 강제 스로틀링 또는 셧다운 |
| TMT1 | Thermal Management Temp 1 | 가벼운 스로틀링 시작 |
| TMT2 | Thermal Management Temp 2 | 강한 스로틀링 시작 |
# 현재 온도 및 임계값 확인
$ nvme smart-log /dev/nvme0 | grep -i temp
temperature : 42°C
warning_temp_time : 0
critical_comp_time : 0
$ nvme id-ctrl /dev/nvme0 | grep -i temp
wctemp : 358 # Warning: 85°C (켈빈 → 섭씨: 358-273=85)
cctemp : 368 # Critical: 95°C
HCTMA (Host Controlled Thermal Management)
NVMe 1.3+에서 호스트가 TMT1/TMT2 임계값을 설정하여 컨트롤러의 스로틀링 시점을 제어할 수 있습니다:
/* drivers/nvme/host/hwmon.c — 열 관리 통합 */
static int nvme_hwmon_write(struct device *dev, u32 attr,
int channel, long val)
{
/* 온도 임계값 설정 (밀리켈빈 → 켈빈) */
temp = millikelvin_to_kelvin(val);
/* Set Features: Thermal Management */
nvme_set_features(ctrl, NVME_FEAT_TEMP_THRESH,
temp | (threshold_type << 20),
NULL, ...);
}
스로틀링 모니터링
리눅스 커널은 NVMe 온도를 hwmon 서브시스템에 통합하여 표준 도구로 모니터링할 수 있습니다:
# hwmon 인터페이스로 온도 모니터링
$ cat /sys/class/nvme/nvme0/hwmon*/temp1_input # 현재 온도 (밀리섭씨)
42000
$ cat /sys/class/nvme/nvme0/hwmon*/temp1_max # WCTEMP
85000
$ cat /sys/class/nvme/nvme0/hwmon*/temp1_crit # CCTEMP
95000
# 센서별 온도 (NVMe 1.4+: 복합 + 개별 센서 최대 8개)
$ sensors nvme-pci-*
nvme-pci-0100
Adapter: PCI adapter
Composite: +42.0°C (high = +85.0°C, crit = +95.0°C)
Sensor 1: +42.0°C (low = -5.0°C, high = +80.0°C)
Sensor 2: +38.0°C (low = -5.0°C, high = +80.0°C)
# SMART 로그의 열 관리 통계
$ nvme smart-log /dev/nvme0 | grep -E "thm_temp|thermal"
thm_temp1_trans_count : 5 # TMT1 전환 횟수
thm_temp2_trans_count : 0 # TMT2 전환 횟수
thm_temp1_total_time : 120 # TMT1 총 시간 (초)
thm_temp2_total_time : 0
NVMe Multipath
NVMe Multipath는 하나의 네임스페이스에 여러 경로(path)를 제공하여 고가용성과 부하 분산(Load Balancing)을 구현합니다. 주로 NVMe-oF 환경이나 듀얼 포트 NVMe SSD에서 활용됩니다.
경로 상태와 I/O 정책
커널의 네이티브 NVMe Multipath는 nvme_ns_head 구조체를 통해 여러 경로의 nvme_ns를 하나의 가상 디바이스(/dev/nvmeXcYnZ)로 통합합니다:
/* NVMe Multipath I/O 정책 */
enum nvme_io_policy {
NVME_IOPOLICY_NUMA, /* NUMA 노드 기반 (기본값) */
NVME_IOPOLICY_RR, /* 라운드 로빈 */
NVME_IOPOLICY_QD, /* Queue Depth 기반 (커널 6.3+) */
};
# Multipath 활성화 확인
$ cat /sys/module/nvme_core/parameters/multipath
Y
# I/O 정책 변경
$ echo numa > /sys/module/nvme_core/parameters/iopolicy
$ echo round-robin > /sys/module/nvme_core/parameters/iopolicy
$ echo queue-depth > /sys/module/nvme_core/parameters/iopolicy
# 경로 상태 확인
$ nvme list-subsys /dev/nvme0n1
nvme-subsys0 - NQN=nqn.2024-01.com.example:nvme
\
+- nvme0 tcp traddr=192.168.1.10,trsvcid=4420 live optimized
+- nvme1 tcp traddr=192.168.1.11,trsvcid=4420 live non-optimized
ANA (Asymmetric Namespace Access)
ANA는 NVMe 1.4에서 도입된 비대칭 접근 메커니즘으로, SCSI ALUA의 NVMe 대응물입니다. 각 컨트롤러-네임스페이스 쌍에 대해 접근 상태를 정의합니다:
| ANA 상태 | 설명 | I/O 허용 |
|---|---|---|
| Optimized | 최적 경로 (가장 낮은 지연) | 읽기/쓰기 |
| Non-optimized | 동작하지만 비최적 경로 | 읽기/쓰기 |
| Inaccessible | 접근 불가 (장애 대기) | 불가 |
| Persistent Loss | 영구적 경로 손실 | 불가 |
| Change | 상태 전환 중 | 재시도 |
/* drivers/nvme/host/multipath.c — ANA 경로 선택 */
static struct nvme_ns *nvme_find_path(struct nvme_ns_head *head)
{
/* NUMA 정책: 같은 NUMA 노드의 optimized 경로 우선 */
test_and_clear_bit(NVME_NSHEAD_DISK_LIVE, &head->flags);
list_for_each_entry_rcu(ns, &head->list, siblings) {
if (nvme_path_is_optimized(ns)) {
if (ns->ctrl->numa_node == numa_node)
return ns; /* 최적 경로 */
fallback = ns;
}
}
return fallback;
}
dm-multipath vs 네이티브: 리눅스는 NVMe 전용 네이티브 멀티패스(nvme_core.multipath=Y)와 범용 dm-multipath를 모두 지원합니다. 네이티브 방식이 오버헤드가 낮고 ANA를 직접 지원하므로 권장됩니다.
NVMe over Fabrics (NVMe-oF)
NVMe-oF는 NVMe 프로토콜을 네트워크 패브릭(RDMA, TCP, FC)을 통해 확장하여, 원격 NVMe 장치를 로컬처럼 사용할 수 있게 합니다. 전통적인 iSCSI/FC보다 낮은 지연과 높은 대역폭을 제공합니다.
NVMe-oF 아키텍처 개요
타겟 구성 (nvmet)
리눅스 커널의 nvmet 모듈은 ConfigFS를 통해 NVMe-oF 타겟을 구성합니다:
# 커널 모듈 로드
$ modprobe nvmet
$ modprobe nvmet-tcp # TCP 전송용
# 서브시스템 생성
$ mkdir -p /sys/kernel/config/nvmet/subsystems/nqn.2024-01.com.example:nvme-target
$ cd /sys/kernel/config/nvmet/subsystems/nqn.2024-01.com.example:nvme-target
$ echo 1 > attr_allow_any_host
# 네임스페이스 추가 (기존 블록 디바이스 노출)
$ mkdir namespaces/1
$ echo /dev/nvme0n1 > namespaces/1/device_path
$ echo 1 > namespaces/1/enable
# TCP 포트 생성 및 바인딩
$ mkdir -p /sys/kernel/config/nvmet/ports/1
$ echo ipv4 > /sys/kernel/config/nvmet/ports/1/addr_adrfam
$ echo 0.0.0.0 > /sys/kernel/config/nvmet/ports/1/addr_traddr
$ echo 4420 > /sys/kernel/config/nvmet/ports/1/addr_trsvcid
$ echo tcp > /sys/kernel/config/nvmet/ports/1/addr_trtype
# 서브시스템을 포트에 연결
$ ln -s /sys/kernel/config/nvmet/subsystems/nqn.2024-01.com.example:nvme-target \
/sys/kernel/config/nvmet/ports/1/subsystems/
호스트 연결
# 호스트 모듈 로드
$ modprobe nvme-tcp
# Discovery Controller를 통한 서브시스템 탐색
$ nvme discover -t tcp -a 192.168.1.100 -s 8009
# 직접 연결
$ nvme connect -t tcp -n nqn.2024-01.com.example:nvme-target \
-a 192.168.1.100 -s 4420
# 연결 확인
$ nvme list-subsys
$ lsblk
# 연결 해제
$ nvme disconnect -n nqn.2024-01.com.example:nvme-target
Discovery Controller와 Subsystem 모델
NVMe-oF는 Discovery Controller를 통해 사용 가능한 서브시스템을 동적으로 탐색합니다:
- Discovery Service NQN:
nqn.2014-08.org.nvmexpress.discovery(표준 NQN) - Discovery Log Page: 사용 가능한 서브시스템의 전송 주소, 포트, NQN 목록 반환
- Persistent Discovery Controller: 커널 6.1+에서 연결 상태를 유지하며 변경 AEN 수신
- Referral: Discovery Controller가 다른 Discovery Controller를 참조하여 계층적 탐색
# Discovery Controller 설정 (타겟 측)
$ mkdir -p /sys/kernel/config/nvmet/ports/2
$ echo tcp > /sys/kernel/config/nvmet/ports/2/addr_trtype
$ echo 0.0.0.0 > /sys/kernel/config/nvmet/ports/2/addr_traddr
$ echo 8009 > /sys/kernel/config/nvmet/ports/2/addr_trsvcid
$ echo ipv4 > /sys/kernel/config/nvmet/ports/2/addr_adrfam
# 호스트에서 자동 연결 (udev + systemd)
$ nvme connect-all -t tcp -a 192.168.1.100 -s 8009
TCP PDU 구조와 TLS 지원
NVMe/TCP는 자체 PDU(Protocol Data Unit) 형식을 정의하여 TCP 스트림 위에서 NVMe 커맨드/데이터를 캡슐화(Encapsulation)합니다:
| PDU 타입 | 방향 | 설명 |
|---|---|---|
| ICReq | Host → Target | 초기화 연결 요청 |
| ICResp | Target → Host | 초기화 연결 응답 |
| CapsuleCmd | Host → Target | NVMe 커맨드 + 인라인 데이터 |
| CapsuleResp | Target → Host | NVMe 완료 응답 |
| H2CData | Host → Target | 호스트→타겟 데이터 전송 |
| C2HData | Target → Host | 타겟→호스트 데이터 전송 |
| R2T | Target → Host | 데이터 전송 요청 |
/* include/linux/nvme-tcp.h — TCP PDU 헤더 */
struct nvme_tcp_hdr {
__u8 type; /* PDU 타입 */
__u8 flags; /* HDGSTF, DDGSTF */
__u8 hlen; /* PDU 헤더 길이 */
__u8 pdo; /* 데이터 오프셋 (정렬) */
__le32 plen; /* 전체 PDU 길이 */
};
TLS 1.3 지원 (커널 6.7+): NVMe/TCP에 커널 TLS를 적용하여 전송 암호화(Encryption)를 제공합니다. nvme connect 시 --tls 옵션으로 활성화합니다.
RDMA 전송
NVMe/RDMA는 커널 바이패스를 통해 최저 지연을 달성합니다. RDMA 전송의 특징:
- Zero-copy: RDMA WRITE/READ로 호스트와 타겟 간 직접 데이터 전송
- SGL 필수: PRP 대신 SGL Keyed Data Block 사용
- QP per Queue: NVMe SQ/CQ 쌍이 RDMA Queue Pair에 매핑
- Memory Registration: 데이터 버퍼를 RDMA에 등록하여 원격 DMA 허용
# RDMA 전송으로 NVMe-oF 연결
$ modprobe nvme-rdma
# 타겟 설정
$ echo rdma > /sys/kernel/config/nvmet/ports/1/addr_trtype
$ echo 192.168.1.100 > /sys/kernel/config/nvmet/ports/1/addr_traddr
$ echo 4420 > /sys/kernel/config/nvmet/ports/1/addr_trsvcid
# 호스트 연결
$ nvme connect -t rdma -n nqn.2024-01.com.example:nvme-target \
-a 192.168.1.100 -s 4420
ZNS (Zoned Namespaces)
ZNS는 NVMe 2.0에서 정의된 존(Zone) 기반 스토리지 인터페이스입니다. SSD 내부의 순차 쓰기 특성을 호스트에 노출하여, FTL(Flash Translation Layer) 복잡성을 줄이고 쓰기 증폭(WAF)을 최소화합니다.
존 상태 모델
| 상태 | 설명 | 허용 명령 |
|---|---|---|
| Empty | 빈 존, 쓰기 포인터 = 시작 | Write, Zone Append |
| Implicitly Open | 쓰기 시 자동 오픈 | Write, Zone Append, Close |
| Explicitly Open | 호스트가 명시적 오픈 | Write, Zone Append, Close |
| Closed | 닫힘, 쓰기 포인터 유지 | Open, Write |
| Full | 존이 가득 참 | Reset, Finish |
| Read Only | 읽기만 가능 | Read |
| Offline | 접근 불가 | 없음 |
# ZNS 디바이스 확인
$ cat /sys/block/nvme0n1/queue/zoned
host-managed
# 존 정보 조회
$ nvme zns report-zones /dev/nvme0n1 --descs=16
SLBA: 0x000000 WP: 0x000000 Cap: 0x040000 State: EMPTY Type: SWR
SLBA: 0x040000 WP: 0x041000 Cap: 0x040000 State: IMP_OPEN Type: SWR
SLBA: 0x080000 WP: 0x0c0000 Cap: 0x040000 State: FULL Type: SWR
# 존 관리
$ nvme zns open-zone /dev/nvme0n1 -s 0 # 존 열기
$ nvme zns close-zone /dev/nvme0n1 -s 0 # 존 닫기
$ nvme zns reset-zone /dev/nvme0n1 -s 0x40000 # 존 리셋
$ nvme zns finish-zone /dev/nvme0n1 -a 1 # 모든 존 완료
블록 계층 연동
리눅스 블록 계층은 ZNS 디바이스를 위한 전용 인터페이스를 제공합니다:
- Zone Append: 호스트가 존의 시작 LBA만 지정하면 컨트롤러가 실제 쓰기 위치를 결정하여 반환합니다. 멀티 큐 환경에서 쓰기 포인터 경합을 제거합니다.
- REQ_OP_ZONE_APPEND: blk-mq에서 Zone Append를 위한 전용 operation 코드
- Zone Write Plug: 커널 6.10+에서 존별 쓰기를 직렬화(Serialization)하여 순서를 보장하는 블록 계층 기능
/* ZNS 지원 파일시스템 */
/* Btrfs: 커널 5.12+에서 ZNS 직접 지원 */
$ mkfs.btrfs -m single -d single /dev/nvme0n1
$ mount -o zoned /dev/nvme0n1 /mnt/zns
/* F2FS: ZNS 네이티브 지원 */
$ mkfs.f2fs -m /dev/nvme0n1
/* dm-zoned: 일반 파일시스템 호환 계층 */
$ dmzadm --format /dev/nvme0n1 /dev/sda # ZNS + 일반 디스크 조합
NVMe 고급 커맨드 세트
NVMe 스펙은 기본 Block I/O 외에 다양한 고급 커맨드 세트를 정의하여, 스토리지 활용의 유연성을 극대화합니다.
Key Value (KV) 커맨드 세트
NVMe KV 커맨드 세트는 전통적인 블록 주소(LBA) 대신 키-값(Key-Value) 쌍으로 데이터를 저장·검색합니다. 데이터베이스나 오브젝트 스토리지에서 FTL과 파일시스템 오버헤드를 제거합니다:
| 커맨드 | Opcode | 설명 |
|---|---|---|
| Store | 0x01 | 키에 값 저장 |
| Retrieve | 0x02 | 키로 값 조회 |
| Delete | 0x10 | 키-값 쌍 삭제 |
| Exist | 0x14 | 키 존재 여부 확인 |
| List | 0x06 | 키 목록 나열 |
/* KV SQE 구조 (간략화) */
struct nvme_kv_command {
__u8 opcode; /* KV 명령 코드 */
__u8 flags;
__u16 command_id;
__le32 nsid;
__le32 key_length; /* 키 길이 (1~16 바이트) */
__le32 value_size; /* 값 크기 */
__u8 key[16]; /* 인라인 키 */
union nvme_data_ptr dptr; /* 값 데이터 포인터 */
};
Computational Storage
NVMe Computational Storage는 스토리지 장치 내에서 연산을 수행하는 TP 4091 스펙입니다. 호스트-디바이스 간 데이터 이동을 줄여 대규모 데이터 처리 효율을 높입니다:
- Compute Program: 장치에 업로드하여 실행하는 프로그램
- Compute Memory: 장치 내 연산용 메모리 공간
- 사용 사례: 데이터 압축/해제, 암호화, 패턴 검색, 간단한 집계
Copy Offload (Simple Copy)
NVMe 1.4의 Simple Copy 커맨드는 호스트를 거치지 않고 컨트롤러 내부에서 데이터를 복사합니다:
/* Simple Copy 커맨드 (Opcode 0x19) */
struct nvme_copy_command {
__u8 opcode; /* 0x19 */
__u8 flags;
__u16 command_id;
__le32 nsid;
__le64 sdlba; /* 대상 시작 LBA */
__u8 nr; /* 소스 범위 수 - 1 */
/* 소스 범위 디스크립터 리스트 (PRP/SGL) */
};
/* 소스 범위 디스크립터 */
struct nvme_copy_range {
__le64 slba; /* 소스 시작 LBA */
__le16 nlb; /* 소스 블록 수 - 1 */
};
# Simple Copy 사용 (nvme-cli)
$ nvme copy /dev/nvme0n1 --sdlba=0x1000 --blocks=255 --slbs=0x0
# 커널에서의 사용: REQ_OP_COPY_OFFLOAD (개발 중)
Streams Directive
Streams Directive(NVMe 1.3)는 호스트가 데이터의 수명(lifetime) 특성을 컨트롤러에 알려주어, FTL이 데이터를 효율적으로 배치하도록 합니다:
- 목적: 유사한 수명의 데이터를 같은 erase block에 배치하여 가비지 컬렉션 효율 향상
- Stream ID: 1~65535의 식별자를 각 I/O에 태깅
- 커널 지원:
write_hintioctl이나 fadvise로 파일별 스트림 ID 지정
/* 스트림 힌트 (include/uapi/linux/fcntl.h) */
enum rw_hint {
WRITE_LIFE_NOT_SET = 0, /* 힌트 없음 */
WRITE_LIFE_NONE = 1, /* 수명 힌트 없음 */
WRITE_LIFE_SHORT = 2, /* 짧은 수명 (hot data) */
WRITE_LIFE_MEDIUM = 3, /* 중간 수명 */
WRITE_LIFE_LONG = 4, /* 긴 수명 (warm data) */
WRITE_LIFE_EXTREME = 5, /* 매우 긴 수명 (cold data) */
};
NVMe 보안
NVMe는 데이터 보호와 접근 제어(Access Control)를 위한 다양한 보안 메커니즘을 제공합니다.
TCG Opal SED
TCG(Trusted Computing Group) Opal은 자체 암호화 드라이브(SED)의 표준입니다. NVMe 디바이스에서 하드웨어 기반 전체 디스크 암호화를 제공합니다:
- Locking Range: 디스크의 특정 LBA 범위를 독립적으로 잠금(Lock)/해제
- Pre-Boot Authentication: 부팅 전 인증으로 OS 로드 전 디스크 잠금 해제
- Crypto Erase: 암호화 키만 삭제하여 즉시 데이터 무효화(Invalidation)
# sedutil-cli로 TCG Opal 관리
$ sedutil-cli --scan # Opal 지원 디바이스 검색
$ sedutil-cli --initialSetup <password> /dev/nvme0n1
$ sedutil-cli --enableLockingRange 0 <password> /dev/nvme0n1
$ sedutil-cli --setLockingRange 0 LK <password> /dev/nvme0n1 # 잠금
$ sedutil-cli --setLockingRange 0 RW <password> /dev/nvme0n1 # 해제
Sanitize
NVMe Sanitize 커맨드는 디바이스의 모든 사용자 데이터를 안전하게 삭제합니다. Format NVM보다 더 철저한 데이터 소거를 보장합니다:
| Sanitize 동작 | 방법 | 속도 | 보안 수준 |
|---|---|---|---|
| Block Erase | Flash 블록 단위 삭제 | 빠름 (수 초~분) | 중간 |
| Crypto Erase | 암호화 키 교체 | 매우 빠름 (즉시) | 높음 (SED 필요) |
| Overwrite | 패턴으로 전체 덮어쓰기 | 매우 느림 (수 시간) | 매우 높음 |
# Sanitize 실행
$ nvme sanitize /dev/nvme0 --sanact=2 # Block Erase
$ nvme sanitize /dev/nvme0 --sanact=4 # Crypto Erase
# Sanitize 진행 상태 확인
$ nvme sanitize-log /dev/nvme0
인증 (DH-HMAC-CHAP)
NVMe 2.0에서 도입된 DH-HMAC-CHAP 인증은 호스트와 컨트롤러 간 상호 인증을 제공합니다. 주로 NVMe-oF 환경에서 사용됩니다:
/* drivers/nvme/host/auth.c — DH-HMAC-CHAP */
/* 인증 흐름:
* 1. 호스트 → 컨트롤러: DH 공개값 + 챌린지
* 2. 컨트롤러 → 호스트: DH 공개값 + 응답 + 챌린지
* 3. 호스트: 응답 검증 + 컨트롤러 챌린지에 응답
* 4. 상호 인증 완료
*/
# 호스트 인증 키 설정
$ nvme gen-dhchap-key -n nqn.2024-01.com.example:nvme-target
DHHC-1:00:YWJjZGVmZw==:
# 타겟에 호스트 키 등록
$ echo "DHHC-1:00:YWJjZGVmZw==:" > \
/sys/kernel/config/nvmet/hosts/nqn.host/dhchap_key
# 양방향 인증 (상호 인증)
$ echo "DHHC-1:00:eHl6MTIz:" > \
/sys/kernel/config/nvmet/hosts/nqn.host/dhchap_ctrl_key
Secure Erase
NVMe Format NVM 커맨드의 Secure Erase 옵션:
# User Data Erase (사용자 데이터만)
$ nvme format /dev/nvme0n1 --ses=1
# Cryptographic Erase (암호화 키 폐기)
$ nvme format /dev/nvme0n1 --ses=2
데이터 파괴: Sanitize와 Format의 Secure Erase는 되돌릴 수 없습니다. 중요 데이터가 없음을 반드시 확인한 후 실행하세요.
NVMe 가상화
NVMe 디바이스를 가상 환경에서 활용하는 방법은 에뮬레이션, 패스스루, SR-IOV의 세 가지가 있습니다.
SR-IOV
NVMe 1.1+에서 SR-IOV(Single Root I/O Virtualization)를 지원하여, 하나의 물리 NVMe 컨트롤러를 여러 가상 함수(VF)로 분할합니다:
# SR-IOV VF 생성
$ echo 4 > /sys/bus/pci/devices/0000:03:00.0/sriov_numvfs
# VF 확인
$ lspci | grep NVMe
03:00.0 Non-Volatile memory controller: ... # PF
03:00.1 Non-Volatile memory controller: ... # VF 1
03:00.2 Non-Volatile memory controller: ... # VF 2
03:00.3 Non-Volatile memory controller: ... # VF 3
03:00.4 Non-Volatile memory controller: ... # VF 4
# 각 VF에 네임스페이스 할당 (Secondary Controller)
$ nvme virt-mgmt /dev/nvme0 --act=1 --cntlid=0x1 --rt=0 --nr=2
VFIO Passthrough
VFIO를 사용하면 NVMe 디바이스를 VM에 직접 할당(passthrough)하여 네이티브에 가까운 성능을 제공합니다:
# VFIO에 NVMe 디바이스 바인딩
$ echo "0000:03:00.0" > /sys/bus/pci/devices/0000:03:00.0/driver/unbind
$ echo "vfio-pci" > /sys/bus/pci/devices/0000:03:00.0/driver_override
$ echo "0000:03:00.0" > /sys/bus/pci/drivers/vfio-pci/bind
# QEMU에서 NVMe passthrough
$ qemu-system-x86_64 \
-device vfio-pci,host=0000:03:00.0 \
...
QEMU 에뮬레이션
QEMU는 소프트웨어로 NVMe 컨트롤러를 에뮬레이션하여, 물리 NVMe 디바이스 없이도 NVMe 기능을 테스트할 수 있습니다:
# QEMU NVMe 에뮬레이션 (기본)
$ qemu-system-x86_64 \
-drive file=nvme.img,format=qcow2,if=none,id=nvm \
-device nvme,serial=deadbeef,drive=nvm
# QEMU NVMe 에뮬레이션 (고급: ZNS + CMB)
$ qemu-system-x86_64 \
-drive file=zns.img,format=raw,if=none,id=zns-drv \
-device nvme,serial=zns001,drive=zns-drv,\
zoned=true,zone_size=64M,zone_capacity=62M,\
max_open=16,max_active=32,\
cmb_size_mb=128
# Multipath 테스트 (다중 컨트롤러, 공유 네임스페이스)
$ qemu-system-x86_64 \
-device nvme-subsys,id=subsys0,nqn=nqn.test \
-device nvme,serial=ctrl0,subsys=subsys0 \
-device nvme,serial=ctrl1,subsys=subsys0 \
-device nvme-ns,drive=nvm,nsid=1,shared=on,subsys=subsys0
컨테이너(Container) 활용
NVMe 디바이스를 컨테이너에서 사용하는 방법:
# Docker: NVMe 디바이스를 컨테이너에 마운트
$ docker run --device=/dev/nvme0n1 -it ubuntu
# Kubernetes: NVMe를 PersistentVolume으로 사용
# (hostPath 또는 CSI 드라이버 통해)
apiVersion: v1
kind: PersistentVolume
metadata:
name: nvme-pv
spec:
capacity:
storage: 100Gi
accessModes: [ReadWriteOnce]
local:
path: /dev/nvme0n1p1
nodeAffinity: ...
NVMe 에러 처리와 복구
NVMe 드라이버는 다단계 에러 처리 메커니즘으로 하드웨어 오류부터 일시적 장애까지 포괄적으로 대응합니다.
상태 코드 체계
NVMe CQE의 상태 필드는 3개 유형으로 분류됩니다:
| 상태 코드 타입 (SCT) | 값 | 설명 | 예시 |
|---|---|---|---|
| Generic Command | 0x0 | 일반적인 커맨드 오류 | Invalid Opcode, Invalid Field |
| Command Specific | 0x1 | 커맨드별 특수 오류 | Conflicting Attributes |
| Media and Data Integrity | 0x2 | 미디어/데이터 무결성(Integrity) 오류 | Unrecovered Read, Write Fault |
| Path Related | 0x3 | 경로 관련 오류 (NVMe-oF) | Host Path Error |
| Vendor Specific | 0x7 | 벤더 정의 오류 | 벤더별 상이 |
/* include/linux/nvme.h — 상태 코드 매크로 */
#define NVME_SC_SUCCESS 0x0
#define NVME_SC_INVALID_OPCODE 0x1
#define NVME_SC_INVALID_FIELD 0x2
#define NVME_SC_NS_NOT_READY 0x82
#define NVME_SC_WRITE_FAULT 0x280
#define NVME_SC_READ_ERROR 0x281
#define NVME_SC_DNR 0x4000 /* Do Not Retry 비트 */
다단계 복구 메커니즘
리눅스 NVMe 드라이버의 에러 복구 레벨:
- 커맨드 재시도: DNR 비트가 없으면 blk-mq가 자동 재시도 (최대
nvme_core.max_retries, 기본 5회) - I/O 타임아웃:
nvme_timeout()에서 30초(기본) 후 처리- 먼저 Abort 커맨드로 개별 커맨드 취소 시도
- Abort 실패 시 컨트롤러 리셋
- 컨트롤러 리셋:
nvme_reset_ctrl()로 전체 컨트롤러 초기화- 모든 I/O 큐 삭제 후 재생성
- 진행 중인 I/O는 실패 처리 후 상위 계층이 재시도
- 컨트롤러 제거: 복구 불가능 시 컨트롤러를 DEAD 상태로 전환, 모든 I/O에 에러 반환
/* drivers/nvme/host/core.c — 타임아웃 처리 */
static enum blk_eh_timer_return nvme_timeout(
struct request *req)
{
/* 1단계: 커맨드 Abort 시도 */
if (nvme_abort_req(req) == 0)
return BLK_EH_RESET_TIMER;
/* 2단계: 컨트롤러 리셋 */
nvme_reset_ctrl(ctrl);
return BLK_EH_DONE;
}
AER (Asynchronous Event Request)
AER은 컨트롤러가 비동기적으로 호스트에 알리는 이벤트 메커니즘입니다. 드라이버 초기화 시 Admin SQ에 AER 커맨드를 미리 제출하고, 이벤트 발생 시 CQ에 완료가 반환됩니다:
| AER 타입 | 설명 | 예시 |
|---|---|---|
| Error Status | 오류 상태 변경 | Persistent Internal Error |
| SMART / Health | 건강 상태 변경 | 온도 임계값 초과, Reliability 저하 |
| Notice | 운영 관련 알림 | 네임스페이스 변경, 펌웨어(Firmware) 업데이트 |
| I/O Command Set Specific | I/O 커맨드 세트 이벤트 | Zone 상태 변경 (ZNS) |
| Vendor Specific | 벤더 정의 이벤트 | 벤더별 상이 |
/* drivers/nvme/host/core.c — AER 처리 */
static void nvme_async_event_work(struct work_struct *work)
{
struct nvme_ctrl *ctrl = container_of(work, ...);
switch (aer_type) {
case NVME_AER_NOTICE_NS_CHANGED:
nvme_scan_work(&ctrl->scan_work); /* NS 재스캔 */
break;
case NVME_AER_NOTICE_ANA:
nvme_mpath_update(ctrl); /* ANA 상태 업데이트 */
break;
case NVME_AER_NOTICE_FW_ACT_STARTING:
nvme_fw_act_work(ctrl); /* 펌웨어 활성화 대기 */
break;
}
/* AER 커맨드 재제출 (다음 이벤트 대기) */
nvme_submit_async_event(ctrl);
}
Character Device와 ioctl
NVMe 디바이스는 블록 디바이스 외에 캐릭터 디바이스 인터페이스를 제공하여, 애플리케이션이 NVMe 커맨드를 직접 전송할 수 있습니다.
/dev/nvmeX passthrough ioctl
컨트롤러 캐릭터 디바이스(/dev/nvme0)를 통해 Admin 커맨드를 직접 전송합니다:
/* include/uapi/linux/nvme_ioctl.h */
struct nvme_passthru_cmd {
__u8 opcode; /* NVMe 명령 코드 */
__u8 flags;
__u16 rsvd1;
__u32 nsid;
__u32 cdw2, cdw3;
__u64 metadata;
__u64 addr; /* 데이터 버퍼 주소 */
__u32 metadata_len;
__u32 data_len; /* 데이터 크기 */
__u32 cdw10, cdw11, cdw12, cdw13, cdw14, cdw15;
__u32 timeout_ms;
__u32 result; /* CQE DW0 결과 */
};
/* ioctl 번호 */
#define NVME_IOCTL_ADMIN_CMD _IOWR('N', 0x41, struct nvme_passthru_cmd)
#define NVME_IOCTL_IO_CMD _IOWR('N', 0x43, struct nvme_passthru_cmd)
#define NVME_IOCTL_ADMIN64_CMD _IOWR('N', 0x47, struct nvme_passthru_cmd64)
/* 사용 예: Identify Controller (C 코드) */
struct nvme_passthru_cmd cmd = {
.opcode = 0x06, /* Identify */
.nsid = 0,
.addr = (__u64)(uintptr_t)buf,
.data_len = 4096,
.cdw10 = 1, /* CNS=1: Controller */
};
ioctl(fd, NVME_IOCTL_ADMIN_CMD, &cmd);
io_uring NVMe passthrough (io_uring_cmd)
커널 5.19+에서 io_uring을 통한 NVMe passthrough가 가능합니다. ioctl 기반보다 높은 IOPS를 달성합니다:
/* io_uring NVMe passthrough (사용자 공간) */
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
sqe->opcode = IORING_OP_URING_CMD;
sqe->fd = nvme_fd; /* /dev/ng0n1 fd */
sqe->cmd_op = NVME_URING_CMD_IO;
/* nvme_uring_cmd 구조체를 sqe->cmd에 인코딩 */
struct nvme_uring_cmd *cmd = (void *)&sqe->cmd;
cmd->opcode = 0x02; /* Read */
cmd->addr = (__u64)buf;
cmd->data_len = 4096;
cmd->cdw10 = slba & 0xFFFFFFFF;
cmd->cdw11 = slba >> 32;
cmd->cdw12 = nlb - 1;
성능: io_uring NVMe passthrough는 블록 계층을 완전히 우회하여, fio 기준 단일 코어에서 170만+ IOPS를 달성할 수 있습니다. /dev/ng0n1 제네릭 캐릭터 디바이스를 사용하세요.
/dev/ngXnY 제네릭 캐릭터 디바이스
커널 5.13+에서 도입된 /dev/ngXnY는 네임스페이스별 캐릭터 디바이스로, I/O 커맨드를 직접 전송할 수 있습니다:
/dev/nvme0: 컨트롤러 레벨 (Admin 커맨드만)/dev/nvme0n1: 네임스페이스 블록 디바이스 (파일시스템/블록 I/O)/dev/ng0n1: 네임스페이스 캐릭터 디바이스 (I/O passthrough, io_uring_cmd)
NVMe sysfs 인터페이스
리눅스 NVMe 드라이버는 sysfs를 통해 컨트롤러, 서브시스템, 네임스페이스 정보를 노출합니다.
/sys/class/nvme/
컨트롤러별 속성:
| 경로 | 설명 | 예시 값 |
|---|---|---|
nvme0/model | 모델명 | Samsung SSD 990 PRO |
nvme0/serial | 시리얼 번호 | S6Z2NF0TA12345 |
nvme0/firmware_rev | 펌웨어 버전 | 4B2QJXM7 |
nvme0/transport | 전송 타입 | pcie / tcp / rdma / fc |
nvme0/state | 컨트롤러 상태 | live / resetting / dead |
nvme0/address | 전송 주소 | 0000:03:00.0 (PCIe) |
nvme0/numa_node | NUMA 노드 | 0 |
nvme0/queue_count | I/O 큐 수 | 9 (8 I/O + 1 Admin) |
nvme0/sqsize | SQ 엔트리 수 | 1023 |
nvme0/cntrltype | 컨트롤러 타입 | io / admin / discovery |
# 컨트롤러 정보 한눈에 보기
$ for f in /sys/class/nvme/nvme0/{model,serial,firmware_rev,transport,state}; do
echo "$(basename $f): $(cat $f)"
done
# 전원 관리 속성
$ cat /sys/class/nvme/nvme0/power/ps_max_latency_us # APST 최대 지연
$ cat /sys/class/nvme/nvme0/power/nuse # 사용 중인 블록 수
/sys/class/nvme-subsystem/
NVMe 서브시스템은 여러 컨트롤러와 네임스페이스를 그룹화합니다:
# 서브시스템 정보
$ cat /sys/class/nvme-subsystem/nvme-subsys0/nqn
nqn.2024-01.com.samsung:990PRO:S6Z2NF0TA12345
$ cat /sys/class/nvme-subsystem/nvme-subsys0/model
Samsung SSD 990 PRO 2TB
# 서브시스템 내 컨트롤러 목록
$ ls /sys/class/nvme-subsystem/nvme-subsys0/nvme*
nvme0 nvme1 # Multipath 환경
# I/O 정책 (Multipath)
$ cat /sys/class/nvme-subsystem/nvme-subsys0/iopolicy
numa
/sys/block/nvmeXnY/queue/
블록 디바이스 큐 속성으로 I/O 동작을 튜닝합니다:
| 속성 | 설명 | 기본값 |
|---|---|---|
scheduler | I/O 스케줄러(Scheduler) | none (NVMe 기본) |
nr_requests | 큐당 최대 요청 수 | 1023 |
read_ahead_kb | 읽기 선행 크기 | 128 |
max_sectors_kb | 최대 I/O 크기 | 512~2048 |
io_poll | I/O 폴링 활성화 | 0 |
io_poll_delay | 폴링 전 대기 시간(Latency) | -1 (자동) |
nomerges | I/O 병합 비활성화 | 0 |
wbt_lat_usec | 쓰기 백프레셔 지연 | 2000 |
zone_append_max_bytes | Zone Append 최대 크기 (ZNS) | 디바이스 의존 |
NVMe 내구성과 수명 관리
NAND 플래시 기반 NVMe SSD는 유한한 쓰기 수명을 가집니다. SMART 로그와 모니터링을 통해 수명을 추적하고 관리합니다.
SMART / Health 로그 해석
# SMART 로그 전체 출력
$ nvme smart-log /dev/nvme0
Smart Log for NVME device:nvme0 namespace-id:ffffffff
critical_warning : 0 # 0=정상
temperature : 42°C
available_spare : 100% # 여유 블록 비율
available_spare_threshold : 10% # 경고 임계값
percentage_used : 1% # 수명 소모율 (100%=TBW 도달)
data_units_read : 15,234,567
data_units_written : 8,456,789
host_read_commands : 245,678,901
host_write_commands : 123,456,789
controller_busy_time : 1,234
power_cycles : 156
power_on_hours : 8,760
unsafe_shutdowns : 3
media_errors : 0 # 미디어 오류 누적
num_err_log_entries : 0
| 지표 | 의미 | 주의 수준 |
|---|---|---|
| percentage_used | TBW 대비 사용률 | >90%: 교체 계획 수립 |
| available_spare | 여유 블록 비율 | <threshold: 경고 |
| media_errors | 복구 불가 미디어 에러 | >0: 즉시 조사 |
| critical_warning | 비트 플래그 | 비트별 의미 확인 |
| unsafe_shutdowns | 비정상 종료 횟수 | 잦으면 전원 환경 점검 |
WAF와 Over-Provisioning
- WAF (Write Amplification Factor): 실제 NAND 쓰기량 / 호스트 쓰기량. 이상적으로 1.0에 가깝지만, 가비지 컬렉션으로 인해 1.5~3.0이 일반적입니다.
- Over-Provisioning: 사용자에게 노출하지 않는 예비 NAND 영역. GC 효율과 내구성을 높입니다.
# WAF 추정 (SMART 로그 기반)
# 벤더별 NAND 쓰기량 로그 위치가 다름
$ nvme intel smart-log-add /dev/nvme0 # Intel
$ nvme samsung vs-smart-add-log /dev/nvme0 # Samsung
# Over-Provisioning: 네임스페이스 축소로 OP 확보
$ nvme delete-ns /dev/nvme0 --namespace-id=1
$ nvme create-ns /dev/nvme0 \
--nsze=1953525168 \ # 전체의 90%만 사용
--ncap=1953525168 \
--block-size=512
$ nvme attach-ns /dev/nvme0 --namespace-id=1 --controllers=0x41
네임스페이스 수준 모니터링
# 네임스페이스별 사용량 (NVMe 1.4+)
$ nvme id-ns /dev/nvme0n1 | grep -E "nsze|ncap|nuse"
nsze : 1953525168 # 네임스페이스 크기 (블록 수)
ncap : 1953525168 # 네임스페이스 용량
nuse : 976762584 # 실제 사용 중인 블록 (Thin Provisioning)
# Endurance Group 로그 (NVMe 1.4+)
$ nvme endurance-log /dev/nvme0 --group-id=1
nvme-cli 관리 도구
nvme-cli는 리눅스 공식 NVMe 관리 유틸리티로, NVMe 스펙의 거의 모든 Admin/I/O 커맨드를 사용자 공간(User Space)에서 실행할 수 있습니다.
정보 조회
# 설치
$ apt install nvme-cli # Debian/Ubuntu
$ dnf install nvme-cli # Fedora/RHEL
# NVMe 디바이스 목록
$ nvme list
Node SN Model Namespace Usage Format FW Rev
/dev/nvme0n1 S6Z2NF0TA12345 Samsung SSD 990 PRO 1 953.9 GB / 2 TB 512B+0B 4B2QJXM7
# 컨트롤러 식별
$ nvme id-ctrl /dev/nvme0 -H # Human-readable
$ nvme id-ctrl /dev/nvme0 -o json # JSON 출력
# 네임스페이스 식별
$ nvme id-ns /dev/nvme0n1 -H
# 서브시스템 목록 (Multipath 포함)
$ nvme list-subsys
# 에러 로그
$ nvme error-log /dev/nvme0 --log-entries=16
# 기능 조회
$ nvme get-feature /dev/nvme0 -f 0x01 -H # Arbitration
$ nvme get-feature /dev/nvme0 -f 0x07 -H # Number of Queues
$ nvme get-feature /dev/nvme0 -f 0x09 -H # Interrupt Coalescing
관리 명령
# 펌웨어 관리
$ nvme fw-download /dev/nvme0 --fw=firmware.bin
$ nvme fw-commit /dev/nvme0 --slot=1 --action=1 # 다음 리셋 시 활성화
$ nvme fw-commit /dev/nvme0 --slot=1 --action=3 # 즉시 활성화
# 포맷 (데이터 파괴!)
$ nvme format /dev/nvme0n1 --lbaf=0 --ses=0
# Self-Test
$ nvme device-self-test /dev/nvme0 --stc=1 # Short test
$ nvme device-self-test /dev/nvme0 --stc=2 # Extended test
$ nvme self-test-log /dev/nvme0 # 결과 확인
# 로그 페이지 조회 (Raw)
$ nvme get-log /dev/nvme0 --log-id=2 --log-len=512 # SMART
$ nvme get-log /dev/nvme0 --log-id=5 --log-len=512 # Commands Supported
플러그인과 자동화
nvme-cli는 벤더별 플러그인으로 확장됩니다:
# 벤더 플러그인 목록
$ nvme help | grep -A1 "vendor"
intel Intel vendor specific extensions
samsung Samsung vendor specific extensions
wdc Western Digital vendor specific extensions
micron Micron vendor specific extensions
# Samsung 벤더 로그
$ nvme samsung vs-smart-add-log /dev/nvme0
# JSON 출력 + jq 활용
$ nvme smart-log /dev/nvme0 -o json | jq '.temperature'
42
# 자동 모니터링 스크립트
$ nvme smart-log /dev/nvme0 -o json | jq '{
temp: .temperature,
spare: .avail_spare,
used: .percent_used,
media_errors: .media_errors,
unsafe_shutdowns: .unsafe_shutdowns
}'
NVMe 성능 튜닝
NVMe의 잠재 성능을 최대로 끌어내기 위한 커널/시스템 수준 튜닝 방법을 다룹니다.
커널 파라미터
| 파라미터 | 기본값 | 설명 | 튜닝 지침 |
|---|---|---|---|
nvme_core.io_timeout | 30 | I/O 타임아웃 (초) | NVMe-oF: 60~120초로 증가 |
nvme_core.max_retries | 5 | 최대 재시도 횟수 | 성능 우선: 2~3으로 감소 |
nvme_core.multipath | Y | 네이티브 멀티패스 | 불필요 시 N으로 비활성화 |
nvme_core.default_ps_max_latency_us | 100000 | APST 최대 지연 (μs) | 벤치마킹: 0 (APST 비활성화) |
nvme.poll_queues | 0 | 폴링 큐 수 | 저지연: CPU 수의 1/4~1/2 |
nvme.write_queues | 0 | 쓰기 전용 큐 수 | 혼합 워크로드: 2~4 |
# I/O 스케줄러: NVMe는 기본 none (직접 디스패치)
$ cat /sys/block/nvme0n1/queue/scheduler
[none] mq-deadline kyber bfq
# IRQ affinity 최적화
$ cat /proc/interrupts | grep nvme
45: ... IR-PCI-MSI 524288-edge nvme0q0 # Admin
46: ... IR-PCI-MSI 524289-edge nvme0q1 # CPU 0
47: ... IR-PCI-MSI 524290-edge nvme0q2 # CPU 1
$ echo 1 > /proc/irq/46/smp_affinity # CPU 0에 고정
$ echo 2 > /proc/irq/47/smp_affinity # CPU 1에 고정
# NUMA-aware I/O: NVMe가 연결된 NUMA 노드에서 I/O 수행
$ cat /sys/class/nvme/nvme0/numa_node
0
$ numactl --cpunodebind=0 --membind=0 fio ...
fio 벤치마크
# 순차 읽기 (대역폭 측정)
$ fio --name=seq-read --filename=/dev/nvme0n1 \
--rw=read --bs=128k --iodepth=64 --numjobs=4 \
--ioengine=io_uring --direct=1 --runtime=30 --group_reporting
# 랜덤 4K 읽기 (IOPS 측정)
$ fio --name=rand-read --filename=/dev/nvme0n1 \
--rw=randread --bs=4k --iodepth=256 --numjobs=4 \
--ioengine=io_uring --direct=1 --runtime=30 --group_reporting
# io_uring 폴링 모드 (최저 지연)
$ fio --name=poll-read --filename=/dev/nvme0n1 \
--rw=randread --bs=4k --iodepth=1 --numjobs=1 \
--ioengine=io_uring --direct=1 --hipri=1 --runtime=30
# io_uring NVMe passthrough (최대 IOPS)
$ fio --name=pt-read --filename=/dev/ng0n1 \
--rw=randread --bs=4k --iodepth=128 --numjobs=4 \
--ioengine=io_uring_cmd --cmd_type=nvme --direct=1 \
--fixedbufs=1 --runtime=30 --group_reporting
perf / BPF 프로파일링(Profiling)
# NVMe I/O 지연 분포 (BCC/bpftrace)
$ biolatency-bpfcc -D nvme0n1 10
usec : count distribution
0 -> 1 : 0 | |
2 -> 3 : 0 | |
4 -> 7 : 12 |* |
8 -> 15 : 8234 |********************|
16 -> 31 : 4521 |*********** |
32 -> 63 : 234 |* |
# NVMe 이벤트 트레이싱 (ftrace)
$ echo 1 > /sys/kernel/debug/tracing/events/nvme/enable
$ cat /sys/kernel/debug/tracing/trace_pipe
# perf로 NVMe 드라이버 오버헤드 분석
$ perf record -g -a -e block:block_rq_issue -e block:block_rq_complete \
-- fio ... --runtime=10
$ perf report --sort=symbol
# bpftrace: NVMe 커맨드별 지연
$ bpftrace -e '
kprobe:nvme_queue_rq { @start[tid] = nsecs; }
kretprobe:nvme_queue_rq /@start[tid]/ {
@latency = hist(nsecs - @start[tid]);
delete(@start[tid]);
}'
커널 빌드 설정 (Kconfig)
NVMe 관련 커널 설정 옵션:
| CONFIG 옵션 | 설명 | 기본값 | 의존성 |
|---|---|---|---|
CONFIG_BLK_DEV_NVME | NVMe PCIe 드라이버 | m | PCI, BLOCK |
CONFIG_NVME_MULTIPATH | 네이티브 멀티패스 | y | BLK_DEV_NVME |
CONFIG_NVME_HWMON | hwmon 온도 모니터링 | y | BLK_DEV_NVME, HWMON |
CONFIG_NVME_FABRICS | NVMe-oF 공통 코드 | m | BLK_DEV_NVME |
CONFIG_NVME_RDMA | NVMe/RDMA 호스트 | m | NVME_FABRICS, INFINIBAND |
CONFIG_NVME_TCP | NVMe/TCP 호스트 | m | NVME_FABRICS, INET |
CONFIG_NVME_FC | NVMe/FC 호스트 | m | NVME_FABRICS |
CONFIG_NVME_AUTH | DH-HMAC-CHAP 인증 | y | NVME_FABRICS, CRYPTO |
CONFIG_NVME_TARGET | NVMe-oF 타겟 코어 | m | BLOCK, CONFIGFS |
CONFIG_NVME_TARGET_TCP | NVMe-oF TCP 타겟 | m | NVME_TARGET, INET |
CONFIG_NVME_TARGET_RDMA | NVMe-oF RDMA 타겟 | m | NVME_TARGET, INFINIBAND |
CONFIG_NVME_TARGET_FC | NVMe-oF FC 타겟 | m | NVME_TARGET |
CONFIG_NVME_TARGET_LOOP | NVMe-oF 루프백 타겟 | m | NVME_TARGET |
CONFIG_NVME_TARGET_AUTH | 타겟 인증 | y | NVME_TARGET, CRYPTO |
NVMe 드라이버 소스 트리 구조:
일반적인 실수와 올바른 패턴
NVMe를 처음 다루거나 최적화할 때 자주 발생하는 실수와 올바른 해결 방법을 정리합니다.
❌ 실수 1: Queue Depth를 무조건 최대로 설정
/* ❌ 잘못된 예: 모든 워크로드에 최대 큐 깊이 사용 */
# /sys/block/nvme0n1/queue/nr_requests를 최대값으로 설정
echo 1024 > /sys/block/nvme0n1/queue/nr_requests
# 문제: 랜덤 I/O에서는 지연 시간만 증가하고 처리량은 정체
/* ✅ 올바른 예: 워크로드 특성에 따라 큐 깊이 조정 */
# 순차 I/O (대용량 스트리밍): 큰 큐 깊이
echo 256 > /sys/block/nvme0n1/queue/nr_requests
# 랜덤 I/O (데이터베이스): 작은 큐 깊이로 지연 최소화
echo 32 > /sys/block/nvme0n1/queue/nr_requests
# fio로 최적값 찾기
fio --name=test --filename=/dev/nvme0n1 --ioengine=libaio \
--iodepth=1,4,8,16,32,64 --rw=randread --bs=4k --numjobs=1
❌ 실수 2: 잘못된 인터럽트 모드 선택
/* ❌ 잘못된 예: 고성능 워크로드에서 인터럽트 모드 사용 */
# polling 비활성화 상태에서 초저지연 요구
echo 0 > /sys/module/nvme/parameters/poll_queues
# 문제: 인터럽트 오버헤드로 지연 시간 증가 (수십 마이크로초)
/* ✅ 올바른 예: 워크로드에 따라 polling vs interrupt 선택 */
# 초저지연 요구 (금융 거래, 실시간 분석): polling 모드
echo 4 > /sys/module/nvme/parameters/poll_queues
# CPU 일부를 polling에 전담 할당
# 일반 워크로드: interrupt 모드 (CPU 효율적)
echo 0 > /sys/module/nvme/parameters/poll_queues
# 혼합 모드: 일부 큐는 polling, 나머지는 interrupt
modprobe nvme poll_queues=2 nr_io_queues=16
# → 16개 I/O 큐 중 2개는 polling, 14개는 interrupt
❌ 실수 3: PRP 페이지 정렬 무시
/* ❌ 잘못된 예: 정렬되지 않은 버퍼로 DMA */
void *buf = malloc(8192); /* 4KB 정렬 보장 안 됨 */
struct nvme_command cmd;
cmd.rw.prp1 = virt_to_phys(buf); /* ❌ 정렬 위반 시 에러 */
/* ✅ 올바른 예: 4KB 정렬된 버퍼 할당 */
void *buf;
posix_memalign(&buf, 4096, 8192); /* 4KB 정렬 */
/* 또는 커널에서 */
void *buf = kmalloc(8192, GFP_KERNEL); /* 자동 정렬 보장 */
/* SGL 사용 시 불연속 메모리 가능 */
struct nvme_sgl_desc sgl[2];
sgl[0].addr = virt_to_phys(buf1);
sgl[1].addr = virt_to_phys(buf2); /* 불연속 OK */
❌ 실수 4: Multipath 설정 없이 이중화 기대
/* ❌ 잘못된 예: 두 컨트롤러에 연결했지만 자동 장애조치 안 됨 */
# NVMe-oF로 두 경로 연결
nvme connect -t tcp -a 192.168.1.10 -s 4420 -n nqn.subsys
nvme connect -t tcp -a 192.168.1.11 -s 4420 -n nqn.subsys
# → /dev/nvme0n1, /dev/nvme1n1 두 개 생성되지만 독립적
# 문제: nvme0 경로 장애 시 수동으로 nvme1 사용해야 함
/* ✅ 올바른 예: Native Multipath 활성화 */
# 커널 파라미터로 multipath 활성화
modprobe nvme-core multipath=Y
# 두 경로 연결 시 자동으로 단일 네임스페이스로 통합
nvme connect -t tcp -a 192.168.1.10 -s 4420 -n nqn.subsys
nvme connect -t tcp -a 192.168.1.11 -s 4420 -n nqn.subsys
# → /dev/nvme0n1 단일 디바이스로 통합, 자동 장애조치
# 상태 확인
nvme list-subsys
# nvme-subsys0 - NQN=nqn.subsys
# \
# +- nvme0 tcp 192.168.1.10:4420 live
# +- nvme1 tcp 192.168.1.11:4420 live
❌ 실수 5: 네임스페이스 공유 시 동기화 누락
/* ❌ 잘못된 예: 여러 호스트에서 동일 네임스페이스 동시 쓰기 */
# 호스트 A와 B가 동일한 NVMe-oF 네임스페이스에 파일시스템 마운트
# 호스트 A:
mkfs.ext4 /dev/nvme0n1
mount /dev/nvme0n1 /mnt
# 호스트 B:
mount /dev/nvme0n1 /mnt
# ❌ 문제: 파일시스템 메타데이터 충돌, 데이터 손상
/* ✅ 올바른 예: 클러스터 파일시스템 사용 또는 네임스페이스 분리 */
# 방법 1: 클러스터 파일시스템 사용 (GFS2, OCFS2)
mkfs.gfs2 -p lock_dlm -t mycluster:myfs /dev/nvme0n1
# 호스트 A, B 모두 마운트 가능 (DLM으로 동기화)
# 방법 2: 네임스페이스 분리
# 타겟에서 네임스페이스 2개 생성
nvmetcli
> cd subsystems/nqn.subsys/namespaces
> create 1 # 호스트 A 전용
> create 2 # 호스트 B 전용
# 방법 3: Read-Only 공유
mount -o ro /dev/nvme0n1 /mnt # 읽기 전용으로 안전 공유
베스트 프랙티스 체크리스트
| 항목 | 권장 사항 | 확인 방법 |
|---|---|---|
| Queue Depth | 워크로드 특성에 맞게 조정 (랜덤: 8-32, 순차: 64-256) | cat /sys/block/nvme0n1/queue/nr_requests |
| 인터럽트 모드 | 저지연 필요 시 polling, 일반 워크로드는 interrupt | cat /sys/module/nvme/parameters/poll_queues |
| CPU Affinity | I/O 큐를 특정 CPU에 고정하여 캐시 효율 향상 | cat /proc/irq/*/smp_affinity_list |
| Multipath | 이중화 환경에서는 반드시 Native Multipath 활성화 | nvme list-subsys |
| 메모리 정렬 | DMA 버퍼는 4KB 정렬 필수 | posix_memalign() 사용 |
| 스케줄러 | NVMe는 'none' 스케줄러 권장 (blk-mq 자체 처리) | cat /sys/block/nvme0n1/queue/scheduler |
| Write Cache | 데이터 보호 중요 시 FUA 사용, 성능 우선 시 활성화 | nvme get-feature -f 0x06 /dev/nvme0 |
| APST | 전력 절약 필요 시 활성화, 지연 민감 시 비활성화 | cat /sys/module/nvme_core/parameters/default_ps_max_latency_us |
성능 최적화 실전 가이드
NVMe 디바이스의 최대 성능을 끌어내기 위한 실전 튜닝 가이드입니다.
I/O 스케줄러 최적화
# NVMe는 하드웨어 큐가 충분하므로 스케줄러 오버헤드 제거
echo none > /sys/block/nvme0n1/queue/scheduler
# 전체 NVMe 디바이스에 일괄 적용
for dev in /sys/block/nvme*; do
echo none > $dev/queue/scheduler
done
# 성능 비교 (mq-deadline vs none)
fio --name=test --filename=/dev/nvme0n1 --ioengine=libaio \
--iodepth=32 --rw=randread --bs=4k --numjobs=4 --runtime=30
# none 스케줄러: ~500K IOPS
# mq-deadline: ~450K IOPS (병합/정렬 오버헤드)
CPU Affinity 및 IRQ 밸런싱
# NVMe 인터럽트를 특정 CPU에 고정하여 캐시 효율 향상
# 1. NVMe 디바이스의 IRQ 번호 찾기
grep nvme /proc/interrupts | awk '{print $1}' | sed 's/://'
# 2. IRQ를 CPU에 할당 (예: CPU 0-3에 분산)
for irq in $(cat /proc/interrupts | grep nvme0q | awk '{print $1}' | sed 's/://'); do
cpu=$((irq % 4))
echo $cpu > /proc/irq/$irq/smp_affinity_list
done
# 3. NUMA 인식 최적화 (로컬 메모리 액세스)
# NVMe가 NUMA 노드 0에 있다면
numactl --cpunodebind=0 --membind=0 fio --name=test ...
Polling 모드 성능 극대화
# Polling 큐 전용 CPU 할당으로 지연 최소화
# 1. Polling 큐 설정 (4개 큐를 polling으로)
modprobe nvme poll_queues=4
# 2. Polling 큐 전용 CPU 격리 (부팅 파라미터)
# /etc/default/grub에 추가:
# GRUB_CMDLINE_LINUX="isolcpus=4,5,6,7"
# 3. io_uring로 polling 모드 I/O
fio --name=poll_test --filename=/dev/nvme0n1 \
--ioengine=io_uring --hipri --iodepth=8 \
--rw=randread --bs=4k --runtime=30
# 지연 시간 비교:
# Interrupt 모드: ~30μs
# Polling 모드: ~10μs (3배 개선)
Write Cache 및 FUA 튜닝
# Write Cache 상태 확인 (Feature ID 0x06)
nvme get-feature -f 0x06 /dev/nvme0
# 결과: 0x00000001 → Volatile Write Cache 활성화
# Write Cache 활성화 (성능 우선)
nvme set-feature -f 0x06 -v 1 /dev/nvme0
# 장점: 쓰기 처리량 2배 향상
# 단점: 전원 장애 시 데이터 손실 위험
# FUA (Force Unit Access) 사용 (안정성 우선)
fio --name=fua_test --filename=/dev/nvme0n1 \
--ioengine=libaio --iodepth=32 --rw=randwrite \
--bs=4k --direct=1 --end_fsync=1
# FUA 플래그로 캐시 우회, 안전하지만 느림
네임스페이스 최적화
# 워크로드별로 네임스페이스 분리하여 간섭 최소화
# 1. 고성능 네임스페이스 생성 (낮은 지연, 높은 IOPS)
nvme create-ns /dev/nvme0 --nsze=209715200 --ncap=209715200 \
--flbas=0 --dps=0 --nmic=0
nvme attach-ns /dev/nvme0 --namespace-id=1 --controllers=0
# 2. 대용량 네임스페이스 생성 (순차 I/O 최적화)
nvme create-ns /dev/nvme0 --nsze=419430400 --ncap=419430400 \
--flbas=0 --dps=0 --nmic=0
nvme attach-ns /dev/nvme0 --namespace-id=2 --controllers=0
# 3. ZNS 네임스페이스 (로그/시계열 데이터)
# ZNS는 순차 쓰기 전용으로 쓰기 증폭 최소화
성능 측정 도구
| 도구 | 용도 | 사용 예시 |
|---|---|---|
fio |
벤치마크 (IOPS, 처리량(Throughput), 지연) | fio --name=test --ioengine=libaio --iodepth=32 --rw=randread --bs=4k |
iostat |
실시간(Real-time) I/O 통계 | iostat -xz 1 nvme0n1 |
blktrace |
I/O 요청 추적 | blktrace -d /dev/nvme0n1 -o trace |
perf |
CPU 사이클, 캐시 미스 분석 | perf stat -e cycles,cache-misses fio ... |
nvme smart-log |
디바이스 건강 상태 | nvme smart-log /dev/nvme0 |
bpftrace |
커널 내부 지연 추적 | bpftrace -e 'kprobe:nvme_queue_rq { @start[tid] = nsecs; }' |
- I/O 스케줄러를 'none'으로 설정 (즉각 적용, 큰 효과)
- 워크로드에 맞는 Queue Depth 조정 (fio로 측정)
- 초저지연 필요 시 Polling 모드 활성화
- CPU Affinity 설정으로 캐시 효율 향상
- NUMA 인식 메모리 할당으로 원격 메모리 접근 최소화
실전 케이스 스터디
실제 프로덕션 환경에서 NVMe를 활용한 사례와 최적화 경험을 공유합니다.
케이스 1: 고성능 데이터베이스 스토리지
문제 상황: PostgreSQL 데이터베이스에서 초당 100만 건의 트랜잭션(Transaction) 처리 필요. 기존 SATA SSD는 IOPS 한계로 병목 발생.
해결 방법:
# 1. NVMe SSD로 마이그레이션 (Intel Optane P5800X)
# 2. I/O 스케줄러 비활성화
echo none > /sys/block/nvme0n1/queue/scheduler
# 3. Queue Depth 최적화 (랜덤 읽기 중심)
echo 16 > /sys/block/nvme0n1/queue/nr_requests
# 4. PostgreSQL WAL을 별도 NVMe 네임스페이스로 분리
# /dev/nvme0n1 → 데이터 파일 (랜덤 I/O)
# /dev/nvme0n2 → WAL (순차 쓰기)
echo 128 > /sys/block/nvme0n2/queue/nr_requests # WAL은 큰 큐
# 5. Direct I/O 활성화 (캐시 중복 제거)
# postgresql.conf:
wal_sync_method = fdatasync
fsync = on
결과:
- 트랜잭션 처리량: 50만 TPS → 120만 TPS (2.4배 향상)
- 평균 지연 시간: 2ms → 0.5ms (4배 개선)
- CPU 사용률: 80% → 60% (I/O 대기 감소)
케이스 2: 분산 스토리지 시스템 (Ceph)
문제 상황: Ceph 클러스터의 OSD 노드에서 복제 쓰기 지연이 누적되어 클라이언트 성능 저하.
해결 방법:
# 1. BlueStore 백엔드로 NVMe 직접 사용 (FileStore 대체)
ceph-volume lvm create --bluestore --data /dev/nvme0n1
# 2. WAL/DB를 고속 NVMe에 배치
ceph-volume lvm create --bluestore \
--data /dev/nvme0n1 \
--block.wal /dev/nvme1n1p1 \
--block.db /dev/nvme1n1p2
# 3. NVMe Multipath로 이중화 (두 NVMe-oF 경로)
modprobe nvme-core multipath=Y
nvme connect -t rdma -a 192.168.1.10 -s 4420 -n nqn.ceph.osd1
nvme connect -t rdma -a 192.168.1.11 -s 4420 -n nqn.ceph.osd1
# 4. CPU Affinity로 OSD 프로세스와 NVMe IRQ 동일 NUMA 노드 배치
numactl --cpunodebind=0 --membind=0 ceph-osd -i 0
결과:
- 쓰기 지연: 15ms → 3ms (5배 개선, 복제 오버헤드 감소)
- 클러스터 전체 처리량: 10GB/s → 35GB/s
- 복구 속도: 100MB/s → 500MB/s (네트워크 병목 해소)
케이스 3: ZNS를 활용한 로그 저장 시스템
문제 상황: 대규모 로그 수집 시스템에서 높은 쓰기 증폭으로 SSD 수명 단축 및 성능 저하.
해결 방법:
# 1. ZNS 네임스페이스 생성 (순차 쓰기 전용)
nvme zns id-ns /dev/nvme0n1
# Zone Size: 1GB, Total Zones: 256
# 2. F2FS 파일시스템을 ZNS 모드로 마운트
mkfs.f2fs -m /dev/nvme0n1
mount -t f2fs -o mode=lfs /dev/nvme0n1 /mnt/logs
# 3. 애플리케이션에서 Zone Append 사용
#include <linux/blkzoned.h>
int append_log(int fd, void *data, size_t len) {
struct blk_zone_range range = { .sector = 0, .nr_sectors = 2097152 };
ioctl(fd, BLKRESETZONE, &range); /* Zone 재설정 */
/* Zone Append 커맨드로 순차 쓰기 */
return write(fd, data, len);
}
결과:
- 쓰기 증폭: 3.5배 → 1.1배 (GC 거의 발생 안 함)
- SSD 수명: 3년 → 10년 예상 (DWPD 0.3 → 1.0)
- 쓰기 처리량: 2GB/s → 3.5GB/s (GC 오버헤드 제거)
케이스 4: NVMe-oF 기반 클라우드 스토리지
문제 상황: 가상 머신에 로컬 NVMe 성능을 제공하면서도 스토리지 풀 공유 필요.
해결 방법:
# 타겟 서버: NVMe-oF TCP 타겟 설정
modprobe nvmet nvmet-tcp
# 1. 서브시스템 생성
mkdir /sys/kernel/config/nvmet/subsystems/nqn.storage.pool1
echo 1 > /sys/kernel/config/nvmet/subsystems/nqn.storage.pool1/attr_allow_any_host
# 2. 네임스페이스 생성 (실제 NVMe 백엔드)
mkdir /sys/kernel/config/nvmet/subsystems/nqn.storage.pool1/namespaces/1
echo /dev/nvme0n1 > /sys/kernel/config/nvmet/subsystems/nqn.storage.pool1/namespaces/1/device_path
echo 1 > /sys/kernel/config/nvmet/subsystems/nqn.storage.pool1/namespaces/1/enable
# 3. TCP 포트 바인딩
mkdir /sys/kernel/config/nvmet/ports/1
echo 0.0.0.0 > /sys/kernel/config/nvmet/ports/1/addr_traddr
echo tcp > /sys/kernel/config/nvmet/ports/1/addr_trtype
echo 4420 > /sys/kernel/config/nvmet/ports/1/addr_trsvcid
# 호스트 (VM): NVMe-oF 연결
nvme connect -t tcp -a 192.168.1.10 -s 4420 -n nqn.storage.pool1
# → /dev/nvme1n1로 로컬 NVMe처럼 사용
결과:
- VM 내 I/O 지연: 로컬 NVMe 대비 +50μs (RDMA 사용 시 +20μs)
- 처리량: 거의 동일 (네트워크 대역폭이 충분한 경우)
- 스토리지 활용률: 60% → 90% (풀링 효과)
문제 해결 FAQ
NVMe를 사용하면서 자주 발생하는 문제와 해결 방법을 정리합니다.
Q1. NVMe 디스크가 인식되지 않습니다
증상: lsblk나 nvme list에서 디스크가 보이지 않음.
원인 및 해결:
| 원인 | 확인 방법 | 해결 |
|---|---|---|
| PCIe 링크 미연결 | lspci | grep -i nvme아무것도 안 나옴 |
물리적 연결 확인, M.2 슬롯 재장착 |
| 드라이버 미로드 | lsmod | grep nvmenvme 모듈 없음 |
modprobe nvme |
| BAR 크기 초과 | dmesg | grep BAR"can't allocate BAR" |
BIOS에서 "Above 4G Decoding" 활성화 |
| 컨트롤러 비활성화 | nvme list 시 "Controller not ready" |
nvme reset /dev/nvme0 |
| 네임스페이스 미생성 | nvme id-ctrl /dev/nvme0NN (Number of Namespaces) = 0 |
nvme create-ns /dev/nvme0 --nsze=... |
Q2. 성능이 예상보다 훨씬 낮습니다
증상: 제조사 스펙은 7GB/s인데 실제 측정 시 1GB/s만 나옴.
체크리스트:
# 1. PCIe 레인 수 확인
lspci -vvv -s $(lspci | grep NVM | awk '{print $1}') | grep LnkSta
# Width x4 → 정상, x1 → 병목
# 2. I/O 스케줄러 확인
cat /sys/block/nvme0n1/queue/scheduler
# [none]이 아니면 변경
# 3. Queue Depth 확인
cat /sys/block/nvme0n1/queue/nr_requests
# 32 미만이면 증가
# 4. fio로 정확한 측정 (버퍼 캐시 우회)
fio --name=test --filename=/dev/nvme0n1 --direct=1 \
--ioengine=libaio --iodepth=128 --rw=read --bs=128k --numjobs=4
# 5. Thermal Throttling 확인
nvme smart-log /dev/nvme0 | grep temperature
# 80°C 이상이면 냉각 개선 필요
Q3. I/O 에러가 반복적으로 발생합니다
증상: dmesg에 "I/O error" 메시지가 지속적으로 출력.
진단 절차:
# 1. SMART 로그로 하드웨어 상태 확인
nvme smart-log /dev/nvme0
# critical_warning: 0x00 (정상)
# media_errors: 0 (미디어 에러 없음)
# num_err_log_entries: 0 (에러 로그 없음)
# 2. 에러 로그 상세 확인
nvme error-log /dev/nvme0
# Status Code Field (SCT/SC)로 에러 타입 분석
# 3. 컨트롤러 레지스터 확인
nvme show-regs /dev/nvme0
# CSTS (Controller Status) 확인
# 4. 펌웨어 업데이트 확인
nvme fw-log /dev/nvme0
# 제조사 사이트에서 최신 펌웨어 확인
흔한 에러 코드:
0x02/0x81: Invalid Command Opcode → 드라이버 버전 확인0x02/0x82: Invalid Field in Command → PRP/SGL 정렬 문제0x00/0x06: Internal Device Error → 하드웨어 장애, RMA 필요
Q4. Multipath 장애조치가 작동하지 않습니다
증상: 한 경로가 끊겨도 자동으로 다른 경로로 전환되지 않음.
해결:
# 1. Multipath 모듈 활성화 확인
cat /sys/module/nvme_core/parameters/multipath
# N이면 활성화 필요
echo 1 > /sys/module/nvme_core/parameters/multipath
# 2. 서브시스템 구조 확인
nvme list-subsys
# 두 컨트롤러가 동일 NQN으로 묶여야 함
# nvme-subsys0 - NQN=nqn.example
# \
# +- nvme0 tcp 192.168.1.10 live
# +- nvme1 tcp 192.168.1.11 live
# 3. ANA (Asymmetric Namespace Access) 상태 확인
nvme list-subsys -v
# ANA State: optimized (활성 경로)
# ANA State: non-optimized (대기 경로)
# 4. 장애조치 테스트
# 한 경로의 네트워크 끊기
ip link set eth0 down
# I/O가 중단 없이 계속되는지 확인
dd if=/dev/nvme0n1 of=/dev/null bs=1M count=1000
Q5. 지연 시간이 갑자기 증가합니다
증상: 평소 100μs였던 지연이 간헐적으로 10ms까지 증가.
원인 및 해결:
| 원인 | 확인 | 해결 |
|---|---|---|
| GC (Garbage Collection) | iostat -x 1에서 주기적 스파이크 |
Over-provisioning 증가, TRIM 활성화 |
| APST 전원 절약 | nvme get-feature -f 0x0c /dev/nvme0 |
echo 0 > /sys/module/nvme_core/parameters/default_ps_max_latency_us |
| CPU C-State | cpupower idle-info |
cpupower idle-set -d 2 (깊은 C-State 비활성화) |
| Thermal Throttling | nvme smart-log /dev/nvme0 | grep temperature |
냉각 개선 (히트싱크, 팬) |
| IRQ 밸런싱 | cat /proc/interrupts | grep nvme |
IRQ를 고정 CPU에 할당 |
Q6. 네임스페이스 크기를 변경할 수 있나요?
답변: NVMe 스펙상 네임스페이스 크기 변경은 지원하지 않습니다. 삭제 후 재생성만 가능합니다.
# 1. 기존 네임스페이스 삭제 (⚠️ 데이터 손실)
nvme detach-ns /dev/nvme0 --namespace-id=1 --controllers=0
nvme delete-ns /dev/nvme0 --namespace-id=1
# 2. 새 크기로 네임스페이스 생성
nvme create-ns /dev/nvme0 --nsze=419430400 --ncap=419430400 --flbas=0
# 3. 네임스페이스 연결
nvme attach-ns /dev/nvme0 --namespace-id=1 --controllers=0
# 대안: LVM을 사용하여 유연한 크기 조정
pvcreate /dev/nvme0n1
vgcreate vg_nvme /dev/nvme0n1
lvcreate -L 100G -n lv_data vg_nvme
# 나중에 lvextend로 확장 가능
nvme-cliGitHub 저장소의 Issue 섹션- 커널 메일링 리스트 (linux-nvme@lists.infradead.org)
dmesg와/var/log/kern.log의 상세 로그strace로 ioctl 호출 추적(Call Trace)
SQ/CQ 링 버퍼(Ring Buffer) 동작
NVMe의 Submission Queue(SQ)와 Completion Queue(CQ)는 호스트와 컨트롤러가 공유 메모리에서 락 없이 커맨드를 교환하기 위한 원형 버퍼(Ring Buffer)입니다. 포인터 이동과 Doorbell 쓰기, Phase Tag를 통해 생산자-소비자 동기화를 달성합니다.
SQ 포인터 동작
SQ는 호스트(생산자)가 SQE를 기록하고, 컨트롤러(소비자)가 페치하는 구조입니다:
- SQ Tail: 호스트가 관리합니다. 새 커맨드를 기록한 후 Tail을 전진시키고, SQ Tail Doorbell(BAR0 + 0x1000 + (2y × (4 << DSTRD)))에 MMIO 쓰기로 컨트롤러에 통지합니다.
- SQ Head: 컨트롤러가 관리합니다. 커맨드를 페치하면 Head를 전진시키며, CQE의
sq_head필드를 통해 호스트에 현재 위치를 알려줍니다. - 큐 풀(Full) 조건:
(Tail + 1) mod size == Head이면 큐가 가득 찬 상태이므로 호스트는 CQ를 처리하여 Head 전진을 기다려야 합니다. - 큐 엠프티(Empty) 조건:
Tail == Head이면 컨트롤러가 페치할 커맨드가 없는 상태입니다.
CQ 포인터 동작과 Phase Tag
CQ는 컨트롤러(생산자)가 CQE를 기록하고, 호스트(소비자)가 처리하는 구조입니다. 특히 Phase Tag(P 비트)가 새로운 엔트리 도착을 판별하는 핵심 메커니즘입니다:
- CQ Head: 호스트가 관리합니다. CQE를 처리한 후 Head를 전진시키고, CQ Head Doorbell에 MMIO 쓰기로 컨트롤러에 통지합니다.
- Phase Tag: CQE의 Status 필드 bit 0입니다. 초기값은 1이며, CQ가 한 바퀴 순환(wrap-around)할 때마다 토글됩니다.
- 새 엔트리 판별: 호스트는 현재 기대하는 Phase 값과 CQE의 P 비트를 비교합니다. 일치하면 새 완료 엔트리이고, 불일치하면 아직 기록되지 않은 엔트리입니다.
- 인터럽트 없이 폴링: Phase Tag 덕분에 호스트는 MSI-X 인터럽트 없이도 CQ를 폴링하여 완료를 감지할 수 있습니다. 이것이 io_poll 모드의 기반입니다.
커널 구현 핵심 경로
/* drivers/nvme/host/pci.c — SQ에 커맨드 제출 */
static void nvme_submit_cmd(struct nvme_queue *nvmeq,
struct nvme_command *cmd)
{
/* SQ Tail 위치에 64바이트 SQE 복사 */
memcpy(nvmeq->sq_cmds + (nvmeq->sq_tail << nvmeq->sqe_shift),
cmd, sizeof(*cmd));
/* Tail 포인터 전진 (원형 버퍼 wrap-around) */
if (++nvmeq->sq_tail == nvmeq->q_depth)
nvmeq->sq_tail = 0;
/* SQ Tail Doorbell에 MMIO Write */
writel(nvmeq->sq_tail, nvmeq->q_db);
}
/* CQ에서 완료 엔트리 처리 */
static inline int nvme_cqe_pending(struct nvme_queue *nvmeq)
{
struct nvme_completion *hcqe = &nvmeq->cqes[nvmeq->cq_head];
/* Phase Tag 비교: 기대값과 일치하면 새 CQE */
return (le16_to_cpu(hcqe->status) & 1) == nvmeq->cq_phase;
}
static void nvme_handle_cqe(struct nvme_queue *nvmeq, u16 idx)
{
/* CQE에서 sq_head 읽어 SQ 공간 확보 확인 */
nvmeq->sq_head = le16_to_cpu(nvmeq->cqes[idx].sq_head);
/* Head 전진, wrap 시 Phase 토글 */
if (++nvmeq->cq_head == nvmeq->q_depth) {
nvmeq->cq_head = 0;
nvmeq->cq_phase ^= 1; /* Phase 토글 */
}
}
dmesg | grep doorbell로 활성화 여부를 확인할 수 있습니다.
PRP과 SGL 메모리 매핑
NVMe의 데이터 전송은 호스트 메모리 주소를 컨트롤러에 전달하는 과정이 핵심입니다. PRP(Physical Region Page)와 SGL(Scatter Gather List)은 각각 페이지 정렬 기반과 임의 오프셋 기반으로 호스트 메모리를 기술하며, 대용량 전송 시 체인/세그먼트 구조로 확장됩니다.
PRP 체인 동작
PRP는 4KB 페이지 경계에 정렬된 주소만 사용합니다. SQE의 dptr 필드에 64비트 PRP 엔트리 2개가 들어갑니다:
- 데이터 ≤ 1 페이지:
prp1만 사용.prp1의 하위 비트가 페이지 내 오프셋 - 데이터 ≤ 2 페이지:
prp1+prp2각각 하나의 페이지를 가리킴 - 데이터 > 2 페이지:
prp1은 첫 페이지,prp2는 PRP List(물리 페이지 배열)의 시작 주소를 가리킴. PRP List의 마지막 엔트리가 다음 PRP List를 가리켜 체인 형성 - PRP List 엔트리 수: 4KB 페이지에 512개(8바이트 × 512) 엔트리가 들어가므로, 한 PRP List로 최대 2MB 전송. 그 이상은 체인
SGL 세그먼트 체인
SGL은 PRP와 달리 임의 오프셋과 가변 길이를 지원합니다. 16바이트 디스크립터가 데이터 블록, 세그먼트(체인), 또는 Keyed Data Block(NVMe-oF)을 기술합니다:
- Data Block: 실제 데이터가 위치한 주소와 길이. 페이지 정렬이 불필요
- Segment: 다음 SGL 디스크립터 리스트의 주소를 가리킴. Last Segment는 마지막 체인
- Keyed Data Block: RDMA rkey를 포함하여 원격 메모리 접근 허용 (NVMe-oF RDMA 전용)
- NVMe-oF 필수: Fabrics 전송에서는 PRP를 사용할 수 없으며, SGL만 허용됩니다
/* drivers/nvme/host/pci.c — PRP vs SGL 선택 로직 */
static blk_status_t nvme_map_data(struct nvme_dev *dev,
struct request *req, struct nvme_command *cmnd)
{
struct nvme_iod *iod = blk_mq_rq_to_pdu(req);
struct scatterlist *sg = iod->sg;
/* SGL 지원 여부와 IOMMU 상황에 따라 분기 */
if (nvme_pci_use_sgls(dev, req, iod->nents))
return nvme_pci_setup_sgls(dev, req, cmnd);
else
return nvme_pci_setup_prps(dev, req, cmnd);
}
CMB와 HMB 데이터 흐름
CMB(Controller Memory Buffer)와 HMB(Host Memory Buffer)는 호스트와 컨트롤러 간의 메모리 공유를 통해 DMA 왕복을 줄이는 두 가지 상보적 메커니즘입니다. CMB는 컨트롤러 측 메모리를 호스트에 노출하고, HMB는 호스트 메모리를 컨트롤러에 제공합니다.
CMB 직접 접근 흐름
CMB가 활성화되면 SQ를 컨트롤러 메모리에 배치하여 SQE 페치 DMA를 제거할 수 있습니다:
HMB 할당/해제 프로토콜
DRAM-less NVMe SSD에서 HMB는 FTL 매핑 테이블 캐시로 중요합니다. 커널이 호스트 메모리를 할당하고, Set Features 커맨드로 컨트롤러에 제공합니다:
| 단계 | 주체 | 동작 |
|---|---|---|
| 1 | 호스트 | Identify Controller에서 hmpre(선호 크기), hmmin(최소 크기) 확인 |
| 2 | 호스트 | 호스트 DRAM에서 Scatter 방식으로 메모리 할당 (연속일 필요 없음) |
| 3 | 호스트 | Host Memory Descriptor List 구성 (주소 + 크기 쌍의 배열) |
| 4 | 호스트 | Set Features (FID=0x0D, Enable=1) + Descriptor List 주소 전송 |
| 5 | 컨트롤러 | DMA로 Descriptor List 읽고 호스트 메모리를 캐시로 활용 |
| 6 | 호스트 | 서스펜드/리셋 시 Set Features (Enable=0)로 비활성화 후 메모리 해제 |
# HMB 상태 확인
$ dmesg | grep -i hmb
nvme nvme0: allocated 64 MiB host memory buffer.
# Identify Controller에서 HMB 크기 확인
$ nvme id-ctrl /dev/nvme0 -H | grep -E "hmpre|hmmin|hmminds"
hmpre : 16384 # 선호 크기: 16384 × 4KB = 64MB
hmmin : 4096 # 최소 크기: 4096 × 4KB = 16MB
hmminds : 16 # 최소 디스크립터 수
# HMB 비활성화 (디버깅용)
$ echo 0 > /sys/class/nvme/nvme0/hmb
NVMe-oF 연결 흐름
NVMe-oF는 Discovery → Connect → I/O의 3단계로 원격 네임스페이스에 접근합니다. 여기서는 전체 연결 흐름, ANA(Asymmetric Namespace Access), 그리고 멀티패스 동작을 상세히 다룹니다.
Discovery → Connect → I/O 전체 흐름
ANA (Asymmetric Namespace Access)
ANA는 NVMe 1.4에서 도입된 멀티패스 메커니즘으로, 네임스페이스별로 각 컨트롤러의 접근 경로 상태를 정의합니다:
| ANA 상태 | 의미 | I/O 동작 |
|---|---|---|
| Optimized | 최적 경로 | 정상 I/O 수행 (최저 지연) |
| Non-Optimized | 차선 경로 | 정상 I/O 수행하지만 지연이 더 높음 |
| Inaccessible | 접근 불가 | 경로 장애 시 I/O 보류, 다른 경로로 전환 |
| Persistent Loss | 영구 손실 | 경로가 영구적으로 사용 불가 |
| Change | 전환 중 | ANA 상태 변경 진행 중, I/O 보류 |
# ANA 상태 확인 (멀티패스 환경)
$ nvme ana-log /dev/nvme0 -o json | jq '.ana_group_desc[]'
{
"grpid": 1,
"nnsids": 1,
"chgcnt": 0,
"state": "optimized",
"nsids": [1]
}
# 커널 NVMe multipath 경로 확인
$ nvme list-subsys
nvme-subsys0 - NQN=nqn.2024-01.com.example:nvme-target
\
+- nvme0 tcp traddr=192.168.1.100 trsvcid=4420 live optimized
+- nvme1 tcp traddr=192.168.1.101 trsvcid=4420 live non-optimized
# multipath 정책 설정
$ cat /sys/module/nvme_core/parameters/multipath_policy
numa # numa | round-robin | queue-depth
# 정책 변경
$ echo round-robin > /sys/module/nvme_core/parameters/multipath_policy
ZNS Zone 상태머신
ZNS의 존 상태머신은 존의 수명주기를 정의하며, 호스트 소프트웨어가 올바른 순서로 존을 관리해야 합니다. 각 상태 전이는 명시적 커맨드 또는 암묵적 동작으로 발생합니다.
Zone Append 상세
Zone Append는 ZNS의 핵심 최적화입니다. 일반 Write는 호스트가 정확한 LBA를 지정하므로 멀티 큐 환경에서 Write Pointer 충돌을 유발하지만, Zone Append는 존의 ZSLBA(Zone Start LBA)만 지정하고 컨트롤러가 실제 기록 위치를 결정합니다:
- 호스트가 보내는 것: ZSLBA (존 시작 주소) + 데이터. 정확한 LBA를 알 필요 없음
- 컨트롤러가 반환하는 것: CQE에 실제 기록된 LBA 반환 (Complete 시 알 수 있음)
- 경합 제거: 여러 CPU가 동시에 같은 존에 Zone Append를 발행해도 컨트롤러가 직렬화
- 최대 크기: Zone Append Size Limit (ZASL)에 제한됨 — 일반적으로 128KB~256KB
/* block/blk-zoned.c — Zone Append 요청 */
struct bio *bio = bio_alloc(GFP_NOIO, nr_pages);
bio->bi_opf = REQ_OP_ZONE_APPEND;
bio->bi_iter.bi_sector = zone_start_sector;
/* 데이터 추가 후 submit */
submit_bio(bio);
/* 완료 후 bi_iter.bi_sector에 실제 기록된 LBA 반환 */
기존 파일시스템 호환
ZNS 디바이스는 순차 쓰기만 허용하므로 기존 파일시스템과의 호환이 필요합니다:
| 방법 | 설명 | 장단점 |
|---|---|---|
| Btrfs | 커널 5.12+에서 ZNS 직접 지원. 존 단위로 extent 할당 | 네이티브 지원, 최적 성능. 메타데이터 존 필요 |
| F2FS | 로그 구조 기반으로 ZNS와 자연스럽게 호환 | 모바일/임베디드 우수. 대규모 서버에는 제한적 |
| dm-zoned | 일반 디스크 + ZNS를 조합하여 임의 쓰기 계층 제공 | 기존 파일시스템(ext4 등) 사용 가능. 성능 오버헤드 |
| zonefs | 존을 파일로 노출하는 최소한의 파일시스템 | 단순 인터페이스. 앱이 직접 존 관리해야 함 |
NVMe 부팅 시퀀스
NVMe 컨트롤러가 사용 가능한 상태가 되기까지의 초기화 과정은 엄격한 순서를 따릅니다. CC(Controller Configuration)와 CSTS(Controller Status) 레지스터의 상호작용이 핵심입니다.
초기화 실패 처리
초기화 중 CSTS.CFS(Controller Fatal Status)가 설정되면 컨트롤러 리셋을 시도합니다:
/* drivers/nvme/host/pci.c — 초기화 타임아웃/오류 처리 */
static int nvme_wait_ready(struct nvme_ctrl *ctrl, u32 mask, u32 val)
{
u64 cap = ctrl->cap;
unsigned long timeout =
((NVME_CAP_TIMEOUT(cap) + 1) * HZ / 2) + jiffies;
while ((readl(ctrl->bar + NVME_REG_CSTS) & mask) != val) {
if (fatal_signal_pending(current))
return -EINTR;
if (time_after(jiffies, timeout)) {
dev_err(ctrl->device,
"Controller not ready; aborting %s, CSTS=0x%x\n",
val ? "initialisation" : "reset",
readl(ctrl->bar + NVME_REG_CSTS));
return -ENODEV;
}
}
return 0;
}
인터럽트 Coalescing
NVMe 인터럽트 관리는 고 IOPS 워크로드에서 CPU 오버헤드를 결정하는 핵심 요소입니다. MSI-X 벡터 분배, IRQ affinity, 인터럽트 통합(coalescing), 폴링(io_poll) 모드를 조합하여 최적의 성능-지연 균형을 달성합니다.
MSI-X 벡터 분배
NVMe 컨트롤러는 MSI-X를 사용하여 큐별 독립 인터럽트 벡터를 제공합니다:
- 벡터 할당: 리눅스 커널은
pci_alloc_irq_vectors()로 MSI-X 벡터를 요청합니다. 일반적으로 CPU 수 + 1(Admin Queue용) 개를 요청 - 1:1 매핑: 이상적으로 각 CPU에 하나의 I/O 큐와 MSI-X 벡터가 매핑됩니다.
/proc/interrupts에서 확인 가능 - 공유 벡터: MSI-X 벡터가 CPU보다 적으면 여러 CQ가 하나의 벡터를 공유합니다. 이 경우 인터럽트 핸들러(Handler)에서 모든 관련 CQ를 스캔
- IRQ affinity:
manage_irq로 커널이 자동 CPU 배치하거나,/proc/irq/N/smp_affinity로 수동 설정
인터럽트 Coalescing 설정
NVMe 스펙의 Interrupt Coalescing 기능(Feature ID 0x08)은 완료 엔트리를 모아서 인터럽트를 발생시킵니다:
# 현재 인터럽트 coalescing 설정 확인
$ nvme get-feature /dev/nvme0 -f 0x08 -H
Interrupt Coalescing: Aggregation Time: 100 μs, Aggregation Threshold: 4
# Coalescing 설정 변경
# dword11: bits[15:8] = TIME (100μs 단위), bits[7:0] = THR (완료 수-1)
$ nvme set-feature /dev/nvme0 -f 0x08 -v 0x0a03
# TIME = 10 (= 1000μs), THR = 3 (= 4개 완료 누적 후 인터럽트)
# Polling 모드 활성화 (io_poll)
$ echo 1 > /sys/block/nvme0n1/queue/io_poll
$ echo 0 > /sys/block/nvme0n1/queue/io_poll_delay # 즉시 폴링
# fio로 폴링 모드 벤치마크
$ fio --ioengine=io_uring --hipri=1 --direct=1 --bs=4k \
--rw=randread --iodepth=1 --numjobs=1 --runtime=10 \
--filename=/dev/nvme0n1
# MSI-X 벡터 분포 확인
$ cat /proc/interrupts | grep nvme
45: 12345 0 0 0 PCI-MSI-X 0000:03:00.0 nvme0q0
46: 98765 0 0 0 PCI-MSI-X 0000:03:00.0 nvme0q1
47: 0 87654 0 0 PCI-MSI-X 0000:03:00.0 nvme0q2
48: 0 0 76543 0 PCI-MSI-X 0000:03:00.0 nvme0q3
NVMe-CLI 실전 가이드
nvme-cli는 NVMe 스펙의 거의 모든 Admin 커맨드를 지원하며, JSON 출력과 벤더 플러그인을 통해 자동화와 모니터링에 활용됩니다. 여기서는 실전에서 자주 사용하는 고급 명령과 스크립팅 패턴을 다룹니다.
진단과 건강 모니터링
# SMART 로그 상세 조회
$ nvme smart-log /dev/nvme0
Smart Log for NVME device:nvme0 namespace-id:ffffffff
critical_warning : 0
temperature : 42 C
available_spare : 100%
available_spare_threshold : 10%
percentage_used : 2%
endurance group critical warning summary: 0
data_units_read : 45,678,901
data_units_written : 23,456,789
host_read_commands : 890,123,456
host_write_commands : 456,789,012
controller_busy_time : 1,234
power_cycles : 156
power_on_hours : 8,760
unsafe_shutdowns : 3
media_errors : 0
num_err_log_entries : 0
# JSON 출력으로 자동 모니터링
$ nvme smart-log /dev/nvme0 -o json | jq '{
temperature: .temperature,
spare_pct: .avail_spare,
used_pct: .percent_used,
media_errors: .media_errors,
data_written_tb: (.data_units_written * 512000 / 1e12 | floor),
unsafe_shutdowns: .unsafe_shutdowns
}'
# Sanitize (보안 삭제: 전체 미디어 소거)
$ nvme sanitize /dev/nvme0 --sanact=2 # Block Erase
$ nvme sanitize-log /dev/nvme0 # 진행 상태 확인
SSTAT: 0x101 (In Progress, Block Erase)
SPROG: 65535 # 완료 시 65535
# Persistent Event Log (NVMe 1.4+)
$ nvme persistent-event-log /dev/nvme0 --action=1 --log-len=4096
자동화 스크립트 예제
#!/bin/bash
# nvme-health-check.sh — 전체 NVMe 디바이스 건강 체크
for dev in /dev/nvme*; do
[[ -c "$dev" ]] || continue
echo "=== $dev ==="
# JSON으로 SMART 로그 읽기
json=$(nvme smart-log "$dev" -o json 2>/dev/null)
[[ -z "$json" ]] && { echo "SMART 로그 읽기 실패"; continue; }
temp=$(echo "$json" | jq '.temperature')
spare=$(echo "$json" | jq '.avail_spare')
used=$(echo "$json" | jq '.percent_used')
errors=$(echo "$json" | jq '.media_errors')
warn=$(echo "$json" | jq '.critical_warning')
# 임계값 체크
[[ "$warn" -ne 0 ]] && echo "[CRITICAL] critical_warning=$warn"
[[ "$temp" -gt 70 ]] && echo "[WARNING] 온도: ${temp}°C"
[[ "$spare" -lt 20 ]] && echo "[WARNING] 잔여 수명: ${spare}%"
[[ "$errors" -gt 0 ]] && echo "[WARNING] 미디어 에러: $errors"
echo "온도=${temp}°C 잔여=${spare}% 사용=${used}% 에러=$errors"
done
NVMe 가상화
NVMe 가상화는 단순 passthrough를 넘어 SR-IOV VF 세밀 관리, 컨트롤러 메모리 분리, mediated device를 통한 유연한 리소스 할당까지 확장됩니다.
SR-IOV VF 관리
NVMe SR-IOV는 Secondary Controller 개념을 통해 PF(Physical Function)에서 VF(Virtual Function)를 세밀하게 제어합니다:
- VQ(Virtual Queue) 할당: 각 VF에 할당되는 I/O 큐 수를 제어합니다.
nvme virt-mgmt커맨드로 설정 - VI(Virtual Interrupt) 할당: 각 VF에 할당되는 MSI-X 벡터 수를 제어합니다
- 네임스페이스 매핑: Namespace Attachment를 통해 VF에 특정 네임스페이스만 노출
- 리소스 격리(Isolation): VF 간 성능 간섭을 최소화하기 위해 큐 수와 네임스페이스를 분리
VF 리소스 관리
# SR-IOV VF 생성 전 Secondary Controller 목록 확인
$ nvme list-secondary /dev/nvme0
Num Entries: 4
Entry[0]:
Secondary Controller Identifier: 0x0001
Primary Controller Identifier: 0x0001
Virtual Function Number: 1
VQ Flexible Resources: 0
VI Flexible Resources: 0
# VF에 큐 리소스 할당 (VQ: Virtual Queue)
$ nvme virt-mgmt /dev/nvme0 --act=8 --cntlid=0x1 --rt=0 --nr=4
# act=8: Set Secondary Controller Resources
# rt=0: VQ (I/O Queue), nr=4: 4개 큐 할당
# VF에 인터럽트 리소스 할당 (VI: Virtual Interrupt)
$ nvme virt-mgmt /dev/nvme0 --act=8 --cntlid=0x1 --rt=1 --nr=4
# rt=1: VI (Interrupt), nr=4: 4개 벡터 할당
# VF 온라인 (Secondary Controller 활성화)
$ nvme virt-mgmt /dev/nvme0 --act=9 --cntlid=0x1
# act=9: Online Secondary Controller
# VF에 네임스페이스 할당
$ nvme attach-ns /dev/nvme0 --namespace-id=1 --controllers=0x1
# VF를 VM에 할당 (VFIO)
$ echo 0000:03:00.1 > /sys/bus/pci/devices/0000:03:00.1/driver/unbind
$ echo vfio-pci > /sys/bus/pci/devices/0000:03:00.1/driver_override
$ echo 0000:03:00.1 > /sys/bus/pci/drivers/vfio-pci/bind
Mediated Device (mdev)
mdev는 하드웨어 SR-IOV 없이도 NVMe 디바이스를 가상화하는 소프트웨어 방식입니다:
- QEMU NVMe 에뮬레이션: 가장 일반적인 mdev 접근법. QEMU가 NVMe 컨트롤러를 소프트웨어로 에뮬레이션하고, 백엔드는 호스트의 블록 디바이스를 사용
- SPDK vhost: 사용자 공간 NVMe 타겟이 QEMU의 virtio-blk/scsi 대신 NVMe 프로토콜을 직접 제공
- 성능 비교: SR-IOV > VFIO passthrough > virtio-blk > QEMU NVMe 에뮬레이션 (지연 기준)
# QEMU에서 다중 네임스페이스 NVMe 에뮬레이션 (고급)
$ qemu-system-x86_64 \
-drive file=ns1.qcow2,format=qcow2,if=none,id=drv1 \
-drive file=ns2.qcow2,format=qcow2,if=none,id=drv2 \
-device nvme-subsys,id=subsys0,nqn=nqn.test:sub1 \
-device nvme,serial=ctrl0,subsys=subsys0,\
max_ioqpairs=4,msix_qsize=8 \
-device nvme-ns,drive=drv1,nsid=1,subsys=subsys0 \
-device nvme-ns,drive=drv2,nsid=2,subsys=subsys0 \
-device nvme,serial=ctrl1,subsys=subsys0 # 멀티패스 테스트
# SPDK NVMe-oF 타겟으로 VM에 NVMe 제공
$ spdk_tgt &
$ rpc.py bdev_nvme_attach_controller -b NVMe0 -a 0000:03:00.0 -t PCIe
$ rpc.py nvmf_create_transport -t TCP -u 16384
$ rpc.py nvmf_create_subsystem nqn.spdk:nvme0 -a
$ rpc.py nvmf_subsystem_add_ns nqn.spdk:nvme0 NVMe0n1
$ rpc.py nvmf_subsystem_add_listener nqn.spdk:nvme0 -t TCP -a 0.0.0.0 -s 4420
- SR-IOV VF: 최고 성능이 필요하고 하드웨어가 지원하는 경우. VF 수에 제한
- VFIO Passthrough: PF 전체를 VM 하나에 전용 할당. 최대 성능이지만 공유 불가
- QEMU 에뮬레이션: 개발/테스트 환경. 유연하지만 성능 오버헤드 존재
- SPDK vhost: 높은 IOPS가 필요한 클라우드 환경. 사용자 공간 폴링으로 저지연
NVMe Copy Offload (TP 4065)
NVMe Simple Copy Command(TP 4065)는 호스트 메모리를 경유하지 않고 컨트롤러 내부에서 직접 데이터를 복사하는 기능입니다. 전통적인 호스트 기반 복사는 소스 LBA에서 호스트 메모리로 읽은 뒤 다시 대상 LBA에 쓰는 2단계가 필요하지만, Copy Offload는 컨트롤러가 내부적으로 처리하므로 PCIe 대역폭과 CPU 자원을 절약합니다.
Copy Command 동작 원리
Copy Command는 하나 이상의 Copy Range Descriptor를 받아, 각 소스 LBA 범위의 데이터를 지정된 대상 LBA로 복사합니다. 단일 커맨드로 여러 불연속 소스 범위를 하나의 연속 대상 영역에 합칠 수 있어, 스냅샷이나 데이터 통합 작업에 효율적입니다.
Linux 커널 통합
Linux 커널은 NVMe Copy Offload를 블록 레이어 수준에서 지원하기 위해 REQ_OP_COPY_OFFLOAD 연산을 도입하고 있습니다. 핵심 경로는 block/blk-copy.c에 구현되며, NVMe 드라이버가 해당 연산을 하드웨어 Copy Command로 변환합니다.
- REQ_OP_COPY_OFFLOAD: 블록 레이어에 추가된 새로운 요청 연산 타입
- copy_offload_supported(): 대상 블록 디바이스가 Copy Offload를 지원하는지 확인하는 함수
- block/blk-copy.c: 블록 레이어의 Copy Offload 핵심 경로 구현
- nvme_setup_copy(): NVMe 드라이버에서 Copy Command SQE를 구성하는 함수
커널 소스 분석: nvme_setup_copy() — NVMe Copy Command 설정
/* NVMe Copy Command 설정 (간략화) — drivers/nvme/host/core.c */
static void nvme_setup_copy(struct nvme_command *cmd,
struct nvme_copy_range *ranges,
u16 nr_ranges, u64 dst_lba)
{
cmd->copy.opcode = nvme_cmd_copy; /* Opcode 0x19 */
cmd->copy.nsid = cpu_to_le32(ns->head->ns_id); /* 네임스페이스 ID */
cmd->copy.sdlba = cpu_to_le64(dst_lba); /* 대상 시작 LBA */
cmd->copy.nr = cpu_to_le16(nr_ranges - 1); /* 소스 범위 수 (0-based) */
/* ranges 배열은 PRP/SGL을 통해 컨트롤러에 전달됨 */
}
/* 소스 범위 디스크립터 구성 */
struct nvme_copy_range {
__le64 slba; /* 소스 시작 LBA */
__le16 nlb; /* 복사할 블록 수 - 1 */
/* 예약 필드 생략 */
};
코드 설명
-
핵심
opcode: NVMe Simple Copy Command의 opcode는
0x19입니다. NVM Command Set에 정의된 I/O 커맨드입니다. - 핵심 sdlba: Source Descriptor List Base Address의 약자가 아닌, Starting Destination LBA입니다. 복사 대상의 시작 논리 블록 주소를 지정합니다.
- 핵심 nr: 0-based 인덱싱으로, 실제 범위 수에서 1을 뺀 값을 설정합니다. 최대 범위 수는 컨트롤러의 MSRC(Maximum Source Range Count) 값으로 제한됩니다.
- 핵심 ranges 배열: 각 소스 범위의 시작 LBA(slba)와 블록 수(nlb)를 정의합니다. 여러 불연속 소스 범위를 하나의 Copy Command로 처리할 수 있어 scatter-gather 복사가 가능합니다.
NAS 및 가상화 활용 시나리오
NVMe Copy Offload는 대량 데이터 복사가 빈번한 환경에서 큰 효과를 발휘합니다:
- 스냅샷 복사: CoW(Copy-on-Write) 스냅샷에서 블록 복제가 발생할 때, 컨트롤러가 내부적으로 처리하여 호스트 I/O 경로를 우회합니다
- VM 클론: qcow2/raw 디스크 이미지 복제 시 호스트 CPU 부하를 제거하고, PCIe 대역폭을 다른 VM의 I/O에 양보합니다
- 파일 복사:
cp --reflink=always의 하드웨어 가속 대안으로, 파일시스템이 Copy Offload를 호출하여 물리적 복사를 컨트롤러에 위임합니다 - 데이터 마이그레이션: 동일 네임스페이스 내에서 데이터를 재배치할 때 호스트 메모리 버퍼 없이 처리합니다
제한사항과 식별 방법
Copy Offload를 사용하기 전에 반드시 컨트롤러의 지원 여부와 제한을 확인해야 합니다:
- 동일 네임스페이스 제한: 소스와 대상이 반드시 같은 네임스페이스 내에 있어야 합니다. 네임스페이스 간 복사는 지원하지 않습니다
- MSSRL (Max Single Source Range Length): 단일 소스 범위에서 복사할 수 있는 최대 블록 수
- MCL (Max Copy Length): 하나의 Copy Command로 복사할 수 있는 총 최대 블록 수
- MSRC (Max Source Range Count): 하나의 Copy Command에 포함할 수 있는 최대 소스 범위 수
- 하드웨어 지원: 아직 모든 NVMe SSD가 Copy Command를 지원하지 않으므로, 도입 전 반드시 확인이 필요합니다
# Copy Command 지원 확인: ONCS(Optional NVM Command Support) 비트 확인
$ nvme id-ctrl /dev/nvme0 | grep oncs
oncs : 0x5f # bit 4가 1이면 Copy 지원
# Copy 관련 컨트롤러 파라미터 확인
$ nvme id-ctrl /dev/nvme0 | grep -E "mssrl|mcl|msrc"
mssrl : 256 # 단일 범위 최대 256블록
mcl : 4096 # Copy Command당 총 최대 4096블록
msrc : 127 # 최대 128개 소스 범위 (0-based)
성능 비교
호스트 기반 읽기-쓰기와 NVMe Copy Offload의 성능 차이는 데이터 크기가 클수록 두드러집니다:
| 방식 | 대역폭 | CPU 사용률 | PCIe 트래픽 | 적합 시나리오 |
|---|---|---|---|---|
| Host read+write | ~3 GB/s | 높음 | 2배 (읽기+쓰기) | 범용, 네임스페이스 간 복사 |
| NVMe Copy | ~6 GB/s | ~0% | 커맨드만 | 동일 NS 내 대량 복사 |
nvme id-ctrl로 ONCS 비트 4를 확인하고, 펌웨어 버전도 함께 점검하세요. 일부 초기 구현에서는 특정 정렬(alignment) 조건이나 크기 제한이 있을 수 있습니다.
참고 링크
- NVM Express 공식 사양 — NVMe Base, NVMe-oF, NVMe-MI 등 모든 NVMe 사양서를 다운로드할 수 있습니다
- Kernel.org — NVMe Documentation — 리눅스 커널 공식 NVMe 드라이버 문서입니다
- Bootlin Elixir — drivers/nvme/ — NVMe 호스트 및 타겟 드라이버의 커널 소스 코드를 탐색할 수 있습니다
- Bootlin Elixir — drivers/nvme/host/core.c — NVMe 호스트 드라이버의 핵심 코어 모듈입니다
- Bootlin Elixir — include/linux/nvme.h — NVMe 프로토콜 구조체와 상수를 정의하는 헤더 파일입니다
- LWN: NVMe over Fabrics — NVMe-oF의 아키텍처와 리눅스 커널 구현을 분석하는 기사입니다
- LWN: Multiqueue block layer and NVMe — blk-mq와 NVMe의 통합 과정을 설명합니다
- NVM Express — Education — NVMe 기술 교육 자료와 백서를 제공합니다
- nvme-cli (GitHub) — NVMe 관리 유틸리티
nvme-cli의 공식 저장소입니다 - Bootlin Elixir — drivers/nvme/target/ — NVMe-oF 타겟(nvmet) 서브시스템의 커널 소스 코드입니다
관련 문서
NVMe와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.