Серия коротких статей о влиянии энергонезависимой памяти (NVM) на платформу Java.

В первой статье я описал основные аппаратные характеристики новой энергонезависимой памяти Intel Optane. В этой статье я рассмотрю несколько вопросов программного обеспечения.

Часть 2. Взгляд со стороны программного обеспечения

При использовании NVRAM будет выглядеть так же, как DRAM, и доступ к ней осуществляется с помощью обычных инструкций, связанных с памятью (загрузка, сохранение и т. д.), как к памяти с виртуальной адресацией.

Структуры данных в файловой системе

Чтобы обеспечить долгосрочную организацию и доступ к данным в NVRAM, данные будут инкапсулированы в файловую систему. Файловая система позволит содержать независимые структуры в отдельных файлах с сопутствующими метаданными (имя файла, разрешения, метки времени и т. д.). Читатели определенного возраста помнят символическую особенность ПК 1980-х, RAM-диск, но в этом случае содержимое не теряется при перезагрузке машины. Как и в случае с RAM-диском, доступ к данным можно получить с помощью операций с файловой системой, но при этом будут утрачены преимущества (оперативность и степень детализации) интерфейса загрузки-сохранения.

Чтобы получить наилучшую производительность и истинную семантику памяти, драйвер файловой системы в ОС должен обеспечивать прямой доступ к загрузке и хранению, а не копирование данных. Типичный шаблон доступа будет состоять в том, чтобы открыть файл в этой файловой системе, отобразить его в адресное пространство приложения, оперировать данными напрямую, а затем закрыть файл, тем самым отменив отображение данных. Это известно как модель прямого доступа (DAX) и уже поддерживается некоторыми основными операционными системами (Linux и Windows).

Размещение долгоживущих данных в файловой системе имеет очевидные преимущества. Есть наработанные решения для резервного копирования и архивирования, иерархической организации, контроля доступа, учета пространства и так далее.

Независимость от позиции

Желательно, чтобы данные в файле DAX были расположены независимо от позиции. Если содержимое должно быть загружено по определенному адресу, существует вероятность конфликта адресов между независимыми файлами, и вероятность этого возрастет, если файл должен быть переносимым в различные системы и приложения. Кроме того, данные в файле DAX могут быть одновременно сопоставлены с разными адресами, в разных процессах или даже в одном процессе. Кроме того, современные операционные системы обеспечивают рандомизацию размещения адресного пространства, чтобы вредоносному ПО было сложнее изменять данные: при каждом запуске адрес сегмента данных рандомизируется, так что изучение адреса данных за один запуск бесполезно при желании. для изменения данных в последующих запусках.

Один из способов добиться независимости от позиции — сделать все внутренние ссылки относительными. Другой способ — внедрить метаданные о перемещении, позволяющие перемещать содержимое либо все сразу при отображении файла, либо постепенно (например, при сбое начальной страницы). Некоторые файлы могут быть очень большими (терабайты и более), поэтому неинкрементное перемещение может свести на нет преимущества энергонезависимости при более быстром запуске. Кроме того, перемещение не поддерживает одновременное сопоставление.

Текущее программное обеспечение обычно не работает с самостоятельными данными, и языки программирования обычно не поддерживают его. Изменение приложения, написанного на неуправляемом языке, например, C, для использования самоотносительной адресации может стать серьезной задачей. Альтернативой является предоставление поддержки языка и компилятора. Например, NVM-Direct, расширенная версия C, требует от программиста различать ссылки в постоянной памяти и делает их самостоятельными; компилятор позаботится о деталях. Это место, где управляемая среда выполнения, например, JVM, может помочь, предоставляя самоотносительную адресацию прозрачно для приложения, которое не будет знать об изменении.

Иерархия энергозависимой памяти выше NVRAM

Доступ к NVRAM обычно опосредован иерархией памяти, которая содержит несколько уровней кэширования и буферизации. Эта иерархия построена из обычной энергозависимой памяти и теряет свое содержимое при отключении питания. Следовательно, сохранение в NVRAM не станет устойчивым до тех пор, пока строка кэша, содержащая измененные данные, не будет записана в NVRAM.

