エンタープライズ・サンドボックス・リソース制限

GraalVMの20.3リリースで、ゲスト・アプリケーションで使用されるリソースを制限できるようにする試験段階のサンドボックス・リソース制限機能が導入されました。これらのリソース制限は、GraalVMのコミュニティ・エディションでは使用できません。このドキュメントでは、ポリグロットAPIのオプションを使用してサンドボックス・リソース制限を構成する方法について説明します

通常、すべてのリソース制限オプションにはsandboxオプション・グループの接頭辞が付いており、GraalVMで提供される言語ランチャ(js --help:toolsなど)を使用してリストできます。ポリグロットのオプションは、言語ランチャを介して、Graal SDKのポリグロット埋込みAPIを使用して、またはJVM上でシステム・プロパティを使用して指定できます。例をより理解するには、リファレンス・マニュアルのポリグロット埋込みガイドを最初に読むことをお薦めします。

現在、すべてのサンドボックス・オプションは試験段階であるため、これらの例では試験段階のオプションが有効であると想定されています(たとえば--experimental-options)。このオプションは、ゲスト・アプリケーションのリソース使用量を制限するためのベスト・エフォート・アプローチです。

リソース制限は、次のオプションを使用して構成できます:

ポリグロット埋込みContextインスタンスごとに異なる構成を指定できます。その他に、実行中の任意の時点で制限をリセットできます。リセットできるのは、sandbox.MaxStatementsおよびsandbox.MaxCPUTimeのみです。

ゲスト言語は、外部実行コンテキスト内で内部コンテキストを作成することを選択する場合があります。制限は、外部コンテキストおよびそこから生成されるすべての内部コンテキストに適用されます。内部コンテキストに対して個別の制限を指定することはできず、内部コンテキストを作成して制限の対象外にすることもできません。

アクティブなCPU時間の制限

sandbox.MaxCPUTimeオプションを使用すると、アプリケーションの実行に費やされる最大CPU時間を指定できます。コンテキストがアクティブなまま最大CPU時間を経過すると、自動的に取り消されてクローズされます。デフォルトでは、時間制限は10ミリ秒ごとにチェックされます。これは、sandbox.MaxCPUTimeCheckIntervalオプションを使用してカスタマイズできます。最大CPU時間制限とチェック間隔は、どちらも正の値である必要があります。デフォルトでは、CPU時間制限は実施されません。時間制限を超えた場合、ポリグロット・コンテキストは取り消され、実行はisResourceExhausted()に対してtrueを返すPolyglotExceptionをスローして停止します。時間制限がトリガーされるとすぐに、アプリケーション・コードはこのコンテキストでそれ以上実行できなくなります。起動されるポリグロット・コンテキストのメソッドに対してPolyglotExceptionをスローし続けます。

コンテキストで使用されたCPU時間には、通常、同期またはIOの待機に費やされた時間は含まれません。すべてのスレッドのCPU時間が合算され、CPU時間制限と照合されます。これは、2つのスレッドで同じコンテキストが実行された場合、時間制限を2倍速く超えることを意味します。

時間制限は、定期的に呼び出される別の優先度の高いスレッドによって実施されます。指定した精度でコンテキストが取り消される保証はありません。ホストVMで完全なガベージ・コレクションが発生した場合など、精度を大幅に外れる場合があります。時間制限を超えない場合、ゲスト・コンテキストのスループットは影響を受けません。あるコンテキストで時間制限を超えた場合、同じ明示的なエンジンを使用する他のコンテキストのスループットが一時的に低下する可能性があります。

時間を指定するために使用できる単位は、ミリ秒の場合はms、秒の場合はs、分の場合はm、時間の場合はh、日の場合はdです。CPU時間制限オプションで負の値を指定したり、時間単位を指定することは許可されていません。

使用例

try (Context context = Context.newBuilder("js")
                           .allowExperimentalOptions(true)
                           .option("sandbox.MaxCPUTime", "500ms")
                           .option("sandbox.MaxCPUTimeCheckInterval", "5ms")
                       .build();) {
    try {
        context.eval("js", "while(true);");
        assert false;
    } catch (PolyglotException e) {
        // triggered after 500ms;
        // context is closed and can no longer be used
        // error message: Maximum CPU time limit of 500ms exceeded.
        assert e.isCancelled();
        assert e.isResourceExhausted();
    }
}

