39 Coherenceリクエストのタイムアウト

Coherence API (特にNamedCache API)をコールする場合、これらは通常、実行に時間がかかるリモート・コールです。ストレージ対応メンバーの負荷、またはクラスタがメンバー離脱またはメンバー参加からリカバリしているかどうかによって、リクエストの実行に通常より長い時間がかかる場合があります。リクエスト時間に対して保証されたSLAが必要で、このSLAを超えた場合にCoherence APIコールをタイムアウトにすることが必要になる場合があります。

Coherenceにリクエスト・タイムアウトを実装するには、いくつかの異なる方法があります。それぞれに異なる利点があり、いくつかの方法には大きな欠点があります。SLA保証として必ずしも適しているわけではないため、リクエスト・タイムアウトの実際の処理および様々なアプローチの動作を理解することが重要です。不適切に使用すると、アプリケーションの問題やキャッシュ・データの不整合が発生し、システムに負荷がかかる可能性があります。

リクエスト・タイムアウトを使用する場合は、タイムアウト例外を処理するための有効な方法が必要です。盲目的に再実行することは、当然適切な戦略ではありません。リクエストがタイムアウトする場合、例外を受信するのはコール元のみです。リクエストはキューに入ったままになり、サーバーで完了するまで実行されます。通常、タイムアウトが発生するのは、システムに負荷がかかっており、リクエストが通常より長くかかっているためです。アプリケーションがタイムアウトを捕捉して無視したり、リクエストを再試行したり、適切なバックオフ戦略なしで通常どおりに実行したりする場合、明らかに通常よりも高い負荷がかかっているときに、サーバーにさらに多くの負荷をかける可能性があります。

リクエスト・タイムアウトに使用される値が小さいほど、システムに高い負荷がかかっているときにアプリケーションで例外が発生する可能性が高くなります。アプリケーションが期待どおりに動作するように、構成されたタイムアウトおよび予想される本番負荷を使用してテストすることが重要です。その場合でも、一般にリクエストに通常よりも時間がかかる場合、タイムアウトが失敗シナリオまたはローリング・アップグレード中にのみ見られることがあるため、理想的にはこれらのシナリオもテストする必要があります。

グローバル・リクエストのタイムアウトの構成

Coherence構成ファイルを使用すると、構成するキャッシュ・スキームのタイプに応じて、リクエスト・タイムアウトを様々な場所で構成できます。

ここで説明する最初のオプションはキャッシュ構成でのリクエスト・タイムアウトの構成ですが、通常はお薦めしません。リストされている他のアプローチの方が適切な選択肢です。

ノート:

構成のリクエスト・タイムアウトは、リクエストの送信者にのみ適用され、リクエストの実行時間がタイムアウトより長い場合、コール元は例外を受け取ります。ただし、リクエストは引き続きサーバーに送信され、そこでキューに入って実行されることに注意することが重要です。リクエスト・タイムアウトを超えた場合、レスポンスが失われるため、コール元は、リクエストがサーバーで正常に実行されたか、失敗したかを確認できません。

リモート・クライアントの構成

Coherence ExtendおよびgRPCクライアントでは、キャッシュ構成ファイルの<remote-cache-scheme>または<remote-grpc-cache-scheme>要素にリクエスト・タイムアウトを構成できます。クライアントのタイムアウトが構成されていない場合、Coherenceで使用されるデフォルトは30秒です。

ノート:

キャッシュ構成ファイルでクライアントのリクエスト・タイムアウトを構成する場合、タイムアウトはクライアントによって行われたすべてのリクエストに適用されます。これには、cache.get()cache.put()などの単純なリクエストだけでなく、内部リクエストも含まれます。たとえば、クライアントが初めてキャッシュをリクエストしたとき、これはキャッシュが存在することを確認するサーバーへのリモート・コールです。キャッシュがサーバーに存在しない場合は、このコールによって、クラスタ全体でキャッシュの初期化が必要になる可能性があります。リクエスト・タイムアウトが小さい値(ミリ秒単位など)に設定されている場合、クラスタのロード中、メンバーの離脱からリカバリ中、または新しいメンバーが参加しているときに、失敗しやすくなる可能性があります。

リクエスト・タイムアウトに適切な値を設定することが重要です。特に値が非常に小さい場合は、Coherence APIコールによってスローされる可能性のあるRequestTimeoutExceptionsを想定して処理するために、アプリケーション・コードを記述する必要があります。

Coherence拡張クライアント<remote-cache-scheme>では、リクエスト・タイムアウトは次のように構成されます。
<remote-cache-scheme>
  <scheme-name>remote-cache</scheme-name>
  <service-name>RemoteCache</service-name>
  <initiator-config>
    <outgoing-message-handler>
        <request-timeout>20s</request-timeout>
    </outgoing-message-handler>
  </initiator-config>
