このエントリは以下の一連のエントリをベースにしたものです。
This entry is based on the following ones written by Mario Wolczko (Architect, Oracle Labs) and Bill Bridge (Software Architect, Oracle).
- Part 1: Introducing NVM
https://medium.com/@mwolczko/non-volatile-memory-and-java-7ba80f1e730c - Part 2: The view from software
https://medium.com/@mwolczko/non-volatile-memory-and-java-part-2-c15954c04e11 - Part 3: Benefits and challenges of Non-Volatile RAM
https://medium.com/@mwolczko/non-volatile-memory-and-java-part-3-ebe305ef4bc4 - Part 4: Java and non-volatility (このエントリ)
https://medium.com/@mwolczko/non-volatile-memory-and-java-part-4-17f7a7f78f1e
また、筆者がこのテーマで語った際のスライド、動画は以下からご覧頂けます。
PROGRAMMING LANGUAGE IMPLEMENTATION SUMMER SCHOOL 2019
The Coming Persistence Apocalypse
https://pliss2019.github.io/mario_wolczko_slides.pdf
Part 4: Java and non-volatility
すでに説明した考慮点に加え、Javaにはほかの機会や課題があります。
Javaは最も広く使用されているプログラミング言語です。私見ですが、最大の課題は、既存の実践者の多くを疎外したり混乱させたりせず、また非揮発性を利用するために既存のコードに大規模な手術を必要とせずにNVMサポートを統合することだと考えています。
全てのプログラミング言語にはデザインセンターがあります。これはプログラマーが経験を積むうちになれてくる一連の値、特性、慣習、関連する知識です。この点で言語間に違いがありますが、作業に適したツールを選択するのが最善なので、これはよいことです。動的な型付け言語は順応性を重視しますし、弱い型付け言語はパフォーマンスを重視します。 対照的に、Javaではプログラマが実行時に型エラーが発生しないように型を指定する必要があります。Java SEのVMは、1990年代後半から動的なフィードバックを介したコンパイルを使用し、優れたピークパフォーマンスを提供します。起動時のパフォーマンスと一時停止時間はより大きな問題ですが、コミュニティの一部は実装者にこれらの改善を迫っており、ベンダーは以下のようなソリューションを提供しています。ヘビーウェイトコンパイル(heavyweight compilation)には独自のスレッドがあり、concurrent low-pause GCは 多くのR&Dの主題であり、事前コンパイル(ahead-of-time compilation)はスタートアップ時間への対応です。
GraalVM Native Image
https://www.graalvm.org/docs/reference-manual/aot-compilation/
このような背景を考えると、NVMの拡張機能は、これらの領域での後退ステップを回避するために最善を尽くすべきです。基盤となるNVRAMはDRAMよりも遅いため、ピークパフォーマンスの低下は避けられそうにありません。さらに、リカバリを実装するための機構にはコストがかかりますが、これらを最小限に抑えて、プログラマの生産性と互換性のハイレベルな目標と一致することが重要です。これは難しい問題であり、解決するには多くの人々の多大な努力が必要です。
これは単なる予感ですが、長所と短所を備えた実行可能ないくつかの選択肢が生まれると考えていますし、それを促すべきです。コミュニティはトレードオフを理解すべきで、そうすれば貴重なフィードバックを提供できる可能性があります。明確な勝者が現れない場合は、長年にわたっていくつかの異なるアプローチが共存する可能性さえあります。一部のアプローチには技術的な優位性がありますが、より多くのプログラマーのトレーニングまたは既存のコードの適応が必要な場合があります。Javaプラットフォームは20年以上にわたって大幅に進化しており、さらに20年続く場合はさらに進化することでしょう。
Resilience is key
NVMが成功するためには、アプリケーションがデータ構造を長期間にわたって正しく維持でき、障害に強くなければなりません。障害の都度NVMの状態を破棄する場合、その唯一の利点は計画的な再起動の高速化です。この回復力(resilience)には、リカバリーの一貫性ポイントを指定するために、プログラマーのより一層の努力が必要です。 さらに悪いことに、控えめに言ってもリカバリのテストは困難です。多くの同時状態と相互作用によって、障害が発生する可能性がある膨大な数のポイントがあるため、障害の徹底的なシミュレーションによって正確性を確立しようとすることは現実的ではないからです。代わりに、プログラマーは主に構造と推論によって正確さに到達する必要があり、ファシリティの設計がこれがどれほど難しいかを決定する上で大きな役割を果たします。
- 障害が容認できるほどに軽減される場合(プログラムは一貫性を正しく実装し、リカバリがうまく機能する)、そして
- ソフトウェアのバージョン管理の問題(動的なソフトウェアアップデートとも呼ばれる)に対処済みの場合。つまり、プログラムの進化と、ツールとトレーニングの実施に合わせて、インメモリデータ構造を進化させる方法を理解している場合。
この追加の取り組みの利点は、再起動時間の短縮と低遅延のアップデートです。そのため、すべてのアーキテクトはそれらを達成するために必要な多大な労力と利点を比較検討する必要があります。
On- or off-heap persistence?
Javaオブジェクトを永続化することは可能でしょうか、それとも永続性をオフヒープメモリに限定する必要がありますか?最近まで、オフヒープデータは、Java Native Interface(JNI)を使ったネイティブコードの呼び出し、sun.misc.Unsafe、またはByteBufferのようなjava.nioインターフェイスを介してアクセスする必要がありました。 IntelのPersistent Collections for Javaライブラリがとるアプローチでは、オフヒープの永続性を利用しています(このライブラリについては、将来の記事で詳しく説明します)。
Persistent Collections for Java
https://github.com/pmem/pcj
JNIによるデータアクセスは扱いにくく、比較的低速です。外部コードとデータにアクセスするための新しいメカニズムがProject Panamaで定義されており、これにより、オフヒープデータへのアクセスが大幅に簡単かつ迅速になります。
Project Panama: Interconnecting JVM and native code
http://openjdk.java.net/projects/panama/
オフヒープの永続性で十分でしょうか、それともJavaを拡張して、Javaオブジェクト、配列、クラスなどをNVRAMに常駐させる必要がありますか?オフヒープアクセスで当面のニーズを満たし、現在はJNIで、まもなくしたらProject Panamaで使用できるようになりますが、通常のJavaエンティティの永続化は避けられなさそうです。結局、NVRAMは、互換性のあるロードストアインターフェイスです。即時の可用性以外に、外部データ/コードインターフェイスを介してNVRAMを活用する利点はなく、プログラミングの複雑さと追加のオーバーヘッドの2つの欠点があります(ヒープ上、オフヒープのメタデータの重複、言語を横断するための追加命令、スペースのオーバーヘッドなど)。
Additional challenges
Unmanaged or “native” code
NVMヒープ破損の潜在的な原因は、同じアドレス空間内のアンマネージコードのバグです。最良の場合、結果はマネージコードのエラーであり、データは以前の一貫性のある正しいバージョンに戻されますが、最悪の場合、データは分からないまま破損し、多額の金額の誤った転送のような本当に悪いことが起こるまでエラーが検出されません。これは新しい問題ではありませんが、誤ったメモリ操作による永続データの破損の可能性は新しいものであり、そのような破損データのが発生する可能性が高くなります。
Garbage collection
非常に大きなヒープ(テラバイト級)のGCはめったに実行されず、それゆえに以下のような潜在的な問題があります。
- 誘導されたpause
- GC専用のCPUリソース
- GC遅延によるメモリの浪費
- GCが追いつかない場合のメモリ枯渇による潜在的なシステム障害、など
NVMには従来の仮想アドレッシングを使ってアクセスしますが、更新の待ち時間が短くなるためページングされる可能性は低く、そのためNVMの使用に厳しい制限があります。プラス面として、永続ヒープ領域が継続的に使用されていない場合、アプリケーションの速度を低下させずにGCを「オフライン」で実行できます。スローダウンの影響を受けやすく、多くの永続的なガベージを作成しないことを理由として、こちらが優先モードになっているアプリケーションが存在する場合があります。
The stability of heap formats and object layouts
これまで、オブジェクト・レイアウトやヒープのフォーマットは関連するJVMインスタンスのみに対する関心事で、JVMが任意のタイミングで再起動されるように変更できます。しかし、ヒープ領域が永続化される場合、オブジェクト・レイアウトやヒープのフォーマットは長期間にわたって安定する必要があります。JVMが複数のフォーマットやレイアウトに対応できないかぎり、変更が影響を及ぼすでしょう(現在のJVMはそのように設計されていません)。同じJVM実装のバージョン間だけでなく、異なる実装間でもNVMヒープデータを移植できるようにすること、またはJava以外の言語から直接アクセスできるようにすることが望ましい場合もあります。
Persistent type information
永続ヒープをアプリケーションにアタッチする場合、永続ヒープに含まれるオブジェクトの型がアプリケーションが期待する型と一致することをどのように保証するのでしょうか。
最小要件は、JVMが提供する整合性を損なわないことを確認するために型をチェックすることです。構造的な整合性を検証するには、各オブジェクトのフィールドの順序と型が、アプリケーションで使用されているものと一致する必要があります。例えば、永続オブジェクトに2個のdouble型が含まれている場合、アプリケーションの関連クラスにも2個のdouble型を含める必要があります。参照型を検出した場合、参照対象のフィールドなどに対し再帰的に照合を実行します。このマッチングがインクリメンタルであることが望ましい場合があります。大きな永続ヒープ内のすべての型のマッチングには時間がかかり、特定の実行ではアクセスされない型もあるからです。
単純な構造マッチングでは、いくつかの基本的なエラーを防ぐことはできません。例えば、アプリケーションがデカルト座標を表すために2個のdouble型のオブジェクトがあると想定しているけれども、それらが極座標のインスタンスとして作成された場合、単純な構造マッチングがすべて提供されていても、エラーを検出しない可能性があります。クラス名の一致を求めることで安全性を高めることができますが、クラスバージョンの不一致によるエラーは検出できません(例えば、ヒープにはxおよびy座標のデカルト座標が含まれますが、アプリケーションはそれらが他の順序であると想定している場合)。フィールド名を含めて一致させるとこのエラーは解消されますが、オブジェクトを作成したバージョンとオブジェクトを消費するバージョン(例えばy軸の想定方向)の間でコードに不一致が残る可能性があります。
この問題の解決策の1つは、永続クラスのすべてのバージョンに対して一意のIDを保証し、アプリケーションで使用されているクラスと照合することです。例えば、バイトコードを含むクラスの暗号学的ハッシュは、すべての依存クラス(継承クラスなど)の暗号学的ハッシュに依存しており、クラスが実際に同じであることを確認するのに十分です。または、プログラマーがIDを手動で割り当てることもできます(NVM-Directが採用したアプローチ)。後者には、クラスファイルの根本的な変更が重要でない場合にIDを変更する必要がないという利点があります。そうでなければ、こうした変更は誤ったエラーを引き起こすでしょう。しかしながら、プログラマーは変更の影響をより詳細に理解する必要があります。
NVM Direct – A C library to support applications that map Non-Volatile Memory into their address space for load/store access.
https://github.com/oracle/nvm-direct
Type evolution
要件がかわると、それに応じてソフトウェアも変更する必要があり、この結果、多くの場合データの構造または解釈に変更が発生します。揮発性データ構造の場合、一般にRAMではなく外部にデータの正規バージョン(Canonical Version)を置くことでこれはデータの正規バージョンを外部に(つまりRAMにではなく)配置して処理します。ソフトウェアの新しいバージョンが起動すると、外部データからデータを取り込んで、アップデートされたデータ構造を構築します。このアプローチは、不揮発性データ構造にも採用できますが、以下の欠点があります。
- 外部にデータコピーが存在し、それを維持するためのすべてのコードが必要です。不揮発性データをミラーリングすることで回復力を得る場合、そのようなコピーは存在しない可能性があります。
- データの再読み込みに時間がかかり、可用性が低下する可能性があります。大きな構造ではあるものの、クラスに新しいフィールドを追加するといった簡単な変更の場合、これは懲罰的です。特にマネージド・ランタイムでは、構造全体を再構築することなくこれを達成できる必要があります。
古いオブジェクトを新しいバージョンで再利用するため、インクリメンタルなアップデートが実行できると望ましいでしょう。実現するには、そのような更新を記述するプログラミング言語への拡張が必要です。おそらく、フィールドの追加などの単純な更新のためのデフォルトのメカニズムを提供し、プログラマーが重要な更新の拡張ロジックを提供できるようにします。Javaの場合、ツールインターフェイスのJVMTI内にクラスの再定義という形で基本的なメカニズムがすでに存在します。
Redefine Classes – JVMTM Tool Interface (9.0)
https://docs.oracle.com/javase/10/docs/specs/jvmti.html#RedefineClasses
Javaアプリケーション内で使用するには、ツールインターフェイスとしてではなく、アプリケーション内で使用するためのより中心的な役割で提供する必要があります。しかも効率性を保証して(例えばインクリメンタルで)完全に実装する必要があります(現在はオプション扱い)。
より野心的な目標は、UpgradeJの方法で、バージョン管理のための直接t的な言語サポートを提供することです。探索に対する潜在的な方針の1つは、永続領域を自己記述的にすることです。つまり、リージョン内に完全なクラス定義(バージョン情報が追加されているかもしれません)が付随します。アプリケーションは、付随するクラスを使ってのみ永続オブジェクトにアクセスすることで、コードがデータと同期しているという確信を高めます。
UpgradeJ: Incremental typecheckingfor class upgrades
https://www.cl.cam.ac.uk/techreports/UCAM-CL-TR-716.pdf
この領域は、これまでの研究でまだ比較的調査されておらず、完全な理解に至るためにかなりの思考と実験が必要になるでしょう。