Поймал себя на ощущении, что очень хочется поделиться своим опытом работы с интеловской энергонезависимой памятью (Intel Optane memory или Intel PMem = persistent memory). Я буду для краткости называть ее ПМем. Думаю, что несмотря на объем продаж в сотни миллионов долларов, пока мало кто с ней сталкивался и знает ее специфику. Я же по долгу службы занимаюсь ей уже довольно продолжительное время и гонял на ней различные приложения и микро-бенчмарки. А также добивался ее эффективного использования модифицируя под нее клиентские коды.
В принципе литературы по ней навалом, по крайней мере на английском, но практические советы и простое и понятное описание ее поведения найти не просто, если вообще возможно. Я хочу рассказать что от нее можно ожидать, специфику ее режимов работы в тесной привязке к производительности. А также поясню, в каких случаях она работает хорошо, и в каких вряд ли оправдает ожидания. На всякий случай здесь интеловская заглавная маркетинговая страница по этой технологии.
В настоящее время уже выпущено два поколения ПМем (100 серия и 200 серия). Сотая серия работает с процессорами Xeon Scalable 2-го поколения (Cascade Lake), а двухсотая – с 3-м поколением (Ice Lake). Это все серверные платформы. В основе ее та же технология Optane, которая впервые появилась в твердотельных дисках (Intel Optane SSD). Диски, кстати, очень быстрые по сравнению с обычными 3DNAND. Время отклика у них лучше по крайней мере раз в 5, а в реальности – в 10-20 и больше. Это связано с особенностями производительности как функции нагрузки. Оптановские диски выдают на гора практически при любой нагрузке, держа время отклика почти стабильным в районе 8 мксек (даже кажется еще снизили в последнем поколении), и уже при небольшом количестве пишущих на них или читающих с них потоков их пропускная способность подходит к 100%. 3DNAND выглядит заметно послабже. Ему нужны десятки потоков, чтобы что-то отдаленно напоминающее максимальную пропускную способность материализовалось в реальности. При этом время отклика естественно уходит за 100 мксек. Впрочем при чтении большими блоками достаточным количеством потоков и те и другие диски выдадут близкие результаты по пропускной способности. На самом деле она для топовых твердотельных дисков часто ограничена их PCIe соединением (им обычно выделяют две линии). С переходом на PCIe 4 в последнем поколении платформ/процессоров (те что Ice Lake) Интел сразу и диски выпустил с удвоенной пропускной способностью.
Но вернемся к памяти. На вид это те же плашки, что и ДДР.
На самом деле ДДР требуется, чтобы система работала. Типичная конфигурация – это 1 модуль ДДР и 1 ПМем на канал памяти, но вообще возможно много вариантов. То есть в Ice Lake сервер с двумя процессорами можно поставить 2х8 модулей ДДР и 2х8 модулей ПМем. Объем памяти одного модуля – 128, 256 или 512ГБ. Думаю он как раз и объясняет, почему многие читатели пока с этой памятью не сталкивались. Хотя она намного дешевле ДДР за гигабайт, но при таких объемах ценники все равно получаются серьезные. С 512-ми модулями в двухпроцессорном сервере можно получить 8ТБ ПМем плюс еще ДДР. Популярное соотношение 8:1, при таком в сумме будет 9ТБ, но суммировать можно не всегда, и я об этом расскажу.
Если очень грубо сравнить производительность ПМем и ДДР, то ПМем медленнее примерно втрое. Это относится как ко времени доступа, так и к пропускной способности. Но есть очень много всевозможных нюансов. При доступе на чтение коэффициент три работает очень неплохо, но при добавлении даже небольшого количества записи картина резко усложняется и не в пользу ПМем. Такому поведению есть причины. Контроллер памяти ПМем (который является частью каждого модуля) оперирует блоками в 256 байт. У него есть крохотный буферок, в который данные изначально поступают (в случае записи). Если например пришло несколько кэш линий в этот буфер, и некоторые из них соседние (допустим писали два потока, каждый последовательно – соседние будут по-любому, но необязательно по порядку), то контроллер их поменяет местами и возможно сумеет укомплектовать 256 байтными блоками, чтобы писать с максимальной эффективностью. Ясно, что если потоков становится много, то шансы резко падают, буферок-то скромнейшего размера.
Подозреваю, что увеличить его проблематично, сложность растет нелинейно, возможно контроллер не справится. Другая сторона проблемы – работа механизма вытеснения кэш-линий. Когда мы что-то пишем в память, на самом деле оно туда сразу не идет, а зависает в кэше (write through cache). Дальше эта кэш линия будет ждать, когда подойдет ее очередь на вытеснение. Процесс непредсказуемый, и соседние кэш линии записанные подряд могут быть вытеснены в разное время. Соответственно, даже при последовательной записи в ПМем получаем случайную, и 256 байтный буфер работает заполненным лишь частично (возможно всего на четверть, то есть одной кэш-линией). Есть два варианта лечения этой проблемы, но в обоих случаях придется править код, чтобы их реализовать. Первый – вызывать инструкцию clflush/clflushopt, чтобы сразу сбросить кэш-линию в память. Она не бесплатная, но имеет дополнительный бонус. Это гарантия, что данные сохранены в постоянной памяти. То есть они переживут выключение электричества. Вторая опция – использование NT writes (non temporal writes). Это инструкции для записи напрямую, минуя кэш. Отличная вещь для ПМем, гарантирует в разы более высокую производительность и персистентность (барьер по записи только требуется). Но есть одно НО. Данные не попадают в кэш. То есть если они будут сразу после этой записи использованы, то придется их загружать из памяти. Это далеко не всегда приемлемо, но может быть все равно более производительным вариантом. Я уже говорил, что чтение работает намного лучше, добавлю, что и пропускная способность при чтении выше раза в 2.5 как минимум, а при менее оптимальном количестве потоков может быть и в 5-6 раз выше.
Еще один любопытный нюанс заключается в том, что у 100-го и 200-го поколений запись масштабируется совершенно по-разному. У сотого она сначала заметно растет от 1 до 4-8 потоков, а потом очень плавно падает. У двухсотого максимум достигается в одном потоке. В абсолютных цифрах это около 23ГБ/сек на запись и 55ГБ/сек на чтение. Это данные для одного сокета, два дадут вдвое больше. В случае совмещения сокетов в один диск с помощью SW RAID двойки уже не получится, а только около 1.5, если не меньше. Как ни странно, такая как бы неважная (отрицательная даже) масштабируемость – это неплохо. Очень много приложений не особо оптимизированных, где пишущих потоков мало, а то и вообще один. И с ПМем они как раз подружатся.
Дефолтная конфигурация memory interleave – interleave по всем Пмем модулям сокета. Соостветственно достигается максимальный параллелизм и производительность. Между сокетами interleave не работает. При желании можно каждый модуль сконфигурировать независимым, но это безумие, так как писать придется параллеьно уже на уровне приложения. Не представляю кому это может пригодиться.
Теперь давайте обсудим варианты конфигураций. На самом высоком уровне их два – 1LM (one level memory) и 2LM (two level memory). Во втором случае только ПМем видна операциононой системе в качестве памяти, ДДР же как бы спрятана на втором уровне и работает как кэш для ПМем (direct cache). ПМем в этом случае не обладает энергонезависимостью, то есть при перезагрузке данные не сохранятся. Смысл обычно в том, чтобы получить больше памяти, чем возможно с ДДР, либо получить столько же, но существенно дешевле.
Плюс такой конфигурации в том, что приложению вообще не надо ничего знать о памяти. Оно просто будет работать. При этом если данные используются многократно, есть неплохие шансы, что они будут жить в основном в ДДР и производительность приложения может практически не уступать случаю с только ДДР. То есть можно иметь и использовать нереально огромную память и при этом работать с производительностью ДДР. Отличный пример – in-memory базы данных. Как правило они демонстрируют 90-100% производительности в сравнении с аналогичной системой оснащенной только ДДР. Недостатки у этой конфигурации тоже есть. Во-первых, ДДР фактически недоступна для данных. То есть аллоцировать напрямую в ней нельзя никак. Во-вторых, это самая низкопроизводительная мода для ПМем. Ко времени доступа добавляется время доступа в ДДР и пропускная способность в разы ниже (~4-5 раз), чем у 1LM. У приложений вычислительного типа шансов на успех немного.
1LM – мода гораздо более интересная. Тут есть варианты. В смысле производительности самой ПМем они практически аналогичны (максимальная), но имеют принципиальные отличия не хардверного свойства. Все варианты относятся к типу AppDirect. Значение этого термина в том, что как бы приложение напрямую заботится о взаимодействии с ПМем, хотя в реальности это не обязательно.
Первый вариант – это Storage over AppDirect. Фактически в этой конфигурации мы получаем диск на ПМем с файловой системой. Самый простой случай – это конфигурация обычного блочного диска. То есть доступ поблочный. Мелкий недостаток – время доступа, блок есть блок. Нужна кэш линия – все равно грузи 4К. Но есть и крупные достоинства. Во-первых, для приложений которым требуется ускорение ввода-вывода не потребуется никаких модификаций. Просто переход на ПМем диск. Во-вторых, в данном случае операционная система сама автоматически кэширует страницы с диска в память. Как только единица данных со страницы прочитана, операционная система грузит всю страницу в ДДР. То есть следующие данные с этой страницы пойдут из ДДР. Для последовательного доступа (а такого очень много) это работает отлично. На мой взгляд, эта конфигурация – самая универсальная и сбалансированная. Побеждает любые самые быстрые системы хранения данных с большим запасом (обычно 2-7 раз) на практически любых тестах на чтение.
Второй вариант – та же Storage over AppDirect, но смонтированная в DAX (direct access) моде. Эта мода требует файловую систему, умеющую работать с ПМем, конкретно дающую доступ по кэш линиям. С этим проблем давно нет, любая современная операционка это поддерживает. В смысле производительности разница по сравнению с просто Storage over AppDirect существенная. Во-первых, гранулярность доступа в одну кэш линию сокращает время доступа в разы. То есть случайный доступ будет просто летать по сравнению с любыми дисками и системами хранения данных. Как вам ускорение в 100 раз например? Вполне реальная цифра. Недостаток этой конфигурации в том, что операционная система больше не вовлечена и не кэширует страницы в ДДР. То есть все, что было аллоциовано в ПМем будет ходить оттуда напрямую в кэш процессора. В этой конфигурации ПМем дико рвет любые самые быстрые системы хранения данных на случайном доступе, но в случае последовательного доступа с переиспользованием данных выиграет не всегда. Аналогично предыдущему случаю нужды модифицировать приложение нет при условии, что задача – ускорить работу ввода-вывода. Это если в основном чтение; если же много записи, то для производительности модификации понадобятся.
И наконец третий вариант – AppDirect Volatile. В данном случае ПМем используется как волатильная память. Производительность очень близка к DAX и даже чуть лучше, потому что больше нет файловой системы. Здесь уже речи о работе с файлами нет, ПМем видна именно как память. Задача разработчика – грамотно аллоцировать данные (и писать правильными инструкциями). Маленькие и динамичные данные идут в ДДР, а большие и более статичные – в ПМем. Если по ходу работы приложения появляется нужда переместить данные из одного типа памяти в другой – это должно будет сделать приложение (то есть позаботиться должен разработчик кода). ПМем видна как дополнительная NUMA-node, так что аллокатор написать не проблема.
Еще один интересный факт – можно сконфигурировать комбинацию таких вариантов, например частично 1LM и частично 2LM. Или создать две файловые системы на ПМем, одну обычную, а другую с прямым доступом. Или и то и другое одновременно. Одним словом, простор для творчества есть.
Что еще надо знать о ПМем? Если погуглить Intel PMem, то сразу всплывет PMDK. Это набор библиотек, позволяющих эффективно работать с ПМем на разных уровнях. Главная идея PMDK – помочь разработчику грамотно (транзакционно) работать с persistence. То есть с помощью ее библиотек обеспечить consistency данных (либо транзакция полностью сохранена в постоянной памяти, либо совсем не сохранена, но не частично). Это можно делать от отдельной переменной до транзакций высокой сложности. На самом деле эти механизмы довольно похожи на синхронизацию потоков и без них потребовалась бы очень кропотливая работа и затратная отладка. Если почитать документацию PMDK, то может сложиться впечатление, что без нее – никуда, но это не так. Даже если приложение работает не с файлами, а с памятью, использовать варианты типа Storage over AppDirect (то есть с персистентностью) совсем не сложно. Для этого достаточно открыть файл, сделать ему memory map и размещать все данные, которые идут в ПМем в этом файле. При этом разработчик сам выбирает какими инструкциями писать в ПМем и обеспечивает consistency данных по тем же в общем принципам как это делается в многопоточных приложениях. PMDK делает то же самое.
Как я уже сказал, чтение работает гораздо производительнее и стабильнее, чем запись. Запись же более капризна, требует правильных инструкций и имеет умеренно негативное масштабирование. Это обстоятельство часто приводит к ситуации «неоправдавшихся надежд» у клиентов, впервые попробовавших ПМем не трогая приложение. Проблема в том, что использование стандартного ввода-вывода операционки отнюдь не оптимизировано под ПМем и часто производительность получается аналогичной твердотельному диску. Из моей практики типичное ускорение при переходе на Пмем (если брать только ввод-вывод) составляет от 3.5 до 6 раз в ситуациях интенсивной записи. Это в сравнении с одним ССД и при использовании правильных инструкций. У нас есть несколько инструментов и методов точного тестирования без изменения приложения, но это отдельная тема.
Любопытной проблемой является определение данных для аллокации в ДДР или ПМем. Методы понятны и характеристики данных можно померять например с помощью VTune Performance Analyzer. Но опять же из моей практики вариантов приложений вычислительного типа, которые в таком режиме будут неплохо работать крайне мало. Базы данных и прочие приложения по обработке больших данных практически единственное направление, которое работает. В большинстве приложений вычислительного типа значительную часть данных надо и читать и писать, а запись приводит к такой деградации производительности ПМем, что ПМем становится невыгодной.
В общем, если совсем кратко, то так. Если речь идет о замещении любых самых лучших диковых систем хранения данных и главный фактор - производительность, то Пмем – это находка. Огромный объем за гораздо меньшие деньги, чем ДДР, хорошая производительность. В случае когда требуется в основном чтение данных, и делать часто ничего не требуется. Ну а если много записи, то поработать придется, и результат вероятно будет менее звездный. Тут придется оценивать цены к производительности. Если же приложение вычислительного типа, то вариантов применения ПМем пока немного. Скорее всего должны быть большие объемы статических данных, которые требуется часто загружать для расчетов.