</remote-cache-scheme>
Coherence gRPCクライアント<remote-grpc-cache-scheme>では、リクエスト・タイムアウトは次のように構成されます。
<remote-grpc-cache-scheme>
    <scheme-name>remote-grpc</scheme-name>
    <service-name>RemoteGrpcCache</service-name>
    <request-timeout>20s</request-timeout>
</remote-grpc-cache-scheme>

クラスタ・メンバーの構成

リクエスト・タイムアウトは、クラスタ・メンバーのキャッシュ構成ファイルのキャッシュ・スキームで構成できます。

たとえば、次の構成は、20秒のリクエスト・タイムアウトが構成された<distributed-scheme>を示しています。
<distributed-scheme>
    <scheme-name>storage</scheme-name>
    <service-name>StorageService</service-name>
    <request-timeout>20s</request-timeout>
    <backing-map-scheme>
        <local-scheme/>
    </backing-map-scheme>
    <autostart>true</autostart>
</distributed-scheme>

クラスタ・メンバーにはデフォルトのタイムアウトがないため、リクエスト・タイムアウトが構成されていない場合、デフォルトは無限です。これは、クラスタ・メンバーでCoherence APIコールを実行するときにCoherenceが永久にブロックすることを意味しません。Coherenceには、クラスタの問題を検出し、リクエスト・レスポンスのタイムアウトを検出する他のメカニズムがあります。

ノート:

リクエスト・タイムアウトは、すべてのリクエストに適用されます。これらはアプリケーションAPIコールの可能性がありますが、さらに重要なのは、内部Coherenceリクエストの可能性があることです。

たとえば、サービスがクラスタ・メンバーで起動し、そのサービスが他のクラスタ・メンバーでクラスタを形成する場合、クラスタ結合プロトコル全体を通過します。これらのクラスタ結合メッセージは、すべて同じタイムアウトが適用されるリクエストです。クラスタ結合には、既存のすべてのクラスタ・メンバーからのレスポンスが必要です。これは、クラスタの負荷が高い場合や、他のメンバーが離脱または参加している場合には特に時間がかかる場合があります。クラスタ結合メッセージの失敗は、クラスタ・メンバーの安定性に大きな影響を与え、正しく起動しない原因となる可能性があります。

クラスタ・メンバーに対するリクエスト・タイムアウトの構成は、特に正当な理由があり、本番レベルの負荷でアプリケーションが十分にテストされる場合を除き、お薦めしません。

リクエスト・タイムアウトでのCoherence PriorityTaskの使用

NamedMapまたはNamedCacheで起動または集計メソッドを使用する場合、Coherenceには、個々のメソッド・コールのリクエスト・タイムアウトを設定するために使用できるPriorityTaskインタフェースがあります。

PriorityProcessorを実装するエントリ・プロセッサでinvokeまたはinvokeAllメソッドがコールされた場合、PriorityProcessorによって指定されたリクエスト・タイムアウト値は、キャッシュに対して構成されたデフォルトのタイムアウトをオーバーライドします。PriorityAggregatorを実装するアグリゲータを使用して集計メソッドをコールする場合も同様です。

ほとんどのCoherenceキャッシュAPIメソッドは、エントリ・プロセッサまたはアグリゲータを使用して実装できるため、優先タスクを使用して、ほぼすべてのキャッシュAPIコールを同等の起動または集計に置き換えることができます。

リクエスト・タイムアウトを指定できるように、一部のNamedCache APIメソッドの代わりとして起動または集計を使用すると、パフォーマンスがわずかに異なる場合があります。たとえば、NamedCache.get()メソッドは非常に効率的ですが、これをExtractorProcessorReducerAggregatorなどに置き換えると、追加のシリアライズによって効率が低下します。起動メソッドまたは集計メソッドを使用すると、NearCacheの使用も無効になります。これは、これらのメソッドは常にサーバーに送信されるためです。様々なアプローチの長所と短所を検討する必要があります。

PriorityProcessorの使用

PriorityProcessorクラスは、別のエントリ・プロセッサにラップおよび委任するPriorityTaskおよびInvocableMap.EntryProcessor実装です。これにより、既存のエントリ・プロセッサをラップし、特定のリクエスト・タイムアウトでコールできます。

たとえば、アプリケーションはExtractorProcessorを使用してキャッシュ・エントリから値を抽出し、その値を返すことができます。キャッシュ・エントリが、ある人物とその人物の名前を返したgetName()メソッドだった場合、次のコードを使用してキャッシュ内のエントリから名前を抽出できます。