実行される文の数の制限

コンテキストが取り消されるまでコンテキストが実行できる文の最大数を指定します。文の制限がトリガーされたコンテキストは使用できなくなり、そのコンテキストの使用のたびにPolyglotException.isCancelled()に対してtrueを返すPolyglotExceptionがスローされます。文の制限は、実行中のスレッド数には関係なく、コンテキストごとに適用されます。また、ポリグロット埋込みAPIのResourceLimits APIを使用して、この制限を指定することもできます。

デフォルトでは、文の制限は適用されません。この制限を負の数に設定して無効にできます。この制限が適用されるかどうかに関係なく、内部ソースはsandbox.MaxStatementsIncludeInternalを使用してのみ構成できます。デフォルトでは、この制限には内部とマークされたソースの文は含まれません。共有エンジンを使用する場合は、1つのエンジンのすべてのコンテキストで同じ内部構成を使用する必要があります。文の最大制限は、エンジンのコンテキストごとに個別に構成できます。

文の制限をコンテキストにアタッチすると、同じエンジンを使用するすべてのゲスト・アプリケーションのスループットが低下します。実行されるすべての文によって文カウンタが更新される必要があります。本番で使用する前に、文の制限の使用をベンチマークすることをお薦めします。

ゲスト言語によっては、単一の文の複雑度が一定時間ではない場合があります。たとえば、JavaScriptの組込み機能(Array.sortなど)を実行する文は単一の文となる場合がありますが、その実行時間は配列のサイズによって異なります。したがって、文の数の制限はタイム・ボクシングの実行には適しておらず、CPU時間制限など、より信頼性の高い他の対策と組み合せる必要があります。

try (Context context = Context.newBuilder("js")
                           .allowExperimentalOptions(true)
                           .option("sandbox.MaxStatements", "2")
                           .option("sandbox.MaxStatementsIncludeInternal", "false")
                       .build();) {
    try {
        context.eval("js", "purpose = 41");
        context.eval("js", "purpose++");
        context.eval("js", "purpose++"); // triggers max statements
        assert false;
    } catch (PolyglotException e) {
        // context is closed and can no longer be used
        // error message: Maximum statements limit of 2 exceeded.
        assert e.isCancelled();
        assert e.isResourceExhausted();
    }
}

関数のAST深度の制限

ゲスト言語関数の最大式の深度の制限。インストゥルメント可能なノードのみが制限に対してカウントされます。制限を超えた場合、コードの評価は失敗し、コンテキストは取り消されます。

AST深度により、関数の複雑度およびそのスタック・フレーム・サイズを見積もることができます。AST深度の制限は、単一の関数による恣意的なスタック領域使用に対する保護手段として機能します。

スタック・フレーム数の制限

コンテキストがスタック上でプッシュできるフレームの最大数を指定します。この制限を超えると、コンテキストは取り消されます。スレッドローカルなスタック・フレーム・カウンタが関数の開始時に増分され、関数の終了時に減分されます。リソース制限をリセットしても、スタック・フレーム・カウンタには影響しません。

スタック・フレーム制限はそれ自体で、無限再帰に対する保護手段として機能します。AST深度制限とともに使用すると、スタック領域使用の合計を見積もるために使用できます。

アクティブ・スレッド数の制限

コンテキストで同時に使用できるスレッドの数を制限します。デフォルトでは、任意の数のスレッドを使用できます。設定された制限を超えた場合、コンテキストに入るとPolyglotExceptionで失敗し、ポリグロット・コンテキストは取り消されます。リソース制限をリセットしても、スレッド制限には影響しません。

最大ヒープ・メモリーの制限

sandbox.MaxHeapMemoryオプションを使用すると、アプリケーションが実行中に保持できる最大ヒープ・メモリーを指定できます。sandbox.MaxHeapMemoryは正である必要があります。このオプションは、HotSpotベースのVMでのみサポートされます。AOTモードでこのオプションを有効にすると、PolyglotExceptionが発生します。制限の超過が検出されると、対応するコンテキストが自動的に取り消され、閉じられます。

