🔥 SQS Standard 와 FIFO: 순서와 중복의 비용

1208자
13분

Standard 큐와 FIFO 큐의 메시지 전달 결과 비교 — 왼쪽 Standard 패널은 같은 메시지가 두 번 나오고 순서도 뒤섞이고, 오른쪽 FIFO 패널은 보낸 순서대로 한 번씩 나온다

결제 한 건이 두 번 청구된 적이 있다. 큐는 Standard 였고 소비자 코드는 정상이었다. 같은 메시지가 두 번 도착해 두 번 처리된 것이 원인이었다. 며칠 동안 원인을 짚다가, SQS 콘솔에서 큐 유형 라디오 버튼 두 개의 의미를 다시 봤다. Standard 와 FIFO 는 같은 'queue' 라는 단어를 쓰지만 보장하는 동작이 다르다. 어느 쪽을 골라야 하는지는 그 차이부터 짚어야 답이 나온다.

Standard 큐의 약속

Standard 큐의 at-least-once 전달 도식 — Producer 가 보낸 한 메시지가 visibility timeout 만료 뒤 같은 Consumer 에게 두 번 전달되고, receipt #1 과 receipt #2 가 같은 msg-42 식별자를 가진다

Standard 큐의 약속은 두 가지다. 첫째, 초당 사실상 무제한 API 호출. AWS Developer Guide 의 Standard queues 페이지는 이 큐가 SendMessage / ReceiveMessage / DeleteMessage 호출을 거의 무제한으로 받는다고 적는다 (AWS Developer Guide). 둘째, at-least-once 전달이다. 같은 메시지가 두 번 이상 도착할 수 있고, 메시지 도착 순서는 보낸 순서와 다를 수 있다. AWS 는 이 동작을 'best-effort attempt' (보낸 순서를 지키려고 하지만 보장하지는 않는 시도) 로 표현한다.

내가 본 결제 청구 사건은 두 번째 약속이 그대로 일어난 결과다. 소비자는 메시지를 받고 결제를 처리한 뒤 DeleteMessage 를 호출했다. 그 호출이 네트워크 지연으로 늦어졌고, visibility timeout 이 만료되어 같은 메시지가 다른 인스턴스로 다시 들어갔다. 두 번째 인스턴스도 결제를 처리했다. Standard 큐는 소비자에게 idempotency 책임을 넘긴다.

FIFO 큐의 약속

FIFO 큐의 exactly-once into queue 도식 — 같은 MessageDeduplicationId 'dedup-001' 로 세 번 SendMessage 했을 때 첫 호출만 큐에 들어가고, 5 분 안에 들어온 두 번째와 세 번째 호출은 중복으로 제거된다

FIFO 큐의 약속은 정확히 반대 방향이다. 보낸 순서가 그대로 지켜지고, 같은 메시지를 두 번 큐에 넣지 않는다. AWS 는 이 동작을 exactly-once processing 으로 부른다 (같은 메시지가 큐에 두 번 들어가지 않는다는 의미). 소비자가 받은 메시지를 두 번 처리하지 않는다는 보장은 아니다. 소비자는 받은 뒤 DeleteMessage 로 메시지를 지워야 하고, visibility timeout 안에 못 지우면 같은 메시지가 다시 visible 상태가 된다. 큐 이름에 .fifo 접미사가 강제다. payments 가 아니라 payments.fifo 로 만든다.

FIFO 큐는 이 보장을 두 가지 식별자로 처리한다. MessageGroupId 는 모든 메시지에 항상 필요하다. MessageDeduplicationId 는 호출 쪽이 직접 넣거나, 큐 속성 ContentBasedDeduplication 을 켜서 SQS 가 자동으로 만들게 한다. 그룹 ID 없이 SendMessage 를 호출하면 SQS 가 그 호출을 거부한다. AWS Developer Guide 의 FIFO 전달 로직 페이지도 그룹 ID 없이 보내면 동작이 실패한다고 직접 적는다 (FIFO delivery logic).