public String getName(String key)
    {
    NamedCache<String, Person> cache = session.getCache("people");
    InvocableMap.EntryProcessor<String, Person, String> processor 
        = Processors.extract(ValueExtractor.of(Person::getName));
    return cache.invoke(key, processor);
    }

アプリケーションで、リクエストに必要な時間より長い時間がかかる場合に、前のメソッドをタイムアウトにする必要がある場合、エントリ・プロセッサをPriorityProcessorおよびそのリクエスト・タイムアウト・セットにラップできます。

たとえば、次のコードでは、リクエスト・タイムアウトが500ミリ秒のプロセッサが構成されるようになりました。リクエストがタイムアウト内に戻らない場合は、RequestTimeoutExceptionがスローされます。

public String getName(String key)
    {
    NamedCache<String, Person> cache = session.getCache("people");
    InvocableMap.EntryProcessor<String, Person, String> processor 
        = Processors.extract(ValueExtractor.of(Person::getName));
    PriorityProcessor<String, Person, String> priority 
        = Processors.priority(processor);
    priority.setRequestTimeoutMillis(500L);
    return cache.invoke(key, priority);
    }

他のリクエスト・タイムアウト実装と同様に、タイムアウトはコール元にのみ適用されます。リクエストは引き続きサーバーに送信され、そこでキューに入って実行されます。リクエストがタイムアウトを超えた場合、サーバーでプロセッサが成功したか失敗したかについて、コール元にフィードバックはありません。

PriorityAggregatorの使用

PriorityAggregatorクラスは、別のアグリゲータにラップおよび委任するPriorityTaskおよびInvocableMap.StreamingAggregator実装です。これにより、既存のアグリゲータをラップし、特定のリクエスト・タイムアウトでコールできます。

たとえば、アプリケーションはReducerAggregatorを使用してキャッシュ・エントリから値を抽出し、その値を返すことができます。キャッシュ・エントリが、ある人物とその人物の名前を返したgetName()メソッドだった場合、次のコードを使用して、Filterと一致するすべてのキャッシュ・エントリのキーおよび対応する名前を抽出できます。

public Map<String, String> getNames(Filter<?> filter)
    {
    NamedCache<String, Person> cache = session.getCache("people");
    ReducerAggregator<String, Person, ?, String> aggregator 
        = Aggregators.reduce(ValueExtractor.of(Person::getName));
    return cache.aggregate(filter, aggregator);
    }

アプリケーションで、リクエストに必要な時間より長い時間がかかる場合に、前のメソッドをタイムアウトにする必要がある場合、アグリゲータをPriorityAggregatorおよびそのリクエスト・タイムアウト・セットにラップできます。

たとえば、次のコードでは、リクエスト・タイムアウトが750ミリ秒のアグリゲータが構成されるようになりました。リクエストがタイムアウト内に戻らない場合は、RequestTimeoutExceptionがスローされます。

public Map<String, String> getNames(Filter<?> filter)
    {
    NamedCache<String, Person> cache = session.getCache("people");
    ReducerAggregator<String, Person, ?, String> aggregator 
        = Aggregators.reduce(ValueExtractor.of(Person::getName));
    PriorityAggregator<String, Person, ?, Map<String, String>> priority   
        = new PriorityAggregator<>(aggregator);
    priority.setRequestTimeoutMillis(750L);
    return cache.aggregate(filter, priority);
    }

他のリクエスト・タイムアウト実装と同様に、タイムアウトはコール元にのみ適用されます。リクエストは引き続きサーバーに送信され、そこでキューに入って実行されます。リクエストがタイムアウトを超えた場合、サーバーで集計が成功したか失敗したかについて、コール元にフィードバックはありません。

AsyncNamedCacheの使用

Coherence AsyncNamedCache APIを使用する場合、キャッシュ上のメソッドはCompletableFutureを返します。

CompletableFutureの結果は、タイムアウトを使用して取得できます。これは、諦める前に一定時間待機するためにアプリケーション・コードで使用できます。これにより、リクエスト・タイムアウトの構成と同じ動作になりますが、個別コールのタイムアウトを制御できます。

たとえば、次のコードでは、キャッシュに対してgetを実行し、指定された時間結果を待機します。

public <K, V> V getWithTimeout(NamedCache<K, V> cache, K key, 
        long timeout, TimeUnit unit) 
        throws TimeoutException, InterruptedException, ExecutionException
    {
    CompletableFuture<V> future = cache.async().get(key);
    return future.get(timeout, unit);
    }

他のメソッドと同様に、リクエストは引き続きサーバーに送信され、そこでキューに入って実行されます。このメソッドを使用する利点は、コールが失敗した場合にサーバーによってスローされるエラーも処理できることです。

