🔥 Dead Letter Queue: 실패 메시지의 격리

#SQS#DLQ#AWS#메시지 큐
988자
10분

SQS Dead Letter Queue 격리 흐름 — Producer 가 보낸 메시지가 Source 큐에서 ApproximateReceiveCount 가 임계값(50)을 넘기는 순간 SQS 가 자동으로 DLQ 로 옮긴다

운영 중이던 큐 하나에서 ApproximateReceiveCount 가 50까지 올라간 메시지를 본 적이 있다. 소비자는 실패했고 DeleteMessage 는 끝내 호출되지 않았다. 메시지는 visibility timeout 이 끝날 때마다 다시 나왔고, 장애는 끝났는데 큐는 끝나지 않았다. 그때 붙인 것이 DLQ 였다. Lambda 비동기 destination DLQEventBridge target DLQ 에서도 같은 단어가 나오지만, SQS DLQ 는 호출자 설정이 아니라 source 큐 자체의 attribute 다.

DLQ 는 특수 큐가 아니라 source 큐의 attribute 다

Standard pair 와 FIFO pair 도식 — Source 와 DLQ 가 같은 큐 type 끼리만 짝이 되고, RedrivePolicy 는 source 의 attribute 라는 점을 두 패널로 비교한다

DLQ 는 별도 서비스가 아니다. 일반 SQS 큐 하나를 만들고 source 큐의 RedrivePolicy 가 그 ARN 을 가리키면 그 큐가 DLQ 역할을 한다. AWS 문서도 source queue 와 dead-letter queue 관계를 이 방식으로 설명한다 (AWS Developer Guide).

핵심 속성은 두 필드다. deadLetterTargetArnmaxReceiveCount 다.

json
{
  "deadLetterTargetArn": "arn:aws:sqs:ap-northeast-2:123456789012:orders-dlq",
  "maxReceiveCount": 5
}
json
{
  "deadLetterTargetArn": "arn:aws:sqs:ap-northeast-2:123456789012:orders-dlq",
  "maxReceiveCount": 5
}

큐 유형은 반드시 맞춰야 한다. Standard source 는 Standard DLQ 만 쓴다. FIFO source 는 FIFO DLQ 만 쓴다. Standard 와 FIFO 차이 에서 본 전달 계약을 DLQ 도 그대로 따른다.

maxReceiveCount 는 실패를 격리로 바꾼다

maxReceiveCount 동작 sequence diagram — Consumer 가 ReceiveMessage 후 DeleteMessage 하지 못해 visibility timeout 만료를 반복하면 ApproximateReceiveCount 가 올라가고, 임계값을 넘는 순간 SQS 가 메시지를 DLQ 로 옮긴다

maxReceiveCount 는 몇 번까지 다시 받게 둘지 정하는 숫자다. 메시지가 ReceiveMessage 로 전달된 뒤 삭제되지 않고 visibility timeout 만료를 반복하면 ApproximateReceiveCount 가 올라간다. 그 값이 임계값을 넘으면 SQS 가 메시지를 source 큐에서 DLQ 로 옮긴다.

숫자는 실패 성격에 맞춰 잡는다. 일시적 오류를 몇 번 흡수하고 싶다면 5에서 10이 흔하다. 한 번 실패하면 거의 항상 코드 버그나 데이터 오류로 이어지는 작업이라면 1에서 3이 더 낫다. 값을 너무 낮게 두면 일시 장애가 곧바로 DLQ 적재로 바뀐다. 값을 너무 높게 두면 같은 poison message 가 다른 메시지를 오래 막는다.

Standard 큐에는 한 가지 특성이 더 있다. AWS 문서는 maxReceiveCount 가 3보다 큰 Standard 큐에서, 삭제되지 않은 메시지를 세 번 이상 받으면 SQS 가 그 메시지를 큐 뒤쪽으로 보낸다고 적는다. 오래 실패하는 메시지 하나가 나머지 메시지를 계속 막지 않게 하려는 동작이다.

DLQ 의 보관 기간은 source 보다 길게 둔다

DLQ 로 옮겼다고 메시지가 영구 보관되는 것은 아니다. retention period 가 끝나면 SQS 가 DLQ 안 메시지도 같이 지운다. 이 숫자를 source 큐와 같게 두면 원인을 보기 전에 메시지가 사라질 수 있다.

여기서 Standard 와 FIFO 는 또 다르다. Standard 는 원래 enqueue timestamp 를 유지한다. source 큐에서 1일을 보낸 메시지가 DLQ 로 옮겨지면, DLQ 에서 새로 4일을 받는 것이 아니라 남은 기간만 산다. FIFO 는 DLQ 로 옮길 때 SQS 가 enqueue timestamp 를 새로 기록한다. 그래서 DLQ retention 은 source 보다 길게 두는 편이 안전하다 (AWS Developer Guide).

redrive 는 DLQ 에서 source 로 다시 보내는 별도 작업이다

redrive task 흐름 도식 — DLQ 에 쌓인 메시지를 StartMessageMoveTask 비동기 작업이 기본으로 원래 source 큐로 다시 보내고, 같은 type 이라면 다른 custom destination 큐도 선택할 수 있다

source 에서 DLQ 로 가는 규칙과, DLQ 에서 다시 꺼내는 작업은 다른 기능이다. 앞은 RedrivePolicy 다. 뒤는 redrive task 다.