SQS 는 5 분 deduplication interval (중복 제거 구간) 안에 들어온 같은 dedup ID 를 한 번만 받는다. 같은 MessageDeduplicationId 로 5 분 안에 두 번째 SendMessage 가 들어오면 SQS 는 그 호출을 무시한다 (큐에 들어가지 않는다). 5 분이 지나면 SQS 는 같은 ID 라도 새 메시지로 받는다. 5 분 한도는 AWS Developer Guide 의 Exactly-once processing 페이지가 직접 명시하는 숫자다 (Exactly-once processing).

MessageGroupId 와 순서의 단위

MessageGroupId 별 순서·병렬 처리 도식 — customer-A / customer-B / customer-C 세 그룹이 각자 보낸 순서를 지키면서, 그룹끼리는 세 명의 Consumer 가 병렬로 받아 처리한다

MessageGroupId 는 순서의 단위다. 같은 그룹 안에서는 SQS 가 보낸 순서를 그대로 유지하고, 다른 그룹끼리는 소비자가 병렬로 받는다. 고객 ID, 세션 ID, 디바이스 ID 같이 자연스럽게 나뉘는 키가 그룹 ID 후보다.

분할 키가 처리량을 결정한다. SQS 는 FIFO 큐 데이터를 파티션 단위로 저장하고, 각 파티션은 send / receive / delete 호출 기준으로 초당 300 건을 받는다. 배치 API (한 호출에 메시지 최대 10 개) 를 쓰면 같은 파티션에서 초당 메시지 3,000 건까지 처리할 수 있다. SQS 는 MessageGroupId 의 해시로 파티션을 정한다. 그룹 ID 종류가 적으면 쓸 수 있는 파티션도 적다. 호출이 한 파티션에 몰리면 초당 300 건 한도를 빨리 넘는다.

내가 만진 한 시스템은 처음에 모든 메시지에 동일한 그룹 ID orders 를 줬다. 메시지 한 줄 단위 순서가 필요하다는 잘못된 판단이었다. 트래픽이 늘자 그 큐는 초당 300 건을 넘는 호출에서 거부 응답을 받기 시작했다. 실제로 강한 순서가 필요한 단위는 '한 고객의 한 주문' 이었지 '전체 주문' 이 아니었다. 그룹 ID 를 customer_id 로 바꾸자 파티션 수가 늘고 처리량 여유가 다시 생겼다.

중복 제거 ID 의 두 가지 방식

MessageDeduplicationId 를 정하는 두 가지 방식 — 왼쪽은 호출 쪽이 직접 ID 를 보내는 caller-supplied 방식, 오른쪽은 ContentBasedDeduplication 을 켜서 SQS 가 메시지 본문의 SHA-256 해시로 자동 생성하는 방식

MessageDeduplicationId 는 두 가지 방식으로 정한다. 호출 쪽이 직접 ID 를 만들거나, 큐 속성 ContentBasedDeduplication 을 켜서 SQS 가 메시지 본문의 SHA-256 해시를 자동 dedup ID 로 쓰게 한다.

직접 만들 때 흔히 쓰는 방식은 외부 사건 ID 다. 결제 큐라면 주문 ID, 주문에 변경이 있을 때마다 (주문 ID + 버전) 조합을 ID 로 쓴다. 같은 주문에 같은 변경이 두 번 들어와도 5 분 안이면 SQS 가 두 번째 호출을 무시한다.

ContentBasedDeduplication 방식에서는 본문이 바뀌지 않는 호출에서만 SQS 가 자동으로 중복을 제거한다. AWS Developer Guide 는 이 동작을 메시지 본문에 SHA-256 해시를 적용해 중복 제거 ID 를 만든다고 적는다. 단점이 두 가지 있다. 첫째, 본문에 timestamp 처럼 매번 달라지는 값이 들어가면 자동 중복 제거가 안 된다. 둘째, 같은 본문을 의도적으로 두 번 보내고 싶은 경우 (재시도가 아닌 진짜 두 번째 요청) 에는 직접 ID 를 만들어 넣어야 한다.

처리량의 차이

Standard 와 FIFO 의 처리량 한도 비교 막대 그래프 — Standard 는 사실상 무제한, FIFO 단일 호출은 파티션당 초당 300 건, FIFO 배치 호출은 파티션당 초당 메시지 3,000 건

