환경

AWS SQS와 DLQ 사용법 - SQS에서 실패한 메시지를 다시 소비하는 방법

왈왈디 2025. 3. 17. 00:00
728x90

회사에서 운영하던 서비스의 리워드 지급 과정에서 대량으로 오류가 발생했다.

리워드 지급 건들을 메시지 큐에 쌓고

워커가 이를 처리하는 방식이었는데, 

워커에서 지속적으로 메시지들이 처리 실패되어 결국 23만 건의 메시지가 DLQ에 쌓이게 되었다.

 

결국 데이터베이스 배치 작업으로 실패 건들을 처리했지만,

SQS와 DLQ에 대해 잘 알고 있었다면 더 잘 대응할 수 있었을 것 같다.

 

메시지큐, SQS, DLQ에 대해 알아보자.

메시지큐란?

메시지 큐(Message Queue)란 서버, 마이크로 서비스 간에 비동기 방식으로

데이터를 주고 받을 수 있도록 도와주는 시스템이다.

 

메시지 생성자 (프로듀서, Producer)와 메시지 소비자(컨슈머, Consumer)가 있을 때,

메시지 생성자가 메시지를 큐에 쌓아 두면, 메시지 소비자는 메시지를 수신하여 메시지에 따라 서비스 로직을 실행하는 방식이다.

 

즉 메시지 생성자와 소비자가 직접 통신하지 않고, 중간에 큐를 통해 데이터를 주고 받음으로서

시스템 간 결합도는 낮아지고, 확장성을 높일 수 있다.

메시지가 유실될 위험도 낮아진다.

 

메시지 큐의 장점을 정리하면 아래와 같다.

  • 비동기 처리: 프로듀서는 메시지를 큐에 넣고 바로 다음 작업을 수행할 수 있다.
  • 부하 분산: 여러 개의 컨슈머가 메시지를 가져가 처리하므로 부하를 분산할 수 있다.
  • 확장성: 시스템이 확장되더라도 메시지 큐를 통해 부하를 조절할 수 있다.
  • 내결함성: 메시지가 손실되지 않도록 보장하는 기능을 제공할 수 있다.

이러한 장점 덕분에,

티켓팅과 같이 서버가 빠르게 응답해야 하면서도 메시지가 손실되지 않아야 하는 상황에 많이 사용된다.

AWS의 SQS란?

AWS의 SQS(Simple Queue Service)도 AWS에서 제공하는 완전관리형 메시지 큐 서비스의 일종으로

서버를 직접 관리할 필요 없이 메시지를 송수신 할 수 있도록 지원한다.

메시지 큐이므로 기본적으로 아래와 같이 동작한다.

  1. 프로듀서가 SQS 큐에 메시지를 보낸다.
  2. SQS는 메시지를 보관하고, 컨슈머가 가져갈 때까지 유지한다.
  3. 컨슈머가 SQS에서 메시지를 가져와 처리한다.
  4. 메시지 처리가 완료되면 해당 메시지는 삭제된다.

SQS는 표준 큐 (Standard Queue)와 FIFO큐(First-In-First-Out) 두 가지 타입이 있다.

먼저 들어온 메시지가 먼저 소비되기 위해서는 FIFO큐를 사용해야 한다.

 

컨슈머를 지정할 때 Elastic Beanstalk에서 Worker 환경으로 생성하면

메시지 큐가 빈스톡에 자동으로 할당되게 하거나, 기존의 큐를 빈스톡에 할당할 수 있다.

 

프로듀서가 해당 큐 url로 메시지를 보내두면, 연결된 컨슈머 워커가 큐의 메시지를 소비하는 것이다.

 

그런데 컨슈머가 항상 정상적으로 메시지를 소비하는 것은 아니다.메시지를 수신한 후 처리하는 과정에서 에러가 발생하면 queue에 에러 응답을 반환한다.그런 경우 SQS가 실패한 메시지를 재시도하게 할 수 있다.

 

만약 메시지가 여러번 반복해도 계속 실패한다면SQS는 무한히 컨슈머에게 메시지를 보내야 할까?이 때 사용할 수 있는 것이 DLQ(Dead Letter Queue)이다.

DLQ란?

DLQ(Dead Letter Queue)는 SQS의 메시지가 소비되지 못한 경우 따로 모아두는 대기열이다.

AWS에서는 '배달 못한 편지 대기열'이라고 부른다.

 

DLQ를 사용하면 실패한 메시지들을 격리하여 더 이상 소비가 시도되지 않게 하고

실패 원인을 파악해볼 수 있다.

 

하나의 DLQ를 둘 이상의 SQS에 연결할 수도 있다.

 

DLQ를 설정할 때 최대 수신 수(Receive Count)를 지정해야 하는데,

이는 SQS에서 최대 몇번 실패했을 때 메시지를 DLQ로 보낼 것인지를 결정한다.

 

위와 같이 설정하면 10번 실패 후 DLQ로 메시지가 보내진다.

DLQ와 기존 SQS는 동일한 리전에 있어야 하며, 동일한 AWS 계정을 사용하여 생성해야 한다.

DLQ에 쌓인 메시지를 소비하는 방법

1. CLI로 StartMessageMoveTast API 사용하기

AWS에서 DLQ의 메시지를 본래의 SQS로 다시 보내는 API를 제공하고 있다.
(사실 특정 큐에서 다른 큐로 메시지를 옮기는 API이지만, 현재는 DLQ에서 본래 큐로의 이동만 허용한다.)
 