Проницательный читатель спросит: в какой момент запись становится долговечной? Между кэшами и чипами NVRAM находится управляющая логика, поддерживающая энергозависимое состояние, такое как дополнительная буферизация. Как узнать, что данные покинули эти буферы и попали в чипы NVM, даже после очистки кеша? Ответ таков: это не имеет значения. Intel гарантирует, что в случае отключения питания энергозависимое состояние контроллеров памяти будет зафиксировано в NVM. Предположительно имеется доступный и достаточный источник энергии (например, суперконденсатор). Это свойство несколько сбивает с толку и называется Асинхронное обновление DRAM (ADR) — связь с обновлением DRAM несколько непрозрачна. Дополнительные сведения см. в Программирование энергонезависимой памяти и в этой статье Intel Энди Рудоффа.

Между прочим, маловероятно, что состояние, хранящееся в ЦП (регистры, кэши), станет энергонезависимым в течение, скажем, гораздо меньшего десятилетия. Хотя некоторые разрабатываемые технологии, например Spin-Transfer Torque RAM, кажутся многообещающими в качестве энергонезависимой замены SRAM (статической RAM, используемой для кэшей), они вряд ли скоро станут конкурентоспособными (Emerging Memory Technologies , Ю и Чен, 2016 г.). Единственным исключением является развивающаяся область так называемых энергонезависимых процессоров. Идея здесь состоит в том, чтобы использовать энергонезависимое состояние в процессорах, предназначенных для приложений IoT, собирающих энергию (то есть, которые работают эфемерно, только когда достаточно энергии может быть получено из их среды). Однако далее мы это рассматривать не будем, так как это не актуально для процессоров в дата-центрах и на десктопах.

Запись строк кэша в NVRAM

Intel добавила в x64 две инструкции для облегчения обратной записи строк кэша:

  • CLFLUSHOPT (оптимизированная строка кэш-памяти) удаляет каждую строку кэша, содержащую указанный адрес. В отличие от старого CLFLUSH, он не упорядочен относительно записи в другие строки кэша.
  • CLWB (обратная запись строки кэша) записывает обратно измененную строку кэша, но не принудительно удаляет ее, и поэтому является более полезным из двух (или трех).

Для обеспечения долговечности в приложения необходимо будет вставлять инструкцию обратной записи после энергонезависимых обновлений каждой строки кэша; программистом (или автором библиотеки) на неуправляемом языке (или, возможно, компилятором, если он знает, когда изменяются энергонезависимые данные), или средой выполнения управляемого языка. Эти инструкции обновляют NVRAM асинхронно; инструкция забора может использоваться для ожидания, пока обновление не станет устойчивым. Альтернативой является использование msync() или эквивалента, но это, скорее всего, просто оболочка для обратной записи и ограждения и влечет за собой накладные расходы на системные вызовы.

Одна из предложенных схем, которая устраняет обратную запись и ограничения, состоит в том, чтобы система хранила достаточно энергии, чтобы в случае потери питания она могла перезаписать из кэшей все несохраненные энергонезависимые данные. В системе, реализующей эту схему, записи, по сути, сразу становятся долговечными и делают специальные инструкции ненужными. Это стало известно как расширенный ADR; я не думаю, что было объявлено о каких-либо системах, включающих это. Инструкция WBINVD (Write Back and Invalidate Cache) в чем-то связана, но не является полным решением: она записывает обратно содержимое всех кэшей, но затем делает их недействительными (нежелательно); похоже, не существует способа узнать, когда обратная запись завершена, поскольку выполнение продолжается немедленно; и это привилегированная инструкция и, таким образом, влечет за собой системные вызовы. WBNOINVD (Write Back and Do Not Invalidate Cache, появившийся в Ice Lake) — то же самое, но без аннулирования.

Проблема наблюдаемости

Как заметил мой коллега Билл Бридж, видимость отдельно от долговечности становится ловушкой для неосторожных (см. статью ниже, чтобы узнать больше). Подводя итог: после того, как поток обновляет общее местоположение в NVRAM, обновление становится видимым (через когерентность кеша) до того, как значение станет устойчивым (после обратной записи). Это оставляет окно, в котором другой поток может выполнять действия, основанные на просмотре нового значения, но до того, как он узнает, что значение является устойчивым. Это может включать в себя создание зависимых обновлений в другом месте, которые могут стать устойчивыми раньше оригинала; сбой на этом этапе, например, из-за сбоя или сбоя питания, может привести к несогласованному состоянию во время восстановления.

В следующей статье я рассмотрю некоторые последствия этих аппаратных и программных характеристик.

