Amazon QLDB ドライバーの推奨事項 - Amazon Quantum Ledger Database (Amazon QLDB)

翻訳は機械翻訳により提供されています。提供された翻訳内容と英語版の間で齟齬、不一致または矛盾がある場合、英語版が優先します。

Amazon QLDB ドライバーの推奨事項

このセクションでは、サポートされている言語の Amazon QLDB ドライバーを設定および使用するためのベストプラクティスについて説明します。提供されているコード例は Java 専用です。

これらの推奨事項は、ほとんどの一般的なユースケースに適用されますが、1 つのサイズがすべてのサイズに適合するとは限りません。アプリケーションに適した以下の推奨事項を使用してください。

設定: QldbDriver 対象

この QldbDriver は、トランザクション間で再利用されるセッションのプールを維持することによって、台帳への接続を管理します。セッションは、台帳との 1 つの接続を表します。QLDB ではセッションごとに 1 つのトランザクションがアクティブに実行されます。

重要

古いドライバーバージョンのセッションプール機能は、現在も、QldbDriver ではなく PooledQldbDriver にあります。次のいずれかのバージョンを使用している場合、このトピックに記載の QldbDriver は、PooledQldbDriver に置き換えてください。

ドライバー バージョン
Java 1.1.0 以前
.NET 0.1.0-beta
Node.js 1.0.0-rc.1 以前
Python 2.0.2 以前

PooledQldbDriver オブジェクトは、ドライバーの最新バージョンでは廃止されています。最新バージョンにアップグレードして、PooledQldbDriver のすべてのインスタンスを QldbDriver に変換することをお勧めします。

設定 QldbDriver グローバルオブジェクトとして

ドライバーとセッションの使用を最適化するには、アプリケーションインスタンスにドライバーのグローバルインスタンスが 1 つのみあるようにします。例えば Java では、SpringGoogle GuiceDagger などの依存関係インジェクションフレームワークを使用できます。次のコード例は、QldbDriver をシングルトンとして設定する方法を示しています。

@Singleton public QldbDriver qldbDriver (AWSCredentialsProvider credentialsProvider, @Named(LEDGER_NAME_CONFIG_PARAM) String ledgerName) { QldbSessionClientBuilder builder = QldbSessionClient.builder(); if (null != credentialsProvider) { builder.credentialsProvider(credentialsProvider); } return QldbDriver.builder() .ledger(ledgerName) .transactionRetryPolicy(RetryPolicy .builder() .maxRetries(3) .build()) .sessionClientBuilder(builder) .build(); }

再試行を設定する

ドライバーは、一般的な一時例外 (SocketTimeoutExceptionNoHttpResponseException など) が発生すると、自動的にトランザクションを再試行します。QldbDriver のインスタンスを作成するときに transactionRetryPolicy 設定オブジェクトの maxRetries パラメータを使用すると、最大再試行回数を設定できます。(前のセクションで説明した古いドライバーバージョンの場合は、PooledQldbDriverretryLimit パラメータを使用します)。

maxRetries の初期値は 4 です。

InvalidParameterException などのクライアント側のエラーは再試行できません。これらが発生すると、トランザクションが中止され、セッションがプールに戻され、例外がドライバーのクライアントにスローされます。

同時セッションと同時トランザクションの最大数を設定する

トランザクションを実行する QldbDriver インスタンスによって使用される台帳セッションの最大数は、その maxConcurrentTransactions パラメータで定義します。(前のセクションで説明した古いドライバーバージョンの場合は、PooledQldbDriverpoolLimit パラメータで定義します)。

この制限は、特定の AWS SDK で定義されているように、ゼロより大きく、セッションクライアントが許可する開いている HTTP 接続の最大数以下であることが必要です。たとえば Java では、接続の最大数は ClientConfiguration オブジェクトで設定されます。

maxConcurrentTransactions のデフォルト値は、AWS SDK で設定した最大接続数と同じものです。

アプリケーションで QldbDriver を設定するときは、スケーリングに関する以下の点を考慮してください。

  • プールには常に、同時に実行する予定のトランザクションの数と同数以上のセッションが必要です。

  • スーパバイザースレッドがワーカースレッドに委任するマルチスレッドモデルでは、そのドライバーにワーカースレッドの数と同数以上のセッションが必要です。それ以外の場合、ピーク負荷時に、スレッドはセッションが使用可能になるまで待機中になります。

  • 台帳ごとの同時アクティブセッションのサービス制限は、「Amazon QLDB でのクォータと制限」で定義されています。すべてのクライアントで 1 つの台帳に使用される同時セッションの数は、この制限を超えないように設定してください。

例外発生時の再試行