콘솔에서는 Start DLQ redrive 로 시작한다. API 에서는 StartMessageMoveTask, ListMessageMoveTasks, CancelMessageMoveTask 를 쓴다. 기본 목적지는 원래 source 큐다. 같은 타입이라면 다른 큐를 목적지로 둘 수도 있다. 이 작업은 비동기이고, 큐당 동시에 하나만 돈다. 콘솔은 system optimized 속도와 사용자 지정 속도를 같이 노출하며, 사용자 지정 최대값은 초당 500 메시지다 (AWS Developer Guide).

시점도 구분해 둘 필요가 있다. 표준 큐의 콘솔 redrive to source 는 2021년 12월 1일에 먼저 나왔고 (AWS What's New), StartMessageMoveTask 계열 SDK·CLI API 는 2023년 6월 8일에 나왔다 (AWS News Blog). 장애 복구 자동화를 짤 때는 둘을 같은 기능으로 보면 안 된다.

RedriveAllowPolicy 는 DLQ 쪽 화이트리스트다

RedriveAllowPolicy 세 옵션 도식 — allowAll 은 모든 source 가 자기를 DLQ 로 쓸 수 있고, denyAll 은 전부 막고, byQueue 는 sourceQueueArns 리스트의 큐만 허용한다

RedriveAllowPolicy 는 DLQ 큐의 attribute 다. 어떤 source 큐가 이 큐를 자기 DLQ 로 지정할 수 있는지 정한다. source 쪽 손잡이가 RedrivePolicy 라면, DLQ 쪽 손잡이는 RedriveAllowPolicy 다.

json
{
  "redrivePermission": "byQueue",
  "sourceQueueArns": [
    "arn:aws:sqs:ap-northeast-2:123456789012:orders"
  ]
}
json
{
  "redrivePermission": "byQueue",
  "sourceQueueArns": [
    "arn:aws:sqs:ap-northeast-2:123456789012:orders"
  ]
}

선택지는 셋이다. allowAll, denyAll, byQueue 다. byQueue 를 쓰면 ARN 기준으로 최대 10개 source 큐를 적는다. IaC 환경에서는 이 속성이 특히 유용하다. 이름이 비슷한 큐가 많을수록, 의도하지 않은 source 가 공용 DLQ 를 가리키는 실수를 막아야 하기 때문이다. 관련 파라미터는 SetQueueAttributes API 문서에도 정리돼 있다 (SetQueueAttributes).

DLQ 가 답이 아닌 경우가 세 가지 있다

DLQ 가 답이 아닌 세 경우 비교 — 일시 장애는 visibility timeout 과 backoff 로 흡수, 코드 버그는 DLQ 가 격리만 하지 고치진 않음, 알람 없는 DLQ 는 메시지가 사라지는 폴더가 된다

  1. 일시적 의존성 장애는 DLQ 보다 재시도 설정이 먼저다. 데이터베이스가 30초 멈췄다면 앞에서 살펴본 SQS 큐의 추상에서 본 visibility timeout 과 소비자 backoff 로 흡수하는 편이 낫다.

  2. 코드 버그는 DLQ 가 고쳐 주지 않는다. DLQ 는 실패 메시지를 격리할 뿐이다. 결국 코드를 고치고 redrive 할지, 메시지를 폐기할지 운영자가 정해야 한다.

  3. DLQ 를 읽지 않으면 와이어링 자체가 의미가 없다. 알람도 없고 분류 규칙도 없으면, DLQ 에 쌓인 실패 메시지를 누구도 다시 들여다보지 않는다.

FIFO 에서는 한 가지를 더 본다. 순서 자체가 업무 규칙이면 DLQ 이동이 그 순서를 바꿀 수 있다. AWS 문서도 정확한 순서를 깨면 안 되는 경우에는 FIFO 큐와 DLQ 조합을 신중히 보라고 적는다.

관리형이 감추는 비용은 운영 판단이다

DLQ 를 붙였다고 실패 처리가 끝나는 것은 아니다. 운영자는 최소 다섯 가지를 직접 맡는다. maxReceiveCount 값 결정, DLQ retention 설정, ApproximateNumberOfMessagesVisible 알람, ApproximateAgeOfOldestMessage 알람, redrive 시점 판단이다.

비용도 남는다. DLQ 도 그냥 SQS 큐다. 메시지가 source 에서 DLQ 로 이동하고, 나중에 다시 redrive 되면 그만큼 큐 요청이 더 생긴다. 실패 메시지를 본문 기준으로 분류할지, 오류 코드 기준으로 나눌지, 새 consumer 로 별도 처리할지도 사용자 몫이다. 관리형이 없앤 것은 큐 서버 운영이지, 실패 해석과 복구 판단이 아니다.

지금은 DLQ 를 실패 보관함이 아니라 실패 분리 장치로 본다. 큐에 붙였다는 사실보다, 그 뒤에 누가 메트릭을 보고 언제 다시 보낼지를 먼저 적게 된다.

YouTube 영상

채널 보기
AI는 데이터를 어떻게 분류할까? 벡터의 거리와 KNN 알고리즘 | 선형대수학
숫자 하나가 AI 모델의 운명을 바꾼다? | 선형대수학
마지막편, 트라이 노드를 50% 이상 줄이는 방법? 압축 트라이 성능 분석 | Trie 자료구조 이야기
AI는 왜 수백 차원의 벡터를 사용할까? 고차원 공간과 행렬 | 선형대수학
숫자 하나가 AI 모델의 운명을 바꾼다? | 선형대수학
행렬의 기본 연산 - 행렬 덧셈, 스칼라 곱, 전치 | 선형대수학
AI를 위한 선형대수학 - 소개 | 선형대수학
트라이(Trie) 자료구조: 파이썬으로 삽입(Insert) 연산 구현하기 | Trie 자료구조 이야기