처리량은 두 큐 사이의 가장 큰 운영 차이다. Standard 는 SendMessage / ReceiveMessage / DeleteMessage 호출에 사실상 한도가 없다. FIFO 는 파티션 단위로 한도가 있고, 그 한도는 그룹 ID 분포가 결정한다.

각 파티션 한도는 두 가지로 표시한다. 단일 호출 기준 send / receive / delete 각각 초당 300 건. 배치 API (한 호출에 메시지 최대 10 개) 기준 초당 메시지 3,000 건. SQS 는 큐의 호출 추세에 맞춰 파티션 수를 자동으로 조정한다 (regional quota 안에서). 파티션 분배도 MessageGroupId 의 해시를 따른다. 그룹 ID 종류가 다양할수록 호출이 여러 파티션에 고르게 나뉜다.

high throughput FIFO 모드는 파티션 한도를 region 별로 더 큰 값으로 풀어 주는 옵션이다. 큐 속성에 DeduplicationScope 와 FifoThroughputLimit 두 가지를 같이 설정해 활성화한다. AWS 는 high throughput 한도를 region 별로 다르게 둔다. 정확한 숫자는 사용 region 의 service quotas 페이지에서 확인한다. 서울 ap-northeast-2 도 high throughput 활성화 가능 region 에 들어 있다.

FIFO 가 답이 아닌 두 경우

FIFO 를 고르기 전에 두 경우를 먼저 확인한다.

첫째, 소비자가 이미 idempotent 한 경우. 결제 처리 코드가 동일한 주문 ID 를 두 번 받아도 한 번만 청구하도록 짜여 있으면, FIFO 의 중복 제거 보장은 추가 안전망일 뿐이다. 그 안전망을 쓰면 그룹 ID 분할 키 설계, 처리량 한도 추적, high throughput 모드 활성화 결정까지 같이 관리해야 한다. Standard 큐와 idempotent 소비자의 조합이 더 단순할 때가 많다.

둘째, 모든 메시지에 강한 순서가 필요한 경우. 모든 메시지를 한 그룹 ID 에 몰아넣으면 FIFO 가 동작하긴 하지만, 그 큐의 처리량은 한 파티션의 300 / 3,000 한도를 넘지 못한다. 한 소비자가 한 메시지를 처리하는 동안 같은 그룹의 다음 메시지는 그 메시지가 삭제되거나 visibility timeout 이 만료될 때까지 대기한다 (head-of-line blocking 으로 부르는 동작). 전체 메시지에 강한 순서가 진짜 업무 규칙이라면 SQS 가 답이 아닐 가능성이 높다. DynamoDB 조건부 쓰기와 정렬 키 조합, 또는 Kinesis Data Streams 가 그 역할에 더 알맞다.

가격 차이는 SQS란 무엇인가: 큐의 추상 에서 짚은 그대로다 (Standard 100 만 요청당 $0.40, FIFO 100 만 요청당 $0.50). 가격이 두 큐 선택의 핵심처럼 보이지만, 실제 결정은 소비자가 idempotent 한지와 강한 순서가 어느 단위까지 필요한지에 달려 있다. 두 질문에 먼저 답하면 큐 유형을 정할 수 있다.

YouTube 영상

채널 보기
AI 추천 시스템의 원리, 벡터 사이의 각도와 코사인 유사도 | 선형대수학
AI를 위한 선형대수학 - 소개 | 선형대수학
우리가 매일 쓰는 맞춤법 검사기와 라우터 속에 숨겨진 알고리즘은? | Trie 자료구조 이야기
Trie 자료구조 파이썬 구현: Search와 Starts With 연산 | Trie 자료구조 이야기
트라이(Trie) 자료구조: 파이썬으로 삽입(Insert) 연산 구현하기 | Trie 자료구조 이야기
직교성과 벡터 투영 | 선형대수학
행렬의 가장 중요한 연산 - 행렬 곱셈 | 선형대수학
AI는 왜 수백 차원의 벡터를 사용할까? 고차원 공간과 행렬 | 선형대수학