このオプションの有効性は(も)、使用しているガベージ・コレクタによって異なります。

使用例

try (Context context = Context.newBuilder("js")
                           .allowExperimentalOptions(true)
                           .option("sandbox.MaxHeapMemory", "100MB")
                       .build()) {
    try {
        context.eval("js", "var r = {}; var o = r; while(true) { o.o = {}; o = o.o; };");
        assert false;
    } catch (PolyglotException e) {
        // triggered after the retained size is greater than 100MB;
        // context is closed and can no longer be used
        // error message: Maximum heap memory limit of 104857600 bytes exceeded. Current memory at least...
        assert e.isCancelled();
        assert e.isResourceExhausted();
    }
}

実装の詳細およびエキスパート・オプション

この制限は、割当て済みバイトまたは低メモリー通知に基づいてトリガーされた保持サイズ計算によってチェックされます。

割り当てられたバイトは、定期的に呼び出される別の優先度の高いスレッドによってチェックされます。メモリー制限されたコンテキストごとにこのようなスレッドが1つ(sandbox.MaxHeapMemoryが設定されたスレッド)あります。保持バイトの計算は、必要に応じて、割り当てられたバイト・チェック・スレッドから起動されるもう1つの優先度の高いスレッドによってさらに行われます。ヒープ・メモリー制限を超えると、保持バイトの計算スレッドもコンテキストを取り消します。さらに、低メモリー・トリガーが呼び出されると、メモリー制限されたコンテキストが少なくとも1つあるエンジン上のすべてのコンテキストが、割り当てチェッカとともに一時停止されます。すべての個々の保持サイズ計算は取り消されます。メモリー制限されたコンテキストごとのヒープ内の保持バイトは、単一の優先順位の高いスレッドによって計算されます。制限を超えるコンテキストは取り消され、実行は再開されます。

ヒープ・メモリー制限の主な目的は、ほとんどの場合ヒープ・メモリー不足関連のエラーを防止し、不適切なコンテキストが存在する場合でもホストVMをスムーズに実行できるようにすることです。実装はベスト・エフォートです。これは、ヒープ・メモリー制限の正確性に対する保証がないことを意味します。また、ヒープ・メモリー制限の設定は、コンテキストでOutOfMemoryエラーが発生しないことを保証するものでもありません。多数のオブジェクトをすばやく連続して割り当てるゲスト・アプリケーションは、オブジェクトを割り当てることがほとんどないアプリケーションよりも正確性が低くなります。ゲスト・コードの実行は、ホスト・ヒープ・メモリーが不足し、ホストVMの低メモリー・トリガーが呼び出された場合にのみ一時停止します。一時停止の範囲はエンジンであるため、sandbox.MaxHeapMemoryオプションが設定されていないコンテキストも、メモリーが制限されている他のコンテキストとエンジンを共有している場合、一時停止します。また、1つのコンテキストが取り消されると、同じ明示的なエンジンを持つ他のコンテキストの速度が低下する可能性があります。コンテキストによって保持されるサイズの計算方法は、次に示すエキスパート・オプションsandbox.AllocatedBytesCheckIntervalsandbox.AllocatedBytesCheckEnabledsandbox.AllocatedBytesCheckFactorsandbox.RetainedBytesCheckIntervalsandbox.RetainedBytesCheckFactorおよびsandbox.UseLowMemoryTriggerを使用してカスタマイズできます。

コンテキストの保持サイズ計算は、保持されたバイトの推定が、指定されたsandbox.MaxHeapMemoryの特定の係数を超えるとトリガーされます。推定は、コンテキストがアクティブなスレッドによって割り当てられたヒープ・メモリーに基づきます。より正確には、推定とは、前の保持バイトの計算結果(使用可能な場合)と、前の計算の開始以降に割り当てられたバイト数です。デフォルトでは、sandbox.MaxHeapMemoryの係数は1.0で、sandbox.AllocatedBytesCheckFactorオプションでカスタマイズできます。係数には正の値を指定する必要があります。たとえば、sandbox.MaxHeapMemoryを100MB、sandbox.AllocatedBytesCheckFactorを0.5とします。保持サイズの計算は、割り当てられたバイトが50MBに達したときに最初にトリガーされます。計算された保持サイズが25MBとすると、追加の25MBが割り当てられたときに次の保持サイズの計算がトリガーされます。