ПРОБЛЕМА НАБЛЮДАЕМОСТИ С ПОСТОЯННОЙ ПАМЯТЬЮ

Билл Бридж, Oracle

Я обнаружил проблему наблюдаемости во всех конструкциях постоянной памяти, о которых я когда-либо слышал. Основная проблема заключается в том, что сохранение в постоянной памяти можно наблюдать до того, как оно станет постоянным. Другой процессор может увидеть данные и сделать другое обновление, которое станет постоянным перед наблюдаемыми данными. Это создает окно, в котором сбой питания может оставить несогласованные данные в постоянной памяти. Исправление предназначено для загрузки из постоянной памяти, чтобы видеть только постоянные данные. Я думаю, что всегда есть программный способ обойти проблему, если вы о ней знаете, но это не надежное решение.

Управление содержимым постоянной памяти

Общепризнано, что программное обеспечение должно контролировать порядок обновлений постоянной памяти, чтобы гарантировать согласованность данных после перезагрузки процессора или сбоя питания.

В современных процессорах без постоянной памяти сохранение в памяти происходит в кэше процессора. В конце концов измененная строка кэша может быть сброшена из кэша в DRAM. Если другой процессор загрузит измененные данные до того, как они попадут в DRAM, он получит данные из кэша исходного процессора. Таким образом, новые данные можно наблюдать до того, как они попадут в DRAM. Это совершенно нормально для DRAM, поскольку все содержимое DRAM теряется при сбое питания или перезагрузке системы. (Это важный метод восстановления для всех компьютеров.)

С постоянной памятью этот механизм неприемлем, так как результат сохранения может находиться в кэше процессора неопределенное время и никогда не стать постоянным. Обычно предлагаемое решение этой проблемы состоит в том, чтобы предоставить программному обеспечению механизм принудительной записи модифицированной строки кэша в постоянную память. Механизм включает в себя средство приостановки до тех пор, пока постоянная память не подтвердит, что данные были успешно записаны и будут видны в случае сброса или сбоя питания.

Предположим, программе необходимо обновить две ячейки постоянной памяти A и B, гарантируя, что B будет постоянным только в том случае, если A будет постоянным. Шаги будут следующими:

  1. Сохранить новое значение в A
  2. Заставить А быть настойчивым
  3. Сохранить новое значение в B
  4. Заставить Б быть настойчивым

Это гарантирует, что любая программа, которая видит новое значение B, также увидит новое значение A. Это верно, даже если система перезагружается после шага 4. Однако, если произошел сброс или сбой питания до завершения шага 4, новое значение значение B не будет видно после перезагрузки, даже если оно было видно другим процессорам после шага 3.

Проблема наблюдаемости

В приведенном выше примере другой программный поток может прочитать новое значение B, как только завершится шаг 3. Затем он может обновить другие ячейки постоянной памяти, увидев новое значение в B. Другие области памяти могут быть на другом контроллере памяти, чтобы они могли стать постоянными до того, как новое значение будет постоянным в B.

Ни в одном из предложений по постоянной памяти, о которых я когда-либо слышал, нет эффективного средства, позволяющего читателю узнать, являются ли данные из инструкции загрузки постоянными или нет. Можно было бы заставить расположение памяти быть постоянным сразу после загрузки. Обычно сила не находит данные грязными, но это все еще много накладных расходов. Сила не может быть выполнена до нагрузки, так как новое значение может быть сохранено сразу после силы.

Реалистичный пример

Циклический буфер — это распространенный метод, используемый для асинхронной связи между двумя разными потоками выполнения. Можно было бы подумать, что содержимое очереди можно сохранить при перезагрузке, поместив структуры данных в постоянную память. Однако использование того же алгоритма с постоянной памятью столкнется с проблемой наблюдаемости. Очередь может постоянно повреждаться даже при правильном принудительном использовании постоянной памяти.

Постоянная циклическая очередь в этом примере имеет следующие характеристики:

  • Существует поток Producer, который сохраняет записи в постоянной очереди.
  • Существует поток-потребитель, который извлекает записи из постоянной очереди.
  • Существует постоянный указатель IN, который продвигается производителем после сохранения записи в очереди.
  • Существует постоянный указатель OUT, который выдвигается Потребителем после обработки записи.
  • Если OUT и IN равны, очередь пуста.
  • Записи после указателя OUT и до указателя IN ожидают в очереди.