<Request>
POST /  HTTP/1.1
Host: sqs.us-east-1.amazonaws.com
X-Amz-Date: <Date>
Content-Type: application/x-www-form-urlencoded
Authorization: <AuthParams>
Content-Length: <PayloadSizeBytes>
Connection: Keep-Alive 
Action=StartMessageMoveTask
&SourceArn=arn:aws:sqs:us-east-1:555555555555:MyDeadLetterQueue
&MaxNumberOfMessagesPerSecond=10

 

Request 파라미터에는 JSON 포맷으로 아래 세 가지가 포함될 수 있다.

 

1. DestinationArn

: String 타입, Not-Required

메시지가 옮겨질 큐의 arn을 명시한다.

이 필드에 값이 없는 경우 DLQ의 본래 큐로 메시지가 옮겨진다.

 

2. MaxNumberOfMessagesPerSecond

: Integer 타입, Not-Required

초당 옮겨질 메시지의 수를 입력한다.

최대 초당 500건이다. 

이 필드에 값이 없는 경우 메시지의 크기에 따라 최적화된 값이 적용되고,

옮겨지는 동안 값이 변할 수 있다.

 

3. SourceArn

: String 타입, Required

옮겨질 메시지를 현재 보유하고 있는 큐의 arn을 명시한다.

현재 AWS SQS의 DLQ인 경우만 가능하다.

 

<Response>

HTTP/1.1 200 OK
<?xml version="1.0"?>
<StartMessageMoveTaskResponse xmlns="http://queue.amazonaws.com/doc/2012-11-05/">
    <StartMessageMoveTaskResult>
        <TaskHandle>eyJ0YXNrSWQiOiJkYzE2OWUwNC0wZTU1LTQ0ZDItYWE5MC1jMDgwY2ExZjM2ZjciLCJzb3VyY2VBcm4iOiJhcm46YXdzOnNxczp1cy1lYXN0LTE6MTc3NzE1MjU3NDM2Ok15RGVhZExldHRlclF1ZXVlIn0=</TaskHandle>
    </StartMessageMoveTaskResult>
    <ResponseMetadata>
        <RequestId>9b20926c-8b35-5d8e-9559-ce1c22e754dc</RequestId>
    </ResponseMetadata>
</StartMessageMoveTaskResponse>

 

요청이 성공하면 200 응답으로 TaskHandle 값이 JSON 포맷으로 반환된다.

- TaskHandle

: String 타입

메시지 이동 작업의 고유 식별자(id)다. 

CancelMessageMoveTask 로 메시지 이동 작업을 취소하고 싶을 때 이 id를 사용할 수 있다.

2. DLQ 메시지를 처리하는 Worker 운영하기

출처: https://theburningmonk.com/2024/01/how-would-you-reprocess-lambda-dead-letter-queue-messages-on-demand/

 

기존 SQS의 컨슈머 워커와 동일한 로직을 수행하는 워커를 DLQ와 연결하여 운영하는 방법이다.

 

다만, SQS에서 실패 후 DLQ에 쌓인 메시지들은 처음 SQS에 들어올 때와 달리

실패에 관한 정보들과 함께 한번 더 가공된 상태인 경우가 많다.

출처: https://theburningmonk.com/2024/01/how-would-you-reprocess-lambda-dead-letter-queue-messages-on-demand/

 

이를 다시 기존 SQS에 맞는 형태로 가공하여 컨슈머 워커에 메시지를 보내야 한다.

3. Event Bridge Pipe로 기존 컨슈머 워커로 메시지 보내기

AWS의 Event Bridge의 파이프를 사용하여

DLQ의 메시지를 기존 컨슈머 워커로 전달할 수도 있다.

 

다만, 위에서 언급했듯이 DLQ의 메시지는 처음 SQS에 보내진 메시지와

그 구조가 다를 수 있기 때문에, 이를 가공(unwrap)하는 람다 등의 기능을 사용해야 할 수도 있다.

실제 내가 사용한 방식

사실 나는 SQS에 쌓여있을 때와 DLQ에 옮겨졌을 때 메시지 구조가 변경될 수 있다는 사실을 간과한 채

Event Bridge Pipe를 이용하여 큐와 큐 간에 메시지를 그대로 옮겼다.

 

그 결과 메시지의 헤더가 유실된 채 기존 SQS로 보내지고

바로 소비되어 모두 실패처리 되어버렸다.

(서비스 로직 상 메시지 헤더가 없는 경우에는 DLQ로 옮기거나 재시도하지 않게 되어 있어 그대로 메시지들이 유실되어 버렸다.)

 

또, 컨슈머가 메시지를 소비하는 속도를 간과한 채

23만건의 메시지를 너무 빠르게 본래 SQS로 옮겨버려서

워커 서버의 CPU 사용량이 급증했다.

헤더가 없어 모든 요청이 초반에 에러를 반환하여 실패처리 되지 않았다면

워커 서버가 다운될 뻔 했다.

(셀프 디도스 공격을 한 셈이었다.)

 

DB에 실패건은 구별 가능하도록 해두어서

배치 작업으로 실패 건들을 모두 정상 처리할 수 있었지만,

메시지가 유실되면 안되는 상황이었다면 참사가 될 뻔 했다.

 

참고 자료

728x90