QLDB で例外が発生したときに再試行する場合は、以下の推奨事項を考慮してください。

OccConflictException 発生時の再試行

オプティミスティック同時実行制御 (OCC) の競合例外は、トランザクションでアクセスしているデータがトランザクションの開始以降に変更された場合に発生します。トランザクションのコミットを試行する間、QLDB はこの例外をスローします。ドライバーは、maxRetries に設定されている回数だけトランザクションを再試行します。

OCC について、また、インデックスを使用して OCC 競合を制限するベストプラクティスについては「Amazon QLDB 同時実行モデル」を参照してください。

その他の例外発生時に QldbDriver の外部で再試行する

アプリケーション定義のカスタム例外が実行中にスローされたときにこのドライバーの外部でトランザクションを再試行するには、トランザクションをラップする必要があります。Java の例を挙げると、以下のコードは、Reslience4J ライブラリを使用して QLDB でトランザクションを再試行する方法を示しています。

private final RetryConfig retryConfig = RetryConfig.custom() .maxAttempts(MAX_RETRIES) .intervalFunction(IntervalFunction.ofExponentialRandomBackoff()) // Retry this exception .retryExceptions(InvalidSessionException.class, MyRetryableException.class) // But fail for any other type of exception extended from RuntimeException .ignoreExceptions(RuntimeException.class) .build(); // Method callable by a client public void myTransactionWithRetries(Params params) { Retry retry = Retry.of("registerDriver", retryConfig); Function<Params, Void> transactionFunction = Retry.decorateFunction( retry, parameters -> transactionNoReturn(params)); transactionFunction.apply(params); } private Void transactionNoReturn(Params params) { try (driver.execute(txn -> { // Transaction code }); } return null; }
注記

QLDB ドライバーの外部で行うトランザクションの再試行には乗数効果があります。例えば、QldbDriver が 3 回再試行するように設定されていて、カスタム再試行ロジックも 3 回再試行する場合、同じトランザクションを最大 9 回再試行できます。

トランザクションをべき等にする

ベストプラクティスとして、再試行の場合に予期しない結果を避けるために、書き込みトランザクションをべき等にしてください。トランザクションは、複数回実行して毎回同じ結果を生成できる場合、idempotent です。

詳細については、「Amazon QLDB 同時実行モデル」を参照してください。

パフォーマンスの最適化

このドライバーを使用してトランザクションを実行するときにパフォーマンスを最適化するには、以下の点を考慮してください。

  • execute オペレーションは常に、以下のコマンドを含む 3 つ以上の SendCommand API コールを QLDB に対して実行します。

    1. StartTransaction

    2. ExecuteStatement

      このコマンドは、execute ブロックで実行する PartiQL ステートメントごとに呼び出されます。

    3. CommitTransaction

    アプリケーションの全体的なワークロードを計算するときに行われる API コールの合計数を考慮してください。

  • 一般的に、シングルスレッドのライターから始め、1 つのトランザクション内で複数のステートメントをバッチ処理することでトランザクションを最適化することをお勧めします。「Amazon QLDB でのクォータと制限」で定義されているように、トランザクションサイズ、ドキュメントサイズ、トランザクションごとのドキュメント数のクォータを最大に設定します。

  • 大量のトランザクション負荷に対してバッチ処理では不十分な場合は、ライターを追加してマルチスレッドを試すことができます。ただし、ドキュメントとトランザクションの順序付けに関するアプリケーション要件と、これにより生じる複雑さを慎重に検討する必要があります。

トランザクションごとに複数のステートメントを実行する

前のセクションで説明したように、トランザクションごとに複数のステートメントを実行して、アプリケーションのパフォーマンスを最適化できます。次のコード例では、トランザクション内で、テーブルをクエリし、そのテーブルのドキュメントを更新しています。そのために、Lambda 式を execute オペレーションに渡しています。