Вот сценарий, в котором очередь становится поврежденной из-за проблемы с наблюдаемостью:

Тема производителя:

  1. Считывает IN и OUT, видя, что они равны и, таким образом, есть место для сохранения новой записи.
  2. Сохраняет запись в ячейке кольцевого буфера, на которую указывает IN.
  3. Заставляет данные записи быть постоянными.
  4. Переходит к IN, чтобы включить новую запись в содержимое очереди.
  5. Прерывание переключается на контекст ОС. Выполнение производителя возобновляется через десять микросекунд.
  6. Forcec указатель IN должен быть постоянным, но происходит сбой питания до того, как новое значение станет постоянным.

Потребительская нить:

  1. Считывает IN и OUT, видя, что они равны, поэтому нет записей для обработки.
  2. Использует MWAIT для ожидания перемещения указателя IN.
  3. MWAIT завершается и видит новый указатель IN, указывающий на наличие записи для обработки. Однако IN пока не обновляется постоянно. (Это проблема наблюдаемости)
  4. Обрабатывает новую запись быстрее, чем обработчик прерываний, приостановивший работу Producer.
  5. Продвигает OUT, чтобы соответствовать новому указателю IN, указывающему, что очередь пуста.
  6. Принуждает указатель OUT быть постоянным. Это завершается успешно, делая OUT постоянным.
  7. Сбой питания, в результате чего IN не продвинулся вперед, а OUT продвинулся вперед.

После перезагрузки очередь не пуста, как должна быть. Поскольку IN не был расширен, OUT равен IN+1. В логике кольцевого буфера это означает, что буфер заполнен. Если очередь имеет записи фиксированного размера, содержимое очереди будет состоять из всех старых записей, которые уже были обработаны. Если очередь обрабатывает записи переменного размера, то указатель OUT может находиться в середине старой записи, и содержимое будет мусором.

Существует аналогичная проблема с заполненной очередью и продвижением указателя OUT, которое не становится постоянным, в то время как продвижение указателя IN становится постоянным. С записями фиксированного размера полная очередь становится пустой.

Решение

Самое надежное решение — гарантировать, что при загрузке из постоянной памяти будут видны только постоянные данные. Скорее всего, это потребует изменения алгоритмов когерентности кэша, чтобы приостановить загрузку до тех пор, пока контроллер памяти не сделает данные постоянными. Это заставит постоянную память работать так, как большинство людей предполагает, что она работает.

Существует только программное решение проблемы кольцевого буфера. Будет указатель IN в DRAM и указатель IN в постоянной памяти. Точно так же будут DRAM и постоянные указатели OUT. Добавление или удаление записи из очереди сначала обновит постоянный указатель и обеспечит его постоянство. Затем указатель DRAM будет обновлен, чтобы отразить постоянный указатель. Только указатели DRAM будут считываться для поиска места или записей в очереди. При перезагрузке указатели DRAM будут повторно инициализированы из постоянных указателей.

Альтернативное решение — сбросить текущее значение указателя IN или OUT в NVM после загрузки его в локальную переменную. Это гарантирует, что значение в локальной переменной является либо текущим постоянным значением, либо значением, которое является предыдущей версией постоянной версии. Сброс в NVM перед загрузкой может привести к появлению нового значения, которое никогда не сохранится.

Я ожидаю, что для каждой структуры данных существует программное решение проблемы. Однако есть несколько проблем с тем, чтобы оставить это на усмотрение программного обеспечения.

  • Многие разработчики не осознавали, что существует проблема
  • Каждая структура данных, скорее всего, будет иметь свое решение, даже несмотря на то, что базовая концепция DRAM и постоянной копии всех метаданных будет работать во многих случаях.
  • Проблема, скорее всего, будет настолько редкой, что ее не обнаружат даже при стресс-тестировании.
  • Если проблема возникнет во время стресс-тестирования или на объекте клиента, ее будет чрезвычайно сложно диагностировать.
  • Если разработчик учел проблему и закодировал ее, похоже, нет никакого способа принудительно запустить условие гонки для проверки кода. Как сказал один мудрец: «Если это не проверено, то сломано».

Заключение

Проблема наблюдаемости с постоянной памятью может привести к повреждению постоянных данных в реальном мире. Повреждения настолько редки, что их вряд ли можно диагностировать. Лучшее решение — проектировать процессоры таким образом, чтобы при загрузке из постоянной памяти отображались только постоянные данные.