デフォルトでは、割り当てられたバイトは10ミリ秒ごとにチェックされます。これは、sandbox.AllocatedBytesCheckIntervalで構成できます。指定可能な最小間隔は1ミリ秒です。より小さい値は1ミリ秒と解釈されます。

同じコンテキストの2つの保持サイズ計算の開始は、デフォルトでは少なくとも10ミリ秒離れている必要があります。これは、sandbox.RetainedBytesCheckIntervalオプションで構成できます。間隔は正数である必要があります。

コンテキストに割り当てられたバイトのチェックは、sandbox.AllocatedBytesCheckEnabledオプションによって無効にできます。デフォルトでは、有効になっています(true)。無効にすると(false)、コンテキストの保持サイズ・チェックは、低メモリー・トリガーによってのみトリガーできます。

ホストVM全体のヒープに割り当てられたバイトの合計数がVMのヒープ・メモリーの合計の特定の係数を超えると、低メモリー通知が起動され、次のプロセスが開始されます。sandbox.MaxHeapMemoryオプションが設定されている実行コンテキストが1つ以上あるすべてのエンジンの実行は一時停止され、メモリー制限されたコンテキストごとにヒープ内の保持バイトが計算され、その制限を超えるコンテキストは取り消されて、実行が再開されます。デフォルトの係数は0.7です。これは、sandbox.RetainedBytesCheckFactorオプションで構成できます。係数は0.0から1.0の間である必要があります。sandbox.MaxHeapMemoryオプションを使用するすべてのコンテキストは、sandbox.RetainedBytesCheckFactorに同じ値を使用する必要があります。

説明されている低メモリー・トリガーは、sandbox.UseLowMemoryTriggerオプションによって無効にできます。デフォルトでは、有効になっています(true)。無効(false)の場合、実行コンテキストの保持サイズ・チェックは、割り当てられたバイト・チェッカによってのみトリガーできます。sandbox.MaxHeapMemoryオプションを使用するすべてのコンテキストは、sandbox.UseLowMemoryTriggerに同じ値を使用する必要があります。

ヒープ・メモリー制限の超過が検出されると、ポリグロット・コンテキストは取り消され、実行はisResourceExhausted()に対してtrueを返すPolyglotExceptionをスローして停止します。メモリー制限がトリガーされるとすぐに、アプリケーション・コードはこのコンテキストでそれ以上実行できなくなります。起動されるポリグロット・コンテキストのメソッドに対してPolyglotExceptionをスローし続けます。

時間を指定するために使用できる単位は、ミリ秒の場合はms、秒の場合はs、分の場合はm、時間の場合はh、日の場合はdです。最大ヒープ・メモリー・オプションで負の値を指定したり、時間単位を指定することは許可されていません。

サイズを指定できる単位は、バイトの場合はB、キロバイトの場合はKB、メガバイトの場合はMB、ギガバイトの場合はGBです。最大ヒープ・メモリー・オプションで負の値を指定したり、サイズ単位を指定することは許可されていません。

Context.resetLimitsを使用してリソース制限をリセットしても、ヒープ・メモリー制限には影響しません。

リソース制限のリセット

ポリグロット埋込みAPIでは、Context.resetLimitsメソッドを使用して任意の時点で制限をリセットできます。これは、既知の信頼できる初期化スクリプトを制限から除外する場合に役立ちます。制限のリセットは、すべての制限には適用されません。

使用例

try (Context context = Context.newBuilder("js")
                           .allowExperimentalOptions(true)
                           .option("sandbox.MaxCPUTime", "500ms")
                       .build();) {
    try {
        context.eval("js", /*... initialization script ...*/);
        context.resetLimits();
        context.eval("js", /*... user script ...*/);
        assert false;
    } catch (PolyglotException e) {
        assert e.isCancelled();
        assert e.isResourceExhausted();
    }
}