次の例では、例外を記録するために、エラー・ハンドラがfutureに追加されています。タイムアウトを超えた場合、クライアントはTimeoutExceptionを受け取りますが、コールは完了まで実行されるため、ハンドラ・コードはサーバーによってスローされた例外を記録します。

public <K, V> V getWithTimeout(NamedCache<K, V> cache, K key, long timeout, TimeUnit unit)
        throws TimeoutException, InterruptedException, ExecutionException
    {
    CompletableFuture<V> future = cache.async().get(key);
    future.handle((v, error) ->
        {
        if (error != null)
            {
            Logger.err(error);
            }
        return v;
        });
    return future.get(timeout, unit);
    }

ノート:

アプリケーション・コードは、AsyncNamedCacheによって返されるCompletableFutureに対してcancel()をコールできますが、これはリクエストには影響しません。cancel()をコールしても、リクエストの実行は停止しませんが、futureの結果を待機しているアプリケーション・コードは例外を受け取ります。

Java CompletableFuture APIの使用

AsyncNamedCacheを使用するかわりに、Java CompletableFuture APIを使用して非同期コールを実行できます。

たとえば、CompletableFuture.supplyAsnc()メソッドを使用して、Java分岐結合スレッド・プールでCoherenceキャッシュ・コールを非同期に実行できます。次のコードは、コールが戻るまで指定された時間待機します。

public <K, V> V getWithTimeout(NamedCache<K, V> cache, K key, long timeout, TimeUnit unit)
        throws TimeoutException, InterruptedException, ExecutionException
    {
    CompletableFuture<V> future = CompletableFuture.supplyAsync(() -> cache.get(key));
    return future.get(timeout, unit);
    }

分岐結合プールを使用するかわりに、別のエグゼキュータを指定することもできます。

public <K, V> V getWithTimeout(NamedCache<K, V> cache, K key, long timeout, TimeUnit unit)
        throws TimeoutException, InterruptedException, ExecutionException
    {
    Executor executor = Executors.newSingleThreadExecutor();
    CompletableFuture<V> future = CompletableFuture.supplyAsync(() -> cache.get(key), executor);
    return future.get(timeout, unit);
    }

AsyncNamedCacheを使用する場合と同様に、サーバーからの例外をログに記録するために、なんらかのエラー・ハンドラを使用することをお薦めします。

public <K, V> V getWithTimeout(NamedCache<K, V> cache, K key, long timeout, TimeUnit unit)
        throws TimeoutException, InterruptedException, ExecutionException
    {
    Executor executor = Executors.newSingleThreadExecutor();
    CompletableFuture<V> future = CompletableFuture.supplyAsync(() ->
        cache.get(key), executor);
    future.handle((v, error) ->
        {
        if (error != null)
            {
            Logger.err(error);
            }
        return v;
        });
    return future.get(timeout, unit);
    }

AsyncNamedCacheを使用する場合とは異なり、futurecancel()をコールすると、リクエストの実行が停止する場合、または停止しない場合があります。CompletableFuture.supplyAsync()メソッドに渡されたラムダまたはjava.util.function.Supplierは、分岐結合プールで実行するためにキューに入れられるか、Executorが使用されます。エグゼキュータによってラムダが実行される前にcancel()がコールされた場合、リクエストは取り消されます。キャッシュ・リクエストがサーバーに送信された後にcancel()がコールされると、キューに入れられて実行されます。

Coherenceタイムアウト・クラスの使用

内部的にCoherenceは、操作をブロックするために割り込み可能なロックを使用するため、指定されたタイムアウト間隔後にこれらの操作を中断できます。com.oracle.coherence.common.base.Timeout APIを使用すると、指定したタイムアウト間隔後にブロック操作を中断し、エラーを発生させることができます。

次に、キャッシュ操作が指定した10秒間のタイムアウト間隔より時間がかかる場合に、タイムアウトAPIを使用してこの操作を中断する方法の例を示します。try/catchブロック内のコードの実行にかかる時間が500ミリ秒を超える場合、スレッドは中断されます。

NamedCache<String, Person> cache = session.getCache("people");

try (Timeout t = Timeout.after(500, TimeUnit.MILLISECOND))
    {
   Person p = cache.get(key);
    // handle result
    }
catch (InterruptedException e)
    {
    // handle error due to time out
    }

他のタイムアウト・メソッドと同様に、これはコール元スレッドのみを中断します。サーバーに送信されたリクエストは、キューに入れられて実行されます。また、他のメソッドのいくつかと同様に、リモート・コールが成功したか失敗したかについてクライアントにフィードバックすることはありません。