ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [SPDK] Submitting I/O to an NVMe Device
    논문 정리/Vertical optimization 2019. 10. 1. 02:00

    The NVMe Specification

    NVMe 스펙은 실제 저장장치와 연결하고 통신하기 위한 하드웨어 인터페이스를 정의한다. 스펙에서는 PCIe를 통해 연결하는 장치를 위한 레지스터 정의 뿐만 아니라 네트워크를 통해 연결되는 원격 장치에 데이터 전송을 위한 스펙 또한 정의하고 있다. 이번 챕터에서는 SPDK를 통해 PCIe 장치로 IO를 전달하는 방법을 알아본다.

    NVMe submission queue & completion queue

    NVMe 장치느 SPDK NVMe driver 등과 같은 다양한 호스트 소프트웨어에서 호스트 메모리에 여러개의 큐를 할당하는 것을 허용하고 있다. 여기서 말하는 호스트란 NVMe 장치가 연결되어 있는 시스템을 의미한다. 각각의 큐는 두개의 큐로 구성되어 있다. 하나는 submission 큐, 다른 하나는 completion queue 이다. 큐들은 모두 고정된 크기를 같는 circular ring 형태의 큐로 정의되어 있다. submission 큐는 64바이트의 커맨드 자료구조 배열과 head와 tail을 추적하기 위한 2개의 integer로 구성되어 있다. completion 큐는 16바이트의 completion 자료구조 배열과, head와 tail을 추적하기 위한 2개의 integer로 구성되어 있다. 또한 둘 모두 doorbell을 담당하는 32-bit 레지스터를 가지고 있다.

    Submission Queue

    NVMe로 보내는 IO는 64바이트의 커맨드를 구성하고 submission 큐의 head에 삽입하고, 해당 head의 위치를 doorbell 레지스터에 등록하는 순으로 수행된다. 동시에 여러개의 command를 submission 큐에 삽입하고 head 위치를 한번만 doorbell 에 등록하여 모든 커맨드를 요청하는 것도 가능하다.

    NVMe 스펙에 submission과 completion의 처리에 관한 자세한 내용이 있으므로 필요할 경우 확인하길 바란다.

    가장 중요한 것은 커맨드에 어떤 명령인지를 명시할 뿐만 아니라 필요할 경우 호스트 메모리 상에 해당 커맨드에 필요한 데이터가 어디 있는지 메모리 주소 정보를 명시할 수 있다는 것이다. 이러한 호스트 메모리에는 쓰기 요청을 위한 데이터가 존재할 수도 있고, 읽기 요청을 받아 들이기 위한 메모리 공간이 존재할 수도 있다. 이러한 주소로의 데이터 읽기/쓰기 요청은 NVMe 장치 내의 DMA를 통해 처리된다.

    Completion Queue

    Completion 큐 또한 비슷한 방식으로 동작하지만, 호스트가 아닌 NVMe 장치가 큐에 완료 정보를 기록한다는 점이 다르다. 큐의 각 원소들은 phase 비트를 갖고 있으며 ring을 한 바퀴 돌 때마다 0과 1로 각각 토글 된다. 커맨드 요청을 위한 두 submission 큐와 completion 큐가 모두 준비 된 경우 생성하는 인터럽트는 completion 큐의 head 정보를 포함하고 있다. Interrupt를 처리하는 것은 큰 오버헤드가 있으므로 SPDK는 완료 명령에 대해 interrupt를 사용하지 않고 phase 비트를 polling하여 감지 하는 방식을 사용한다.

    The SPDK NVMe Drive I/O Path

    SPDK 사용자들의 실제 IO가 필요한 시점이 아닌 자신이 원하는 아무 시점에서나 커맨드 전송을 위한 큐 엔트리 쌍을 만들 수 있다. 이후에 원하는 시점에 spdk_nvme_ns_cmd_read() 와 같은 API를 호출하여 IO를 수행할 수 있다. 사용자는 데이터의 입출력이 필요할 경우 해당 데이터의 버퍼에 대한 정보와 함께 대상 LBA, 길이 뿐만 아니라 NVMe의 어떤 네임스페이스를 대상으로 하는지와 어떤 NVMe 큐 를 사용할 것 인지 등과 같은 다양한 정보를 전달하게 된다. 또한 커맨드 완료를 처리하기 위한 callback 함수와 현재 프로그램의 context 정보를 함께 전달하여 이후 커맨드 완료를 처리하고 싶은 시점에 spdk_nvme_qpair_process_completions() API 등을 호출하여 사용할 수 있다.

    1. Allocate Request Object

    SPDK의 NVMe 드라이버는 요청된 작업을 추적하기 위한 request object를 먼저 할당 한다. 커맨드 요청은 비동기적으로 실행되므로 단순하게 call stack을 따라 가는 것으로 요청에 대한 상태를 추적할 수는 없다. 필요할 때마다 heap에 새로운 request object를 생성할 경우 성능 저하가 발생할 수 있으므로 SPDK는 struct spdk_nvme_qpair라는 자료구조에 미리 할당된 request object들을 저장하고 관리한다. 할당 된 request object의 개수는 실제 NVMe 장치의 submission queue의 개수보다 많으며 이를 통해 다음과 같은 몇가지 편의성을 제공한다. 첫 째는 하드웨어 큐의 개수보다 많은 명령을 유저가 요청 할 수 있게 하고 SPDK가 이를 자동적으로 큐잉하여 관리하는 역할이다. 두 번째는 여러가지 이유로 인한 커맨드의 split을 처리하기 위함이다. request object의 개수는 queue를 생성하는 단계에서 설정할 수 있으며, 지정되지 않은 경우 하드웨어 큐의 개수에 따라 적절한 개수를 SPDK가 알아서 할당한다.

    2. Create a NVMe Command and Allocate Slot & Tracker

    request object를 할당한 이후에는 NVMe 커맨드를 생성한다. 커맨드는 request object 내에 기록되며 NVMe submission 큐에 직접 기록되지 않는다. 커맨드가 완전히 생성된 이후에 NVMe submission queue에서 비어있는 slot을 할당한다. Submission 큐의 각 요소들은 tracker 라고 부르는 객체를 할당한다. tracker는 인덱스를 통해 곧바로 접근할 수 있도록 배열에 곧바로 할당되며, 해당 슬롯에 기록될 request의 정보를 포인팅 하고 있다. 트래커의 할당까지 모두 완료 된 이후 할당된 트래커의 인덱스를 커맨드의 CID 값에 기록한다. 이 CID는 커맨드가 completion 될 때 같이 제공되며 이를 통해 request가 무엇이었는지 바로 알아낼 수 있다.

    3. Create PRP Lists

    슬롯 (트래커)를 할당한 이후에는 해당 커맨드에 필요한 데이터 버퍼를 PRP 리스트로 변환한다. PRP는 원래 NVMe의 scatter gather list이지만 몇가지 제약이 있다. 사용자는 SPDK에 데이터 버퍼의 virtual address 를 제공하기 때문에, SPDK는 페이지 테이블을 통해 실제 physical address를 획득하거나 IO virtual address (iova)를 획득해야 한다. 가상 메모리 주소상으로 연속적인 공간일 지라도 물리 메모리 상에서는 연속적이지 않을 수 있으므로 하나의 데이터 버퍼가 여러개의 PRP list 원소를 생성할 수 있다. 더 나아가 하나의 PRP list에 기록할 수 없을 만큼 여러개의 주소로 분리 될 경우 SPDK가 자동적으로 사용자의 요청을 2개 이상의 request로 분리한다. 메모리 관리에 대한 자세한 내용은 Direct Memory Access (DMA) From User Space 를 참고하길 바란다.

    슬롯 (트래커)를 할당하기 전에 PRP list를 미리 만들지 않는 이유는 PRP에 기록될 메모리 주소는 DMA가 가능한 메모리 영역이어야 하며, 매우 큰 공간일 수 있기 때문이다. SPDK는 일반적으로 많은 양의 request를 할당하고 있으므로, PRP list의 최악의 상황을 대비한 메모리 공간을 추가로 미리 할당하는 것은 비효율적이다.

    각각의 NVMe 커맨드들은 두개의 PRP 리스트 요소를 내장하고 있어, 요청이 4KiB 이거나 혹은 8KiB 이고 완벽하게 align 되어 있는 경우에는 별도의 PRP 리스트가 필요하지 않다. 실제 프로파일링 결과를 보면 이 부분을 다루는 코드는 전체 CPU 사용량에 큰 영향을 주지 않음을 알 수 있다.

    4. Insert a command into NVMe Submission Queue

    트래커에 원하는 정보를 모두 기록한 이후 생성된 64 바이트 커맨드를 실제 NVMe submission 큐에 복사하고 doorbell에 queue의 tail 을 기록해서 실제 IO가 진행되도록 한다. IO의 완료는 기다리지 않으며, submission 이후 바로 유저 프로세스로 복귀한다.

    5. Complete a command

    사용자는 주기적으로 spdk_nvme_qpair_process_completions()를 호출해 SPDK의 completion queue를 확인할 수 있다. 해당 함수는 completion 될 차례의 슬롯에 대한 phase 를 확인하고, flip 되어 완료가 확인된 경우 CID 값을 통해 tracker를 추적하여 원래의 request 정보를 찾아낸다. 이후 request에 저장된 callback 함수를 실행하여 커맨드의 처리를 완료한다.

    spdk_nvme_qpair_process_completions() 함수는 완료된 명령이 없을 때까지 계속해서 completion queue를 처리하며, 확인 종료 시점에 해당 위치의 queue head를 completion queue doorbell에 기록하여 저장 장치가 큐의 어느 부분까지 사용할 수 있는 지 알 수 있도록 한다.

Designed by Tistory.