Java
// This code snippet is intentionally trivial. In reality you wouldn't do this because you'd // set your UPDATE to filter on vin and insured, and check if you updated something or not. public static boolean InsureCar(QldbDriver qldbDriver, final String vin) { final IonSystem ionSystem = IonSystemBuilder.standard().build(); final IonString ionVin = ionSystem.newString(vin); return qldbDriver.execute(txn -> { Result result = txn.execute( "SELECT insured FROM Vehicles WHERE vin = ? AND insured = FALSE", ionVin); if (!result.isEmpty()) { txn.execute("UPDATE Vehicles SET insured = TRUE WHERE vin = ?", ionVin); return true; } return false; }); }
.NET
// This code snippet is intentionally trivial. In reality you wouldn't do this because you'd // set your UPDATE to filter on vin and insured, and check if you updated something or not. public static async Task<bool> InsureVehicle(IAsyncQldbDriver driver, string vin) { ValueFactory valueFactory = new ValueFactory(); IIonValue ionVin = valueFactory.NewString(vin); return await driver.Execute(async txn => { // Check if the vehicle is insured. Amazon.QLDB.Driver.IAsyncResult result = await txn.Execute( "SELECT insured FROM Vehicles WHERE vin = ? AND insured = FALSE", ionVin); if (await result.CountAsync() > 0) { // If the vehicle is not insured, insure it. await txn.Execute( "UPDATE Vehicles SET insured = TRUE WHERE vin = ?", ionVin); return true; } return false; }); }
Go
// This code snippet is intentionally trivial. In reality you wouldn't do this because you'd // set your UPDATE to filter on vin and insured, and check if you updated something or not. func InsureCar(driver *qldbdriver.QLDBDriver, vin string) (bool, error) { insured, err := driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) { result, err := txn.Execute( "SELECT insured FROM Vehicles WHERE vin = ? AND insured = FALSE", vin) if err != nil { return false, err } hasNext := result.Next(txn) if !hasNext && result.Err() != nil { return false, result.Err() } if hasNext { _, err = txn.Execute( "UPDATE Vehicles SET insured = TRUE WHERE vin = ?", vin) if err != nil { return false, err } return true, nil } return false, nil }) if err != nil { panic(err) } return insured.(bool), err }
Node.js
// This code snippet is intentionally trivial. In reality you wouldn't do this because you'd // set your UPDATE to filter on vin and insured, and check if you updated something or not. async function insureCar(driver: QldbDriver, vin: string): Promise<boolean> { return await driver.executeLambda(async (txn: TransactionExecutor) => { const results: dom.Value[] = (await txn.execute( "SELECT insured FROM Vehicles WHERE vin = ? AND insured = FALSE", vin)).getResultList(); if (results.length > 0) { await txn.execute( "UPDATE Vehicles SET insured = TRUE WHERE vin = ?", vin); return true; } return false; }); };
Python
# This code snippet is intentionally trivial. In reality you wouldn't do this because you'd # set your UPDATE to filter on vin and insured, and check if you updated something or not. def do_insure_car(transaction_executor, vin): cursor = transaction_executor.execute_statement( "SELECT insured FROM Vehicles WHERE vin = ? AND insured = FALSE", vin) first_record = next(cursor, None) if first_record: transaction_executor.execute_statement( "UPDATE Vehicles SET insured = TRUE WHERE vin = ?", vin) return True else: return False def insure_car(qldb_driver, vin_to_insure): return qldb_driver.execute_lambda( lambda executor: do_insure_car(executor, vin_to_insure))

ドライバーの execute オペレーションは、セッションとそのセッション内のトランザクションを暗黙で開始します。Lambda 式で実行する各ステートメントは、トランザクションでラップされます。すべてのステートメントが実行されると、ドライバーはトランザクションを自動的にコミットします。自動的な再試行の制限を超えたためにステートメントが失敗すると、トランザクションは中止されます。

トランザクションでの例外の伝播

トランザクションごとに複数のステートメントを実行する場合、通常、トランザクション内で例外をキャッチして続行することはお勧めしません。

たとえば、Java の場合、次のプログラムは RuntimeException のインスタンスをキャッチしてエラーをログに記録し、続行します。このコード例は、UPDATE ステートメントが失敗してもトランザクションが成功するため、推奨されません。クライアントは、更新が失敗しても成功したと思い込む可能性があります。

警告

このコード例は使用しないでください。これは、推奨されない悪いパターンの例として提供されています。

// DO NOT USE this code example because it is considered bad practice public static void main(final String... args) { ConnectToLedger.getDriver().execute(txn -> { final Result selectTableResult = txn.execute("SELECT * FROM Vehicle WHERE VIN ='123456789'"); // Catching an error inside the transaction is an anti-pattern because the operation might // not succeed. // In this example, the transaction succeeds even when the update statement fails. // So, the client might assume that the update succeeded when it didn't. try { processResults(selectTableResult); String model = // some code that extracts the model final Result updateResult = txn.execute("UPDATE Vehicle SET model = ? WHERE VIN = '123456789'", Constants.MAPPER.writeValueAsIonValue(model)); } catch (RuntimeException e) { log.error("Exception when updating the Vehicle table {}", e.getMessage()); } }); log.info("Vehicle table updated successfully."); }

その代わりに例外を伝播 (バブルアップ) します。トランザクションのいずれかの部分が失敗した場合は、execute オペレーションにトランザクションを中止させて、クライアントが適切に例外を処理できるようにします。