JEP 352: Non-Volatile Mapped Byte Buffers
Байтовые буферы, отображённые на энергонезависимую память
| Ответственный | Andrew Dinn |
| Тип | Feature |
| Область | JDK |
| Статус | Closed / Delivered |
| Выпуск | 14 |
| Компонент | core-libs / java.nio |
| Обсуждение | core dash libs dash dev at openjdk dot java dot net |
| Рецензенты | Alan Bateman, Vladimir Kozlov |
| Одобрен | Brian Goetz |
| Создан | 2018/07/19 15:36 |
| Обновлён | 2026/07/29 18:44 |
| Задача | 8207851 |
Аннотация
Добавить новые режимы отображения файлов, специфичные для JDK, чтобы с помощью API FileChannel можно было создавать экземпляры MappedByteBuffer, которые ссылаются на энергонезависимую память.
Цели
Этот JEP предлагает доработать MappedByteBuffer, чтобы поддержать доступ к энергонезависимой памяти (NVM). Единственное необходимое изменение API — новое перечисление, с помощью которого клиенты FileChannel запрашивают отображение файла, расположенного в файловой системе на основе NVM, а не в обычной системе хранения файлов. Благодаря недавним изменениям в API MappedByteBuffer он поддерживает всё поведение, необходимое для прямого обновления памяти и для гарантий сохранности (durability), которые нужны клиентским библиотекам Java более высокого уровня для реализации персистентных типов данных (например, блочных файловых систем, журналов, персистентных объектов и т. д.). Реализации FileChannel и MappedByteBuffer нужно доработать, чтобы они учитывали этот новый тип носителя для отображаемого файла.
Основная цель этого JEP — сделать так, чтобы клиенты могли обращаться к NVM и обновлять её из программы на Java эффективно и согласованно. Ключевой элемент этой цели — сделать так, чтобы отдельные записи (или небольшие группы смежных записей) в область буфера можно было фиксировать с минимальными накладными расходами, т. е. гарантировать, что все изменения, которые ещё могут находиться в кэше, записаны обратно в память.
Вторая, подчинённая цель — реализовать это поведение фиксации с помощью ограниченного внутреннего API JDK, определённого в классе Unsafe, чтобы его могли повторно использовать и другие классы, кроме MappedByteBuffer, которым может понадобиться фиксировать NVM.
Последняя, связанная с ними цель — позволить существующим API мониторинга и управления отслеживать буферы, отображённые на NVM.
Примечание. Уже сейчас можно отобразить файл устройства NVM в MappedByteBuffer и фиксировать записи текущим методом force(), например используя библиотеку Intel libpmem в качестве драйвера устройства или вызывая libpmem как нативную библиотеку. Однако с текущим API обе эти реализации дают решение «кувалдой». Операция force не различает чистые и грязные строки и требует системного вызова или вызова JNI для каждой обратной записи. По обеим этим причинам существующая возможность не удовлетворяет требованию эффективности этого JEP.
Целевые сочетания ОС и процессора для этого JEP — Linux/x64 и Linux/AArch64. Это ограничение введено по двум причинам. Эта возможность будет работать только в ОС, которые поддерживают флаг MAP_SYNC системного вызова mmap, позволяющий синхронно отображать энергонезависимую память. Это верно для недавних выпусков Linux. Кроме того, она будет работать только на процессорах, которые поддерживают обратную запись строк кэша под управлением пользовательского пространства. И x64, и AArch64 предоставляют инструкции, отвечающие этому требованию.
Что не является целью
Цели этого JEP не выходят за рамки предоставления доступа к NVM и гарантий её сохранности. В частности, этот JEP не ставит целью поддержку других важных видов поведения, таких как атомарное обновление NVM, изоляция читателей и писателей или согласованность независимо сохраняемых состояний памяти.
Недавние выпуски Windows/x64 поддерживают флаг mmap MAP_SYNC. Однако цель предоставить эту возможность для этого сочетания ОС и процессора (или для любых других возможных платформ) откладывается до более позднего обновления.
Критерии успеха
Цель по эффективности трудно точно измерить количественно. Однако стоимость сохранения данных в память должна быть значительно ниже по сравнению с двумя существующими альтернативами. Во-первых, она должна быть значительно ниже затрат на синхронную запись данных в обычное файловое хранилище, т. е. с учётом обычных задержек, необходимых, чтобы гарантировать, что отдельные записи попадут на диск. Во-вторых, стоимость также должна быть значительно ниже затрат на запись в NVM с помощью решения на основе драйвера, которое полагается на системные вызовы, такие как libpmem. Разумно ожидать, что затраты снизятся на порядок по сравнению с синхронной записью в файлы и в два раза по сравнению с использованием системных вызовов.
Мотивация
NVM даёт прикладным программистам возможность создавать и обновлять состояние программы между её запусками без значительных затрат на копирование и/или преобразование, которые обычно подразумевают вывод на персистентный носитель и ввод с него. Это особенно важно для транзакционных программ, где для восстановления после сбоя требуется регулярно сохранять неопределённое (in-doubt) состояние.
Существующие библиотеки C (например, libpmem от Intel) предоставляют программам на C очень эффективный доступ к NVM на базовом уровне. На этой основе они также поддерживают простое управление разнообразными персистентными типами данных. Сейчас использование из Java даже одной лишь базовой библиотеки обходится дорого из-за частой необходимости выполнять системные вызовы или вызовы JNI, чтобы вызвать примитивную операцию, которая гарантирует сохранность изменений памяти. Та же проблема ограничивает использование библиотек более высокого уровня и усугубляется тем, что персистентные типы данных, предоставляемые в C, размещаются в памяти, недоступной напрямую из Java. Это ставит приложения и промежуточное ПО на Java (например, менеджер транзакций на Java) в крайне невыгодное положение по сравнению с C или языками, которые могут с малыми затратами подключать библиотеки C.
Это предложение пытается решить первую проблему, позволяя эффективно выполнять обратную запись NVM, отображённой в ByteBuffer. Поскольку память, отображённая через ByteBuffer, напрямую доступна из Java, это позволяет решить вторую проблему, реализовав клиентские библиотеки, эквивалентные библиотекам на C, для управления хранением различных персистентных типов данных.
Описание
Предварительные изменения
Этот JEP использует два связанных улучшения API Java SE:
-
Поддержка режимов отображения, определяемых реализацией (JDK-8221397)
-
Метод
MappedByteBuffer::forceдля указания диапазона (JDK-8221696)
Предлагаемые изменения API, специфичного для JDK
- Предоставить новые значения перечисления
MapModeчерез публичный API в новом модуле
Новый модуль jdk.nio.mapmode будет экспортировать единственный новый пакет с тем же именем. В этот пакет будет добавлено публичное перечисление-расширение ExtendedMapMode:
package jdk.nio.mapmode;
. . .
public class ExtendedMapMode {
private ExtendedMapMode() { }
public static final MapMode READ_ONLY_SYNC = . . .
public static final MapMode READ_WRITE_SYNC = . . .
}
Новые значения перечисления используются при вызове метода FileChannel::map, чтобы создать соответственно доступный только для чтения или для чтения и записи MappedByteBuffer, отображённый на файл устройства NVM. Если передать эти флаги на платформах, которые не поддерживают отображение файлов устройств NVM, будет выброшено UnsupportedOperationException. На поддерживаемых платформах передавать эти новые значения в качестве аргументов уместно, только когда целевой экземпляр FileChannel получен из файла, открытого через устройство NVM. В любом другом случае будет выброшено IOException.
- Опубликовать
BufferPoolMXBean, отслеживающий статистику персистентныхMappedByteBuffer
Класс ManagementFactory предоставляет метод List<T> getPlatformMXBeans(Class<T>), с помощью которого можно получить список экземпляров BufferPoolMXBean, отслеживающих count, total_capacity и memory_used для существующих категорий отображённых или прямых байтовых буферов. Он будет изменён так, чтобы возвращать дополнительный новый BufferPoolMXBean с именем "mapped - 'non-volatile memory'", который будет отслеживать эту статистику для всех экземпляров MappedByteBuffer, отображённых в данный момент в режиме ExtendedMapMode.READ_ONLY_SYNC или ExtendedMapMode.READ_WRITE_SYNC. Существующий BufferPoolMXBean с именем mapped будет по-прежнему отслеживать статистику только для экземпляров MappedByteBuffer, отображённых в данный момент в режиме MapMode.READ_ONLY, MapMode.READ_WRITE или MapMode.PRIVATE.
Предлагаемые изменения внутреннего API JDK
-
Добавить новый метод
writebackMemoryв классjdk.internal.misc.Unsafepublic void writebackMemory(long address, long length)
Вызов этого метода гарантирует, что все изменения памяти в диапазоне адресов, который начинается с address и продолжается до address + length (но не обязательно включая его), записаны обратно из кэша в память. Реализация должна гарантировать, что все операции сохранения текущего потока, которые i) ожидают выполнения в момент вызова и ii) адресуют память в целевом диапазоне, включаются в обратную запись (т. е. вызывающей стороне не нужно выполнять никаких операций барьера памяти перед вызовом). Она также должна гарантировать, что обратная запись всех адресуемых байтов завершена до возврата (т. е. вызывающей стороне не нужно выполнять никаких операций барьера памяти после вызова).
Операция обратной записи памяти будет реализована с помощью небольшого числа интринсиков, распознаваемых JIT-компилятором. Цель — реализовать обратную запись каждой последовательной строки кэша в указанном диапазоне адресов с помощью интринсика, который транслируется в инструкцию процессора для обратной записи строки кэша, и тем самым свести затраты на сохранение данных к абсолютному минимуму. Предполагаемый дизайн также использует интринсики синхронизации памяти до и после обратной записи. Они могут транслироваться в инструкцию синхронизации памяти или в пустую операцию в зависимости от конкретного выбора инструкции для обратной записи процессора (у x64 есть три возможных кандидата) и требований к упорядочиванию, которые влечёт этот выбор.
Примечание. Веская причина реализовать эту возможность в классе Unsafe в том, что она, вероятно, окажется полезной в более общих случаях, скажем, для альтернативных реализаций сохранения данных, использующих энергонезависимую память.
Альтернативы
В исходном прототипе были проверены две альтернативы.
Первый вариант — использовать libpmem в режиме драйвера, т. е. 1) установить libpmem как драйвер устройства NVM, 2) отобразить файл так же, как любой другой MappedByteBuffer, и 3) полагаться на метод force для выполнения обновления.
Вторая альтернатива — использовать libpmem (или какую-то её часть) как нативную библиотеку JNI, чтобы обеспечить нужное отображение буфера и поведение обратной записи.
Оба варианта оказались крайне неудовлетворительными. Первый страдал от высокой стоимости системных вызовов и накладных расходов на принудительную запись (force) всего отображённого буфера, а не какой-то его части. Второй страдал от высокой стоимости интерфейса JNI. Последовательные итерации второго подхода (сначала с добавлением зарегистрированных нативных методов, а затем с их реализацией в виде интринсиков) дали выигрыш в производительности, сходный с текущей черновой реализацией
Третья рассмотренная альтернатива — дождаться, когда проект Panama обеспечит доступ к внешним библиотекам и внешним типам данных, отображённым на NVRAM, без накладных расходов JNI. Хотя это по-прежнему считается стоящим вариантом на будущее, было решено, что текущее предложение стоит развивать по двум причинам: во-первых, чтобы пользователи могли сразу экспериментировать с использованием NVRAM из Java, по мере того как она начинает становиться доступной; и во-вторых, чтобы облегчить такой переход, поддерживая модель использования NVRAM, основанную на существующем и привычном API MappedByteBuffer.
Тестирование
Для тестирования потребуется хост x64 или AArch64 с устройством NVM и достаточно свежим ядром Linux (4.16).
Тестирование на AArch64 может оказаться невозможным, пока для этой архитектуры не появятся подходящие устройства NVM. В качестве альтернативы тестирование, возможно, придётся проводить, отображая энергозависимую память и используя её для имитации поведения устройства NVM.
Тестирование на обеих целевых архитектурах может быть затруднено; в частности, возможны ложноположительные результаты. Сбой в коде обратной записи можно обнаружить, только если удастся завершить JVM, пока эти ожидающие изменения не сброшены, а затем обнаружить это упущение при перезапуске.
Такую ситуацию может быть трудно создать при обычном завершении JVM (обычное завершение работы может привести к тому, что эти ожидающие изменения будут записаны обратно). Поскольку JVM не полностью управляет работой системы памяти, обнаружить проблему может оказаться трудно даже при аварийном завершении (скажем, завершении по kill -KILL).
Риски и допущения
Эта реализация позволяет управлять NVM как ресурсом вне кучи через ByteBuffer. В связанном улучшении, JDK-8153111, рассматривается использование NVM для данных кучи. Возможно, также придётся рассмотреть использование NVM для хранения метаданных JVM. Может оказаться, что эти разные режимы управления NVM несовместимы или, возможно, просто неуместны при совместном использовании.
Предлагаемый API может работать только с отображёнными областями размером до 2 ГБ. Возможно, придётся доработать предлагаемую реализацию, чтобы она соответствовала изменениям, предложенным в JDK-8180628 для снятия этого ограничения.
API ByteBuffer в основном ориентирован на доступ относительно позиции (курсора), что ограничивает возможности одновременного обновления независимых областей буфера. Такие обновления требуют блокировки буфера на время обновления, как подробно описано в JDK-5029431, где также было реализовано решение. Проблема в некоторой степени смягчается наличием методов доступа к примитивным значениям, которые работают по абсолютному индексу без обращения к курсору, что позволяет обращаться к буферу без блокировки; а также возможностью использовать срезы ByteBuffer и MethodHandles для одновременной записи и чтения примитивных значений.