트랜잭션 실패
데이터베이스에서 트랜잭션을 사용하는 경우 여러 이유로 트랜잭션이 실패할 수 있다. 실패에는 여러 이유가 있겠지만, 일시적인 실패와 아닌 것을 구분하고 재시도하여 작업이 유실되는 것을 방지해야 한다.
예를 들어 MongoDB에서 다수의 트랜잭션이 같은 document를 수정하는 경우, MongoDB 트랜잭션은 낙관락이기 때문에 첫 트랜잭션만 성공한다. 나머지 트랜잭션은 WriteConflict로 실패하고 수정사항은 유실된다.
MongoDB 드라이버에서 지원하는 트랜잭션 사용 방식, 발생할 수 있는 에러들의 종류, 각 트랜잭션 방식 별 에러를 처리하는 방법에 대해서 알아보자.
트랜잭션을 사용하는 방법
일단 MongoDB드라이버에서 트랜잭션을 사용하는 방법은 두개가 있다.
- Callback API
- 함수를 받아서 트랜잭션을 열고 함수를 실행, 자동으로 커밋한다.
- 트랜잭션이 발생시키는 오류에 대해 자동으로 처리하는 로직이 포함돼 있다.
- pymongo에서는
session.with_tranasction()을 사용하면 된다.client = MongoClient(uriString) def callback(session): session.client.mydb1.foo.insert_one({"abc": 1}, session=session) session.client.mydb2.bar.insert_one({"xyz": 999}, session=session) with client.start_session() as session: # callback 함수를 트랜잭션 내에서 실행 session.with_transaction(callback)
- Core API
- 명시적으로 개발자가 트랜잭션을 열고 커밋해야 한다.
- 트랜잭션이 발생시키는 오류를 개발자가 직접 처리해야 한다.
- pymongodb에서는
session.startTransaction()로 열고session.abortTransaction()로 롤백하거나session.commitTransaction()로 커밋할 수 있다.# 트랜잭션 시작 session.startTransaction( { readConcern: { level: "local" }, writeConcern: { w: "majority" } } ); # 트랜잭션 내 작업 try { coll1.insertOne( { abc: 1 } ); coll2.insertOne( { xyz: 999 } ); } catch (error) { # 에러 시 트랜잭션 abort하여 롤백 session.abortTransaction(); throw error; } # 트랜젹션 커밋 session.commitTransaction(); session.endSession();
어떤 방식을 사용해야 할까?
트랜잭션 실패 핸들링을 커스텀하고 싶다면 Core API를 사용해야 한다. 반대로 별 다른 니즈가 없는 경우 자동으로 트랜잭션 실패에 대해 처리해주는 Callback API를 사용하는 것이 좋아보인다. 트랜잭션에서 어떤 오류가 발생할 수 있는지, 각 방식에서 오류들을 어떻게 처리하는지 알아본다.
트랜잭션 사용 중 발생할 수 있는 에러
트랜잭션 사용 중 아래 3가지 에러가 발생할 수 있다.
TransientTransactionError
- 일시적인 트랜잭션 에러의 경우이다. Primary 노드가 잠시 없는 상태, WriteConflict 등…
- 일시적인 에러이고 커밋 않았기 때문에 트랜잭션을 처음부터 다시 시도하면 된다.
UnknownTransactionCommitResult
- 트랜잭션을 커밋하는 도중 발생하는 에러이다.
- 커밋은 재시도해도 되는 작업이기 때문에, 커밋만 재시도하면 된다.
- MongodDB 6.2부터 추가되었다.
TransactionTooLargeForCache
- 트랜잭션이 캐시의 75%를 초과하는 경우 발생하는 에러이다.
- 캐시가 작거나 트랜잭션이 커서 발생한 것이기 때문에 재시도해도 똑같이 발생할 확률이 높기 때문에 재시도하지 말도록 한다.
트랜잭션 방식 별 에러 처리 방식
pymongo 기준으로 알아보자. 다른 드라이버도 대부분 비슷할 것이다.
CallbackAPI 사용 시 에러 처리 방법
TransientTransactionError- pymongo 실제 구현
- pymongo가 재시도해서 따로 처리하지 않아도 된다.
- 재시도 시 트랜잭션 처음부터 다시 진행한다.
- 재시도에 exponential backoff 및 jitter 가 적용돼 있다.
- while true로 반복하고, 재시도 타임아웃은 120초으로 구현돼 있다.
UnknownTransactionCommitResult- pymongo 실제 구현
- pymongo가 재시도해서 따로 처리하지 않아도 된다.
- 재시도 시 트랜잭션 커밋만 다시 진행한다.
- 재시도에 exponential backoff, jitter 가 적용돼 있다.
- while true로 반복하고, 재시도 타임아웃은 120초가 적용돼 있다.
TransactionTooLargeForCache- 재시도하지 않는다.
raise
- 재시도하지 않는다.
CoreAPI의 처리 방법
기본적으로 Callback API와 같은 방식으로 처리하면 되고, 필요에 따라 약간씩 커스텀하면 된다.
TransientTransactionError- 트랜잭션 작업 전체에서 try-except로 잡아서 작업을 처음부터 재시도 한다.
ConnectionFailure,OperationFailure를잡고has_error_label("TransientTransactionError")로 검사한다.
- 기본적으로 Callback API 구현을 참고해서 구현하고, 필요한 부분만 커스텀하는 것이 좋겠다.
- 공식 문서 예시는 무한반복이지만 혹시 모르니 꼭 timeout을 구현한다.
- 재시도 시 한번에 몰리지 않게 exponential backoff, jitter를 적용하는 것이 좋다.
- 공식 문서 예시
def run_transaction_with_retry(txn_func, session): while True: try: txn_func(session) # performs transaction break except (ConnectionFailure, OperationFailure) as exc: # If transient error, retry the whole transaction if exc.has_error_label("TransientTransactionError"): print("TransientTransactionError, retrying transaction ...") continue else: raise- 트랜잭션 작업 전체에서 try-except로 잡아서 작업을 처음부터 재시도 한다.
UnknownTransactionCommitResult- session.commit_transaction() 호출 시 try-except 로 잡아서 다시 커밋을 시도한다.
ConnectionFailure,OperationFailure를잡고has_error_label("UnknownTransactionCommitResult")로 검사한다.
- TransientTransactionError와 마찬가지로
- 무한 재시도를 방지하기위해 timeout 적용을 권장한다.
- Thundering herd 방지를 위해 jitter와 exponential backoff를 적용을 권장한다.
- 공식 예시
def commit_with_retry(session): while True: try: # Commit uses write concern set at transaction start. session.commit_transaction() break except (ConnectionFailure, OperationFailure) as exc: # Can retry commit if exc.has_error_label("UnknownTransactionCommitResult"): print("UnknownTransactionCommitResult, retrying commit operation ...") continue else: print("Error during commit ...") raise- session.commit_transaction() 호출 시 try-except 로 잡아서 다시 커밋을 시도한다.
TransactionTooLargeForCache- 재시도하지 않는다.
트랜잭션 사용 시 주의점
- CallbackAPI를 사용하는 경우, callback 함수는 절대로 Mongo커맨드에서 발생한 에러를 조용히 삼키면 안 된다. callback함수 밖으로 전파돼야 한다.
- 예를 들어
DuplicateKeyError에러가 발생했는데 이를 조용히 삼키고 지나가는 경우, 트랜잭션을 커밋할 때NoSuchTransaction가 발생할 것이다. 이 에러는TranseientTransactionError이기 때문에 pymongo는 계속 재시도하려고 해서 무한루프에 빠진다.
- 예를 들어
레퍼런스
- www.mongodb.com/docs/manual/core/transactions-in-applications/
- https://pymongo.readthedocs.io/en/stable/api/pymongo/client_session.html#pymongo.client_session.ClientSession.with_transaction
- https://stackoverflow.com/questions/52153538/what-is-a-transienttransactionerror-in-mongoose-or-mongodb
- https://github.com/mongodb/mongo-python-driver/blob/e532774b25fdc3667c2295484b3c678fc8a96cab/pymongo/synchronous/client_session.py#L507