Spanner와 같은 분산 클라우드 데이터베이스에서 안정적인 메시지 대기열과 마이크로서비스를 빌드하는 데는 고유한 문제가 있습니다. 이 문서에서는 Spanner 대기열에서 정확히 한 번 처리 및 최대 한 번 확인을 실행하는 데 사용되는 핵심 개념과 설계 패턴을 설명합니다.
핵심 개념 및 멱등성 딜레마
'멱등성'은 작업이 몇 번 실행되든 결과가 항상 동일하다는 의미입니다. 메시지 대기열이나 작업 전송 시스템을 빌드할 때 idempotency를 처리하는 세 가지 기본 전송 방법이 있습니다.
- At-least-once: 메시지가 전송되고 처리되도록 보장됩니다. 네트워크 오류가 발생하면 메시지가 여러 번 전송되고 처리될 수 있습니다.
- At-most-once: 메시지가 최대 한 번 전송됩니다. 중복 처리는 방지되지만 오류가 발생하면 메시지가 손실되거나 삭제될 수 있습니다.
- 정확히 한 번: 모든 메시지는 정확히 한 번 처리되며 손실되거나 중복되지 않습니다.
Spanner 대기열은 자동으로 최소 1회 전송 및 최대 1회 확인을 제공합니다. 하지만 다음 섹션에 설명된 디자인 패턴과 예시를 사용하여 중복 처리와 재전송을 프로그래매틱 방식으로 줄일 수 있습니다.
알 수 없는 커밋 상태 문제
단일 머신 데이터베이스에서 트랜잭션은 성공하거나 실패합니다. 분산 클라우드 데이터베이스에서는 데이터가 여러 물리적 데이터 센터에 복제되므로 트랜잭션이 손실될 수 있습니다.
애플리케이션이 메시지를 처리하고 DELETE FROM MessageQueue WHERE message_id = '123' ASSERT_ROWS_MODIFIED 1와 같은 확인을 커밋하면 요청이 여러 네트워크 홉을 통과합니다.
[Your App] <--(gRPC)--> [Spanner API] <--(Google network)--> [Spanner Storage]
Spanner 스토리지 리더가 커밋을 수신하면 스토리지 복제본 간에 동기화됩니다. 합의가 이루어지고 데이터가 디스크에 작성되면 트랜잭션이 데이터베이스에 성공적으로 커밋됩니다.
하지만 데이터가 디스크에 커밋된 후 성공 확인이 애플리케이션에 도달하기 전에 일시적인 네트워크 장애 (예: 프록시 재부팅 또는 네트워크 연결 해제)가 발생할 수 있습니다. 이 경우 애플리케이션은 네트워크 시간 초과 오류 (DEADLINE_EXCEEDED 또는 UNAVAILABLE)를 수신합니다.
연결이 대역 외로 끊어졌기 때문에 애플리케이션은 커밋이 성공하기 전인지 후인지 알 수 없습니다. 이를 '알 수 없는 커밋 상태' 문제라고 합니다.
자동 재시도가 위험할 수 있는 이유
애플리케이션이 네트워크 시간 초과 오류를 가로채고 중복 삭제 없이 정확히 동일한 트랜잭션을 재시도하면 비즈니스 로직이 두 번 실행될 위험이 있습니다. 예를 들어 거래에 고객의 신용카드 청구 또는 상품 배송이 포함된 경우 성공했지만 확인되지 않은 거래를 다시 시도하면 중복 청구 또는 배송이 발생합니다.
사용자를 보호하기 위해 공식 Google Cloud 클라이언트 라이브러리는 알 수 없는 커밋 오류로 인해 실패한 트랜잭션을 자동으로 재시도하지 않습니다. 트랜잭션 결과가 알 수 없음을 알리는 명시적 오류를 발생시켜 애플리케이션 코드가 멱등성 패턴을 사용하여 안전하게 재시도를 처리하도록 합니다.
정확히 한 번 처리를 위한 실제 전략
트랜잭션을 안전하게 재시도하고 중복 처리 및 메시지 재전송을 줄이려면 다음 기본 설계 패턴 중 하나를 구현하세요.
수정된 행 어설션
모든 비즈니스 로직 (예: 다른 테이블 업데이트)과 대기열 메시지 확인이 단일 Spanner 트랜잭션에서 발생하는 경우 DELETE 문에서 ASSERT_ROWS_MODIFIED를 사용할 수 있습니다. 별도의 중복 삭제 테이블이 필요하지 않으므로 정확히 한 번 처리하는 가장 간단한 전략입니다.
ASSERT_ROWS_MODIFIED 1를 문에 추가하면 메시지가 정확히 하나 삭제된 경우에만 확인이 성공합니다. 네트워크 오류가 발생하고 애플리케이션에서 트랜잭션을 재시도하면 메시지가 더 이상 대기열에 없습니다. 재시도 시 0개의 행이 삭제되고 OUT_OF_RANGE 오류와 함께 문이 실패합니다.
OUT_OF_RANGE 오류는 영구적입니다. 행이 이미 삭제되었으므로 문을 다시 실행하면 항상 0개의 행이 수정되고 다시 실패합니다. 또한 문 수준입니다. DELETE만 실패합니다. 트랜잭션은 열린 상태로 유지되며 이미 트랜잭션에서 작성한 내용은 계속 버퍼링되고 계속 진행하면 커밋됩니다. Spanner는 트랜잭션을 중단하거나 롤백하지 않습니다. 의도한 정확히 한 번 보호를 받으려면 트랜잭션이 롤백되었는지 확인해야 합니다.
- 오류 전파 허용: 러너 기반 API (예: Go의
ReadWriteTransaction)를 사용하는 경우OUT_OF_RANGE오류가 트랜잭션 함수에서 전파되도록 합니다. 클라이언트 라이브러리는 트랜잭션을 포기하고 롤백하여 비즈니스 로직 쓰기가 커밋되지 않도록 합니다. - 포착 후 계속하지 않음: 트랜잭션 함수 내에서 어설션 오류를 포착하고 계속하면 안 됩니다. 이 경우 Spanner는 다른 문을 계속 커밋하므로 이 패턴이 방지해야 하는 정확한 중복 업데이트가 발생합니다.
- 수동 롤백: 트랜잭션을 수동으로 관리하는 경우 (예: Go의
ReadWriteStmtBasedTransaction또는 REST/gRPC API 사용) 어설션이 실패하면 해당 롤백 메서드를 명시적으로 호출해야 합니다.
트랜잭션 아웃박스
비즈니스 로직이 단일 Spanner 트랜잭션 내에서 발생할 수 없거나 (예: 다단계 워크플로 또는 여러 시스템에 걸친 업데이트) 메시지 처리에 외부 서비스 호출이 필요한 경우 (예: SMS 전송 또는 결제 처리) 비즈니스 로직을 큐 승인과 원자적으로 결합할 수 없습니다.
이러한 시나리오에서는 Spanner 트랜잭션 블록 내에서 외부 API를 호출하거나 트랜잭션이 아닌 작업을 직접 실행하면 안 됩니다. Spanner가 트랜잭션을 재시도하거나 중단하면 애플리케이션이 해당 비즈니스 로직을 여러 번 실행할 수 있습니다.
애플리케이션에서 자체적으로 멱등성 전략을 구현해야 하지만 다음 패턴을 사용하여 중복 작업을 줄일 수 있습니다.
빠른 작업 (10초의 기본 리스 내에 완료됨): 메시지를 수신할 때 비즈니스 로직 또는 외부 API 호출을 실행합니다. 작업이 성공한 후에만 메시지를 확인합니다. 작업이 실패하거나 작업자가 확인 전에 비정상 종료되는 경우 메시지를 확인하지 마세요. 리스가 만료되고 Spanner가 재시도를 위해 메시지를 자동으로 전송합니다.
장기 실행 작업(10초 이상 소요): 메시지를 수신하면 이를 확인하고 동일한 트랜잭션 내에서 예상 실행 시간보다 큰 배송 지연을 사용하여 향후 배송이 예약된 새 메시지를 다시 전송합니다. 또는
RENEWLEASE_QUEUE_NAME()TVF를 사용하여 메시지 리스를 주기적으로 연장합니다. 비즈니스 로직이나 외부 호출이 성공하면 새로 대기열에 추가되거나 연장된 메시지를 승인합니다.
다중화 세션 멱등성
Spanner 세션은 다중화할 수 있습니다. 다중화 세션은 공유 세션 전반에서 메모리의 트랜잭션 상태를 추적합니다. 이렇게 하면 클라이언트 라이브러리가 다시 연결되고 알 수 없는 커밋 상태를 더 안정적으로 해결할 수 있으므로 클라이언트 측 네트워크 장애로부터 보호할 수 있습니다.
하지만 프런트엔드 서버가 비정상 종료되면 멀티플렉싱된 세션으로 알 수 없는 커밋 결과를 없앨 수는 없습니다. 세션의 트랜잭션 테이블을 호스팅하는 특정 서버가 커밋 직후에 재부팅되면 메모리 내 상태가 손실되어 다시 연결 시 알 수 없는 트랜잭션 결과 오류가 반환됩니다. 이 위험을 해결하려면 이 페이지에서 멱등성 패턴을 구현하세요.
다음 단계
- 권장사항 및 모니터링을 포함하여 Spanner 대기열을 사용하는 방법을 알아봅니다.
- Spanner 큐 시나리오 및 예시를 자세히 살펴보세요.
- 대기열에 대한 세분화된 액세스 제어로 액세스 제어를 구성합니다.