Amazon QLDB 드라이버 권장 사항 - Amazon Quantum Ledger Database(QLDB)

기계 번역으로 제공되는 번역입니다. 제공된 번역과 원본 영어의 내용이 상충하는 경우에는 영어 버전이 우선합니다.

Amazon QLDB 드라이버 권장 사항

이 섹션에서는 지원되는 모든 언어에 대해 Amazon QLDB 드라이버를 구성하고 사용하는 모범 사례를 설명합니다. 제공된 코드 예제는 Java 전용입니다.

이러한 권장 사항은 대부분의 일반적인 사용 사례에 적용되지만 한 가지 방법이 모든 사용 사례에 적합하지는 않습니다. 애플리케이션에 적합하다고 판단되는 대로 다음 권장 사항을 사용하세요.

QLDBDriver 객체 구성

QldbDriver 객체는 트랜잭션 간에 재사용되는 세션 풀을 유지 관리하여 원장과의 연결을 관리합니다. 세션은 원장에 대한 단일 연결을 나타냅니다. QLDB는 세션당 하나의 활성 실행 트랜잭션을 지원합니다.

중요

이전 드라이버 버전의 경우 세션 풀링 기능은 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를 글로벌 객체로 구성

드라이버 및 세션 사용을 최적화하려면 애플리케이션 인스턴스에 드라이버의 글로벌 인스턴스가 하나만 있어야 합니다. 예를 들어, Java에서는 Spring, Google Guice 또는 Dagger와 같은 종속성 주입 프레임워크를 사용할 수 있습니다. 다음 코드 예제는 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(); }

재시도 시도 구성

드라이버는 일반적인 일시적 예외(예: SocketTimeoutException 또는 NoHttpResponseException)가 발생할 경우 자동으로 트랜잭션을 재시도합니다. 최대 재시도 횟수를 설정하려면 QldbDriver의 인스턴스를 만들 때 transactionRetryPolicy 구성 객체의 maxRetries 파라미터를 사용할 수 있습니다. (이전 섹션에 나열된 이전 드라이버 버전의 경우 PooledQldbDriverretryLimit 파라미터를 사용하세요.)

maxRetries의 기본값은 4입니다.

InvalidParameterException와 같은 클라이언트 측 오류는 다시 시도할 수 없습니다. 오류가 발생하면 트랜잭션이 중단되고 세션이 풀로 반환되며 드라이버의 클라이언트에 예외가 발생합니다.

동시에 접속할 수 있는 최대 세션 및 트랜잭션 수 설정

QldbDriver의 인스턴스가 트랜잭션을 실행하는 데 사용하는 최대 원장 세션 수는 해당 maxConcurrentTransactions 파라미터에 의해 정의됩니다. (이전 섹션에 나열된 이전 드라이버 버전의 경우 이는 PooledQldbDriverpoolLimit 파라미터로 정의됩니다.)

이 한도는 특정 AWS SDK에서 정의한 대로 0보다 크고 세션 클라이언트가 허용하는 최대 공개 HTTP 연결 수보다 작거나 같아야 합니다. 예를 들어, Java의 경우 최대 연결 수는 ClientConfiguration 객체에 설정됩니다.

maxConcurrentTransactions의 기본값은 AWS SDK의 최대 연결 설정입니다.

애플리케이션에서 QldbDriver를 구성할 때는 다음과 같은 크기 조정 고려 사항을 고려하세요.

  • 풀에는 항상 계획한 동시 실행 트랜잭션 수만큼 많은 세션이 있어야 합니다.

  • 감독자 스레드가 작업자 스레드에 위임하는 다중 스레드 모델에서는 드라이버에 최소한 작업자 스레드 수 만큼의 세션이 있어야 합니다. 그렇지 않으면 부하가 최대일 때 스레드가 사용 가능한 세션을 기다리게 됩니다.

  • 원장당 동시 활성 세션의 서비스 한도는 Amazon QLDB 할당량 및 제한에 정의되어 있습니다. 모든 클라이언트의 단일 원장에 사용할 동시 세션 한도를 초과하여 구성하지 않도록 하십시오.

예외에 대한 재시도

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가 세 번 재시도하도록 구성되어 있고, 사용자 지정 재시도 로직도 세 번 재시도하면 동일한 트랜잭션을 최대 9번까지 재시도할 수 있습니다.

트랜잭션에 멱등성 부여하기

재시도 시 예상치 못한 부작용이 발생하지 않도록 쓰기 트랜잭션에 멱등성을 부여하는 것이 가장 좋은 방법입니다. 여러 번 실행하여 매번 동일한 결과를 생성할 수 있는 트랜잭션은 멱등성을 가집니다.

자세한 내용은 Amazon QLDB 동시성 모델 섹션을 참조하세요.

성능 최적화

드라이버를 사용하여 트랜잭션을 실행할 때 성능을 최적화하려면 다음 사항을 고려하세요.

  • execute 작업은 항상 다음 명령을 포함하여 QLDB에 대해 최소 세 번의 SendCommand API 호출을 수행합니다.

    1. StartTransaction

    2. ExecuteStatement

      이 명령은 execute 블록에서 실행하는 각 PartiQL 명령문에 대해 호출됩니다.

    3. CommitTransaction

    애플리케이션의 전체 워크로드를 계산할 때 수행되는 총 API 호출 수를 고려하세요.

  • 일반적으로 단일 스레드 작성기로 시작하여 단일 트랜잭션 내에서 여러 명령문을 일괄 처리하여 트랜잭션을 최적화하는 것이 좋습니다. 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 작업을 통해 트랜잭션을 중단합니다.