SPDK (Storage Performance Developer Kit) – это набор инструментов и библиотек с открытым исходным кодом, которые призваны содействовать разработке высокопроизводительных масштабируемых приложений, ориентированных на взаимодействие с дисковыми накопителями. В этом материале мы сосредоточимся на имеющемся в SPDK NVMe-драйвере, работающем в пользовательском пространстве Linux, а также рассмотрим реализацию приложения-примера «Hello World» на платформе Intel.
В наших экспериментах задействован сервер на чипсете Intel C610 (степпинг C1, системная шина QPI, 9.6 ГТ/с) с двумя сокетами, в котором установлены 12-ядерные процессоры Intel Xeon E5-2697 (тактовая частота – 2.7 ГГц, 24 логических ядра в режиме HT). Конфигурация ОЗУ – 8x8 Гб (Samsung M393B1G73BH0 DDR3 1866). В системе имеется твердотельный накопитель Intel SSD DC P3700 Series. В качестве ОС использована CentOS 7.2.1511 (ядро 3.10.0).
Исторически сложилось так, что дисковые накопители на порядки медленнее других компонентов компьютерных систем, таких, как оперативная память и процессор. Это означает, что операционная система и процессор вынуждены взаимодействовать с дисками, используя механизм прерываний. Например, сеанс подобного взаимодействия может выглядеть так:
Модель прерываний создаёт дополнительную нагрузку на систему. Однако обычно эта нагрузка была значительно меньше, чем задержки, характерные для обычных жёстких дисков. В результате на эту дополнительную нагрузку не обращали особого внимания, так как она не могла заметно снизить эффективность работы подсистем хранения данных.
В наши дни SSD-диски и технологии следующего поколения, например, хранилища 3D XPoint, работают значительно быстрее традиционных HDD. В результате узкое место подсистем хранения данных, которым раньше являлось аппаратное обеспечение, переместилось в сферу программных механизмов. Теперь, как можно видеть на нижеприведённом рисунке, задержки, которые вносят в процесс работы с накопителями прерывания и операционная система, в сравнении со скоростью отклика накопителей, выглядят весьма значительными.
SSD-накопители и системы хранения данных на базе технологии 3D XPoint работают значительно быстрее чем традиционные HDD. В результате теперь узким местом подсистем хранения данных стало ПО
Драйвер NVMe, который работает в пользовательском пространстве Linux, решает «проблему прерываний». Вместо ожидания сообщения о завершении операции, он опрашивает устройство хранения данных в ходе чтения или записи. Кроме того, и это очень важно, драйвер NVMe работает внутри пользовательского пространства. Это значит, что приложения могут напрямую взаимодействовать с NVMe-устройством, минуя ядро Linux. Одно из преимуществ такого подхода – избавление от системных вызовов, требующих переключения контекста. Это приводит к дополнительной нагрузке на систему. Архитектура NVMe не предусматривает блокировок, это направлено на то, чтобы не использовать механизмы процессора для синхронизации данных между потоками. Тот же подход предусматривает и параллельное исполнение команд ввода-вывода.
Сравнивая NVMe-драйвер пользовательского пространства из SPDK с подходом, предусматривающим использование ядра Linux, можно обнаружить, что при использовании NVMe-драйвера задержки, вызванные дополнительной нагрузкой на систему, снижаются примерно в 10 раз.
Задержки, в наносекундах, вызываемые при использовании для работы с накопителями механизмов ядра Linux и SPDK
SPDK может, используя одно ядро процессора, обслуживать 8 твердотельных накопителей NVMe, что даёт более 3.5 миллиона IOPs.
Изменение производительности операций ввода-вывода при работе с разным количеством SSD-накопителей с помощью механизмов уровня ядра Linux и SPDK
SPDK поддерживает работу в таких ОС, как Fedora, CentOS, Ubuntu, Debian, FreeBSD. Полный список пакетов, необходимых для работы SPDK, можно найти здесь.
Прежде чем собирать SPDK, необходимо установить DPDK (Data Plane Development Kit), так как SPDK полагается на возможности по управлению памятью и по работе с очередями, которые уже есть в DPDK. DPDK – зрелая библиотека, которую обычно используют для обработки сетевых пакетов. Она отлично оптимизирована для управления памятью и быстрой работы с очередями данных.
Исходный код SPDK можно клонировать из GitHub-репозитория такой командой:
После того, как собранный DPDK находится в папке SPDK, нам нужно вернуться к этой директории и собрать SPDK, передав make путь к DPDK.
Нижеприведённая команда позволяет включить использование больших страниц памяти (hugepages) и отвязать от драйверов ядра любые NVMe и I/OAT-устройства.
Использование больших страниц важно для производительности, так как они имеют размер 2 Мб. Это гораздо больше, чем стандартные страницы по 4 Кб. Благодаря увеличенному размеру страниц памяти уменьшается вероятность промаха в буфере ассоциативной трансляции (Translate Lookaside Buffer, TLB). TLB – это компонент внутри процессора, который отвечает за трансляцию виртуальных адресов в физические адреса памяти. Таким образом, работа со страницами большого размера ведёт к более эффективному использованию TLB.
В SPDK включено множество примеров, имеется здесь и качественная документация. Всё это позволяет быстро начать работу. Мы рассмотрим пример, в котором фразу «Hello World» сначала сохраняют на NVMe-устройстве, а потом считывают обратно в буфер.
Прежде чем заняться кодом, стоит поговорить о том, как структурированы NVMe-устройства и привести пример того, как NVMe-драйвер будет использовать эти сведения для обнаружения устройств, записи данных и затем их чтения.
NVMe-устройство (называемое так же NVMe-контроллером) структурировано исходя из следующих соображений:
Теперь приступим к нашему пошаговому примеру.
Вот полный код разобранного здесь примера, размещённый на GitHub. На сайте spdk.io можно найти документацию по API SPDK NVMe.
После запуска нашего «Hello World» должно быть выведено следующее:
Результаты работы примера «Hello World»
В SPDK включено множество примеров, которые призваны помочь программистам быстро разобраться с тем, как работает SPDK и начать разработку собственных проектов.
Вот, например, результаты работы примера perf, который тестирует производительность NVMe-диска.
Пример perf, тестирующий производительность NVMe-дисков
Разработчики, которым требуется доступ к сведениям о NVMe-дисках, таким, как функциональные возможности, атрибуты административного набора команд, атрибуты набора команд NVMe, данные об управлении питанием, сведения о техническом состоянии устройства, могут воспользоваться примером identify.
Пример identify, выводящий сведения о NVMe-диске
Мы рассказали о том, как использовать SPDK и драйвер, работающий в пользовательском пространстве Linux, для работы с NVMe-дисками. Такой подход позволяет свести к минимуму дополнительные задержки, вызываемые применением механизмов ядра для доступа к устройствам хранения данных, что позволяет повысить скорость передачи данных между накопителем и системой.
Если вас интересует разработка высокопроизводительных приложений, работающих с дисковыми накопителями с использованием SPDK, здесь можно подписаться на рассылку SPDK. А вот и вот — полезные видеоматериалы.
В наших экспериментах задействован сервер на чипсете Intel C610 (степпинг C1, системная шина QPI, 9.6 ГТ/с) с двумя сокетами, в котором установлены 12-ядерные процессоры Intel Xeon E5-2697 (тактовая частота – 2.7 ГГц, 24 логических ядра в режиме HT). Конфигурация ОЗУ – 8x8 Гб (Samsung M393B1G73BH0 DDR3 1866). В системе имеется твердотельный накопитель Intel SSD DC P3700 Series. В качестве ОС использована CentOS 7.2.1511 (ядро 3.10.0).
Зачем нужен NVMe-драйвер, работающий в пользовательском пространстве Linux?
Исторически сложилось так, что дисковые накопители на порядки медленнее других компонентов компьютерных систем, таких, как оперативная память и процессор. Это означает, что операционная система и процессор вынуждены взаимодействовать с дисками, используя механизм прерываний. Например, сеанс подобного взаимодействия может выглядеть так:
- Выполняется запрос к ОС на чтение данных с диска.
- Драйвер обрабатывает этот запрос и связывается с аппаратным обеспечением.
- Пластина диска раскручивается.
- Головка чтения-записи перемещается к нужному участку пластины, готовясь начать считывать данные.
- Данные считываются и записываются в буфер.
- Генерируется прерывание, которое уведомляет процессор о том, что данные готовы к использованию в системе.
- И, наконец, производится чтение данных из буфера.
Модель прерываний создаёт дополнительную нагрузку на систему. Однако обычно эта нагрузка была значительно меньше, чем задержки, характерные для обычных жёстких дисков. В результате на эту дополнительную нагрузку не обращали особого внимания, так как она не могла заметно снизить эффективность работы подсистем хранения данных.
В наши дни SSD-диски и технологии следующего поколения, например, хранилища 3D XPoint, работают значительно быстрее традиционных HDD. В результате узкое место подсистем хранения данных, которым раньше являлось аппаратное обеспечение, переместилось в сферу программных механизмов. Теперь, как можно видеть на нижеприведённом рисунке, задержки, которые вносят в процесс работы с накопителями прерывания и операционная система, в сравнении со скоростью отклика накопителей, выглядят весьма значительными.
SSD-накопители и системы хранения данных на базе технологии 3D XPoint работают значительно быстрее чем традиционные HDD. В результате теперь узким местом подсистем хранения данных стало ПО
Драйвер NVMe, который работает в пользовательском пространстве Linux, решает «проблему прерываний». Вместо ожидания сообщения о завершении операции, он опрашивает устройство хранения данных в ходе чтения или записи. Кроме того, и это очень важно, драйвер NVMe работает внутри пользовательского пространства. Это значит, что приложения могут напрямую взаимодействовать с NVMe-устройством, минуя ядро Linux. Одно из преимуществ такого подхода – избавление от системных вызовов, требующих переключения контекста. Это приводит к дополнительной нагрузке на систему. Архитектура NVMe не предусматривает блокировок, это направлено на то, чтобы не использовать механизмы процессора для синхронизации данных между потоками. Тот же подход предусматривает и параллельное исполнение команд ввода-вывода.
Сравнивая NVMe-драйвер пользовательского пространства из SPDK с подходом, предусматривающим использование ядра Linux, можно обнаружить, что при использовании NVMe-драйвера задержки, вызванные дополнительной нагрузкой на систему, снижаются примерно в 10 раз.
Задержки, в наносекундах, вызываемые при использовании для работы с накопителями механизмов ядра Linux и SPDK
SPDK может, используя одно ядро процессора, обслуживать 8 твердотельных накопителей NVMe, что даёт более 3.5 миллиона IOPs.
Изменение производительности операций ввода-вывода при работе с разным количеством SSD-накопителей с помощью механизмов уровня ядра Linux и SPDK
Предварительные требования и сборка SPDK
SPDK поддерживает работу в таких ОС, как Fedora, CentOS, Ubuntu, Debian, FreeBSD. Полный список пакетов, необходимых для работы SPDK, можно найти здесь.
Прежде чем собирать SPDK, необходимо установить DPDK (Data Plane Development Kit), так как SPDK полагается на возможности по управлению памятью и по работе с очередями, которые уже есть в DPDK. DPDK – зрелая библиотека, которую обычно используют для обработки сетевых пакетов. Она отлично оптимизирована для управления памятью и быстрой работы с очередями данных.
Исходный код SPDK можно клонировать из GitHub-репозитория такой командой:
git clone https://github.com/spdk/spdk.git
?Сборка DPDK (для Linux)
cd /path/to/build/spdk
wget http://fast.dpdk.org/rel/dpdk-16.07.tar.xz
tar xf dpdk-16.07.tar.xz
cd dpdk-16.07 && make install T=x86_64-native-linuxapp-gcc DESTDIR=.
?Сборка SPDK (для Linux)
После того, как собранный DPDK находится в папке SPDK, нам нужно вернуться к этой директории и собрать SPDK, передав make путь к DPDK.
cd /path/to/build/spdk
make DPDK_DIR=./dpdk-16.07/x86_64-native-linuxapp-gcc
?Настройка системы перед запуском SPDK-приложения
Нижеприведённая команда позволяет включить использование больших страниц памяти (hugepages) и отвязать от драйверов ядра любые NVMe и I/OAT-устройства.
sudo scripts/setup.sh
Использование больших страниц важно для производительности, так как они имеют размер 2 Мб. Это гораздо больше, чем стандартные страницы по 4 Кб. Благодаря увеличенному размеру страниц памяти уменьшается вероятность промаха в буфере ассоциативной трансляции (Translate Lookaside Buffer, TLB). TLB – это компонент внутри процессора, который отвечает за трансляцию виртуальных адресов в физические адреса памяти. Таким образом, работа со страницами большого размера ведёт к более эффективному использованию TLB.
Приложение-пример «Hello World»
В SPDK включено множество примеров, имеется здесь и качественная документация. Всё это позволяет быстро начать работу. Мы рассмотрим пример, в котором фразу «Hello World» сначала сохраняют на NVMe-устройстве, а потом считывают обратно в буфер.
Прежде чем заняться кодом, стоит поговорить о том, как структурированы NVMe-устройства и привести пример того, как NVMe-драйвер будет использовать эти сведения для обнаружения устройств, записи данных и затем их чтения.
NVMe-устройство (называемое так же NVMe-контроллером) структурировано исходя из следующих соображений:
- В системе может присутствовать одно или несколько NVMe-устройств.
- Каждое NVMe-устройство состоит из некоторого количества пространств имён (оно может быть только одно в данном случае).
- Каждое пространство имён состоит из некоторого количества адресов логических блоков (Logical Block Addresses, LBA).
Теперь приступим к нашему пошаговому примеру.
?Настройка
- Инициализируем слой абстракции окружения DPDK (Environment Abstraction Layer, EAL). В коде, приведённом ниже,
-c
– это битовая маска, которая служит для выбора ядер, на которых будет исполняться код.–n
– это ID ядра, а--proc-type
– это директория, где будет смонтирована файловая система hugetlbfs.
static char *ealargs[] = { "hello_world", "-c 0x1", "-n 4", "--proc-type=auto", }; rte_eal_init(sizeof(ealargs) / sizeof(ealargs[0]), ealargs);
- Создадим пул буфера запроса, который используется внутри SPDK для хранения данных каждого запроса ввода-вывода.
request_mempool = rte_mempool_create("nvme_request", 8192, spdk_nvme_request_size(), 128, 0, NULL, NULL, NULL, NULL, SOCKET_ID_ANY, 0);
- Проверим систему на наличие NVMe-устройств.
rc = spdk_nvme_probe(NULL, probe_cb, attach_cb, NULL);
- Перечислим NVMe-устройства, возвращая SPDK логическое значение, указывающее на то, нужно ли присоединить устройство.
static bool probe_cb(void *cb_ctx, struct spdk_pci_device *dev, struct spdk_nvme_ctrlr_opts *opts) { printf("Attaching to %04x:%02x:%02x.%02x\n", spdk_pci_device_get_domain(dev), spdk_pci_device_get_bus(dev), spdk_pci_device_get_dev(dev), spdk_pci_device_get_func(dev)); return true; }
- Устройство присоединено. Теперь можно запросить данные о количестве пространств имён.
static void attach_cb(void *cb_ctx, struct spdk_pci_device *dev, struct spdk_nvme_ctrlr *ctrlr, const struct spdk_nvme_ctrlr_opts *opts) { int nsid, num_ns; const struct spdk_nvme_ctrlr_data *cdata = spdk_nvme_ctrlr_get_data(ctrlr); printf("Attached to %04x:%02x:%02x.%02x\n", spdk_pci_device_get_domain(dev), spdk_pci_device_get_bus(dev), spdk_pci_device_get_dev(dev), spdk_pci_device_get_func(dev)); snprintf(entry->name, sizeof(entry->name), "%-20.20s (%-20.20s)", cdata->mn, cdata->sn); num_ns = spdk_nvme_ctrlr_get_num_ns(ctrlr); printf("Using controller %s with %d namespaces.\n", entry->name, num_ns); for (nsid = 1; nsid <= num_ns; nsid++) { register_ns(ctrlr, spdk_nvme_ctrlr_get_ns(ctrlr, nsid)); } }
- Перечислим пространства имён для того, чтобы получить сведения о них, например, такие как размер.
static void register_ns(struct spdk_nvme_ctrlr *ctrlr, struct spdk_nvme_ns *ns) { printf(" Namespace ID: %d size: %juGB\n", spdk_nvme_ns_get_id(ns), spdk_nvme_ns_get_size(ns) / 1000000000); }
- Создадим пару очередей (queue pair) ввода вывода для отправки пространству имён запроса на чтение/запись.
ns_entry->qpair = spdk_nvme_ctrlr_alloc_io_qpair(ns_entry->ctrlr, 0);
?Чтение/запись данных
- Выделим буфер для данных, которые будут прочитаны/записаны.
sequence.buf = rte_zmalloc(NULL, 0x1000, 0x1000);
- Скопируем строку «Hello World» в буфер.
sprintf(sequence.buf, "Hello world!\n");
- Отправим запрос на запись заданному пространству имён, предоставив пару очередей, указатель на буфер, индекс LBA, функцию обратного вызова, которая сработает после записи данных, и указатель на данные, которые должны быть переданы функции обратного вызова.
rc = spdk_nvme_ns_cmd_write(ns_entry->ns, ns_entry->qpair, sequence.buf, 0, /* Начало LBA */ 1, /* количество блоков */ write_complete, &sequence, 0);
- Функция обратного вызова, после завершения процесса записи, будет вызвана синхронно.
- Отправим запрос на чтение заданному пространству имён, предоставив тот же набор служебных данных, который использовался для запроса на запись.
rc = spdk_nvme_ns_cmd_read(ns_entry->ns, ns_entry->qpair, sequence->buf, 0, /* LBA start */ 1, /* number of LBAs */ read_complete, (void *)sequence, 0);
- Функция обратного вызова, после завершения процесса чтения, будет вызвана синхронно.
- Проверим флаг, который, который указывает на завершение операций чтения и записи. Если запрос всё ещё обрабатывается, мы можем проверить состояние заданной пары очередей. Хотя реальные операции чтения и записи данных выполняются асинхронно, функция
spdk_nvme_qpair_process_completions
проверяет ход работы и возвращает число завершённых запросов ввода-вывода, и, кроме того, вызывает функции обратного вызова, сигнализирующие о завершении процедур чтения и записи, описанные выше.
while (!sequence.is_completed) { spdk_nvme_qpair_process_completions(ns_entry->qpair, 0); }
- Освободим пару очередей и другие ресурсы перед выходом.
spdk_nvme_ctrlr_free_io_qpair(ns_entry->qpair);
Вот полный код разобранного здесь примера, размещённый на GitHub. На сайте spdk.io можно найти документацию по API SPDK NVMe.
После запуска нашего «Hello World» должно быть выведено следующее:
Результаты работы примера «Hello World»
Другие примеры, включённые в SPDK
В SPDK включено множество примеров, которые призваны помочь программистам быстро разобраться с тем, как работает SPDK и начать разработку собственных проектов.
Вот, например, результаты работы примера perf, который тестирует производительность NVMe-диска.
Пример perf, тестирующий производительность NVMe-дисков
Разработчики, которым требуется доступ к сведениям о NVMe-дисках, таким, как функциональные возможности, атрибуты административного набора команд, атрибуты набора команд NVMe, данные об управлении питанием, сведения о техническом состоянии устройства, могут воспользоваться примером identify.
Пример identify, выводящий сведения о NVMe-диске
Выводы
Мы рассказали о том, как использовать SPDK и драйвер, работающий в пользовательском пространстве Linux, для работы с NVMe-дисками. Такой подход позволяет свести к минимуму дополнительные задержки, вызываемые применением механизмов ядра для доступа к устройствам хранения данных, что позволяет повысить скорость передачи данных между накопителем и системой.
Если вас интересует разработка высокопроизводительных приложений, работающих с дисковыми накопителями с использованием SPDK, здесь можно подписаться на рассылку SPDK. А вот и вот — полезные видеоматериалы.
Поделиться с друзьями
amarao
Мертворождённая технология. В каком-то смысле, буквальный откат в маразматическое детство компьютеров, когда программы должны были напрямую взаимодействовать с дисковыми накопителями, разбираться в их особенностях и знать про их существование.
ОС для того и существует, чтобы программисты могли писать новые программы, а не реализовывать из раза в раз одни и те же примитивы доступа к данным для каждого нового устройства.
Что делать? Переписывать стек работы с блочным устройством, добавлять альтернативный класс устройств в ядро.
acmnu
> добавлять альтернативный класс устройств в ядро.
Это ничего не изменит, поскольку будет переключение контекста для работы с устройством. Подход при котором инструменты ядра не используются совсем не нов, например для работы с сетью или тот же direct access к дискам, который используют базы данных.
Deosis
Такова стоимость абстракций.
Либо использовать абстракцию и мирится с накладными расходами.
Либо использовать прямой доступ и разбираться со всем зоопарком технологий, скрываемых абстракцией.